Hacker News new | ask | show | jobs
by voidfunc 1897 days ago
Java gets a bad rap from people that used it late 90's through early 2000's and got burned out by XML and design pattern heavy frameworks but its a lovely language that with a little discipline can be used to create very lean looking code.

Go is one of the HN darling languages and I work in Go everyday for work (and generally like it), but I really wish I could reach for Java most days.

7 comments

“Java is a big DSL to transform XML into stacktraces” — so was the joke at the time when domain-specific languages were the hype.

This, and the FizzBuzz, Enterprise Edition: https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...

More seriously, what are you missing in Go that is well-done in Java? I assume verbosity of the code is still the defining characteristic of Java?

> More seriously, what are you missing in Go that is well-done in Java?

1. Generics. And yea, I know Go is getting generics "Real Soon Now" (tm), but it is incredibly annoying to write the same collection code over and over and there's some third-party libs that would really benefit from generics (looking at you Azure Go SDK).

2. Error handling... with the big caveat that I actually like Go's error handling mechanism at small scale but wish there was a good way to chain several operations together and return an error to the top if any failed... I find myself writing a lot of `err != nil` checks in sequence and I've found baking my own abstractions around this to be leaky or difficult to grow as the program requirements change.

3. Diverse collections API.

4. Iterators.

5. Not Java-the-language but the JVM has amazing monitoring tools.

> I assume verbosity of the code is still the defining characteristic of Java?

Pound for pound... I think Go and Java have about the same verbosity. I'm honestly never quite sure what people mean by "verbosity" in Java. Generally I interpret this as "frameworks" but I predicated my OP on the idea that legacy framework bloat is where most of people's frustration with Java lies... not the language itself.

> I'm honestly never quite sure what people mean by "verbosity" in Java.

Java improved with "var" keyword (use with caution!) and introduction of records. These are not the only code-shortening features (there are e.g. interface methods, diamond operator, lambdas, convenience "of(...)" methods, even "fluid style"), but they, used well, can really reduce verbosity.

Also, a lot of verbosity in Java came from people going off the rails with design patterns to hide crap multiple levels deep in a file with 5+ words in the class name.

If you have a hammer everything’s a nail.

Frameworks like Spring/Spring Boot kind of force that as well with their runtime creation of proxy classes.
I'm surprised you didn't mention the Streams API.
Much of the verbosity came from the false presumption that every field in every class needs a public getter and setter. (I cannot express in words how terrible this is.) Some can come from the framework, mostly poorly-designed ones.

I think Java 11 and higher are reasonably terse/expressive without being overly dense.

Remind me, why doesn’t Java have object-properties yet?

So far the only reason I’ve concluded is Java’s language designers’ egos were so damaged by C#, Swift, Kotlin, TypeScript, etc that after dogmatically denying the developer-productivity benefits of properties for the past 20 years that to concede now would mark the end of Java as a language entirely.

...I kid, but seriously I haven’t heard any compelling argument from the Java camp yet for refusing to add this feature. Object-properties are easily the lowest-hanging-fruit with the biggest productivity-gains.

Heck, even some of the most popular C++ compilers support object-properties as proprietary extensions.

It's so crazy - with all the huge and radical features they've added this would surely have been so simple by comparison and yet added more value than about 80% of the things they've done. It's about half the reason I still use Groovy now since Java has solved most of the rest over time. But I just cannot go back to writing getters and setters!
I believe the Java lang team simply dislikes the Java Beans convention, and if they see a better future, they rather not maintain properties forever.

I think the current future view is Withers ( https://github.com/openjdk/amber-docs/blob/master/eg-drafts/... ).

Couldn’t agree more, the madness of idiomatically adding setters and getters to every field was something I ended up putting an end to in my last gig, and my life improved immeasurably as a result.
I think I'm going to convince my team to get to 16 (with records) before I can convince them to stop adding getters and setters everywhere, but just out of curiosity... how did you manage to make this happen?
Well - I was the boss :) so that helped. But I’d also taken over a fairly unhappy and unproductive team that was ready to try new stuff. I’d been out of the game for a few years and had some fresh perspectives too.

So I did some small standalone projects and demonstrated how much easier life was with this and other (more significant) changes. For example, we had loads of operational problems with dependency injection - the usual cognitive and debugging issues - so I threw all of that out too, along with the frameworks that implemented them. Things started booting in a few seconds instead of minutes and I’d say the lack of setters/getters was mostly done because I built trust in my team that I was making their lives better so they just followed me.

Probably not the answer you were looking for but there was friction from the devs and in the end I just showed them how much nicer life could be...

Kotlin property access solves this. You access to the property but behind the hood it will call the getter/setter if it exists.
I agree with your points here, but just want to add that, while Java's generics are better than the non-existent generics in Go, I still find Java's type system to be really subpar for today's era.

The type system is not strong or expressive enough to do full type erasure, but then we're stuck with type-erased generics, so it's just incredibly frustrating.

Sorry that I find it necessary for yet another answer to your verbosity question, but: The real verbosity is in the standard libraries. What would be a one-liner in any other language is usually at least 2, often 3, and sometimes enough that you end up writing your own wrapper function or library. Especially noticeable is the agony of Lists & Maps, which are first-class citizens in most languages (classic job interview question: what is the difference between ArrayList and LinkedList? Real answer: Nobody actually uses LinkedList).

