119 comments

[ 3.0 ms ] story [ 210 ms ] thread
This title confused me very much, although it's technically correct it took me three tries to read it properly
For some reason I also kept looking what this person missed from Java.
Me too. Poorly worded title; suggests the opposite. I thought it might be something different so was intrigued and then disappointed to see the normal angle.
glad it wasn't just me. Wish it'd been named "What I miss from Kotlin while in Java" or something
I really like that Kotlin, Groovy and Scala keep advancing the JVM ecosystem.

Java in my opinion should evolve by keeping the original approach (a better C++) and leave programming language experimentation to the big three.

Honestly IMO Java is in a very good spot right now. Mature technology, maybe a bit "unsexy", but rock-solid and future-proof.

There are still pain points, but the language is leaps and bounds better than the Java 1.6 days.

Yet, Java-codebases look the same now as they did then. The program structure is still "externalized" to a container because of a lack of abstractions. It's still exactly the same.
I really don’t get what you mean. Java is not the most expressive language, but it is eons ahead of the expressivity of C or Go and yet I don’t believe it would be fair to claim that those languages also externalize their structure?
What I mean is that a Java program is almost always sitting in a Spring container with most important stuff happening as "decorated magic"; this has been the case since even before 1.6. To say that it is somehow better now than it was, is therefore not worth much. Sure, I'm sure you guys'll put a map or a stream in there somewhere but it's ... still the same thing. There's no real evolution at all.

The springification is what I mean by it being externalized and you generally don't see that in C, no. The pre-processor might be sort of the same thing but you generally don't bootstrap the entire thing using it.

Well, Spring is mainly for backend systems that have a very specific functionality that is best handled by a generic solution. In my experience this is in line with how most other web frameworks operate.

But this is absolutely domain-specific, Java the language is indifferent to exposing a library that will handle incoming request and dispatch them based on some library logic, or creating a “traditional” program for the litany of other domains where java is used st.

Spring isn't a web framework, it's gaffer tape for a language that just isn't good enough without it; that's why it's [spring] still around and its use is so prevalent. It's not at all mainly for very specific functionality in the sense that your post sort of implies [for web, and databasy things I presume]; in fact: it's just for wiring and for applying/ attaching things to "abstractions". As in: please decorate these sets of things with this behaviour.

If you'd chosen languages that permit abstractions in a real and usable sense, then you would not need this. In fact: with _one_ exception, I've never had a container in any Haskell, Scala or F# project ever. Do you think we ever needed one?

And this is my point: those other languages have seen _tons_ of development in the "abstraction arena" with (internal) DSL:s and super powerful frameworks for making powerful DSL:s. Java has had none of this because these concerns har all (poorly) handled at the container level.

The tacked on "functional stuff" is for sure cool coming from a Java backround, but they're pretty much useless.

Spot-on analysis. I work on a team (that strongly prefers using Kotlin) in the CI-CD world. Despite web dev only being a minor part of our work, Spring just attaches itself to everything like a disease. I feel like it has gotten to the point where most Java developers that interview know Spring and not Java. Dependencies themselves also become attached to Spring (our example: bringing in Kafka -- super painful without Spring and hardly any examples or docs.)

This "containerizing" is very unique to Java -- I've never seen anything like it in the other various languages I've used. I won't even mention the total disaster of JavaEE containers.

Yes, it is terrible and it's the main reason I'll never be doing any Java, ever again. I did 10 years up until Java 6 (or so) and then jumped ship in favour of Scala (at that point.) Now, Scala for sure isn't perfect and Java isn't terrible, but mainstream Java really is.

My point, though, is not to throw up all over Java but rather to quibble with the statement that Java has somehow evolved since then. It really hasn't!

It’s still heavily object orientated. My preferred approach to applications these days is immutable core / imperative shell and Java makes that awkward.
Can you elaborate on the immutable core/imperative shell paradigm? Or maybe link me to some references? Sounds interesting! Thx :-)
There is a talk by that name that popularised it.

https://www.destroyallsoftware.com/screencasts/catalog/funct...

Basically you have business domain objects that have value semantics and are immutable. You have functions that manipulate these objects. This makes the business domain unit-testable. Then on the edge of the application you have imperative code that manipulates databases, serves requests, etc. It’s like Haskell but as a design pattern rather than a language.

In the JVM world, Scala and Clojure communities are adopting this approach.

