Java 21: What’s New?
loicmathieu.fr
loicmathieu.fr
// Java 21
if (obj instanceof Point(int x, int y)) {
System.out.println(x+y);
}
// Older Java
if (obj instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x+y);
}I hope someone will design some jvm SimpleLang eventually, which will have very few syntactic constructions but cover all/most of use cases.
(let [p obj]
(if (instance? Point p)
(let [x (.x p)
y (.y p)]
(println (+ x y))))As for the syntax, there is very little, which can make it a harder lift but once you have the hang of it you won't deal with the issues identified in the parent comment.
that's because of all legacy dynamicaly typed codebases
(if (? p::Point obj)
(... p:x p:y))
This succeeds if obj matches the pattern p::Point, declaring p to be the value of obj coerced to a Point.
This is integrated with Kawa's static type-checking/inference.but if it is a java record class, then there's currently no standard library way, since it's not a bean (getter/setter naming of properties). You'd have to write custom code for it, which sucks.
Using quasiquote-style pattern:
(if-match ^#S(point x ,x y ,y) obj
(prinl (+ x y))
Or using @(struct ...) pattern operator: (if-match @(struct point x @x y @y) obj
(prinl (+ x y))
All done with macros. Expansion, compilation, disassembly: 4> (flow '(if-match @(struct point x @x y @y) (new (point 1 2))
(prinl (+ x y)))
expand
prinl
compile-toplevel
disassemble)
(let ((#:g0062 (struct-from-args 'point 1 2)))
(let* (#:result-0065
x y)
(if (if (subtypep (typeof #:g0062)
'point)
(let ((#:x0063 (slot #:g0062 'x))
(#:y0064 (slot #:g0062 'y)))
(sys:setq x #:x0063)
(sys:setq y #:y0064)
(sys:setq #:result-0065
(prinl (+ x y)))
t))
#:result-0065
())))
data:
0: point
1: 1
2: 2
3: x
4: y
syms:
0: struct-from-args
1: subtypep
2: typeof
3: slot
4: prinl
5: sys:b+
code:
0: 2003000E gcall t14 0 d0 d1 d2
1: 04000000
2: 04020401
3: 20010005 gcall t5 2 t14
4: 000E0002
5: 20020002 gcall t2 1 t5 d0
6: 00050001
7: 00000400
8: 38000016 if t2 22
9: 00000002
10: 2002000A gcall t10 3 t14 d3
11: 000E0003
12: 00000403
13: 20020009 gcall t9 3 t14 d4
14: 000E0003
15: 00000404
16: 20020006 gcall t6 5 t10 t9
17: 000A0005
18: 00000009
19: 2001000D gcall t13 4 t6
20: 00060004
21: 1000000D end t13
22: 10000000 end nil
instruction count:
10
#<sys:vm-desc: 9966330>
I see this is doing something silly: (subtypep (typeof x) y) is just (typep x y), but costs an extra gcall instruction.Either the pattern matcher should do this, or the compiler should have that as an algebraic reduction, or both.
Let's do the compiler for fun:
$ git diff stdlib
diff --git a/stdlib/compiler.tl b/stdlib/compiler.tl
index 58685741..a837571e 100644
--- a/stdlib/compiler.tl
+++ b/stdlib/compiler.tl
@@ -1411,6 +1411,8 @@ (defmeth compiler comp-fun-form (me oreg env form)
(set form (rlcp ^(,bin ,a ,b) form)))
((- @a)
(set form (rlcp ^(neg ,a) form)))
+ ((subtypep (typeof @a) @b)
+ (set form (rlcp ^(typep ,a ,b) form)))
((@(or ignore nilf) . @args)
(if (eql sym 'ignore)
(each ((a args))
Note that we are using pattern matching here to recognize and rewrite this case. In return we will obtain better code from pattern matching: $ make
TXR stdlib/compiler.tl -> stdlib/compiler.tlo
$ ./txr
This is the TXR Lisp interactive listener of TXR 291.
Quit with :quit or Ctrl-D on an empty line. Ctrl-X ? for cheatsheet.
TXR is not a toy, but should be kept within easy reach of children.
1> (flow '(if-match @(struct point x @x y @y) (new (point 1 2))
(prinl (+ x y)))
expand
prinl
compile-toplevel
disassemble)
(let ((#:g0024 (struct-from-args 'point 1 2)))
(let* (#:result-0028
x y)
(if (if (subtypep (typeof #:g0024) ;; this hasn't changed, of course
'point)
(let ((#:x0026 (slot #:g0024 'x))
(#:y0027 (slot #:g0024 'y)))
(sys:setq x #:x0026)
(sys:setq y #:y0027)
(sys:setq #:result-0028
(prinl (+ x y)))
t))
#:result-0028
())))
** expr-1:1: warning: if-match: no such struct type: point
** expr-1:1: warning: new: point does not name a struct type
data:
0: point
1: 1
2: 2
3: x
4: y
syms:
0: struct-from-args
1: typep ;;; no mention of subtypep here any more!
2: slot
3: prinl
4: sys:b+
code:
0: 2003000E gcall t14 0 d0 d1 d2
1: 04000000
2: 04020401
3: 20020002 gcall t2 1 t14 d0
4: 000E0001
5: 00000400
6: 38000014 if t2 20
7: 00000002
8: 2002000A gcall t10 2 t14 d3
9: 000E0002
10: 00000403
11: 20020009 gcall t9 2 t14 d4
12: 000E0002
13: 00000404
14: 20020006 gcall t6 4 t10 t9
15: 000A0004
16: 00000009
17: 2001000D gcall t13 3 t6
18: 00060003
19: 1000000D end t13
20: 10000000 end nil
instruction count:
9 ;; down by one
#<sys:vm-desc: 92c94d0>
Ship it!Also without the :: operator, it’s uglier than the arrow equivalent and it’s yet-another-construct to parse.
As for there now being multiple ways to write the same thing, this seems like something that could be easily linted into consistency. So you would really only have to learn the less verbose way for any newer Java projects.
yes, new SimpleLang can have lessons learned during last 20 years implemented, as in example from another commenter: https://news.ycombinator.com/item?id=37068380
Currently, for a given task, Java usually have 5 legacy deprecated ways to solve, and two more in newish Java versions, and dev needs to keep all of them in memory to be able to read and understand other's code.
> As for there now being multiple ways to write the same thing, this seems like something that could be easily linted into consistency.
that's if you don't work with any legacy systems and 3p libraries and never switch projects/jobs
Kotlin sure moves faster than Java, but except for features that were experimental or unstable to begin with (coroutines, -X flags), I can't think of a case where more than 1 deprecated version exists (and the "deprecated" versions are not nearly as crufty as Java).
Sure there are a few warts, e.g. arrays work a bit differently than Lists, and primitive-object discrepancy, but every language will pick up similar issues over time.
But memory management/safety, weaker IDE support and ecosystem make it less productive than Java for business dev.
The distinction between fields and getters is intentionally blurred because these are immutable value classes.
Storing data is also where I thought records would be a good use case. But in the real world data is not Point(x,y). If you have more than 3 or 4 fields in your record, that's simply not going to work. I thought the rest of the industry decided over a decade ago that using positional properties (e.g. new MyRecord(a,b,c,d,e,f,g)) to initialize an object was a bad idea. I think Rich Hickey gave a whole talk on that subject. If there's a future plan to be able to create records using named props, e.g. new MyRecord([firstName: "Bob"]), then I would reconsider my opinion.
IMO it's a significant syntactical improvement, as it considerably reduces boilerplate assignments.
the problem is that Java not modern language, but carrying all legacy features from last 20 years, so having all legacy features + new modern creative ideas make Java very complex.
Overwhelmingly, I see complaints that java has been too simple compared to virtually all other languages. I agree that cognitive load is important. However, it feels really strange to me to suggest that java has reached the complexity of other similar languages. What am I missing?
- using DataInputStream
- using Scanner
- using Reader
- using Files.readLines
and they all suck.
I think they all connected, for example with added lambdas they also added streams, so collections library now allows to do the same thing using old iterators or new streams APIs, and somehow those APIs are not compatible, it is not easy to produce stream from iterator.
which I never seen anyone uses and it makes everything more complicated
they probably should've have some ParallelStream interface
core ecosystem, e.g. apache commons will add several more ways.
Java’s standard library is one of the best stdlibs out there.
programmers in 95% cases want something like following to read lines from text file:
for s in open('file.txt'): print(s)
Java has 5 different io APIs and no one provides such construct, you need to stack various verbose abstractions to just read text file.
How it is the best? Ergonomics and brain load is very bad.
string[] lines = ReadFile("path", "filename");
Why is the above somehow hard?- it will produce OOM if file is very large
- iterating array is usually verbose in many of languages (at least in Java)
- not sure why you would want to have path and filename as separate params
In Java, it would be more ergonomical if they add something like following:
Iterable<String> Files.readAllLines(String fileName);
Also, I really don’t understand your last example - that’s literally available in java returning a List of lines. If you add a .stream().collect(joining(“\n”)), you get the whole string in one go.
I think in python it will lazely iterate through the file, and not load everything at once
> Java’s List is much more ergonomic that arrays.
right, but commenter above proposed array..
> that’s literally available in java returning a List of lines.
current API has two issues:
- OOM if file is large
- it takes Path instance which you need to construct and not "String file" parameter, thus hit on ergonomics
- it will produce OOM if file is very large
A well designed API won't give you an OOM it'll throw a 'this file is too big' - not sure why you would want to have path and filename as separate params
Throws path not found. Throws file not found. Why does the programmer need to deal with twee-fiddly path magic for say each file in a directory of config files? Iterable<String> Files.readAllLines(String fileName);
Oh sure that's totally more readable. I think in python it will lazely iterate through the file,
I wrote a program to suck in a list of log files and generate a bunch of metrics and display them. Users liked to select a dozen 100MB log files. Lazy iteration was way way too slow. Fastest was suck each one into memory and call split.this is possible solution, still it doesn't allow to iterate lines one by one and handle large files
> Throws path not found. Throws file not found.
I think it is some very niche use cases when you need to distinguish between these cases. From another hand there are many places in the code which operate absolute file paths already, and you force them to split path, which is cumbersome. At least two alternative APIs probably are justified to have.
> Fastest was suck each one into memory and call split.
I really don't think pre-splitting is any kind of performance bottleneck, it is likely 1% of performance compared to all IO stack and subsequent string/objects memory management.
I mean, how are you going to use it? Something like this?
Point p = getPoint();
if (p instanceof Point(int x, int y)) {
System.out.println(x+y);
}
... that seems pretty bad. Or am I missing something?This type of pattern matching is mostly useful when you have some T and a few cases, there is also a switch/case equivalent for when you've got a lot of them.
Maybe we'll see some syntax for deconstructing values directly into variables, a la Python in the future.
You can then use pattern matching to switch on instances of those subclasses while destructuring objects to access attributes at the same time. Kind of like enums and pattern matching in Rust.
In your example there is no reason to use `instanceof` with `p`, you already know what it is.
Sure, but in Java the standard handling of that is polymorphism. instanceof has been so far a bit of an antipattern, breaking out of the OOP model.
> In your example there is no reason to use `instanceof` with `p`, you already know what it is.
That's my point. This seems like a very limited version of destructuring if you can use it only when you don't know the type. Like in JavaScript you'd routinely write something like:
const {x, y} = getPoint();A more practical (though not necessarily realistic) example might be:
sealed interface List<T> permits Cons<T>, None<T> {
record Cons<T>(T head, List<T> tail) {}
record None<T>() {}
}
// some function only wanting the first elem of a list
switch (List<T> list) {
case Cons<>(T head, var tail) -> // do something
default -> {}
}
If you have used Haskell or similarly strongly typed FP language, you can see how great pattern matching can be.I think that's deliberate, Java is moving towards the modern programming zeitgeist that doesn't really care about strict adherence to the OOP model. This is a feature that comes from languages that don't adhere to Java's OOP principles.
I didn't downvote you, by the way. Don't know why that happened.
This, along with the underscore, and even the sequence type, shows that the language designers are well aware of what Scala does, and are picking and choosing which things to bring in without risking exposing Java devs to the traditional monad salads that you see in many a Scala codebase.
But yeah, of course the design team knows about features from Scala, they are probably one of the most competent people on the Earth when it comes to PLs, and are literal CS giants.
> There really aren't that many to learn.
I think there are a lot to learn in java ecosystem.
There are many of us who think Java has very little to learn - asking us to go slower - while probably good for society in general, is a losing strategy in a Capitalist economy.
You can keep up, or be left behind. Meanwhile, fortunately the “communicate effectively” is 100x more valuable as “produce software more reliably and faster” ie learning all of these new languages and language improvements.
Just look at C# or C++, both have at least an order of magnitude larger language “surface area”.
I think the closest thing is if someone would create linter for java which would allow only essential structures and library API. Java has very good foundational core.
In that case, there's absolutely no ambiguity: there's only one `x` you can be talking about.
By the way, value/case classes are such a boon. Very easy to get used to, and once you have, you want to use them everywhere, I guarantee.
----
if (obj instanceof Point) {
Point p = (Point) obj;
int x = p.x();
int y = p.y();
System.out.println(x + y);
}obj match { case Point(x,y)=> println(x+y) }
Which probably explains the person saying "I hate it".
Destructuring for other kind of objects are planned with user-defined destructors. But it does have plenty prior art, e.g. in haskell you can deconstruct lists arbitrarily like _:secondElem:rest would bind secondElem logically.
if (obj instanceof Point p) {
System.out.println(p.x()+p.y());
}
But I can maybe see the value if you need to use x/y more than once.However in Java it's common to have getX() style accessors, for which this syntax doesn't seem that helpful as your variable would end up being called getX
That sounds like a deque, and it's been in Java for a long time. (No set or map implementations though.)
https://docs.oracle.com/en/java/javase/18/docs/api/java.base...
I'm not sure what's on the Kotlin roadmap / whether JetBrains see value in making major improvements to the language.
Also, all hail `val`; `final var` is objectively worse
In Kotlin one can get finality via the much nicer "val" keyword, and seems to be idiomatically the default, versus Java's mutable-by-default stance
Saying either language "won't catch type errors" is not something I can agree with
final var addressMap = getAddresses();
final Tuple args = Tuple.of("a", 200L, x);
So you may as well inline the method call and give up on any type checking.Another example:
final long limit = getLimit();
for (var i = 0; i < limit; i++) {}
The wrapping behavior is determined by whether there is an L (or lowercase, l) after the 0. If limit happens to be greater than Integer.MAX_VALUE, that L is the difference between an infinite loop and a well-behaved routine. Some junior programmer could easily remove the L accidentally. And you wouldn't run into the bug until limit gets big enough. Which could happen at runtime in production. All because you wanted to save one character by using var instead of long.This is just what I could think of off the top of my head. I'm sure there are other examples. You are absolutely giving up some compile-time sanity checks with var. If it's just some throwaway script, then fine. But if so why use Java in the first place?
But that’s a completely useless code. Here is a real example:
var list = getSomeFancyLongAssNestedGenericTypedList();
list.append(new HashMap<…>());
If I change the return type of that function to something that doesn’t have a compatible append method, your method will fail. In fact, you implicitly encode all its usage in that var, so it will be maximally safe, while not being more restrictive than needed (e.g. changing to another object that has append may be fine).Also, var is not a must-use, there are useful cases where it gives more clear code and the type is not important (say, when the right side is a constructor call), and there are cases where you really should be annotating your types. But it will be type safe either way.
Sure, you might append to the list. But in the world of increasing immutability, more likely the return value of getAddresses would be immutable. You don't want to rely on "well hopefully there's a method call that catches the error before it goes into the args list."
What did I gravely misunderstand? And if I gravely misunderstood it, then why can't you point out what's wrong with both of my examples instead of running as fast as possible to your own toy example? I'm actually laughing at your totally shameless pivot.
>maximally safe
You don't know the definition of maximum. Maximum means there exist no levels which exceed it. You have amnesia about the type checks that I just demonstrated which do not occur with var that would have prevented the runtime problems I just explained.
If your argument is that, meh, var is "good enough" type checking, then make that argument. Don't pretend like you can't read basic Java code.
I gave a more realistic example where you actually make use of your objects
Secondly, it's not unrealistic. It's not even realistic. It's real. R-E-A-L real. It's in production systems right now, as I said. Modern startups. If you think you can design it better, by all means. "Put up or shut up" is the phrase, I think.
Thirdly, any time you have an API that accepts a supertype of the instance in question, this will happen. Run away and hide from the truth. Or man up and admit you oversold your incorrect theory of var.
Fourthly, you didn't even address the readability issue. Which of course you wouldn't because... var. Like literally VAR. lol.
Fifthly, you still haven't come up with an answer as to what I misunderstood, specifically. Sad!
The only category I would say is objectively "better" is Java (and other JVM languages like Kotlin) has the best IDEs out of _any_ language. There is nothing on-par for Python, Ruby, C/Cpp, etc as far as autocompletion, type inference, automated refactoring, partial recompiles, hot code swapping, unit test integration, Tomcat or server integration, code formatting, etc. Java is great because of it's IDEs. If we were writing Java in notepad.exe it'd be a pretty clunky experience, but nobody serious does that.
Kotlin was made by the people behind IntelliJ. I'm not sure I understand your point.
Makes switching to vscode feeling going from an framing nailer to beating in rusty iron nails with a rock.
One feature that comes to mind is rearrange methods. I haven’t used Java in years so someone who actually uses both can find more cases where Kotlin support is worse than Java’s.
Valhalla might. There's been a lot of time put into nulls and primitives for valhalla. How that manifests seems to be up in the air still. But I would not be surprised to find non-nullable types in the future (like `int`).
I don't think this is something that will only apply to null safety objects. Maybe initially, but I don't see there being major restrictions to extending this to other classes.
And that, in turn, answers the question of where Kotlin is different enough from Java that it might be worth switching.
Java is very strict on backwards compatibility. That has its advantages. But it also has a lot of downsides. Kotlin doesn't want/need to be backwards compatible, and it can afford to break semantics enough to make null not allowable(*) in non-nullable types.
It's a trade-off, of course. But it explains why some people prefer Kotlin.
(*) Save for Java interop.
When targeting Java 8, Kotlin creates helper classes for the default methods on interfaces as that is not supported in Java 8, whereas on Java 9+ it uses Java default functions. However, you can target multiple Java versions, as I've done that with my projects for 11/17 support.
I understand that, but for starting new projects the equation can be different.
> many of us don’t struggle with null
Sorry, but I don't believe there's anybody like that. I guess many people accepted/internalized the struggle.
Anything relying on that is brittle. I work on a 20 years old code base with the majority of the base classes being 15+ years old. This is exactly where Java should shine, and it's not doing bad, but it's very hard to enforce top-notch coding standards across ~200 devs committing during that time frame.
As a result, there isn't much of an information on the nullability of return types, and you end up defensively handling nulls everywhere which is just noise.
> Last time I saw uncaught NPE in server logs was a long time ago
Luckily these are rare also for us, I guess most are caught during development and then on some test environment / CI, but catching these anytime later than compilation is way too late. The cost of a bug is directly connected with time between introduction and fixing it.
I don't think for new projects the equation is any different. Why would I throw away a language and ecosystem that I understand, that I'm productive in and with language designers that I trust to do the correct thing for the future? Why go learn another language and ecosystem when the things it brings to the table are of no value _to me_ ?
I honestly do not struggle with null. If you actually read the code you call and test the code you write you won't have a problem.
The language is conceptually very similar and ecosystem is shared to a large degree.
> I honestly do not struggle with null. If you actually read the code you call and test the code you write you won't have a problem.
Looks like you work on small projects only. If you can just read the code you call, then you don't need much of a type system at all.
In my case, the call I make often executes 10,000s of lines of code and figuring out if it can return null in some cases can take hours or days.
Ideally everything should be non-nullable, with optionally specified nullability.
Of course, in the type system.
Just like we're "documenting" types themselves in the type system and not in JavaDocs.
Don’t be dismissive. I could easily just say “looks like you’re just a bad dev if you can’t handle null.”
I work on large code bases too. There’s nothing special about them.
If a function in a null-safe language is declared to return "String", then it signals that, effectively, you don't have to check for null. So, yes, compiler-enforced null safety does reduce the amount of nulls you need to check.
The trick, of course, is to not make everything nullable, but as few things as possible.
In Kotlin, of course, this is only true up to the point where Java interop might interfere with it (because Kotlin might infer a Java return type to be non-nullable when it's in fact nullable).
A notable exception is some kind of generated objects, from say, wsdls getting transformed to different dtos, but in this case MapStruct is a godsend, and is overall less error-prone than doing it even in a null-safe language. (Manually copying fields won’t tell you if a new field is added that should also be copied, for example).
For Android? Kotlin all the way.
Servers? Java is much closer to the JVM and Kotlin is an abstraction over it which honestly does not provide much extra compared to Java 21.
Kotlin has several design decisions which are neater 80% of time, but Java is the better general purpose language for those 20%. OC, much is a matter of taste. And in Kotlin will find yourself in awkward positions where you have to use Java Collections and you lose all the advantages of Kotlin (HashMaps not having non-nullable values is one of them).
Java has come a long way in recent years and it's far better than it was in the past. Kotlin stands on the great baseline of features that Java has established and there are certainly many advantages to (mostly ergonomics), to using Kotlin over Java at least for the time being.
Not sure about that example since it’s easy to do
class MapWithDefault<K, V>(
private val map: Map<K, V>,
private val defaultValue: V
) {
fun get(k: K) = map.get(k) ?: defaultValue
// …
}
Edit: or I’m sure there’s something that does this already.<R> R collect (Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner)
The fact that you see a type like Comparable<T> and can implement it with an anonymous function is unique to Java. In other languages you would have Function<Int, T, T> which simply baffles the mind. Java has a powerful and performant model which also benefits from static and default methods (e.g. functional composition).
Granted, the collect endpoint looks pretty wild but in 99% of cases you will simply use ".toList" or Collectors.toSet().
(T) -> R
vs
Function<T,R>
Maybe Kotlin lacks implementing interfaces functionally but in practice, I can't remember it being a pain point. Funtional programming in Java to me, still feels very clunky compared to scala and kotlin.
That’s a feature, not a bug.
> We could eliminate suspend modifiers from pure-Kotlin code, but should we? I’m inclining to answer no. Having to mark asynchronous functions with suspend modifier is a small price to pay, but in return you get better insight into your code. You immediately see which functions are allowed to perform potentially long communications and which are supposed to complete quickly.
When WebAssembly/gc becomes generally available, it will be exciting to see which languages will successfully target it. Kotlin has quite good chances of succeeding as a full-stack language, because of its growing ecosystem of multi-platform libraries that work in the JVM, in a browser, and on a native system. Note that the Java standard library is great, but it does not interoperate well with the JavaScript ecosystem. The same goes for C# and Rust.
As a personal pet project, and to check if the multi-platform concept actually works, I have written an emulator for an old computer in Kotlin. Initial development was done in the JVM, because I was familiar with JavaFX and sound output in Java. I then later compiled it to JavaScript, which was trivial and required no code changes, except for an implementation of canvas and audio rendering.
A serious problem is the IDE experience though. Interactive web development in VSCode with TypeScript is a breeze, with a subsecond edit-compile-run cycle. Kotlin compilation is still clunky, and fixing a typo requires at least 10 seconds of my patience, even on a decent workstation. Work is underway to improve this, but only time will tell if things succeed.
Or perhaps I'm missing some sarcasm here :)
1) Cloud is better than data centers without sacrifices
2) Meal kits are better than grocery shopping without sacrifices
3) Online shopping is better than going to store without sacrifices
4) SAAS is better than locally installable software without sacrifices
- Compile speed - Kotlin is still painfully slow - about 4-5 times slower than equivalent Java functionality (rough estimate)
- Kotlin is completely proprietary, no community or openness, no alternative to Jetbrain's IDE
- Kotlin (for me) has passed from being a simpler/more terse/attractive version of Java into being a complex language with lots of strange corner cases. Particularly largish Kotlin code-bases can be hell to read as Kotlin supports and encourages a DSL like approach.
(Plus if you're going to go to the trouble of using a non-Java language it would be a real waste to use Kotlin over Scala)
I'm not sure that's a reason to pick Java over Kotlin though – streams are just generally cumbersome, even in Java, compared to Kotlin's sequences. By choosing Java you're getting more exposure to them whereas in Kotlin you can mostly avoid them in favor of sequences.
I would not start Kotlin or learn it unless you go back 5 or more years.
Then there's other people like me who come from other ecosystems. I used to write Ruby and I just can't stand how verbose and convoluted Java can be. It makes reading code a real pain. But with Kotlin I found a language that struck a good balance between expressivity and compile-time safety.
Maybe Kotlin should do more to convert people from other ecosystems rather than trying to cater to the kinds of people who were never offended by the dullness of writing Java.
The move from 8 to 11 was way harder than upgrading from 11 to anything else. In general Java compatibility between versions is very good.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
The new java way is strange, but maybe it was to avoid having to touch the language parser too much.
STR"x=\{x}"Users can define their own template processors to control how the interpolation behaves: sanitize inputs, etc.
This part honestly seems like overengineering. Like Java architects resisted operator overloading for decades, and now they pull off this in the first version of templated strings ...
This has existed in JavaScript and Scala for years, and is a very useful feature.
Like for SQL queries:
sql`SELECT * FROM account WHERE name = ${name}`Personally I write JavaScript daily, and I've never seen this feature being used.
Even this example - will this create a prepared statement or insert a correctly escaped value into the SQL string?
Either way this seems pretty niche. Are there some major use cases for it?
I see it be used for SQL specifically in loads of projects.
https://www.npmjs.com/package/sql-template-strings
https://www.npmjs.com/package/squid
> will this create a prepared statement or insert a correctly escaped value into the SQL string?
Always prefer server-side parameterization. If you are referring to java.sql.PreparedStatement specifically, that requires a connection, so this is only an intermediate value to creating that.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
> Even this example - will this create a prepared statement or insert a correctly escaped value into the SQL string?
That tagged template doesn't evaluate to a string, it evaluates to a parameterized query object that contains the parameterized query string and the parameter values. You pass it to the mysql library and it executes the parameterized query.
const query = SQL`SELECT author FROM books WHERE name = ${book} AND author = ${author}`
typeof query // => 'object'
query.text // => 'SELECT author FROM books WHERE name = $1 AND author = $2'
query.sql // => 'SELECT author FROM books WHERE name = ? AND author = ?'
query.values // => ['harry potter', 'J. K. Rowling']Not at all.
Thanks for the explanation, so it's not just escaped param substitution.
Which brings up the question if you can combine normal text substitution with SQL substitution, e.g.:
SELECT author FROM books WHERE name LIKE '%${book}%' AND author = ${author} AND ${extraCondition}
(it's pretty common to have to join query segments) const query = SQL`SELECT * FROM books`
if (params.name) {
query.append(SQL` WHERE name = ${params.name}`)
}
query.append(SQL` LIMIT 10 OFFSET ${params.offset || 0}`)
or if you need raw values like a table name then just pass append a string. db.query(SQL`SELECT * FROM "`.append(table).append(SQL`" WHERE author = ${author} ORDER BY ${column} `).append(order))It runs the sql function, with separation between the static string parts and the dynamic values, and it returns whatever the sql function returns.
It's just really useful if you can have an API that can decide how interpolated stuff gets handled. You can santize, do custom formatting, lazy evaluation, all sorts of fun stuff.
StringTemplate foo() { return RAW.""; }
is foo() == foo()?
This is a very useful feature of JS tagged template functions, as it lets a template processor do expensive one-time processing of a StringTemplate and cache the result, then use that to quickly apply that to particular values. I don't see an answer here: https://cr.openjdk.org/~jlaskey/templates/docs/api/java.base...
I’m tempted to pass the exam with Java 17, since the wait for Java 21 will be too long, assuming it will be several months after the GA date, September 21st.
The usefulness seemed to cut-off with Java 11 though and now the RS Java client is being deprecated.
Laziness by default is my favorite Haskell feature.
On the whole, I like it in Haskell. In languages without lazy evaluation (by default) I tend to miss it.
I've never seen it "ridiculed" by anyone proficient with the language or who used it for an actual project, either.
Meanwhile, no honest Haskell developer can claim to have never been surprised by a space leak at one time or another.
Why?
Becauze laziness is simply harder to reason about.
It's a lot like coding in something like Prolog, in that you need to internalize not just the behaviour of the program, but the behaviour of the Haskell runtime as well, and how laziness is actually implemented.
This makes it very challenge to reason about both space and time reliably, even for a seasoned developer.
The fact that whole tutorials are dedicated to picking the right fold so you don't leak space is a perfect encapsulation of the problem.
And for a language that emphasizes compile-time safety, it's a major class of bug that neither the compiler nor runtime help the user detect.
I've worked in many different languages and runtimes over the years and I've never encountered another feature that was equally complex.
I did answer. I've been hit by them. In general, the problems are fewer than without laziness, though. I've seen more problems in production caused by careless eager evaluation.
Are you an "honest Haskell developer" by the way, that you speak for all of them? "Be honest" in your answer, please.
> Becauze laziness is simply harder to reason about.
No, it's harder to reason about some aspects and easier to reason about some others, like gluing together code.
> I've worked in many different languages
But not Haskell, right? "Be honest".
I've never seen laziness ridiculed (not critiqued, but ridiculed, remember the context of the conversation you're in) by an actual practitioner, have you? "Be honest".
I absolutely have worked with Haskell and written a number of non-trivial programs in it, including a bytecode interpreter for a toy virtual machine.
I also spent quite a bit of time writing solutions to various coding challenges in Haskell, which gave me an appreciation for how hard it is to write high performance code in the language (anything touching strings, in particular, was a real joy...). I've never felt like I had to fight the runtime quite the way that I had to fight it with Haskell.
But yes, if I'm writing normal, run-of-the-mill, low complexity code, it's pretty easy to get Haskell right. But then in that circumstance it's easy to get it right in any language.
Of course, why would you believe me? You're already so sure your opinion is fact that I doubt anything I have to say will sway you.
Fair enough. Haskell developers do not think the language is perfect, and I've seen a fair share of critiques, but I've never seen laziness "ridiculed".
And since I believed you, believe me: whenever I deal with languages without easy recourse to lazy evaluation, I miss it. Every expression is clumsier.
> You're already so sure your opinion is fact that I doubt anything I have to say will sway you.
Are you a mind reader?
I suggest you go back and look at your first reply to me. Do you think it was crafted to elicit good mannered conversation?
Yup, that's fair, I came out swinging because I read this:
> They only ridicule it because they don't use it or understand it.
As belittling the intelligence of people who didn't share your opinion.
If that wasn't your intention and I read too much into your comment, then I absolutely apologize for jumping down your throat so aggressively.
Once your language has aged a little it's hard to impossible to take old primitives away, but you still want to adopt ergonomic improvements in order to not be left behind. Until at some point you become a language nobody can master in its entirety.
curious; can you give a few examples? TIA
I would say the analogous feature to typeclasses are interfaces.
In terms of actual examples, I'll point to JEP 443, linked in the Java 21 article that kicked off this topic. That feature comes from Scala, by way of Kotlin.
We took way less time to move from Java 11 to 17 compare to the 8 -> 11 migration due to the change in deployment methods: instead of deploying jar file and run it with the server-provided JRE, we now use docker to ship our app + JRE to server.
Pretty poor tradeoff on the whole if you ask me - but Scala is always there for anyone with enough sense to pick it up.
"JDK21 is a version, for which many vendors offer support."
tl;dr: the Java version is not what gets "long-term support", it's a specific runtime JDK released by some Java vendor, e.g., Oracle, Eclipse Adoptium, Azul, Microsoft, Amazon (Coretto), etc.
Java's rocket sled continues to burn.
Rant:
JFC Java, its 2023 and people use Java to run actual databases and develop machine learning infrastructure. Yet the ability to do SIMD in the JVM is on its sixth incubation.
Even for native platform like C/C++ or Rust, SIMD intrinsic took a while to develop, and required multiple iterations to mature for each supported platform. Plus, with newer CPU instructions being introduced, the job is never fully done.
As noted in the JEP. The Vector API cannot be move to preview before value classes are available. Hence it is still in incubation and will be for quite some time.