The stdlib often seems to be written by people who had no intention of using it. I guess maybe this applies to C++ as well, so for some folks it seems normal.

There is List.of() and Map.of() now. And I would say, Java has one of the better standard libs out there in my opinion.
About verbosity: you needed to define classes for so many things. Eg you want others to hook into processing in certain places. So you create a listener interface with some methods. And the provider needed to work with those, and the consumer also needs to work with them.

In modern Java maybe you can make do with functional interfaces and lambda, but it doesn’t always work I think.

And in emacs: the provider does run-hooks, and the consumer defines a function and says add-hook. Done. Very little ceremony.

Now we've got Spring, which in my, admittedly limited, experience does a great job of transforming what could have been compile-time errors into run-time errors.
The wheel of karma is ever-turning, though [0],[1].

Disclosure: I work for VMware, which sponsors Spring development.

[0] https://spring.io/blog/2021/03/11/announcing-spring-native-b...

[1] https://github.com/spring-projects-experimental/spring-fu

Spring native is a game changer for so many organizations using containers for startup times. There are other spring related issues you still have to deal with
Those are nice advances but I hope that spring will one day support compile time validation of generated queries and of beans like Micronaut do.
I'm confident it'll reach that point.
...and probably well before Micronaut gets rid of its bugs, comes close to feature parity and generally matures as a framework.
Well said :) . Spring looks so much as to show how useful ideas can be misapplied with catastrophic results. For example, liberate encouraging of dependency injections leads to multitude of interfaces which are only ever implemented once by a production code class, and maybe one more time by a test class, even though Java has all methods virtual and testing could be done without requiring the interface.
It's reasonable to provide a single implementation of an interface, if the goal is to facilitate dependency injection for that component. The problem I find with Spring is that it is designed for dependency injection at all levels of its architecture, leading it to be one of the ultimate examples of Ravioli Code.

https://wiki.c2.com/?RavioliCode

DI is a powerful concept, but Spring projects rely on DI in such a generic way that it often doesn't even make sense for your application. You have to gain intimate knowledge of the abstractions, and inject a bunch of code in a bunch of places just to make it do the very-straightforward thing you were trying to do.

> It's reasonable to provide a single implementation of an interface, if the goal is to facilitate dependency injection for that component.

You don't really need dependency injection if there is only single choice of what to inject. You can just refer in code to the only possible component.

>> Java has all methods virtual and testing could be done without requiring the interface.

Not sure what you mean by that. I thought that interface/implementation divide is the way to implement virtual calls. Unless you want testing frameworks to do bytecode instrumentation.

If your interface is going to be implemented by just one class, I'd offer to skip interface - it reduces amount of code. If you still need to pass, as a parameter, a class A in order to test a class B, you can instead pass class C inherited from A, with all necessary methods overloaded.

If you just create a class A in java, no interfaces involved, class A will have its methods virtual. I'm not sure what do you mean by interface/implementation divide.

Modern testing frameworks can indeed mock concrete classes, which beats the bytecode generation that Spring would be doing anyway.
Running ‘mvn install’ on even a simple Spring project with default dependencies is scary to watch on the console. So many things happen and it’s not even the verbose mode.

Running ‘dotnet build’ is much more saner and one can reasonably understand what happens.

Try `npm install` then..
Just use micronaut if compile time errors is what you want. It can even validate generated queries.
Spring is a flow of control obfuscation framework.
Yep. Give it a one line error description and it turn into stack trace longer than "War and Peace".
Not OP, but...

> More seriously, what are you missing in Go that is well-done in Java?

As cliche as it is to say, Go missing generics (for the time being) does hold it back in many ways relative to Java. I like and use both languages regularly, and as I thought about my answer to this question I realized that essentially all of my complaints stem from the lack of generics - things like streams, a rich collection framework, and non-channel concurrency features (java.util.concurrent among them) don't exist in go because you can build them as generically as needed for them to be useful.

You might have said "well-done" as a way of excluding generics in Java since many people like to suggest they're not well done; of course most developers would like more, but they already enable an enormous amount of stuff not possible in Go.

Verbosity of code is more of a go thing these days:

  hasThing := false
  for _, v := range stuff {
    if checkForThing(v) {
      hasThing = true
      break
    }
  }
  if !hasThing {
    return false
  }
Java:

  if !stuff.stream().anyMatch(v -> checkForThing(v)) {
    return false;
  }
In the 2020s, loops are the new "goto", too much boiler-plate and ways to subtly be incorrect, much safer to use higher-level collection methods.

    return stuff.stream().anyMatch(this::checkForThing)
The java ecosystem is very nice. Not necessarily the language itself, but everything else. The jvm, the tools, the libraries are all very mature and good.

Skipping java and instead using Kotlin allows one to reuse all that knowledge in a great language as well.

I guess it depends upon what you mean by ecosystem.

Java the language is ok. The culture (which is part of the ecosystem) and how you're pushed to write code is the biggest problem with Java. The wide array of tooling, frameworks, and libraries within the ecosystem is nice.

But the way you have to use them tends to be shit due to the culture surrounding the language. At least it seems to be shifting to something more sane.

Good language ought to discourage (enough) the bad practices. Culture forms slowly, and it's Java fault that the culture managed to produce such excessities as proverbial FactoryFactoryFactory.

Edit: I still think Java is a good language, especially later versions, and both original goals and recent advances are quite noble.