The sibling comment explains it well on a conceptual basis. I can maybe add a few notes since I'm in the process of gradually moving code to this architecture (for the lack of a better word).

It is essentially straight forward to do. You look at a bunch of code in a module and determine which parts do these kinds of things:

- data integrity, validation

- parsing, transformation between formats

- domain logic, decision making

- algorithms and data structures

This is your functional core. Note it doesn't matter too much if you have local mutations as long as they don't leak out. What you want to achieve is to have an interface where you can ask questions and you get the same answer for a particular question. This is certainly more productive if your language affords you with functional constructs, but its not strictly necessary to get the benefits.

You code these parts in a way that they are data in, data out essentially. This leads to super easy and reliable unit/example tests and generative tests. And this part becomes very simple and easy to reason about.

Secondly you have the part of your code that actually _does_ stuff, affecting your environment in some way.

- basically any kind of I/O

- cross cutting concerns between other side effecting modules and services (think DI and such)

- error handling, error reporting and logging

- caching, throttling, filtering etc.

That's your imperative shell (I just call them modules). This part you write in an imperative/procedural style and you call the functional core parts from here, never the other way around. You coordinate effects so to speak.

It is clear that you test these with integration tests, where you need to setup specific program state (or use mocks, but I try to avoid that) in order to find different code paths. This is harder to test properly but at the same time you also get to see everything that actually happens in condensed form.

The degree to which you extract "pure" logic from your imperative shell is actually quite flexible. You can go as far as defining purely functional FSMs to extract much of the control for example. I think it really depends on code size and complexity as well as performance implications. The cool thing is that you can be quite pragmatic and iterative about it.

Big fan of that approach as well, but there's no reason why you can't do it in Java. Records and streams are your friends. It's actually rather straightforward to start from a neat, functional pipeline and then do whatever at the edge.

Of course you can't have the pure/impure distinction enforced, but nothing that isn't a purely functional language is going to give you that.

The only really dumb limitation is that you can't have freestanding functions, but that's aesthetics more than substance.

It won’t make Java into Haskell, but just as an interesting tidbit, Checker Framework can check besides null-safety, pureness as well by annotations.
The lack of currying and do-notation really hurts.
The latest additions to the standard library generally follow that approach: java.time, java.nio etc. But of course the older API are still fully mutable OO. A new collections API, immutable by default, is still needed and that's not happening any time soon.
I don't know. To me it feels like Java is a bit of a patchwork, with too many features added on top of it that weren't accounted for in the original design of Java. Generics are probably the oldest example, but there are plenty of others.
I agree. It needs more standardized tooling and libraries. But as a language it does not need anything more right now. Not even pattern matching. But some small touches here and there could be good.
Groovy and Scala have nowhere near the potential of Kotlin.

I think Kotlin actually has the potential to dethrone Java on the JVM, it is that good.

Kotlin is not much different from Java, conceptually. It is basically Java 2.0. Where Goovy does away with compile time typing, and Scala bolts a Haskell onto Java; Kotlin is "just a better Java".

Probably a good way to characterize Kotlin. Scala has a lot of interesting features but also a lot of complexity. And not all of those features are necessarily very good. Arguably, Kotlin was born out of frustration with that as Jetbrains at the time rejected the notion of using Scala internally and instead started the long process of developing their own language. It was mainly intended to be a better Java but also to avoid some of the complexity of Scala.

Groovy's lack of compile time safety is like throwing out the baby with the bathwater. It's just not worth it for me. Kotlin gets you most of the language features but without doing that.

Do you have any good articles on what would make Kotlin better than Scala? I work only in Scala and have never touched Kotlin.
> Scala has a lot of interesting features but also a lot of complexity

I really don’t get it - I am fairly certain that feature wise Kotlin is actually a bigger, more complex language with many exceptions/edge cases. Scala is a really elegant language in that many of its features are solved by a singular base, for example implicits. And frankly, other than that it is a very easy language - programs written in it can be complex due to its high expressivity.

Flix will blow your mind then: https://flix.dev/
Threw me down this rabbit hole: https://en.wikipedia.org/wiki/Datalog

Looks like Flix is in a galaxy of languages implementing Datalog, a variant of (or somehow related to) Prolog. I guess Flix is Turing-complete while Datalog is not?

> I guess Flix is Turing-complete while Datalog is not?

Flix is a regular programming language, and supports datalog as a DSL basically, supported by the compiler and type system.

