5 ms·
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 chu
by ragnese 6y ago
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.
- MaxBarraclough 6y ago> 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 https://news.ycombinator.com/item?id=26538842 [1] https://blog.regehr.org/archives/1401 https://blog.regehr.org/archives/1401
- int_19h 6y ago> 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.
- ragnese 6y agoMy 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_rationalist 6y agoYour 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.
- ragnese 6y agoKotlin 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!