I somewhat agree, but Java grew up in a different era when communication about these things wasn't as easy. It's hard to change the direction of something as large and widely deployed as Java. It's happening but it will take time and its always going to dealing with its legacy as there just so much of it out there.
Huh, one needs to see first party Java libraries from earlier times. It is pretty clear engineers at Sun also believed that AbstractFactoryFactory everything will be the way world need to be rebuilt.

Of course one can show empathy and understand justifications for things they like and simply laugh out "LOL Go No Generics' when they don't.

I'd say the defining characteristic of Java is an unparalleled combination of performance, observability, and maintainability.
I at the same time laughed and got nauseated just by looking at that FizzBuzzEnterprise code LOL. I'm a minimalistic programmer myself, so the thing I hate the most in coding is over engineered code. Yes , it's a joke, but a joke based on real life haha.
Logging.
I disagree that it's a lovely language. I think, as developers, we very quickly develop Stockholm syndrome. Once you "learn" a language, it's really easy to churn out code and apply idioms without even realizing that you're constantly writing workarounds and kludges for your language's deficiencies.

As a polyglot dev, the following are my gripes with Java:

* null - we all know, so I'm not going to bother expanding except to say that @NotNull is NOT a solution and it doesn't guarantee shit.

* interfaces are extremely lacking compared to type classes and lead to verbose and cumbersome patterns such as "Adapter".

* Type-erased generics. Why shouldn't I be able to implement a generic interface for multiple types? E.g., class MyNumber implements Comparable<Integer>, Comparable<Short>, Comparable<Long> {}

* It only just got Records and sealed interfaces, so thank goodness for that. But prior versions of Java are extremely lacking in expressiveness without these.

* I don't hate checked exceptions as a concept, but the fact that you can't be "generic" over the exceptions in a function signature is frustrating. This is why everyone says they're "incompatible" with Java's closure APIs.

* No unsigned ints.

* Silent integer overflow/wrap-around. It's not C- did it really have to copy this insanity?

* Dates and timezones are still insane, even in Java 8+.

* The fact that arrays got type variance wrong.

* JDBC sucks. JPA also kind of sucks.

* No concept of `const` or immutability.

I'm not saying that Java is the worst language in the world or anything, but it's far from great, IMO. Most programming languages are pretty awful, IMO.

> No unsigned ints.

I think that's a mixed blessing. I believe Java did this deliberately to avoid the trouble that C and C++ have with signed and unsigned integer types having to coexist. Personally I've never been inconvenienced by Java's lack of unsigned integer types, but I'm sure it can be annoying in some situations.

I'm quite fond of Ada's approach to integer types, but I suspect I'm in a minority.

> Silent integer overflow/wrap-around. It's not C- did it really have to copy this insanity?

Curiously this cropped up 10 days ago. [0] You're not alone. The great John Regehr put it thus: [1]

> Java-style wrapping integers should never be the default, this is arguably even worse than C and C++’s UB-on-overflow which at least permits an implementation to trap.

> The fact that arrays got type variance wrong.

At least Java has the defence that they didn't know how it would pan out. C# has no such excuse in copying Java.

> No concept of `const` or immutability.

I recall a Java wizard commenting that although a const system is the sort of feature that aligns with Java's philosophy, it's just too difficult to retrofit it.

[0] https://news.ycombinator.com/item?id=26538842

[1] https://blog.regehr.org/archives/1401

> I believe Java did this deliberately to avoid the trouble that C and C++ have with signed and unsigned integer types having to coexist.

It did, from http://www.gotw.ca/publications/c_family_interview.htm

> For me as a language designer, which I don't really count myself as these days, what "simple" really ended up meaning was could I expect J. Random Developer to hold the spec in his head. That definition says that, for instance, Java isn't -- and in fact a lot of these languages end up with a lot of corner cases, things that nobody really understands. Quiz any C developer about unsigned, and pretty soon you discover that almost no C developers actually understand what goes on with unsigned, what unsigned arithmetic is. Things like that made C complex. The language part of Java is, I think, pretty simple. The libraries you have to look up.

Since Java 8, the standard library has unsigned manipulation arithmetic classes, though.

I don't know about Ada, but I enjoy Rust's strictness when it comes to numeric types.

> Java-style wrapping integers should never be the default, this is arguably even worse than C and C++’s UB-on-overflow which at least permits an implementation to trap.

EXACTLY. It's f-ing stupid. C's excuse was compilers doing magic on UB or whatever. Java has no such excuse. They just wanted it to behave the same as C/C++ to attract C++ devs.

> At least Java has the defence that they didn't know how it would pan out. C# has no such excuse in copying Java.

My understanding was that they DID know it was wrong and chose to do it anyway because it was more convenient and ergonomic to allow it that way. I guess they realized that was a terrible idea, because the generic collection interfaces do it correctly.

I don't see how const and immutability align with Java's original philosophy of being object-oriented, which is all about opaque objects that control internal mutable state. The very fact that it's taken until now to have records is proof-positive that "everything is an object" was taken pretty literally for most of its life. Immutable data doesn't really jive with that.

> I don't see how const and immutability align with Java's original philosophy of being object-oriented, which is all about opaque objects that control internal mutable state.

That's an interesting point, but an object presents an interface and promises to deliver some particular behaviour. A const system is a way of letting the type-system formalise some of an object's promises, no?