The only thing I like more about Java than Kotlin is compile times. Man is kotlinc slow.
Be thankful you don't have to interact with tsc or eslint on a daily basis.

kotlinc is way slower than javac but damn, it's still pretty fast compared to what else is out there.

`tsc` and `eslint` are not slow at all...
I use both on a daily basis. TSC is much faster than kotlinc for me. I'll give you that eslint is slow too.

I've recently been profiling kotlinc and there are files in our codebase that compile at 5loc/s!

>In Java, there’s no way to know whether a variable is null.

What? What about `foo == null`?

The author means not knowing it at compile-time. The expression you mention must be evaluated at runtime.
In practice, there are plenty of static analysis programs that will give you the same thing at compile time with @NonNull and similar annotations.
Too bad you need external tools anda lot of boilerplate that you must remember to enforce to achieve this, because it's an after thought and not a language feature.

And there's 7-8 different annotations to mark nullability, all slightly different in some detail.

It's a bloody and messy hell.

Sure, language-support would be better indeed, but in practice the often encountered annotations are supported by every tool, and the only relevant setting is whether everything is nullable by default or nothing is.
It's still not enforced by the language itself, so nothing stops a third party library (that you have to integrate) from not using the annotations, and then the unknown nullability exerts a domino effect on your own code, "infecting" it with uncertainty.
TBF that is true for the Java-Kotlin boundary as well. You can use values coming from Java (like results from Android platform calls) as non-nullable if you wish, and it will blow up at runtime. The linter will catch those cases, but it's definitely less than ideal.
That's true, but this much is inevitable. Kotlin can't help that the whole world isn't in Kotlin.
Bytecode analysis is pretty trivial. I'm not sure if modern tools do it, but figuring out whether that specific bytecode accepts null values or throws NPE with it is not hard (unless bytecode is available in runtime-only and compile-time dependencies contain only interfaces, but that model seems outdated).
They don't mean checking at runtime.

Looking at a method/property signature it should be explicitly and unambiguously clear whether or not null is an acceptable value - but in java there is no way to enforce this.

The issue isn't so much whether it is null, but whether it can be null. Because if it can be null, you have to check whether it is null before you can actually do anything with it. That adds a lot of null-checking boilerplate to your code.

If instead the compiler can guarantee that a value will never be null, then you don't have to check, leading to cleaner code. Scala does this with the Option type, Typescript by forcing you to declare a type as `string|null` if it can be null. These sort of things make your code safer and cleaner.

Just a note, Scala 3 has an optional compiler flag to remove null from the type system meaning String can’t contain an NPE.
The author misses one main advantage of Kotlin: more concise syntax
I would argue that more concise syntax is actually why Scala became a niche and didn't get wider adoption and why some some examples of advanced Kotlin are really off putting. At certain point more concise syntax goes against readability.

... so preoccupied with whether they could, they didn’t stop to think if they should.

Tangent: I find the choice of keywords val vs. var in Kotlin a bit unfortunate. The way we read text (or rather skim) is a lot of the times by the shapes of entire words. Val and var are a bit too similar in this regard and I think let vs. var in other languages is much nicer.
On the other hand, when skimming some code, does it matter on the first pass whether a given variable is mutable? Probably not if we're just trying to get an idea of how it works; later on a closer read we can look closer if it matters.
It absolutey matters when skimming a well written code. Whether a variable is immutable or not is almost as important as its type.
Well, it is part of its type.
I find this super-important. If a variable if mutable then I need to look closely through the whole of the rest of the scope to see if/where it is mutated. If it's not then I can often mostly forget about it. Thus marking variables as immutable where possible dramatically reduces the mental overhead of reading a function.
What I've found helpful is to configure IDE to use a separate formatting for mutable variables. You don't even need `var`/`val` for that, just smart enough IDE. Something like single underline for those variables. This way they're obvious and you'll know it when reading the code.
I mostly use JavaScript (which uses `const` and `let`) and Rust (which uses `let` and `let mut`), so I've find them quite easy to tell apart just based on the syntax :)
This is the reasoning behind the "const all the things" in C and similar languages, which doesn't have distinct keywords but where you can use "const" to make things immutable.

If a variable is const-declared, I react in exactly the same way and know that I don't have to look for further uses, which I find very valuable.

I even do stuff like this, which might be extreme but I find it coherent and nice:

    void * allocate_and_fill(size_t size, unsigned char x)
    {
      void * const p = malloc(size);
      if (p != NULL)
        memset(p, x, size);
      return p;
    }
