Java record pattern matching in JDK 19
openjdk.java.net
openjdk.java.net
People often say that Java 8 was the big important update to the language (streams, lambdas, java.time, and more), but I think Java 9, which began this process was the true rebirth of the language.
And the movement from Java 8 to Java 11 was slowed by bytecode changes and the advent of modules, which declared which of their internals you could dick with. Sure, you could add --add-opens to your commands, but it felt (as intended), hacky as all hell.
For an example, I've submitted PRs to Apache Kafka as part of the effort to replace Powermock with Mockito. Why? Because a) Powermock doesn't play nice with with JUnit 5 and b) Because it breaks totally on Java 16+, due to Java modules being enforced by default as of that version.
But for companies that are happy to keep up with the faster release schedule, there's been access to new and useful features at every new Java version, and it means that early adopters can provide feedback on new features like record classes etc. before they're crystallised in an LTS version.
And given the history of Java improvements between Java 6 and Java 8, the current release cycle is positively blistering in comparison.
While modules were added in 9, as you mention, strong encapsulation was only turned on in JDK 16; until then, visibility was identical to 8, and no flags were neededl. Modules were not the reason for the difficulties in migration from 8 to 9+. By far, the main reason was libraries that depended on JDK, not subject to backward compatibility, that were changed. Such libraries were non-portable by design.
Strong encapsulation (in effect only since JDK 16) is intended to prevent that. You only need to --add-opens when you bypass the specification, and when you do, you know that your code is not portable. add-opens is hacky because it is needed to enable hackiness; a codebase that isn't hacky doesn't need it.
So now that strong encapsulation is in effect, modules will actually prevent those kinds of difficulties.
From my experience, a lot of the upgrade issues were around things fiddling with a bunch bytecode like Spring.
I'm really glad strong encapsulation is enabled by default now, it highlights dubious stuff- reusing my Powermock example, the lib is trying to mock methods on Object itself...
That's a feature, not a bug: https://news.ycombinator.com/item?id=31380032
When you say, "so long desired," the question is, by whom? Java (the language) isn't aimed at PL enthusiasts, but the majority of programmers, and believe it or not, most programmers are quite averse to new language features, so great care is needed in adopting them if a lanugage wants to be very popular, as Java aims to be.
I can tell you that even when there's a feature we strongly believe is "right", we are wary of adding it to Java until it has been "so long available in other languages." An example I personally worked on — and it's not even a language feature, but a simple API in a single class — is structured concurrency. We love the concept, but because it hasn't yet been "so long available in other languages", we've only proposed it as an "incubator module" to let it mature, and even then we're only adding it because we believe it is too essential to be left out.
It appears that you can be extremely popular or you can be an adventurous innovator/early adopter on the language-feature front, but you can't be both. The Java platform offers multiple language for those who like more adventurous and/or feature-rich languages — and we're happy to accommodate such languages — but they are definitely in the minority (my rough estimate is that only about 10% of programmers prefer such languages).
Kotlin shouldn’t have happened but they let Java lang fester for too long.
Related, Typescript is the only language I know of that through flow analysis does not require you to redefine the variable. E.g. you can do something like
if (typeof foo === 'string') {
//Typescript knows foo is a string here
}
All the other languages I know of, like this proposal for Java, require the variable to be redefined. I personally find the Typescript way, as VSCode will give you appropriate type warnings in the right places, easier to use in practice. Just curious if there are any other languages that do it like TS. if (obj instanceof String s) {
// s is a String here, but obj is not
} if (typeof obj === 'string') {
// obj is a string
}
is really not anything meaningfully different than this if (obj is string as obj) { // as in C#
// obj is a string (shadowing the outer scoped obj)
}
or this if (obj instanceof String obj) {
// obj is a String here
}
you can imagine a pseudo TypeScript language like this if (typeof obj === 'string' as obj) {
// obj is a string
}
Actual TypeScript merely allows you to elide the "as obj" and all it needs to trigger this is the "typeof obj === <string literal>" inside of an if expression. This can be done by a simple syntactic replacement, it doesn't require control flow analysis to get this specific feature.
But yes, if you have a more general computed expression, that would apply, but that was not the case you were stating.
Ie the variable isn't the issue (redefining in this case seems like a distinction without a difference)... However this does work in TS, which is a demonstration of CFA: const isStr = typeof obj === "string";
if (isStr) {
// obj is str
}
For what it's worth I think CFA is useful in TypeScript based on it at its core being a structural typed bandaid over JS, but I think these specific CFA type narrowing features are redundant in stronger typed languages. let foo : obj = failwith ""
match foo with
| :? SomeType as blah ->
// use blah : SomeType
(You're allowed to just use the identifier `foo` in the match arm.) match thing {
case a: String =>
// use a as string
case a @ MyCaseClass(b: String, c, d) =>
// use a as the instance of MyCaseClass or use b as a string
} public static void main(String[] args) {
Parent instance = new Child();
if (instance instanceof Child p) {
instance.print(); // prints Parent
p.print(); // prints Child
}
}
static class Parent {
static void print() {
System.out.println("Parent");
}
}
static class Child extends Parent {
static void print() {
System.out.println("Child");
}
}
Hence, with flow typing, existing code could break in subtle ways.> instance.print(); // prints Parent
why? the parent print should be shadowed/overidden by the child bruh
This is, in C#, all methods are called according to an object's apparent-type, not its actual-type, by default. To get the Java-behavior, a C#-method would need to be declared `virtual` (or `abstract`), and then more-derived methods would need to choose to `override` them (rather than hide them, often via `new`).
Part of the advantage might be performance. This is, methods that go with the apparent-type don't need to do a virtual-lookup-table resolution, which can save some work in method-calls.
Another advantage is that it can help provide more flexibility in class-hierarchies, since more-derived classes can "hide" less-derived classes' methods without overriding them. It's probably not something that folks really need to do too often, but it's nice to have an easy solution when such a case occurs.
For example, [this comment](https://news.ycombinator.com/item?id=31379783 ) provided code showing the issue in this branch of the thread using `static` in Java. Here's the same thing in C# without using `static` (except for `Program.Main()`):
public class Program
{
public static void Main()
{
A a = new B();
if (a is B b)
{
a.Print(); // Prints "A".
b.Print(); // Prints "B".
}
}
}
public class A { public void Print() { System.Console.WriteLine("A"); } }
public class B:A { public new void Print() { System.Console.WriteLine("B"); } }
`static` can work too because it implies non-virtual, but it's not necessary.Generally speaking, virtual-methods resolve with dependence on the object they're called on since they consult a [virtual-method table](https://en.wikipedia.org/wiki/Virtual_method_table ). Non-virtual methods can resolve without considering the object they're called on (whether static or not) because they call the method that belongs to the apparent-type.
That's what you started talking about, but that's not what I'm talking about.
> "calling static methods on instances of classes"
That's from an ancestor, somewhere upthread. Here it is as a c# expression.
"abc".Format("size: {0}", 7)
Sensibly, c# doesn't allow this, but some languages do. I don't recall which, but I've definitely seen this.Maybe I'm not understanding a word or something, but the description of the problem is definitely about "static".
For example:
public class A
{
// If a programmer writes this:
public void DoSomething(int x) { /* ... */ }
// ...then C# sees this:
public static void DoSomething(A this, int x)
{
if (this == null) { throw new NullArgumentException(); }
/* ... */
}
}
So if a programmer then writes `a.DoSomething(7);`, C# basically automatically converts that into `A.DoSomething(a, 7);` for them. It's basically syntactic-sugar.C# even lets programmers write methods that can be called on an instance OR as `static`: [extension methods](https://docs.microsoft.com/en-us/dotnet/csharp/programming-g... ).
Then there's a difference between C# and Java: in C#, non-static methods can be `virtual` if marked as such, whereas in Java, all non-static methods are automatically `virtual`.
---
Anyway, my point about the perspective from [this comment](https://news.ycombinator.com/item?id=31383483 ),
> If you’re writing code calling static methods on instances of classes, you deserve to have that code broken.
, was that that's basically what C# does by-default, ignoring syntactic-sugar.
Because, in C#, methods aren't `virtual` by-default, so when C#-programmers call a default method on an instance, they're calling it non-virtually, much like a Java-`static` method -- it may look a little different in C# due to the syntactic-sugar, but it's basically the same thing.
To demonstrate that same-ness, I took the Java code that used `static` from [this comment](https://news.ycombinator.com/item?id=31379783 ), then showed the same without `static` in C# in [this comment](https://news.ycombinator.com/item?id=31385859 ).
Of course, I don't mean that they look exactly the same, due to the syntactic-sugar. Just that they're conceptually the same in terms of logical-structuring and behaviorally the same in terms of what they actually do (e.g., how they printed the same responses in those examples).
fun foo(i: Int) = ...
fun foo(n: Number) = ...
fun main() {
val n: Number = 12;
foo(n); // calls foo(Number)
if (n is Int) {
foo(n); // calls foo(Int)
}
}
It's as if smart casting causes the static type of `n` to change in different parts of the main function. As such it also seems to affect overload resolution. Maybe this is exactly what you want. On the other hand it seems like it could lead to some very subtle errors. I'm not a Kotlin programmer, so Kotlin experts please feel free to correct me on details.> Related, Typescript is the only language I know of that through flow analysis does not require you to redefine the variable. E.g. you can do something like
This is called smart casting and is widely used in Kotlin
if isinstance(foo, str):
# code lfoo = nonlocal_foo
if isinstance(lfoo, ...):
Since the checker is correct in these cases, I accepted the ugly and got used to it pretty quicklyThis would be more an example of CFA.
const a = “string”;
if (typeof v === a) case eqT @a @String of
Just Refl -> ...A couple of interesting exceptions: explicit interfaces make it impractical in C#, and there’s a project called liquid Haskell that adds refinement typing on top of an already strong type system.
It isn't even block-based scoping in the flow analysis above - if in your block there was the code foo = 1; the typescript engine would then expect that foo will behave like a Number at runtime.
I haven't dived in deep enough, but I suspect foo could even have different structural type information within the same expression, e.g. a ternary (typeof foo === 'string') ? something(foo) : somethingelse(foo)
can you give a few examples? This makes no sense to me and I've touched quite a few ecosystems in my time. Maybe I am misunderstanding but my current understanding makes this seem not realistic
Object foo = ...
if (foo instanceof String) {
System.out.println(foo.toLowerCase())
}
The above is not valid Java, because foo has type Object, which does not have a toLowerCase() method, so the compiler refuses to compile it. What you need to do is something like: Object foo = ...
if (foo instanceof String) {
String sfoo = (String) foo
System.out.println(sfoo.toLowerCase())
}
Granted, you don't necessarily need to explicitly redefine in, as Java allows anonymous casting: Object foo = ...
if (foo instanceof String) {
System.out.println(((String) foo).toLowerCase())
}
Regardless, the point still stands. Java (and most strongly typed languages with an inheritance based type system) requires an explicit cast, even if a straight forward static analysis would show that the object in question is already the desired type. Object foo = ...
if (foo instanceof String s) {
System.out.println(s.toLowerCase())
}
In this case, the scope of the pattern variable `s` is just the then block, so are free to reuse it elsewhere in the block.This might be a casual dismissal of what is in fact a nuanced aspect of programming languages.
Multiple ways of doing the same thing can make the language less accessible because it becomes confusing to reason about what the "right" way is.
That said, this particular addition merely reduces repetitive toil, and it follows similar existing Java conventions, for example see the Java-7 equivalent of Python "with-statements".
Bjarne Stroustrup once noted in Design & Evolution of C++ that people want different things from the seemingly simple artifacts we call "Programming Languages". Some people want an algorithmic language to express procedures cleanly in, others want a design language to express large scale systems in. Some want a terse and uniform "executable mathematics" notation, others want a down-to-earth worse-is-better lets-make-some-goddamn-apps-and-put-some-fucking-bread-on-the-table working class language. Some want a language with an emphasis on the lone author, others an emphasis on the corporate business team. A language as a tool that you wield skillfully for the single purpose it was built for, a language as a toolbox full of gadgets, a language as a material to build the previous two from, a language as a community hub to organize around, etc etc etc. I have seen people argue that "Language" the wrong metaphor to understand computer notations entirely.
This is why you can't say anything general about programming languages.
You're welcome to switch your language of choice to go if that's really your bag.
Java has none.
If anything, this is _not_ something JS devs are asking for, but rather devs using functional languages.
And if you're strongly typing your system rather than relying on strings, extra so.
This feature aims at the conciseness of first model (instanceof) with the safety of the second approach (i.e. all types are handled).
I would love this feature for my project! ( https://www.adama-platform.com ) since I use Java to implement a programming language.
This is... just wrong.
Plenty of people are using JS without any sort of typing whatsoever. Other dynamic languages, like Clojure, Gradle, Elixir, Pharao (a smalltalk), just to mention a few, are still going strong.
> they have this webassembly, that runs in a schizophrenia-type VM wannabe
wasm is mostly being used as a target for C(++), Rust and even C# programs that want to run in the browser. It's there to expand the reach of the browser. Most JS projects don't touch the stuff.
because... JS isn't good enough?
Thinking you have an I-told-you-so moment with static typing and dynamic languages only shows you don't understand the details and tradeoffs in the design space. For example TypeScript's approach to static typing is very different to Java's and these differences reflect consideration of the problems being solved, JavaScript's specific use cases, its history and the path dependence of its design. Because these details are invisible to you the whole thing is simplified in your mind to "everyone is switching to static typing just like I said they should!" In fact this type of prognostication was never useful or meaningful. The hard part wasn't deciding to build static typing onto JavaScript. The hard part was determining specifically what that should look like, how it would fit nicely with the language and then doing the work of building it and driving adoption.
TypeScript is successful not because it is static typing but because it is a form of static typing that's exceptionally well suited to JavaScript and its use cases. If people like you had their way it would have been a failed attempt to bolt a Java style type system onto JavaScript, worsening the design debt in a language and ecosystem that is already loaded with it. This already happened on a smaller scale with classes in ES6 which were driven by Java devs writing JavaScript and wanting something familiar instead of trying to understand the language.
You might be right that adding pattern matching to Java isn't a great idea but at the moment your disagreement is based in ignorance. You've been given ample explanations of pattern matching in this thread. Put down the ego and take some time to learn on your own.
It's ironic because you're throwing shade at Javascript when JS developers are doing the same thing against Typescript. Fearing what you don't know or understand under the guise that change is bad or things are "good enough".
I think messy fits.
Showing your opinion doesn't make it immune from critique.
The pattern shows up a lot with result and option types but you can do it any other time where a tagged union makes sense. The classic way to do a tagged union in Java was just a personal taste amongst bad options: tagging interface and instanceof on implementations or an encapsulating class that will return null (or throw an exception) if the gets are called for the types which aren't encapsulated.
If in your career you never needed to enumerate cases that required different data in each case and you couldn't see how binding those requirements into an invariant via the type system was helpful then you probably won't use these new features either. Some people thought it was helpful but the boilerplate cost was too high so wouldn't do it in practice (I am in this category).
Edit: you could also make tagged unions more type safe with callbacks, but then implementing a closure to pull the value out of the callback was just annoying.
It's fair that you've found sensible ways to achieve your goals without ever needing pattern matching. Nobody should fault you for that. But can you grant that other developers might have other sensible ways to achieve other goals (or sometimes other ways to achieve the same goals!)?
I think most Java developers will be familiar with the visitor pattern. In almost all cases (there are exceptions!), I detest it; I find sum types with pattern matching to be a far superior way to say what I mean. Java added `sealed` interfaces recently, so you can actually model a closed family of types. Now pattern matching closes the loop, giving you a supremely ergonomic way of dispatching over that closed family without using a visitor.
Maybe it's not clear from other responses, but the part of the JEP about an "exhaustive switch" is critical here. Java statically guarantees that you've handled each member of your closed family, just like if you forgot to implement a method for a visitor.
In most standard IT applications it may be less common. I've seen it used fairly well in scientific applications. There are of course other ways of approaching working with and acting on wide taxonomies of objects that could exist but this is one of them.
In applications yes, but instanceof is heavily used if you write frameworks.
And as someone else has mentioned, the more functional style java you write the more use you start to make of instanceof
Now that functional stuff and instanceof fluff is something else. I don't like it either.
It much depends on the task of course. My biggest project in Java was a SAT solver years ago, and millions of little clauses did not sit well with it at all. I ended up changing the data organization internally. My first attempt, the natural AOS would have been also the best choice performance-wise in C, but in Java it choked the GC. I had to switch it around to SOA which was quite awkward but it reduced the number of objects from millions to a small constant.
Also thinking of the number of lines of code running for JIT, GC, whatever... in the background all the time...
In projects where you can’t get away from object allocation (due to it being less deterministic for example) with some clever trick like arenas and the like, chances are the GC will be actually much faster than malloc-ing them all — and while it may not sit well with you, GC running in the background just amortizes the cost of each deallocation to basically zero time overhead (of course at the cost of memory overhead).
But I’m not trying to dismiss your experience or anything, the JVM has its place and of course there are plenty of use cases where it is not the best fit at all.
And that statement was not about the allocation time. But my understanding is that deallocation (GC) is not amortizable to constant for long-lived objects.
We can probably agree that having to code a separate class (with object overhead) to represent each little tuple of 2 integers or so, is bulky - programming-wise but also concerning the memory overhead (and possibly GC overhead).
Programming in C does not imply using malloc() to allocate individual objects. This is a bad practice.
In terms of the classic 'patterns', it's the difference between standard dispatch and the 'Visitor' pattern. There are cases where you want to specify each case of data structure locally, and others where you want to specify each method of each variant locally. Welcome to the expression problem!
There are cases in Java where you do want a true POJO - just a bag of structured data. This is probably a very strong candidate for using along side that.
For some examples relating to business logic, see https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/...
Lots of compilers are written in functional languages and use pattern matching for traversing abstract syntax trees, dispatching on the node type. The OO way to do this is to put all logic relating to a particular class in the class itself. The functional approach is to put all logic for a given method/function in one place and handle all the different types. They are two different philosophies both with different tradeoffs, and which is best depends on the type of program you're writing. But it's nice for a language to support both, so you can pick and choose which approach is best for a given problem.
I started programming in Java in 2002. I had some of your stated attitude learning Scala ten years ago. Probably because I grew up on Turbo Pascal and C++ , majored in EE, and didn't hear about PL theory. I cannot fathom anyone wishing to go back to Java after using Scala (as a better Java, without cats/scalaz&Co) or at least Kotlin. The way you describe new Java features I'd think you come from a golang background.
I have nothing against staying on the JVM for the rest of my career. For this to be feasible we need modern languages fully supported and popular. I personally want more concise syntax and more elegant abstractions in my daily life.
In Scala, constructing a `case class` allows positional/named arguments, which prevents some bugs. But destructring has to be positional. Where is the mathematical beauty of duality?
Also if I only want a few fields from a wide record, I have to add the underscores for all other fields. `case SomeClass(_, _, _, field1, field2) =>` And when I add another field, all those `case`s have to be changed.
OCaml gets it right. Pattern matching records uses named fields. It has field punning, you only have to write the name once if new variable's name is the field name (like JS). Extra fields are discarded with one single `; _` in the pattern.
I don't really care what Java does. I just hope Kotlin won't mess it up.
D. J. Pearce, “Sound and Complete Flow Typing with Unions, Intersections and Negations,” in Verification, Model Checking, and Abstract Interpretation, vol. 7737, R. Giacobazzi, J. Berdine, and I. Mastroeni, Eds. Berlin, Heidelberg: Springer Berlin Heidelberg, 2013, pp. 335–354. doi: 10.1007/978-3-642-35873-9_21.
https://ecs.wgtn.ac.nz/foswiki/pub/Main/TechnicalReportSerie...
Java should have focused more on improving libraries, build infra (Java 9 was good) and changes in syntax focused on the traditional paradigm. If people needed pattern matching. Thy could mix languages. This is huge.
I find myself more and more distant from java these days because of this.
I suspect though that internally theres still a lot of plumbing that needs to be changed on top of what they've already changed. It was the same way for reflection, lambdas, threads. They really do a great job on iterating and keeping backwards compatability too.
static void printUpperLeftColoredPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
System.out.println(ul.c());
}
}
But lr is not used in this example. Where is the syntax for ignoring a component? Can we drop lr, or use a placeholder like '_' multiple times?But as always, the language team does an excellent job on improvements which is exceedingly hard while you have to balance backward compatibility.
Just checked some other languages:
- SRFI (Scheme Request for Implementation): https://srfi.schemers.org/srfi-200/srfi-200.html
Syntax:
> | ($ record-name pat_1 ... pat_n) a record
- GNU Guile: https://www.gnu.org/software/guile/manual/html_node/Pattern-...
Seems to be implementing the SRFI:
> | ($ record-name pat_1 ... pat_n) a record
- Rust: https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html and https://doc.rust-lang.org/reference/patterns.html#struct-pat...
> We can also use patterns to destructure structs, enums, and tuples to use different parts of these values. Let’s walk through each value.
> Struct patterns match struct values that match all criteria defined by its subpatterns. They are also used to destructure a struct.
Java has been steadily moving in this direction for quite a few years now. It's tricky business. There's the language proper, and there's the JVM, which are interdependent. Both are aiming to remain backwards compatible. Introducing new major language features is not an easy feat.
It's exhausting trying to stay up-to-date with the language, in addition to changes in the frameworks and tools on top of the challenges of my day job.
I might be showing my age, but I've come to appreciate that features come after long and careful consideration, and not every single release.
Back in 1997, James Gosling published an article that serves as a blueprint to Java's evolution to this day: https://www.win.tue.nl/~evink/education/avp/pdf/feel-of-java... He explained it further, as related by Brian Goetz in the first 20 minutes of this talk: https://www.youtube.com/watch?v=Dq2WQuWVrgQ
In short, Java attempts to be "a wolf in sheep's clothing," with a state-of-the-art, innovative runtime, wrapped in a language that's intentionally conservative. Java the language only adopts features that have already proven themselves as worthwhile elsewhere, and when mainstream programmers are ready for them. We don't always live up to that standard, but we try.
So another way of putting what you said is that this is another feature that has proven itself enough, for long enough, and that's matured enough for Java to adopt it. Many if not most programming language features don't make it to that stage.
The flip side is to examine which features have never made it to Java and probably never will (at least, they're not on our long-term roadmap). Those include, among others, macros, extension methods, first-class properties, and async/await.
I can tell you that if Java's language team encounters some language feature that solves a particular problem, they'd rather spend spend several years thinking how to avoid adding that feature while still addressing the problem, than the six months needed to implement it. A good strategy for doing that is to wait, and try to think of ways to solve multiple problems with one feature (even if imperfectly) rather than solve multiple problems with multiple features.
For Kotlin, extensions methods are essential otherwise you can not extend existing Java types with Kotlin types.
For C#, you need extension methods for LINQ because modifying the .Net runtime was not an option at that time.
For Java, changing the VM is not an issue, instead of extension methods you add default methods which can be overridden by implementations.
Same feature, different tradeoffs.
Why would those be considered in the first place especially after Loom lands?
If I came across this new thing organically, I could go "ah, neat, we have that now" and not necessarily need to go look up the JEP (meanwhile Python's PEP 636 had me going wtf) -- though over time I'd expect to come across it in a negative context where the original programmer made a scope mistake because they didn't read the JEPs, or because they tried to make a change without their IDE's assistance where through various means it could have pointed out shadowing or scope concerns, that I now have to fix.
It's in no minor part thanks to Java's verbosity that the overall Java ecosystem is so verbose. Having mostly non-confusing syntax whose meaning you can mostly guess at on first exposure is nice, yes, but isn't so helpful for the more important aspect of having non-confusing programs. For that you really want a more concise and tasteful language over a verbose one. Anyway, I've found that for anything non-trivial about the program itself, I'm going to need an IDE because the meat of the thing is going to be verbosely spread out in many places (sometimes for good reasons, at least in the Java world). Sure, after I acquire the mountain of (program) context, I can review small changes even on paper printouts, but that's true of most anything.
Verbosity is not a good property for a language to have in general -- Java itself admits this, otherwise we wouldn't have so many shortcuts in syntax like omitting this, or java 7's diamond operator or try-with-resources or catching multiple exceptions or for-each syntax, or java 8's lambdas and special syntax for simple lambdas, or java 9's var, or... And of course, almost always in some other more concise language a concept is much clearer. There's a reason pseudo-code isn't written to look like Java.
The way `Optional` is implemented and the roundabout way to just grab the first item in a list (`list.stream().findFirst()` or `var e = null; if (!list.isEmpty()) e = list.get(0);`?) where other languages have added helpers years ago. Or, even worse, the lack of tuples, leading to your average medium-sized Java project containing five different implementations of Tuple/Pair. There are also runtime restrictions that sometimes crop up because Oracle can't break compatibility (type erasure, for example).
If code clarity was the only metric for language quality then we'd all be writing some BASIC derivative. I don't think more modern languages like Swift and Kotlin are all that applicable to all areas where Java shines, but ever since dotnet went open source and cross platform I'm really starting to wonder why anyone still bothers with Java.