I don't think this is particularly 'leaky' (in the sense of leaky abstractions). Java's String class doesn't let me access its internal character array, but it still matters to me that it promises never to mutate it, nor to let anyone else mutate it (at least ignoring reflection). That's relevant at the level of the interface, not only at the level of the implementation.

I get what you're saying and I don't really disagree with you. An object's methods are an interface and its method signatures are a contract about what "messages" (in Alan Kay parlance) it will accept and return.

A C++ style const system would seem to be compatible with that.

And, in every practical sense, I would love such a thing existing in Java. I don't give a crap about whatever "OOP philosophy" and purity, even if my statement were correct/true.

However, (and this is just navel-gazing, honestly), adding const to object methods is exposing information about its internal state. That's not very "objecty" in the Alan-Kay-ish, Actor-model-ish sense. An object's internal state is "none of your business."

> Java's String class doesn't let me access its internal character array, but it still matters to me that it promises never to mutate it, nor to let anyone else mutate it (at least ignoring reflection).

I feel like this is a little different. Strings in Java are technically a class, but they're really treated like primitives (evidenced by the fact that literals are magically made into String objects).

But, it doesn't really matter. I agree. It's great that String promises to be immutable.

I'd argue that immutable class instances aren't really "objects" anymore- they're just (possibly opaque) data types.

> An object's internal state is "none of your business."

An object's state is my business, as immutable objects can be used in ways that mutable ones cannot. They can be passed to arbitrary functions with no need for defensive copying. They can also be useful in concurrent programming. None of that means breaching the separation of interface and implementation.

> Strings in Java are technically a class, but they're really treated like primitives (evidenced by the fact that literals are magically made into String objects).

Immutable objects can generally be treated as values, that's their charm. There's a good talk on this topic, The Value of Values. [0]

> immutable class instances aren't really "objects" anymore- they're just (possibly opaque) data types

They're certainly still objects. The essence of object-orientation is in dynamic dispatch, not in stateful programming.

[0] https://www.infoq.com/presentations/Value-Values/ (Perhaps skip to 22:00 to get a sense of the general point.)

> EXACTLY. It's f-ing stupid. C's excuse was compilers doing magic on UB or whatever. Java has no such excuse. They just wanted it to behave the same as C/C++ to attract C++ devs.

But... as you yourself are saying, Java's behavior is not "the same as C/C++". Java wraps while in C and C++ signed overflow is undefined. (Interestingly, C++ is now moving away from UB for this, and defining wrapping semantics. While I'm not one for proof by authority, it looks like some very well-informed people disagree with you about the usefulness of this feature.)

Signed integer overflow checking can be almost free. Until it isn't, because it doesn't play nicely with SIMD code. So the code you want to run fastest will pay the biggest price. This article is from 2016 so take it with a grain of salt, but it looks like this can cause 20% to 40% slowdowns: https://blog.regehr.org/archives/1384

I understand that there are performance implications.

But a 20% to 40% slowdown for number crunching in a language that is primarily designed for writing super indirection-heavy, heap-allocation-heavy, application architectures is just nothing.

Having some kind of high performance math section of the standard library would be fine. But the default behavior is, frankly, dangerous. And for a 20% speed up on operations that are probably far less than 1% of the typical Java application?

> a language that is primarily designed for writing super indirection-heavy, heap-allocation-heavy, application architectures

Are there Java design documents that describe the language in these terms, as opposed to something like "a general-purpose object oriented language"?

> But the default behavior is, frankly, dangerous.

You keep saying variations of this, but you haven't really made the case.

True, if you increment a number, you will typically expect the result to be greater. But how many application domains are there where 2^32 - 1 is really the exact upper limit of the range of valid values? I would think that in most cases catching a overflow would come much too late, because the actual error is exceeding some application-specific limit rather than the artificial limit of the range of int. Or put differently, I bet 99.9% of ArrayIndexOutOfBounds errors are because indices leave their legal range without ever overflowing int.

> I believe Java did this deliberately to avoid the trouble that C and C++ have with signed and unsigned integer types having to coexist.

The problems really only come from mixing those types, and the simple solution is to disallow such mixing without explicit casts in cases where the result type is not wide enough to represent all possible values - this is exactly what C# does.

I think Java designers just assumed that high-level code doesn't need those, and low-level code can use wrappers that work on signed types as if they were unsigned (esp. since with wraparound, many common operations are the same).

> Java-style wrapping integers should never be the default

The ironic thing about this one is that C# introduced "checked" and "unchecked" specifically to control this... and then defaulted to "unchecked", so most C# code out there assumes the same. Opportunity lost.

While we're on the subject of numeric types - the other mistake, IMO, is pushing binary floating point numbers as the default representation for reals. It makes sense perf-wise, sure - but humans think in decimal, and it makes for a very big difference with floats, that sometimes translates to very expensive bugs. At the very least, a modern high-level language should offer decimal floating-point types that are at least as easy to use as binary floating-point (e.g. first-class literals, overloaded operators etc).

C# almost got it right with "decimal"... except that fractional literals still default to "double", so you need to slap the "M" suffix everywhere. It really ought to be the other way around - slower but safer choice by default, and opt into fast binary floating-point where you actually need perf.

> At least Java has the defence that they didn't know how it would pan out. C# has no such excuse in copying Java.