The above const-declares the local pointer p, so that it can never be intentionally clobbered inside the function, which would leak the memory. Since it's the pointer itself that is constant, not the data being pointed at, there is no problem in modifying that memory, and the return value is not const-declared in any way.
Pretty sure val/var in Kotlin comes from Scala.

Syntax wise the languages are quite similar (before we get to type systems).

At first I really hated how similar the keywords were (something like let/const seemed preferable).

After a few years of Scala you get used to it.

In fact val somehow feels "nice" and var feels "dirty" so you avoid it as much as possible.

If I'm not mistaken, that's how Swift (which is syntactically most similar to Kotlin as far as I can tell) has it.

But "let" is already a thing in Kotlin (a scope function in the standard library).

In my experience, in practice "var" are an order of magnitude more rare than "val", so I'd be perfectly fine with "val" vs. "mutable". I wouldn't mind the verbosity - it would gently discourage the use of mutability, while making it very apparent wherever it's actually involved.

Yes, it's "let" in Swift and in some (semi)functional programming languages too.

> in practice "var" are an order of magnitude more rare than "val"

Or rather, it should be that way. Agreed, and I think because the world has been moving more and more towards functional style over the past decades, I'd predict that the next big language that will make it to the mainstream will likely take the approach "immutability by default".

On Android - my platform - 99% of var usage is just for the sake of dependency injection, which is an inevitable concession because basic building blocks of an app (like Activities, Fragments) are instantiated by the SDK, not your code, so neither your code nor the libraries it relies on have control over it and thus the DI has to happen "on the fly", by mutating fields as soon as it becomes possible. It leaves a bit of a bad taste still, but it's very much a fixed convention and therefore kind of... compartmentalized, I guess. And harmless, because I don't expect Dagger 2 to suddenly misbehave and forget to set a field.
I mean, in well-architected codebases of course. I've been lucky (or picky) enough to mostly work with such though.
Why not val/mut? Or const/mut, const/var.
const is already a keyword - it denotes compile-time constants (basically primitive types). val is constant in runtime, but it can still be computed on the fly; just assigned only once.

mut is a nice proposition, I'd certainly consider it if I were to design Kotlin

Interesting. Why not simply overload the meaning of const based on rvalue?
Like in C++, right? The compiler can decide whether it's a compile-time or run-time constant.
Kotlin's "const" corresponds directly to Java's "const", so my guess is that they don't want to burn bridges with Java where there's no substantial benefit.
I also find I make the `fun` and `func` mistake often when switching back and forth from Swift. Seems like a weird choice on Kotlin's part.
I really like this feature of Scala, but I do understand the reason why Java went with final var instead.
In java i miss property literals, without them you can't have linq to sql easily.

Does kotlin have them?

The current language team doesn’t even want to hear about properties and frankly I can understand their point. Properties while heavily used, are a misfeature.

I would rather see the ecosystem adapt records. Or once Valhalla happens, one can probably have a near zero-overhead Property<T> field for properties inside a record, properly supporting everything required by Java Beans.

Due to limitations of JVM itself, reified generics in Kotlin is limited to a very narrow usecase. Basically functions that get inlined.

So it's not "true" reified generics as in C#.

You can't go "if (T is Whatever)" and put it in your SomeClass<T>.

This is not Kotlin's fault, just legacy of Java with which it has to put up in order to maintain full interoperability.

For reference: it's type erasure.
Yes, exactly. T always becomes Object in runtime, type information is physically gone.
Not quite true, one pedantic point of mine is that it may be a concrete class if that was marked as T’s superclass, the other is less nitpicky, generic information is actually available at the component itself, so plenty of tools can use it in certain circumstances.
I can echo the Author's sentiment. I've been working with Java and lately Kotlin for the past 25 years. The last five of which almost exclusively with Kotlin.

Switching back to Java feels increasingly awkward at this point. I can do it, but there are no use-cases where I would prefer Java over Kotlin. I certainly don't enjoy it.

Sure, they've added features to the language and they help but it's just not even close to the language ergonomics that Kotlin has.

The val vs. var thing is super relevant because this is actually something that is more cultural than language related. As the author points out, you could just litter your Java code with final and it would be similarly safe code. But the reality is that very few people do that. Java makes you do the wrong/less safe thing by default. Kotlin has this opposite culture where you feel slightly guilty/dirty every time you even make something a var. Having a difference between mutable and immutable lists and maps is likewise something that Kotlin inverts. In Java you could always make lists and maps immutable but they'd be immutable by default. With Kotlin it's the reverse: everything is immutable unless you make it mutable.

