But Java is a mainstream language, and so we generally try to adopt features that a large majority of programmers are ready for, which means, pretty much by definition, that we adopts things later than most languages. Just to give you an example, we only recently added algebraic data types, but in some ways we still added them too early because far too many programmers don't yet know how to work with ADTs, but we felt we had to add them because we thought they're the best way to work with simple data in a language like Java, especially since we didn't want to add property or Kotlin-like data classes, as we think these are bad features. This emphasises another point, and that is that we're not "catching up". Most features don't prove their worth and will never find their way to Java, because we try to pick the minimal set of features that will give the most bang, and that reduces the growth of language complexity, which can truly harm adoption. In this way we also managed to avoid async/await.
Another example that people often ask about is named and default parameters. It looks like lots of languages have them, but the problem is that it's usually implemented in a way that harms those languages. For example, C#, Kotlin, and Swift all generally try to support separate compilation and binary compatibility, but their named & default parameters break that. We'll only add the feature to Java once we know how to do it right (something that I think no other language that cares about separate compilation has done yet), and we have some ideas.
And I disagree that FP features are awkward to use in Java (well, some pure-functional theorems don't work because of null, but they also don't work because of reflection, anyway, and they don't work in most non-pure-FP languages, either).
You're misattributing cause and effect. Some programmers aren't "ready" for keyword params, proper FP, etc. because Java decided not to include these things, even though languages that were much older, such as Smalltalk, had them. And of course, many other programmers have been "ready" for these things for ages (since the days of LISP).
> And I disagree that FP features are awkward to use in Java
Languages aren't meant to serve features; they're meant to serve users. Our goal is not to educate users about language features, nor to evangelise for features, but to give users the features that will hopefully result in the largest value from the software they write. Back in 1995 I guess Java's original designers decided that ultimately educating users about functional programming may harm Java's adoption (because most programmers prefer familiarity, and most programmers didn't know Lisp, ML, or Smalltalk) and so would offer less value than not trying to do that. More productivity was to be gained by offering a GC in a familiar language than offering it in a language that would have been slightly more productive by more threatening. You can't educate the market on too many things at once; that's just not a winning move from a product perspective.
A feature contributes such value ones there's some reasonable combination of productivity benefit and demand, and that combination differs over time; the same feature can have more value in, say, 2015 than in 2005. The difference can be because the kind of software people write is different, the hardware is different, or fashion is different (which affects demand).
> Two words: checked exceptions.
Checked exceptions come into play primarily when there's IO or other side effects involved, so we're already in an area that isn't entirely functional and there are special challenges with FP, anyway. Checked exceptions are rare in pure computation (and arise there primarily around parsing strings). I fully acknowledge there's a problem in the interaction of checked exceptions and functional combinators (and we're exploring solutions), but while it makes functional composition of IO cumbersome, I wouldn't go so far as to say that it makes FP awkward.
BTW, while it is certainly not as elegant as it could be, combining IO with FP is already much better in JDK 21:
<T> Result<T> doManyIOTasks(List<Callable<T>> ioTasks) throws MyException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var ts = ioTasks.stream().map(scope::fork).toList();
scope.join().throwIfFailed(this::handleException); // handle/rethrow exceptions
// all IO is done & no more checked exceptions here; we can now do functional processing
return ts.stream().map(Supplier::get). ...;
}
}
Java was pushed onto universities, so of course what people are familiar with is what Java decided to support. I don't think that it's any harder for people to learn about algebraic data types than it to learn about inheritance, generics, covariance and contravariance - it's just an accident of history that the industry still thinks that overcomplicated OOP hierachies, annotation driven development, the Java classloader etc. are "natural", but other approaches aren't.
You're acting as if Java is just reacting to what people are demanding - but I think that Java has been actively shaping how people learn programming for several decades now, and so should take responsibility for it.
> I fully acknowledge there's a problem in the interaction of checked exceptions and functional combinators (and we're exploring solutions)
Why does it take Java 10 years to fix something that other languages have done correctly from day 1? Swift has "rethrows", what's wrong with just adopting a similar approach?
> You're acting as if Java is just reacting to what people are demanding - but I think that Java has been actively shaping how people learn programming for several decades now, and so should take responsibility for it.
Obviously it's a combination of both, but education about programming paradigms is not the ultimate goal. All features are there to serve the goal of producing working software. Of course, we add new features to serve that goal and to use them requires education but there's only so much you can educate your market to change their habits. You have to do it slowly, as we're doing now with ADTs. Also, PL fans tend to overestimate the actual impact of language paradigms. For example, for years Haskell fans have said that their paradigm leads to significantly more correct programs, but research hasn't found much evidence to support that. For various theoretical and practical reasons (some of which were predicted long ago), programming languages now see diminishing returns, and productivity differences between languages in similar domains are not that big.
The other languages in Java's very small club of super-popular languages are JS and Python (and to a lesser degree C and C++). They're not very innovative language-wise, either, because you can't be unfamiliar and popular. The under-20-year-old languages that are doing very well are TypeScript (which does innovate in the language but in the field of how to add gradual typing to JS) and Go (which is less innovative than Java).
Trying out novel language approaches and seeing how much of an impact they actually make (which is growing ever smaller) is the job for less popular languages. We try to pick ideas that have proven themselves after years of practice.
> Why does it take Java 10 years to fix something that other languages have done correctly from day 1?
Sometimes it's because they haven't done it correctly enough and the problem is harder than it seems, and sometimes we just have more urgent things to do.
> Swift has "rethrows", what's wrong with just adopting a similar approach?
Maybe nothing; maybe the fact that it doesn't work with streams (where exceptions thrown by lambdas aren't thrown by the intermediate operators but by the terminal operation). But whether its something like this or something completely different, we have more urgent things to work on.
Another example that people often ask about is named and default parameters. It looks like lots of languages have them, but the problem is that it's usually implemented in a way that harms those languages. For example, C#, Kotlin, and Swift all generally try to support separate compilation and binary compatibility, but their named & default parameters break that. We'll only add the feature to Java once we know how to do it right (something that I think no other language that cares about separate compilation has done yet), and we have some ideas.
And I disagree that FP features are awkward to use in Java (well, some pure-functional theorems don't work because of null, but they also don't work because of reflection, anyway, and they don't work in most non-pure-FP languages, either).