I think both Java and C# did it as an attempt to offer some generic data structure that could cover as many use cases as possible, since neither had user-defined generic types. In retrospect, it was an error - but before true generics became a thing, it was also a godsend in some cases.

My opinion is that a high-level language like Java has no business making me guess how many bytes my numeric values will occupy. It's insane. Since when does Java give a crap about memory space? "Allocations are cheap!" they said. "Computers are fast!" they said about indirection costs. Then they stopped and asked me if I want my number to occupy 1, 2, 4 or 8 bytes? Are you kidding me?

Yes, you should have those types available so that your Java code can interact with a SQL database, or do some low-ish level network crap, or FFI with C or something. But the default should basically be a smart version of BigInteger that maybe the JVM and/or compiler could guesstimate the size of or optimize while running.

Thus, IMO, there should be a handful of numeric types that are strict in behavior and do not willy-nilly cast back and forth. Ideally you'd have Integer, UInteger, PositiveInteger, and a similar suite for Decimal types.

Schemes have done numbers correctly since basically forever.

> the default should basically be a smart version of BigInteger that maybe the JVM and/or compiler could guesstimate the size of or optimize while running.

I suspect this would be disastrous for performance. I believe Haskell uses a similar approach though.

Sometimes you want to store 20 million very small values in an array. Forcing use of bigint would preclude doing this efficiently (in the absence of very smart compiler optimisations that is).

As int_19h points out, the Ada approach lets us escape the low-level world of int8/int16/int32/int64 while retaining efficiency and portability and avoiding use of bigint.

> there should be a handful of numeric types that are strict in behavior and do not willy-nilly cast back and forth

I agree that reducing the number of implicit conversions allowed in a language is generally a good move. Preventing bugs is typically far more valuable than improving writeability. This is another thing Ada gets right.

> I suspect this would be disastrous for performance. I believe Haskell uses a similar approach though. > > Sometimes you want to store 20 million very small values in an array. Forcing use of bigint would preclude doing this efficiently (in the absence of very smart compiler optimisations that is).

I suspect that it would. I also suspect that I don't care. :p

We're talking about Java. Yes, you can write high-performance Java and I wouldn't want to take that option away. But look at the "default" Java application. You have giant graphs of object instances- all heap allocated, with tons of pointer chasing. You have collections (not arrays) that we don't have to guess the maximum size of.

If you're storing 20 million small values in an array, then go ahead and use byte[] or whatever. But that should be in some kind of high performance package in the standard library. The "standard" Integer type should err toward correctness over performance- the very same reason Java decided to be "C++ with garbage collection".

I'm also not literally talking about the BigInteger class as it's written today. I'm talking about a hypothetical Java that exists in a parallel universe where the built-in Integer type is just arbitrarily large. It could start with a default size of 4 or 8 bytes, since that is a sane default. Maybe the compiler would have some analysis that sees the number could never actually be large enough to need 4 bytes and just compile it to a short or byte. These things should be immutable anyway, so maybe the plus operator can detect overflow (or better if the JVM could do some kind of lower-level exception mechanism so the happy path is optimized) and upsize the returned value size. Remember, integer overflow doesn't actually happen very often- that's exactly the reason people don't typically complain about it or ever notice it (except me ;)), so it's okay if the JVM burps for a few microseconds on each overflow.

All this doesn't matter because it'll never, ever, actually happen. I just think they made the wrong call and it has unfortunately led to lots of real world bugs. It's hard to right correct, robust, software in Java.

OP mentioned "Ada's approach to types", as well. Ada lets you write stuff like "T is range 1 .. 20" or "T is range -1.0 .. 1.0 digits 18". This then gets mapped to the appropriate hardware integer or floating-point type.
Yeah, I've read little snippets like that from blog posts and stuff, but I've never written a single line of Ada, so I really don't know how that works out in practice.

What happens if you overflow at runtime? A crash, I assume/hope?

My point of view is that this is the opposite of what I'm talking about anyway. Java is a high level language where we are usually writing in Java because we're agreeing to give up a lot of raw performance (heap allocations, tons of pointer chasing) in order to have convenient models (objects) and not have to worry about memory management, etc.

In light of the above, I don't see why the default for Java is to have these really nitty-gritty numeric types. I don't want to guess how big a number can be before launching my cool new product. Just like I don't use raw arrays in Java and have to guess their max size- I just use List<> and it will grow forever.

Your analysis is interesting and far from exhaustive, It would be nice to have a collaborative feature matrix for languages, on github.

Kotlin solve the following points:

null - we all know, so I'm not going to bother expanding except to say that @NotNull is NOT a solution and it doesn't guarantee shit.

I don't hate checked exceptions as a concept, but the fact that you can't be "generic" over the exceptions in a function signature is frustrating. This is why everyone says they're "incompatible" with Java's closure APIs.

No unsigned ints.

No concept of `const` or immutability.

Kotlin allow to specify either variance or contravariance so I guess it fixe this point too? * The fact that arrays got type variance wrong*

* interfaces are extremely lacking compared to type classes and lead to verbose and cumbersome patterns such as "Adapter".* Interesting, can you link to an example? Kotlin has first class support for delegation as an alternative.

* Type-erased generics. Why shouldn't I be able to implement a generic interface for multiple types? E.g., class MyNumber implements Comparable<Integer>, Comparable<Short>, Comparable<Long> {}* Kotlin has support for reified generics to some extent. The JVM is currently getting state of the art support for it too and for specialization.