It's this safe by default attitude that pervades Kotlin that just shows up in a lot of places. Nullability as part of the type system removes an entire category of bugs. The only time I still see NPEs, it usually involves some Java library that I'm using.

Considering how easy it is to switch from Java to Kotlin, it's not surprising that you see essentially all the major Java frameworks come with excellent Kotlin support. I use Spring Boot with Kotlin. It's quite obvious to me that using spring without Kotlin is a bad move at this point. There's so much good stuff you miss out on that is added to Spring via extension functions. E.g. having co-routines sit on top of webflux, makes the whole business of using asynchronous IO with Spring a lot less painful. Without that, I'd probably still be doing synchronous IO.

Kotlin also discourages inheritance. It's opt-in, not opt-out, because classes are final by default: you need to explicitly mark them as "open" if you want to extend them. The same with functions if you want to override.
What I miss in Java is actually more the nullability, as the author points out. I prefer the Swift way of doing that. To me, it feels cleaner than the Kotlin way, despite being extremely similar. I think it boils down to var/let vs var/val.
As a C# Developer who occasionally uses Java I understand the sentiment. Java just feels dated. You have to do a lot by hand which should be done by the language. And new features like streams are just a bad riff of .NET Linq, which is much better in ergonomics. Finally, Generics are a big turn off, who ever thought that the Java implementation is a good idea was delusional.
On the other hand Java's generics made it easier for Clojure to be developed.
Can you say more about this? My first thought is that you're talking about the various data types in Clojure being implemented with Java interfaces. But my understanding is (I'm not a Java developer) that interfaces existed pre generics, just without static type checking support, and thus Clojure could have been developed with a pre-generics Java version just fine?
Java streams have some advantages over C# Linq.

IMHO, there's a more fundamental concept that everybody seems to be missing, which is that general purpose programming languages somehow fail at general purpose queries across trival, in-memory data structures.

A random example I like to bring up is: Can your LINQ-equivalent efficiently and idiomatically calculate both the "min" and "max" of a list without enumerating it twice?

Imagine you're writing some abstract library that can take any IEnumerable as the input(s). Will it scan a 1TB database table twice because the idiomatic coding style is:

    var min = somelist.min();
    var max = somelist.max();
This is such as basic, trivial example, yet C#, Java, Kotlin, etc... languages just fall flat on their face. Meanwhile, in SQL, a 48-year-old query language:

    SELECT min(...), max(...) FROM sometable
But this is just scratching the surface. Ideally, all LINQ-style data inputs should be treated as something like a B-Tree index. Something that can be "walked" (not just enumerated!) efficiently using prefixes, ranges, and similar conditionals. Multiple queries should be able share such as "walk" to minimise the cost of streaming data in from memory... or wherever. Data should be partitioned across CPU cores just as efficiently, and the query language should make this natural.

I'm yet to see a language that scratches this itch.

Well, you can calculate min and max simultaneously with a reduce, but I do get your general point.
Can you honestly tell me any usage of reified generics outside of perhaps ArrayList<int> possibly having another more performant implementation?

The amount of time I’ve seen it complained about, yet in practice I am very seldom hit by that (just a note, Haskell also erases every type yet it doesn’t bother anyone there).

The post gives an example - overloading a method based on the type parameter (e.g separate overloads for Option<Integer> and Option<Double>). This is why Java's generic erasure is more of a problem than Haskell's type erasure: Java erases generic parameters at compile time, whereas Haskell retains type information for long enough to use it when linking modules.
The devil is in the details: the capitalize example will badly fail on an empty string right? (substring will throw a StringIndexOutOfBoundsException).

So we guarantee you you'll get a String back. And hear this: we also guarantee you, 100%, it's going to be non null. And we guarantee you that the reference to that non null String is going to be immutable.

Oh but wait: maybe you'll get a RuntimeException instead...

