I think Kotlin helped push Java in the right direction, but at this point it seems like a weaker choice. Spending years to migrate a massive Java code base like this feels like wasted time.
I think Kotlin helped push Java in the right direction, but at this point it seems like a weaker choice. Spending years to migrate a massive Java code base like this feels like wasted time.
I like:
* data class Foo(val theImplementationAndStyleOfDataClasses: String)
* elvis?.operator?.chaining
* the order of variable declaration (type afterward)
* how function pointers at the end of a method call can be auto-inserted as the next curly bracket
* how you can have fun foo() = returnValue; in one line.
* fun `method names that can have spaces - and dashes - in them`()
The preceding 3, combined, allow for: @Test
fun `much easier testing`() = commonTestSetupWrapper {
// the code under test
}
* val immutable by default
While I agree that Kotlin has definitely helped push Java in the right direction; and I agree that it's probably not especially necessary to migrate 10MM lines of Java code to Kotlin, especially since they're fully interoperable, I definitely would prefer writing _new_ code in Kotlin over Java for the for-the-lazy devex improvements, if nothing else.fwiw, my "good at it" programming history follows the lineage in historical order of:
* Java (8?? years of it including competition programming)
* C# (5+ years)
* Python (2016ish to current)
* Ruby (3-ish years, lots of Rails)
* Kotlin (2-3 years, through current - written over 40k lines of Kotlin in the last year, alone) `method names that can have spaces - and dashes - in them`
Eugh, that is a turnoff. Maybe I have programmer Stockholm’s, but at least with a single connected word, I can always double click to select the token. Maybe, I might have wanted dashes at some point, but spaces seem like a step way too far. class MyTestableClass {
fun `methodName - when input does not parse to valid regex throw exception`() {
}
}
It's pretty clear what is under test in a situation like that. That's basically the only situation I ever see it used in (and would code-smell the heck out of it if I saw it in other circumstances).People who are familiar with RSpec-style testing are very used to this sort of thing.
describe MyTestableClass do
context 'input parsing issues' do
context 'when not valid regex' do
it 'throws exception' do
...
end
end
end
end
Anecdotally, I've also found that such style naming for tests allows me to write out the desired names for all the tests ahead of time more easily and then implement them. That happens to be my flow.They did a study and it isn't hard to read camelCase. https://ieeexplore.ieee.org/document/5090039
It's important to note that the researchers only tested short phrases with 2 and 3 words. "getNextPath" is a different beast compared to "methodName_whenInputDoesNotParseToValidRegexThrowException".
On the other hand, there is a good body of research that generally shows that proper sentences (not just 2-3 words) without spaces are harder to read, at least for English speakers[1]. Early medieval Latin manuscripts did not use space between words[2]. The fact that spaces were seen as an improvement when they were introduced, is probably telling.
[1] https://scholar.google.com/scholar?hl=en&as_sdt=0%2C5&q=unsp...
[2] Roman-era inscriptions did use an interpunct (a small middle dot) to separate words, but this practice fell out of fashion in Late Antiquity and Early Middle Ages and I'm not sure if it was ever the norm in handwriting.
That study compared camel case to underscore spaces. Well underscore space is still not as readable as methods with spaces and dashes.
The study does not say that camelCase is unequivocally as easy, or easier, to read.
Selecting the correct identifier from a list of 2-3 words (expandAliasTable, expandAliasTitle, etc.) is a wholly different exercise.
@ParameterizedTest
@ValueSource(strings = {"first", "second"})
void fooGoesBar(String param) {
...
}
Also, once you get used to assertj, you'll never want single-assert again. It's fantastically useful to know (for example) not just that two lists aren't equal, but specifically what elements differ.Pytest shows the diff as well when using assert:
> assert y == x
E assert [1, 2, 3] == [1, 4, 3, 6]
E
E At index 1 diff: 2 != 4
E Right contains one more item: 6
E Use -v to get more diff assertThat(someCollection).map(Thing::getValue).containsExactlyInAnyOrder(3, 4, 5); assert sorted(x) == sorted(y)
If you want to add map, then you have to write assert list(sorted(i.getValue() for i in someCollection)) == [3, 4, 5]
If the list contains non-sortable values, but they are hashable and unique, you can use sets: assert set(x) == set(y)
If the values are not unique, but hashable, you can use a counter (like a set but with count for repeating values): assert Counter(x) == Counter(y)
(by the way I learned about this trick from a LLM)And if the values are neither sortable, nor hashable, you'll have to write a helper function.
But still, pytest tests are less wordy and they don't require you to create a class.
This situation is familiar to me, I had to write such helper function when writing tests in PHP, for some reason it is not included in PHPUnit assertions.
In fact I've occasionally found myself writing reflection code to parse the test method name as input just to avoid that problem altogether (for name and data, not for documentation). And that was in plain Java even, the pattern could be far more useful with that kotlin naming freedom.
When a test fails, the method name describes what it was supposed to do written out like a sentence enclosed in double quotes which seems like a win but not much of one.
When you need to add a new test or analyze if an existing test needs to be changed, you have to eyeball all the code in the test class because even with methods named with spaces, it's not always indicative of what it does.
With Java, I can sort a list of method names and have a better idea immediately what needs to be updated or added.
Refactoring Spock tests sucks due to how the context is different based on the clause you’re in.
expectInternalServerErrorOnBadInput
Vs
expect internal server error on bad input
I'd expect an IDE for Kotlin to detect `stuff like this` and select the entire name. Considering Kotlin came from JetBrains, IDEs are most likely already in the Kotlin community's DNA.
(i never wrote or anything related to Kotlin myself but i do appreciate IDEs helping with stuff like this)
Idea understands and performs dramatically better with Java.
It also really doesn't like Java with generics for some reason. Still beats any other Java IDE I've seen.
val isBar = foo.`is`(bar)
rather than needing to use reflection like you would to accomplish the same in Java.The Kotlin style guide basically says "never use this outside of test names" and some targets, such as old Android, don't even support the feature in test cases.
In general you would not use spaces for method names of anything that’s a shared/common use facility. They are best used to give test methods readable names. E.g. “test parallel writes are thread-safe”. And not “write with timeout.”
If you're using anything that can do syntax coloring correctly, you can still do that.
That's because you're too used to C/Algol style languages
In 2020 I started a new project in Kotlin.
I like Kotlin, but what I hate is that in the meantime I migrated all my programming to Neovim and Helix. All programming? No, not Kotlin, because the LSP isn't there and JetBrains clearly has no interest, they want to sell their IDEs.
Now don't get me wrong, I have an IntelliJ license since 10 years, even back when I was employed and paid it out of my own pocket.
It's not about the price. I would gladly pay for the LSP implementation. But I don't want to use IntelliJ anymore.
So a new project on the JVM where I have a say in the stack? Java or maybe Scala. No more Kotlin.
As for Kotlin in general, I agree with your list. I really enjoy the way Kotlin improves the dev experience not so much with audacious new (or "new to a java-like environment", of course I'm looking at you, Scala) concepts but with small trivialities like being able to assign the outcome of an expression chain to a name without polluting that name's scope with intermediates that really don't have any purpose after the result of that chain is assgned.
And I don't hold it against Java that it does not follow that path (it focuses on more impactful changes), I would consider it almost out of character if it introduces e.g. .let. The only thing I'm a bit torn about when it comes to Kotlin is wether the "this" variants of scope functions were a good idea. They are certainly part of kotlin, and some "DSLy" styles would really not be the same without them, but if I were to pull what I like about kotlin into other languages I'd probably introduce let and apply (and the this-less run) and skip run/with/apply (assuming that the target language even has a this to run/with/apply on)
For the purists there are records now in Java (which can be combined with destructuring / pattern matching - there were significant improvements in this area in the last few JDK releases, and more are coming!). In this regard Java had surpassed Kotlin by some margin.
For the enterprise coders, there is the Immutables project (https://immutables.org) which you can say is Lombok on steroids (but implemented correctly, that is, by generating classes which you can inspect as regular source code, instead of relying on hacking the compiler...).
> * elvis?.operator?.chaining
This will hopefully be finally resolved by one of the subprojects of project Valhalla, as a prerequisite for value class support (which cannot be null, so...).
The others on your list are small and subjective stylistic differences, of course your preference may vary, but should not weigh heavily in any Java vs Kotlin discussion.
This makes Java records extremely painful to use for any data has more than 3 fields or so. You could use them together with an annotation processor like RecordBuilder (or Immutables or Lombok, but I don't think they support records?), but that's just not pure Java. Annotation processors that generate byte code always come with their own complexity.
Kotlin data classes have their own warts (order-based destructuring can be dangerous and the copy() method is a bit hacky) but the overall ergonomics for large value objects are better than Java records at this point.
Lombok is so ubiquitous today that I effectively consider it part of the language. And even with Lombok, the Java toolchain is so much faster, easier to use, and more stable than Kotlin.
Kotlin is a great language, marginally better than Java, but I'm not sure it's worth enduring the toolchain. And Java keeps improving.
Java will never get close to Kotlin because even though Java can, and thankfully does, still improve, it's too late to revise many of it design decisions, and large language features often need to be considered in a language's design from the start.
On Android, Kotlin is an even bigger win because Android uses an old Java version and has become Kotlin-first.
In my opinion, Java will remain the best choice for libraries that target multiple JVM languages. For everything else, I use Kotlin without thinking twice. Kotlin 2.0 laid the groundwork for toolchain improvements, which have indeed been a long-standing pain point.
Having worked professionally in both, I find that good programmers can write excellent code in both languages; bad programmers can write terrible code in both languages. The average cultural practice of the Kotlin community is perhaps better than the average cultural practices of the Java community (which huge and defies averaging anyway). But Java code which emphasizes immutability, uses Optional instead of nulls, uses streams, etc is pretty indistinguishable from Kotlin.
Here are some of my favorite Kotlin improvements over Java that I leverage all the time:
* Much improved type system (nullable types, function types, declaration site variance, type aliases, contracts, better type inference, reified function type arguments)
* Local variables are final by default ("val")
* Type-level distinction between read-only and mutable collections (but compiled to Java collections under the hood, so no conversion required when interacting with Java)
* Much improved collection API
* Much improved lambdas (e.g., no pain points w/ mutating variables and checked exceptions)
* Extension functions (incredibly useful in practice)
* Much better DSL capabilities (great for UIs, generating HTML, etc.)
* Lazy properties (more generally: delegated properties)
* Coroutines (looking forward to Java's Loom; by then coroutines will have dramatically improved my async code for 5+ years)
* Great serialization support (kotlinx.serialization)
* Pragmatic macro-like capabilities via inline functions and compiler plugins (removes lots of boilerplate)
* Multiplatform support (JVM/JS/WASM/native; Graal native image is a good alternative to Kotlin/native and also works for Java)
* Yes, the Kotlin type system is better, no question. And yet it doesn't really matter for 90% of code, Java's type system is "good enough". And in the places where it tends to suffer, IntelliJ ends up covering the ground anyway (validating various annotation values, spring properties, sql, etc).
* The difference between nullable types and Optional<?> types is not material. TBH I quite like Optional<?> and how it meshes with streams.
* "final by default" is indeed nice. But putting `final` everywhere (which we do) is at most a tiny annoyance. This is not material.
* Clear type distinctions between mutable and readonly collections is better, but not material, because we treat all collections as immutable.
* Java streams hit 90% good enough, and StreamEx tends to fill in another 9%.
* We don't have pain points with mutating variables in closures because we don't use mutable variables.
* We don't have problems with checked exceptions because we don't use checked exceptions (and when interfacing with foreign code that throws checked exceptions, we use @SneakyThrows or wrap).
* Coroutines are a major net negative for Kotlin, and Loom is already here. After many years of async programming in JS/TS, Ruby, and (going back far enough) C++, I've concluded that async programming (and function coloring) is absolutely toxic and should be avoided unless it's absolutely required for performance (or the platform, ie GUIs).
* Multiplatform support has great future potential, but it doesn't seem to be ready yet. I just started a greenfield project and _really_ wanted to use KMP. Decided against it in favor of a React frontend. I don't want to live on the bleeding edge, I have work to do. And I've been bitten by deadended technologies enough times in my life to know the risk isn't worth it.
I could go on but I think you get the point - my Java looks like Kotlin, and the parts of Kotlin that would really diverge from my code aren't things I want. You could tell me that I should use coroutines everywhere and I just simply disagree.
I think we'll see them come in through https://openjdk.org/jeps/8303099 although I'm not a huge fan of the approach chosen (tri state nullability: explicitly nullable, explicitly non-nullable, legacy code that should just be treated as nullable).
Except for native support for reification, almost all of Kotlin can be reimplemented by Java if you wait long enough and stack enough class annotations. It's all stylistic up to a certain point, but the classic Java style combined with the glacial improvement process is one reason why some people want to use a different language.
I think I'll have an easier time convincing someone to consider Kotlin than I'll have convincing someone to add Immutables+Manifold to the dependency chain. You end up writing Java that's not really Java like, but also not something that there's a good standard style for, and that usually ends up in a mess.
I'm glad with pattern matching, Java is now pushing Kotlin to improve rather than the other way around. I'll hope to use those features one day, when companies finally upgrade from Java 11 to Java 17 in five or ten years.
I did just glance at Java's pattern matching; and yeah, it does look like it is a bit more powerful than Kotlin's pattern matching in that it can, at the same time, look for both type and a conditional. That's relatively neat - not something I've personally needed / wanted; but neat just the same :)
The JVM is a really nice virtual machine in how it lets us use both of these languages fully interchangeably to pick the one we like more.
I'm glad Java has been investing in some of these areas, too. Everyone improves when paradigms and new learnings are distilled.
What would be compelling: Array types that don't hate generics, or generic collection types that don't hate primitive types. Does Kotlin improve on Java at all here? It's such a pain having to remember this bonus complexity when all I want is a consistent way to manage a bunch of things. (I suspect not, as I think the limitations are at the JVM level, but I don't know.)
Bottom line is, you could have your cake and eat it too - have an int[] that looks like a List<Int>, and vice versa, while retaining the performance characteristics of an array.
interface Box<T> {
T get();
void set(T val);
}
interface IntBox extends Box<int> {
int get();
void set(int val);
}
https://openjdk.org/projects/valhalla/[this] https://inside.java/2024/12/16/devoxxbelgium-valhalla/
So it's possible.
You just swap them out with fastutils or trove and call it a day.
That said, I tend to get bitten by the fact that == on 2 boxed numeric values is roughly as misleading as possible -- it does reference comparison, but interns small integer values only, so (Integer)42 == (Integer)42, but (Integer)420 != (Integer)420. (So the small values you probably used in your unit tests will appear to work just fine with your broken, ==-using code.)
ETA: Deleted a misplaced "new". I also don't love that, e.g., ++x does a heap allocation + deallocation if x is boxed (since boxed types are immutable). But usually I care more about the cycles I spend coming up with correct code than cycles the CPU spends executing it.
record Foo(String theImplementationAndStyleOfDataClasses) {}
The string is final. While sadly there is no elvis operator, the Optional type helps: evlis.map(e->e.operator).map(e->e.chaining)
I still strongly dislike working in Java but Java 23 is a long way ahead of the Java 6 I first learnt Java with.https://docs.oracle.com/en/java/javase/17/language/switch-ex...
I'd personally prefer modern C# as it's been outpacing advancements in other major languages for DX, IMO.
It is sad that its adoption seems to have boundaries defined either by large ecosystems dependent on the JVM or ideological.
If I had to switch and use C# as my main work language for a company, I would be perfectly happy to.
This is one feature I wouldn't mind making it into other languages. It makes so much sense for doing UI stuff.
What makes Kotlin such a strong choice for many orgs today is its batteries-included multiplatform capability. We are able to write data models, validation logic, business logic, etc just once and compile to JVM, WASM, and native targets. For orgs with very large codebases and frontend applications (web + iOS + Android) this is an attractive capability because we can have a single codebase for a ton of core functionality, and have each frontend import a library which is native to its own platform.
Of course many technologies with this promise have come and gone over the years, but this is the first one with a strong backing that has allowed us to _natively_ interoperate with each target platform.
I believe these are all driving factors that have been pushing well known companies, that were previously Java shops, toward Kotlin. If you speak to a broad range of people in the industry you'll find many more orgs moving from Java to Kotlin than from Kotlin back to Java. We can simply get more work done with less code and ship to all our frontend platforms, and unless Java can do the same, I don't see the industry moving in that direction.
The question of optional things is always laid out as if the choices of my coworkers do not exist. Are you expecting me to work solo, or to be a bloody tyrant who pushes through all of my own ideas over the objections of an entire team? These are some of the most antisocial people in any debate on software. No I don’t get to just chose to use or ignore a tool in an ecosystem. That’s why picking a system with batteries included is a simpler transaction. I can go to a new company and have expectations, not arguments.
Not familiar with Kotlin but how does that work? Does it come included with a PAL? Because it you want to be platform agnostic, you can't for instance use a Java RegularExpression in your platform agnostic code.
In shared code you can define interfaces that have to be implemented by any platform you use.
I think on Android the choices are either to start from scratch or to migrate to Kotlin for Facebook. Sticking with Java on Android just makes everything harder. All the new frameworks are Kotlin centric. Most Android developers they'd be hiring would have been using Kotlin for years. Etc. So, I can see why Facebook is making the effort to keep their code base alive and future proof. Android is more Kotlin focused and has been for years. And that's only going to be more the case now that compose multi platform is a thing.
Btw, most of the stuff you mention is probably more applicable to server side Java and less to Android. IMHO, Spring + Kotlin is a very nice combo on servers. Very well supported by Spring and they bundle a lot of convenient things for Kotlin. And I don't need any of the tools you mention because it does the right thing by default.
I mostly agree with you, but Spring+Kotlin does require the allopen plugin, so it's kind of hacking Kotlin to do the wrong thing (all classes open) in order to support an arguably bad design choice by Spring.
It is wild but such is the consequence of the culture that punishes explicit failure, and ignores failure to be able to ship fixes for the unpredicted situation and ignores failure to have a productive code base. I cry for the mounds of copy-pasted code that must result from this policy that treats its engineers code like radioactive garbage.
Exception: final parameters. They should never be assignable.
One of the huge problems with a language migration is that you're left with old and new and all the parallel tooling, context switching, and impedance mismatches. There's often a claim that the existing code will be migrated eventually, but IME that doesn't actually happen with large code bases. Then if there's ever a third migration, you're left with three languages in play.
It's much better to aim for 100% migration, as close to 100% automated as possible. Then when you're done, you're really done. Maintenance, training, and the next migration will be easier.
Contrary to the popular notion of dismissing certain syntax differences as sugar, I consider it one of the most important factors simply because we spend more time reading than writing code. To me Java has always been verbose and dreadful to read, there's something fundamental wrong if you need your IDE generate so much then force to train your eyes to skip over most of them while reading.
I find Kotlin to be more elegant and fluent especially with native syntax support of the above features. I can read it at least 25% faster than Java. Perhaps which one is better is personal taste, but I'd maintain syntax is very important, just like natural languages.
Nothing on Kotlin will ever matter on the decisions taken by Oracle, IBM, Amazon, Azul, PTC, Aicas, microEJ, Microsoft, Red-Hat,.... on their JVM/JDK implementations and related tooling.
What I saw happen was Kotlin taking over the mantle of "better java" from Scala, and aiming for an 80/20 type language compared to Scala. And for the most part, it's those 80% that are finding their way into Java.
Its Meta. They have enough money to spare not to need to worry too much about whether there is a good RoI on rewriting some mobile apps.
I very much doubt FB's mobile apps are written for any sort of efficiency, (either in engineering or financial terms) - 18,000 classes in the apple one! https://quellish.tumblr.com/post/126712999812/how-on-earth-t...
So super inefficient binaries, but I guess more efficient to develop (or I assume that was the idea)?
By the Dalvik Virtual Machine (DVM). 65k method limit is what Facebook hit. tbf, DVM was engineered to run on embedded systems with 64MiB RAM (as a replacement for J2ME).
Nobody wants to use a mountain of tools just to achieve basic feature parity with other languages. Also java syntax is awful.
Identifier-first can also be a lot more readable when gnarly (and thus lengthy) types are involved, since you don't have to scan the line to find it. It makes for neater-looking code with multiple declarations one after another, too.
As far as commonality, both syntaxes are very common, and have been so historically as well (roughly speaking, C vs Pascal syntax families). It should also be noted that most new languages go for identifier-first, so if anything, it's more future-proof in this sense.
And again, the point is that Kotlin brings along extra characters AND variation. What's next, `fnctn myFunction(x : Int)~ Int { /* code */ }`?
Why? Just to be quirky? To avoid some hypothetical lawsuit?
And I already explained the reasoning. No, it's not to avoid the lawsuit, it's because many people genuinely find it more readable for various reasons. Which is why it keeps showing up in all kinds of languages that are otherwise wildly different syntactically.
And again, in the latter part I'm talking about Kotlin's use of 'fun' and ':'.
Using val instead of let is similarly off-putting.
Maybe it has a more PHP-like feel to it. No accounting for taste at that company.
What's dubious about it. Kotlin has a more comprehensive dx, and has features like extension functions that aren't on the java road map.
Does a Kotlin codebase have more safety from a legal perspective?
IMO, Android is moving away from modern versions of Java. Java and its underlying standard library will always play a big role in Android development.
The way I see it, Kotlin makes a lot of sense for Android development because Kotlin can introduce new things in its standard library that make older versions of Java look ancient. It's not like you can use new Java features in Android, so using Kotlin makes people not care as much about new features of Java that are only available in modern versions of Java.
Hence ART is updatable via Play Store since Android 12, and is currently on Java 17 LTS.
It is good enough Java for relevant libraries to work on Android.
Including the main sales pitch of Kotlin, its Interoperability with Java.
It's not "the JVM"; JVM is a spec that has many implementations, you are probably referring to Oracle JRE/JDK.
But if built-in null safety and lower verbosity was all that Kotlin had to offer I doubt it would have won. Kotlin offers a lot more features that Java does and most probably will not offer in the next 10 years:
- Extension methods: No more terrible FooUtil or BarHelper classes.
- Context parameters (preview feature)
- Coroutines (they are not just about concurrency - you can use them to write generators as well[1]).
- Keyword arguments: This makes using constructing data classes possible without generating builders, and in general lets you deal with multi-argument methods in a safer and more readable way. It also cuts down the boilerplate of defining 20 different overloads that call each other.
- Interface Implementation by delegation: This feature makes it easy and painless to follow the motto "composition over inheritance". Java makes implementation inheritance 50 orders of magnitude easier than composition with manual delegation, so it's pretty natural that the vast majority of Java code- bases overuse inheritance in very bad ways.
- Properties: This reduces the need to worry about direct field access causing API breakage in the future, and removes the need for the absolutely massive boilerplate associated with getters and setters in Java. It's true that records remove the need for that, but records only work for purely immutable data classes (and right now they are only practically usable with a small number of fields).
- Delegated Properties: Not something I use every day, but it's very useful when I do use it.
- Unsigned integers (specifically bytes): Writing any byte-wrangling code in Java is a downright nightmare.
- Reified Generics: Java may get something equivalent to that with Project Valhalla, but I'm not sure how comprehensive it would be.
- If expression: Java made switch an expression with JEP 361, but if is still a statement. This leads to a lot of bloated and somewhat unsafe code where you have to define a variable before the if and then assign to he variable in each branch.
- Expression Bodies for function: This is a very small feature, but it's pretty nice to cut down boilerplate and make functions more readable when you need it.
DSL Features ------------
Kotlin is perfect for DSLs thanks to features like:
- Closure blocks: Kotlin lets you pass the last closure argument to a function as a block of code inside curly braces that follows the function call. This features lends itself very well to readable DSL. In Java you would need to embed the closure inside the function argument list and add an extra parentheses. This gives you some of (see the next couple of points) the DSL capabilities of Kotlin, but the DSL becomes very hard to read and use when a lot of blocks are involved. There is a good reason why Kotlin DSLs are based on blocks, while Java DSLs are based on the less-flexible fluent interface pattern: block-based DSLs are just too hard to read and write in Java.
- Inline functions: Inline functions allow the compiler to perform some optimizations in advance that may be harder for the JIT to do (namely inlining the provided block closures), but the real kicker is that you can use flow control statements (break, continue and return) that affect the calling scope.
- Multiple "this" in context. You can have multiple values for this inside a nested scope. The compiler manages to find the right reference for the "this" alias based on your usage, but in case of ambiguity (where the deepest "this" wins), you can disambiguate your choice. This feature sounds overly complex and unnecessary at first, but it allows a lot of power for the Kotlin DSL system.
- Closures with this receivers: Using the multiple this values from above, you can have closures that receive their own "this" to introduce DSL methods. Being able to introduce these methods without shadowing the "this" from the parent context is crucial for powerful DSLs.
- Infix functions and operator overloading: Needless to say, these features make DSLs even nicer.
I've probably skipped a couple of features I can't recall right now, but I hope that it shows that Kotlin is a lot more than just "Java with null-safety and data classes" as some people think.
While we're on this subject, I'd like to rant for a bit. Why is this not a standard feature in every OO language today? The adage that composition should be preferred to (implementation) inheritance is itself older than Java, and was already fairly common wisdom 20 years ago - yet PL design has seemingly not reflected this.