4 ms·
I can't help but notice that the majority of the libraries he mentions are Java-first. I don't have an extremely strong argument that this is the root of many
by ragnese 4y ago
I can't help but notice that the majority of the libraries he mentions are Java-first.
I don't have an extremely strong argument that this is the root of many of his problem with version mismatches, but I do have a strong hunch that it is.
I've spent a little bit of time with Scala, a little bit with Clojure, and a lot with Kotlin. My biggest conclusion from all these years of working with non-Java JVM languages is that Java interop is actually a language mistake. Or, rather, Java interop ends up being "design debt". It's great to bootstrap your new JVM language into popularity because people can try to gradually switch from Java or use mature Java libraries when your $NEW_LANG ecosystem is still nascent. But, the problem is that Java libraries are written in Java style and are written within the constraints of Java and Java's type system.
The most obvious (albeit likely the least problematic) example is null in Kotlin and Scala. Both languages have superior ways to handle the "bottom type" to Java, but a Java library obviously doesn't get to use those.
The other problems come from how much information can be expressed at compile time. The biggest design flaw in Java as a language is that it combines type-erased generics and runtime type reflection/inspection. Either one of those is fine, but they are quite literally incompatible concepts. You can't apply type-based logic on a type that doesn't exist.
Yet, because of how unexpressive Java's type system is, any sufficiently complex Java library/framework will make ubiquitous use of type reflection. Usually while leveraging annotations.
I can't tell you how many type bugs I've found in the JacksonXML Kotlin compatibility module and in Vert.x with Kotlin. Nulls sneaking in where they aren't supposed to, value classes randomly causing issues because something boxes up a Function object and then uses reflection somewhere surprising, things not serializing correctly because JacksonXML or Gson doesn't understand some perfectly valid type I've defined, etc.
But something like JacksonXML should never exist in Scala. Scala has type-classes, so you can define your serialization and deserialization logic on the type itself. Then it can be checked at compile time. For Java, defining and passing around a separate serialization object for each type would be too cumbersome, so all of the serialization libraries use reflection and just hope they can figure out the deserialization at runtime. When you find out that it can't because your app crashed, then you can backtrack and "teach" Jackson/Gson how to deserialize your troublesome type with a custom deserializer class and an annotation.
And it's not just the type systems being incompatible. It's also the style and idioms. Java frameworks really like to be "magical" and hands-off, because it's so verbose to define and pass around separate classes for everything.
I believe this is also manifested in the OP's issues with libraries depending on other libraries and causing version conflicts. If Java libraries were less "plug and play", they would ask the user to provide logic, instead of saying "Don't worry, I'll do everything for you under the hood."
Is this just a cliché Java flame comment? Maybe it is. But it's based on my many years of dealing with Java and every popular JVM language (excepting Groovy; I don't have any experience with it outside of gradle).
I now have a policy that if I'm working on a Clojure, Kotlin, or Scala project, Java dependencies are nearly forbidden. It's a very high bar to include a Java library dependency, and if it uses generics or reflection, it's basically an instant "no".