Can just return "" in that case? I think you are focusing on the wrong details of an _example_. The details here is being able to extend and function chaining; not the algorithm of capitalize().
Oh I didn't explain myself correctly. I've got nothing against the example in the blog post. What I mean is that no matter if Java or Kotlin makes guarantees about null safety, invariability and the type of the object returned, in the end you still have to deal with the glorified GOTO statements that exceptions are.
They are the same thing as returning a sum type, just your goto hides in an if statement in that case you explicitly have to write out one way or another.
I like Java but being able to tell at compile time that don't allow null on something no matter what, gives you a lot of advantage (and some disadvantage if you design wrong). Exceptions are not necessary in this case as null safety can not be isolated (unless you choose it). It is either everywhere or nowhere or you force an arbitrary line by telling something not-nullable when it can be (not a good idea).

A good example is the state Dart is in right now. Almost everything has been transformed to null-safety including most of the community packages and it works really nicely.

And it will happen in Haskell as well with `head “”`, even though it is a pure language. Your point?
"finally" in Java and afaik "val" in Kotlin because of it are a bit of a sham: They work fine for primitives, but for reference types only apply to the object identity. You can still mutate the pointed-to object willy-nilly.
It means non-reassignable, nothing more.
The way this is solved in modern code is that you very rarely have mutable objects. So basically you can expect all objects to be immutable (excluding obvious exceptions like builder objects).

Some frameworks like JPA are not up-to-date with this methodology, sadly. But Java direction with immutable records is obvious and they'll adapt.

Isn't that the case for most programming languages? The only exceptions I can think of are C++ and Rust.
It's not like this in the functional languages I'm familiar with, F# and Elm
It is not like this in Java either if the components are all immutable as well.

It will be shallowly immutable, but applying it recursively will make it totally immutable. The reason I say that is that in practice it is not that different than FP languages, they just differ in the ratio of FP data structures.

const in C++ is more like Readonly from TypeScript - you cannot modify state of an object, but it still might be possible through other reference/pointer - you have to check source of that value to be sure
i guess interior mutability just a design decision - it's not a _sham_, it works as intended. i'd like to know the reasons for this.
I’m not sure this is the reason, but OOP is supposed to handle encapsulation of state. For example lazy initialization of an object can be done by proper encapsulation and interior mutability, while the outside view can be completely immutable.
Because that is how final works in Java.
> Such functions can be called in the usual way as if they were methods of the original class. This mechanism is called an extension function.

well, that's not exactly true, because extension functions can't access private fields.

I like fast compile times, backwards compatibility, all my code base in one language, vast tool support …

If I wanted to mix in or use a new language, I would want something that was a substantial upgrade in terms of expressiveness and platform.

To me Kotlin is a bit like what Coffescript is to JavaScript.

Hmm, I been on a total of 2 kotlin projects, both went badly.

On the other hand, we have several hundred java projects, with a very low project failure rate.

Maybe java is missing some things that are trendy and you get used to using, the things we missed most in kotlin was working code that was easy to debug, repurpose and deploy.... Stupid issues like the compiler not working properly or just taking ages burnt to much developer time, and to hard to find staff to make it up.

But heh, maybe I'm old fashioned, it doesn't seem like a popular opinion. But nothing in this thought piece seems to address those fundamental issues.

Your comment seems to be the definition of word "anecdotal".
I agree with you that it is an anecdote, but I don't see anything wrong with sharing personal experiences of using a certain technology... this is a discussion forum after all and not a scientific paper.
Same exact experience. I recently tried my hand at rewriting one of my old Java Android projects in Kotlin. Maybe it's just my brain getting older, but I found Kotlin development experience to be far less enjoyable than I was anticipating based on all the high praise.

Don't get me wrong, Kotlin has some nice language improvements, but overall I found it to be more confusing than helpful. A weirdly confusing co-routine implementation, new unintuitive syntax, lots of new keywords, etc.

I dunno. I'm sure it's a fine language if that's what you're starting with, but as a former Java dev that was actually productive, Kotlin was anything but a breath of fresh air.

When used in conjunction with named parameters with default values, it makes it possible to write function signatures that are significantly more readable than those written in Java, and with far less boilerplate.
Never let this guy learn F#... it'll ruin all programming for him.
I disagree with point 1). The real problem is not that immutable references are inconvenient to use, but that you cannot mark the whole instance as read-only and get something similar to the const-correctness that you can achieve with C++. There are tools that help with this, but having something either in the language or in the JVM/standard library would be much more useful.

Point 3) and 4) are in my opinion not that big of an issue. Annoying, yes, but not deal-breaker.

Null safety is really something that I miss in Java and what I consider the biggest flaw in the language. Again, there are libraries and tools that help with that, but it is not the same as something which is part of the standard.