* Dates and timezones are still insane, even in Java 8+.* I always hear that Java has got the best Time library, what is the complaint about?

* JDBC sucks. JPA also kind of sucks.* Do yourself a favor and use the JDBI or so many other sexy alternatives.

Of all your points the only unaddressed are: * interfaces are extremely lacking compared to type classes and lead to verbose and cumbersome patterns such as "Adapter".

And Silent integer overflow/wrap-around*

Strong agree about stockolmization and cargo cultism of language constructs.

Most programming languages are pretty awful Correct, hence why Kotlin stands out.

Kotlin does indeed (mostly) fix null.

Kotlin does not fix the issues with checked exceptions. It gives up and gives us nothing for error handling. So, for all the beauty and magic of a strong static type system, I have absolutely no idea if `fun foo(i: Int): Int` is just going to crash my program when I give it -1.

"Do you have a fatal error? Throw an exception."

"Do you have a non-fatal error that's totally expected as part of your API? Throw an exception."

"Do you want to cancel a coroutine? Throw an exception."

"Do you want to define a class and validate the parameters you pass to the constructor? Screw the type system! Write an init {} that throws an exception!"

Kotlin also doesn't really "fix" Java's lack of unsigned ints. The implementation that Kotlin provides is really poor. That's partly because of Java's lack of unsigned ints, so it's not entirely their fault, but it's a really bad API and going between signed and unsigned ints is very bug-prone. They also don't work with serialization libraries because they're implemented as inline classes, which don't work with serialization libraries.

Kotlin doesn't have the issue with array variance because it doesn't really have arrays like Java has. So that's good.

With respect to interfaces vs. type classes. The issue is this: let's say you're writing an API and you decide that you want to define an interface. Let's define it as `interface MaybeEmpty { fun isEmpty(): Boolean }`. So in your API, you might have some function like `fun foo(maybeEmpty: MaybeEmpty) { /* do something with maybeEmpty.isEmpty() */ }`

See where this is going? You realize: "Hey! I want to implement `MaybeEmpty` for a bunch of types to use in my function."

How do you implement `MaybeEmpty` for `String`? What about `Collection`? You can't. What you have to do in Java+Kotlin is define at least two new classes: `class MaybeEmptyStringAdapter(val value: String): MaybeEmpty { override fun isEmpty() = value.isEmpty() }` and `class MaybeEmptyCollectionAdapter<out T>(val value: Collection<T>): MaybeEmpty { override fun isEmpty() = value.isEmpty() }`.

Then when you actually want to use a String or a Collection in your fancy code, you have to write:

val s: String = getSomeString()

foo(MaybeEmptyStringAdapter(s))

With type classes, which exist in Scala, Rust, Swift, and Haskell, you can extend types with interfaces AFTER their definition. So I could implement MaybeEmpty right on String itself and then just pass the String value right into my function. No extra classes, no extra wrapping, no performance overhead.

Kotlin has extension functions, which are a neutered version of type classes.

Kotlin does not support reified generics in classes. They can only be used in inline functions because it gets transpiled into the equivalent of `fun <T> foo(clazz: Class<T>)`. The JVM is not going to get reified generics any time soon. It's been in the works for years and years and will probably break things.

Java's dates and times depend on a global, mutable, timezone setting. The datetime classes use it pervasively. Dealing with Calendar is awkward as heck. The whole TemporalAccessor interface is madness- you never have any idea what method is going to throw an exception for a given implementer. JDBC's dates and times are utterly broken because they use the old Java Date class and it will never change.

JDBI relies on JDBC, so I'm not convinced it actually fixes the problems other than having a nicer API. I stand partly corrected: "By default, SQL null mapped to a primitive type will adopt the Java default value. This may be disabled by configuring jdbi.getConfig(ColumnMappers.class).setCoalesceNullPrimitivesToDefaults(false)." So at least it's only wrong by default...

So, it seems to me that Kotlin actually addresses only two things on my list of complaints: null and array type variance.

EDIT: I accidentally forgot about immutability. Kotlin doesn't have that either. `val` doesn't mean "immutable", it means "not reassignable". I can 100% mutate the ever living crap out of:

class Foo( val inner: MutableList )

val foo = Foo(mutableListOf(1))

foo.inner.add(2)

Look at all those vals! Not a "var" in sight!

Just as an additional detail: Java has a new DateTime and related classes, that I believe solves every(?) problem with the old Date class.
Well, Java 8 isn't really "new" anymore. :)

But, I was complaining about the 8+ DateTime API. Now, granted, it's WAY, WAY, better than the old API. And it's much better than what's available in at least several other languages (cough JavaScript with no time zones at all).

It still depends on a global, mutable, time zone variable. It uses it in several places where it's kind of hard to get it to just use a given time zone.

The API is also just really hard to discover and use correctly. Look at what classes implement TemporalAccessor. It's not easy at all to guess which methods will throw an exception for a given implementer. Further, what if your function is just given a TemporalAccessor? I have NO IDEA what I'm actually able to call on the object without it exploding.

Calendar is awkward and hard to use.

Switching around between TimeZone, ZoneOffset, and ZoneID is pretty cumbersome. Some stuff takes Strings, some take ZoneID, etc. Some things that seem to take ZoneID don't seem to be able to take a custom TimeZone (not that I think I ever really needed that- I was trying to do something hacky IIRC).

