For me personally Kotlin does not bring that much to the table over a modern version of Java. Sure if you're stuck in Java 8 it can be a win, but compared to Java 21 it just doesn't bring enough to warrant the switch for me.
Honestly the main thing I find killer about Kotlin over Java is its stricter null safety (types do not permit null values unless you mark the type as nullable with a "?" suffix). (I know Java does have the @Nonnull annotation but it's extremely verbose to use much and Java doesn't enforce its use on values that are expected to be non-null, which both greatly diminish its usefulness.) I've been spoiled by languages like Typescript and Rust which have great null safety like Kotlin, and it's very annoying going back to a language without it and questioning whether a null is expected or possible for every single value.
I wish I could get a strong sense of the hype around null safety. Sure, I would like to have the extra expressive option. However, optional has been fine for me. I still barely use it. It has to be at least 3-4 years since I last encountered a null pointer exception in my daily java programming. I feel like there are plenty of other things that pain me on a daily basis that rank far higher on my asks for the language. Collection and stream ergonomics cause constant pain, for example.
Infinitely chainable expressions, .let, .also and friends. You shouldn't overdo it (lengthwise) and it's almost hilariously superficial, but I miss it in every other language since.
Tying down a value to a name only if you have something meaningful to say in the name, and only when the value is actually finished instead of being a work in progress, that gives me great piece of mind and frees up mental resources for other things. I wish every language had a "kotlinized" sibling that simply added the scope functions on syntax level (or a subset of the scope functions, not sure you need a zoo that size)
It's not always better, I give you that. If something does not work as expected you might even find me transforming code to conventional imperative style. But I feel like I have a much better shot at getting it right in the expression chain style, and in all styles if scopes are not polluted by dubious intermediaries and/or badly named one-callsite functions thanks to letisms. By the time the "let tax" grows too high, chances are that the block in question is big enough to make the "factored into a function tax" bearable.
I’ve seen this sentiment a lot and I don’t understand it. I actually feel like very little has been added between Java 8 and Java 21. Virtual threads is probably the biggest thing, and it looks very promising, but the language itself is still a chore to use, and Kotlin does a good job at cleaning a lot of that up.
I think Kotlin is at risk, though, of having the JVM force them to support two competing models to accomplish the same thing - coroutines and virtual threads is a good example (although Java still doesn’t have language level structured concurrency). This could really make things messy.
> I’ve seen this sentiment a lot and I don’t understand it.
I just don't think any feature of the language of Kotlin is that much better than Java's implementation. I'm also a big supporter of brevity != clarity when it comes to code.
What language feature in Kotlin do you think is that much better?
I was you not that long ago. I've had the same conversation with a lot of other Java developers too - they/we always say the same things.
It's weird being on the other side of the fence now - hearing the same things I used to say.
Kotlin was a gateway drug out of not just Java, but OO/Solid and into FP for me (FP being another thing I used to not "get"). The beauty of Kotlin is you don't have to ever write FP code if you don't want to - but you will want to before too long.
I would recommend trying it out for yourself. It's Spring integration is amazing, and you'll have a great time. Write even a little Kotlin and you'll have an "ah ha!" moment and everything will forever be different.
It's one of those things where you need to experience it for yourself before you actually understand how great it is. There is no argument you will hear that will convince you, sadly. I was the same once...
To me Kotlin is different enough to be annoying to learn, but not different enough to matter that I use it over Java. So it just kinda feels like a waste of time.
I'll agree that Kotlin is not hardcore FP (although you can get there some of the way with Arrow etc.), but FP in Java is clunky.
- Having to remember the difference between "Function", "Consumer", "Predicate" and "Supplier" is weird. In Kotlin (and basically every other typed FP language that I know), I just write the type signature the natural way.
- Having to use streams in order to use map, etc., is noisy. Why weren't these APIs retrofitted to collections?
- Checked exceptions are a pain with functional code, e.g. map.
- Mutability is still the default behaviour of almost everything in Java, save records (and strings).
> Having to use streams in order to use map, etc., is noisy. Why weren't these APIs retrofitted to collections?
Sure, methods along the lines of the following could have been added:
public <R> List<R> map(Function<? super E, ? extends R> mapper) {
return stream().map(mapper).toList();
}
Yes, having them would make Java less verbose in the case where you only want to do a single map, filter, whatever operation on a collection, but I'm personally glad that they weren't because they produce extremely inefficient behavior when chained. So, it would add a performance footgun to save ~18 characters.
In Kotlin, asSequence gives you lazy behaviour (i.e. chains of map, filter, etc. are merged the way it's automatically done in Haskell), but if you don't care about the performance penalty, you can just call map, filter etc. on eager collections too. I'm of the opinion that you shouldn't prematurely optimise, for small enough data, it often doesn't matter much.
And checked exceptions are widely seen as an anti-feature - I don't think any other language has them, with the possible exception of Swift (see below), and for good reason. Other languages encode errors (more or less ergonomically) in the type system (e.g. Rust, Haskell, and to an extent it's what Kotlin advocates as well), and that has the advantage of playing well together with other language mechanisms - e.g. no extra allowances need to be made so that map works with them.
But for the sake of the argument: Swift also has something similar to checked exceptions, but only in the sense that functions can either throw or not throw. However, Swift also at least has constructs like "rethrows", which lets you say that a function such as map should throw iff the higher order function it wraps throws.
Java simply decided that it doesn't need to deal with this problem at all (for whatever reason - they simply could have added something like "rethrows") and now it's up to callers to deal with the incredible awkwardness that is using FP with Java library code that makes heavy use of checked exceptions.
Null safety checks and if being an expression are major pain point resolvers that are still not available in Java (OK, maybe pattern matching helps the if-expression part, because pattern matching IS an expression in Java).
Oh, and the legacy library APIs, which Kotlin elegantly wraps with FP-compatible classes. Instead of fiddling with streams (which also lack some basic operations, or have some not so nice API), you can just .map or .fold over your (read-only by default) collection. Not only less verbose, but also easier to use and understand.
Having said this, these are fairly minor things, which may not warrant a switch to Kotlin, if you are otherwise happy with Java...
> I think Kotlin is at risk, though, of having the JVM force them to support two competing models to accomplish the same thing - coroutines and virtual threads is a good example
I don't think it's that good an example, though there's some merit to it. Kotlin coroutines, to me, target three main use cases:
1. An abstraction over an underlying threading model. Get threaded execution without having to explicitly deal with thread pools and such. I'd consider structured concurrency to be part of this use case.
2. A way to write non-blocking code that is structured as if it were blocking. (Incidentally: I love Kotlin's decision to make "await" opt-out instead of opt-in.)
3. A way to write code that generates sequences of values without having to either write an explicit class that maintains state across calls or use callbacks/CPS. Or in other words, a way to use the "yield" function.
Use case 1 still makes sense with virtual threads. Run your coroutines on an executor that uses a virtual thread pool instead of a platform thread pool. If you prefer the coroutines API, you can still use it for structured concurrency and such.
Use case 2 is much less interesting in the presence of virtual threads; you can just write blocking code directly, no need for the language to turn it into async code for you.
Use case 3 has nothing to do with threads to begin with, so it remains exactly as valuable as before and there's no reason to change any existing code at all.
Do coroutines get less useful in the presence of virtual threads? Absolutely. But they aren't competing with virtual threads; there are substantial non-overlapping use cases that make them a worthwhile language feature.
This is a pretty unimpressive list for 9 years of progress, and is merely a small subset of features Kotlin was already providing.
I left the Java world for the C# world around 2008 and then returned to the Java world last year. C#/.NET changed much more significantly much faster. Almost all of the significant differences in day to day life with respect to the Java language and standard libraries that occurred between 2008 and 2022 are due to changes introduced with Java 8.
The issue with these new structures is they're necessarily clunky, to maintain interoperability with non-modern Java. Java's backwards compatibility is legendary, but is also why some of it's newer features are often criticized as not being as good as they could be.
It's one thing to say, "very little has been added between Java 8 and Java 21," and something else entirely to say, "newer features are often criticized as not being as good as they could be [to maintain backwards compatibility]."