A lot of methods take Long, which you have to just read the docs to know if that's seconds or milliseconds.

It's just kind of a bleh API.

Then you mix in the fact that JDBC is forced to use the old Date classes and that messes everything up because of timezone crap.

>got burned out by XML and design pattern heavy frameworks

Most succinct summary I have heard lately :) I think a good bunch of blame is also on the developers who blindly adhere to said patterns. https://www.quora.com/What-are-the-most-ridiculous-Java-clas... to rest the case.

"Go is one of the HN darling languages"

It's def not the case, I spend too much time here and on Reddit and people are always complaining about Go ( generics, errors, type system etc ... ), if you want the godly language it would be Rust, anything about Rust will be upvoted.

As for Java, it's a good language / runtime that is overly complicated behind layers of abstraction. Take Spring for examnple, magic everywhere, add an anotation there and it does x.y.z, you can't see it in the code but it does something.

The hype around Rust is largely justified. There. I said it.

Rust is nowhere near perfect. It's also supposed to be a systems language- it was probably not originally intended to replace languages like Java for general "app" development.

But it's so much better of a language than most of the higher level languages you see in popular use: Java, PHP, JavaScript, Python, etc, that people are actually willing to deal with the lack of garbage collection just to get to use its excellent type system and well-designed standard library API. I think that says a lot about Rust and the programming language environment today.

It surprises me that we don’t yet have an applications language with an ultra-modern type-system. It’s so strange that the current leaders are a scripting-transpiler (TypeScript) and a systems language (Rust) - but not an apps language in-between.

...then again it’s understandable when you see how the traditional apps languages (C#, Java, etc) are severely hobbled by their VM/runtime, because that’s usually the source of constraints on their type-system. Kotlin and F# do some neat tricks to work-around the limitations they inherited from the JVM and the .NET CLR respectively, but I think we’ve reached the limits of what platforms originally designed ~25 years ago can reach. I just don’t see the JVM nor the CLR getting anything like first-class support for higher-kinded types or true algebraic types: there’s too much pressure from establishment banks and insurance companies not to break their decades-old codebases maintained by outsourcing companies. Languages like Swift and Go are free to break their own molds because they’re happy not leaving a legacy, but what will that mean for Rust? Systems code is the last place you need major breaking changes, but I don’t see Rust’s progress slowing down the way ISO C++ languished for almost 20 years. Hmmm.

The situation is changing in the CLR land, now that they can just say that legacy codebases can stay on .NET 4.x, and .NET Core is where all the fancy development is happening. There are already some C# language features that are Core-only because they require the corresponding runtime changes.

The other thing is - people always forget that, unlike JVM, CLR has more than just the object layer. By design, it has enough low-level constructs to compile a language like C++, complete with multiple inheritance, unions, varargs etc. Obviously, you can do pretty much anything on top of that - the only problem is that you won't be able to interop with other .NET code, except through C-style FFI. But, hypothetically, it would be possible to establish a higher-level ABI without baking it into the VM as an object model, thus allowing a reboot without throwing everything away.

> By design, it has enough low-level constructs to compile a language like C++, complete with multiple inheritance, unions, varargs etc

But the CLR doesn't support that. If you compile C++ code to IL then you'll get a compiler error if you use any types that use multiple-inheritance. The CLR's underlying type system is a huge limitation when it comes to using even simple modern ADTs.

For example, in F# you can define a union type, and the union subtypes can contain normal library types, but you cannot define a library type as a union subtype, whereas you can in TypeScript (and Rust too, I think?).

You're confusing managed C++ with C++/CLI. The latter tries to introduce additional constructs to C++, so that it can partake in the CLR object model - and there you get all those limitations like no multiple inheritance. But you can, in fact, compile any random C++ code to IL - just run cl.exe with /clr:pure. The only thing that doesn't work in that case is setjmp/longjmp; everything else is available.

If you want to see it for yourself, take some .cpp file, and compile it with cl.exe /clr:pure /O2 /FAs. The latter switch will dump the IL assembly into the corresponding .asm file. Or you can inspect the output with ILSpy etc. You'll see that it compiles native C++ types down to CLR structs with no fields, but with explicitly set size (via StructLayout.Explicit); and then uses pointer arithmetic to access field values.

It surprises me as well. And to belabor my own point some more: there's a reason the #1 question about Rust from newbies seems to be "Is there a language like Rust, but with a garbage collector?" - sometimes reworded as "Is there a way to turn off the borrow checker?"

To be fair, I think that a decent amount is possible on JVM and CLR. Scala, for as much hate as it gets, has a much stronger type system than Kotlin/Java.

And Swift has its own backwards baggage! It has to work with Objective-C. There's actually a few weird things in Swift that I've bumped into that I'm pretty sure are only there because of Objective-C.

The Rust devs certainly take backwards compatibility very seriously and they guarantee backwards compatibility forever. There will be no breaking changes in Rust except in extreme cases of finding unsoundness or whatever.

There are currently ZERO good high level app-building languages, IMO.

Rust isn't it because of the lack of garbage collection (which is not a point against Rust- just for the "domain" of optimizing for user-space app development).

Swift would probably be it, but it's pretty much Apple-only and I don't expect that to change.

TypeScript is close, but it's held back by needing to work with JavaScript and the JavaScript standard library and ecosystem suck. It also can't do threads.

Kotlin/Java are decent, but not good at concurrency, and having things like different sized integer types is really silly for a high-level language where everything goes on the heap anyway...

Python is slow and can't do threads.

Some languages like Lisps and MLs might be good for building apps if they just had the ecosystem around it. Haskell would be a giant pain the ass because real apps are full of "IO".

So, yeah. It's kind of a miracle that we can get anything done. I guess that's why we get paid so well...

Kotlin is the application language you're looking for (and Scala 3 to a lesser extent). Contrary to what you say it is the best language I've ever used for concurrency. It has it all, structured concurrency, cancelation, Flow, transparency (no await), etc. Regarding your second point the JVM is increasingly using the stack and with the soon complete generics you'll be able to avoid the boxed versions of the primitive types (though they remain useful when you want identity)
> Contrary to what you say it is the best language I've ever used for concurrency. It has it all, structured concurrency, cancelation, Flow, transparency (no await), etc.

I disagree.

Have you ever tried to actually implement something non-trivial that takes advantage of structured concurrency with cancellation? It's pretty hard to do correctly. Can you really tell me off the top of your head what the difference is between `withContext(coroutineContext) {}` and `coroutineScope {}` from within a suspend function?

Coroutines use unchecked exceptions for control flow. Kotlin also uses unchecked exceptions for fatal and non-fatal error handling. Figuring out how all these things interplay when it comes to coroutines and suspend functions has some subtleties that, IMO, are very difficult to figure out from just documentation and blog posts.

Also, Kotlin's standard types are entirely unsafe to use concurrently. The fact that MutableList inherits from List means that a function that accepts a List parameter CANNOT assume that the list wont change while the function is executing. So if you write `if (list.isNotEmpty()) { doSomething(list.first()) }` - that's a race condition because the list can literally become empty between the if clause and the body.

"But, wait! You should have just been smart enough to make a copy of your List before sending it between threads/coroutines." Okay, great. Let's do full copies of potentially-large collections. Thank goodness Kotlin is so concurrency ready that the standard collection types are persistent .. colle..ctions... oh.

Kotlin's concurrency story is really not that awesome. Scala is better, but still not perfect. Clojure is better still. Rust is good. Elixir (or anything with some kind of actor framework, I guess) is good. Haskell is good.

But I agree, overall, that if I had to pick a best app language today, it's either Kotlin or Scala, or Swift if you're writing for Apple stuff. I'll admit that I have a glaring experience gap with .NET languages, so I can't honestly say anything about C# and F#.

> Kotlin/Java are decent, but not good at concurrency

Since when JVM languages aren’t good at concurrency?

Also, primitives are usually not heap-allocated, so I don’t see what’s the problem with it. It makes the JVM a beast when it comes to number crunching.

> Since when JVM languages aren’t good at concurrency?

Since it's trivially easy to accidentally mutate data across concurrent contexts (threads). You have to actively remember to reach for Mutexes. You also have do understand how `synchronized` works and remember to actually use it. If you use a class that someone else wrote, you have to dig into their code (if available) to make sure they made their class thread-safe.

> Also, primitives are usually not heap-allocated, so I don’t see what’s the problem with it. It makes the JVM a beast when it comes to number crunching.

You don't need a number crunching beast to write most application-level software. That's my point. It's great for number crunching that you have byte, short, int, long as separate primitives. You know what most applications actually want? A number that wont magically wrap around to negative-LARGE_NUMBER when you guess the maximum size wrong. Java has fixed-size arrays, too, for super-performance mode. But applications just use List<> that has no size limit that you have to guess. It should do that same for Integers.

Java itself is most definitely not overly complicated. Many frameworks, and created programs are due to enterpriseTM software development, but it’s orthogonal. And spring is a really feature-packed framework which has a solution for almost every business problem that one can face. Of course it comes with a great deal of complexity, but it is a tradeoff. I rather tinker with how to use a given feature than implement it myself, often in an inferior way.
> if you want the godly language it would be Rust, anything about Rust will be upvoted.

That is just better half part, sad part is anything negative about Rust will be heavily downvoted. Most of the time I disagree with Java/Go in their thread it would be just fine but not with Rust.

The same can be said about pro chromium or contra mozilla comments, it's a kind of cargo cultism/echo chamber
You can just stop using Spring. There's plenty of good web libraries out there. Javalin, Spark, Micronaut, Quarkus, heck I'd probably even take JEE over Spring nowadays.
It also gets a bad rep because of Oracle.
Java isn't the worst C++ program I know.
I got burned out by endless JVM preening and devs configuring their runtime options to use 128MB in a 2GB container, or capitalizing their -Xm option wrong, or what have you. So now with cloud computing you have the JVM in a container on a virtual host, all of which have their own constraints to set—matryoshka dolls all the way down.

Like, people always say, "Well that just means they didn't know what they were doing with the JVM"—yes, and it's been a problem for two decades and is about as likely to go away as buffer overflows in C.

Honestly the thing I like about Go, Rust, C++ the most is the resultant binary just runs in userspace, and you can set the constraints there. Even Python scripts don't have to muck with the JVM.

FactoryFactoryFactoryFactory code is the least of Java's issues.

Starting with OpenJDK 11 (and I think available in OpenJDK8 with an optional flag), the JVM will use the memory constraints of the container, so no more -Xmx flags and so on if you are running the JVM in a container environment.
No flag necessary for latest versions of OpenJDK 8, either.