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.
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.
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?
Skipping java and instead using Kotlin allows one to reuse all that knowledge in a great language as well.
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.
Edit: I still think Java is a good language, especially later versions, and both original goals and recent advances are quite noble.
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.
What first-party stuff are you referring to?
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
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 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.
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.
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.
Running ‘dotnet build’ is much more saner and one can reasonably understand what happens.
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.
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.
If you have a hammer everything’s a nail.
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.
I think Java 11 and higher are reasonably terse/expressive without being overly dense.
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.
I think the current future view is Withers ( https://github.com/openjdk/amber-docs/blob/master/eg-drafts/... ).
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...
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.
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.
> 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.
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)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.
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.
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.
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.
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.
In Ada, if range constraints are broken at runtime, a Constraint_Error is raised (or 'thrown', if you prefer). [0] (That's assuming of course that range checks haven't been disabled, which is an option that Ada compilers offer you.)
> I don't see why the default for Java is to have these really nitty-gritty numeric types
At the risk of retreading our earlier discussion:
I think the short answer is performance. Java has lofty goals of abstraction, yes, but it also aims to be pretty fast. If it didn't, its appeal would diminish considerably, so it's reasonable that they struck a balance like this. Same goes for why primitives aren't objects.
[0] https://learn.adacore.com/courses/Ada_For_The_CPP_Java_Devel...
type Unsigned_32 is mod 2**32;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 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.
A more realistic change might be to have Java default to throwing on overflow. The addExact methods can give this behaviour in Java. In C# it's much more ergonomic: you just use the checked keyword in your source, or else configure the compiler to default to checked arithmetic (i.e. throw-on-exception). This almost certainly brings a performance penalty though.
I also agree that a "more realistic" option is to just throw on overflow by default, the same way we throw on divide-by-zero.
But that won't happen either. :/
> 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.
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
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?
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'm sure there aren't. And truthfully, I understand that Java was supposed to be efficient enough to run on small devices and whatnot. But if you look at the evolution of the language as well as where it's mostly used in recent history (no more web browser applets, for example), it seems to me that it has a bit of an identity crisis. Is it the low level implementation language of the JVM platform, or is it a high level app development language?
> True, if you increment a number, you will typically expect the result to be greater.
I'd say this is a pretty big deal for people who are reasoning about code.
> 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.
Agreed. But I've almost never seen code that actually checks value ranges before and after math operations. And Java doesn't make it easy or efficient to do a "newtype" pattern so that the types actually are limited in any meaningful way.
Instead, most enterprisey backend systems I've worked on just accept a JSON request, deserialize some `quantity` fields to an `int`, and go to town with it.
> Or put differently, I bet 99.9% of ArrayIndexOutOfBounds errors are because indices leave their legal range without ever overflowing int.
I'm sure that's true. But indexing a collection is not the issue I was thinking about.
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.
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 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.)
I'm not advocating for object oriented programming. What I'm saying is that if you "buy in" to the actual, abstract, concept of object oriented programming, then the internal structure or state of the object you're communicating with is, by definition, out of your control. Of course, in practice, you know that sending a "+ 3" message to the object "Integer(2)" is always going to return the same result, but you have no idea if the Integer(2) object you're talking to is logging, writing to a database, tweeting, or anything else. And in "true" OOP, you're not supposed to know- you just take your Integer(5) response message and go on your way. When I say "true OOP" I'm thinking about something like Smalltalk or an Actor framework/language.
I'm not talking about anything practical here. Just the "pure" concepts. Obviously, Java has made pragmatic choices to allow escape hatches from "true" OOP in a few places: unboxed primitives, static methods, and a handful of other things, probably.
So it's just very un-Smalltalk-like for an object's API/protocol/contract to make any kind of reference or promise about its internal state at all. That is implementation in a pure OO sense.
That's not specific to OOP though, it's a very general concept in programming.
A program is generally decomposed into smaller units which make some promise about how they will behave, hiding their internal workings from the programmer who makes use of them. This is just as true for C/Forth/Haskell as for Python/Java/Smalltalk, depending on how a program is designed.
> you have no idea if the Integer(2) object you're talking to is logging, writing to a database, tweeting, or anything else. And in "true" OOP, you're not supposed to know- you just take your Integer(5) response message and go on your way
Right, you're meant to interact with an object in such a way that you rely only on the documented behaviour that the object promises to provide, you aren't meant to rely on knowledge of its internals. Objects are also a good way of cleanly separating concerns, and then composing the solutions.
On further thought I got it wrong earlier. You're right that internal state isn't my business, but immutability isn't about internal state.
Whether String (or some other class) is mutable or not isn't an implementation detail, it's an important property of the public interface offered by the class, and it's only a property of the public interface. I don't care whether my JVM implements String in Java or in assembly code, neither do I care if it's immutable internally, but I do care that the implementation satisfies the advertised behaviour of the class, and String promises to be (that is, to appear) immutable.
The internal implementation is required to meet the constraints imposed by the class's public interface, and in the case of String, those constraints include that the class must appear immutable to the user, even under concurrent workloads. In principle the implementation is permitted to have mutable internal state, provided the object always appears immutable to the user.
Similarly, whether a class is thread-safe, is a public-facing attribute of the class. The class can implement thread-safety any way it wants.
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.
https://docs.oracle.com/en/java/javase/15/docs/api/java.base...
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 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!
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.
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.
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.
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.
...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.
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...
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#.
That's... horrible.
Why can't Kotlin do it the way C# does with heap-allocated state-machines?
But, as an "end user", the exception thing drives me absolutely nuts. It wouldn't be nearly as bad if Kotlin either didn't use exceptions for ALL error handling, or even if it had checked exceptions so that "normal" errors would be, mentally, separate from fatal errors and/or coroutine cancellation.
Yes, but it's opt-in (it requires the author of the async method to add a `CancellationToken` parameter and to respect `IsCancellationRequested`).
C#/.NET comes with language-level mutexes (`lock`) and the .NET library has thread-safe generic collections (ConcurrentDictionary, ConcurrentBag) and true immutable collections (ImmutableArray, ImmutableList, ImmutableDictionary) with optimized copy operations (e.g. ImmutableList.Add is O(1), but ImmutableArray.Add is O(n)). It's a nice addition to the library with only a few warts.
Java/Kotlin also have mutexes- just not as language built-ins (well, it does have `synchronized`).
They also have a ConcurrentFoo set of collections as well. And actually an ImmutableMap (but I don't see ImmutableList, etc. Why?).
The "problem" is that they're opt-in.
I spent years writing multi-threaded C++. But I've become very spoiled with modern languages that make concurrency safe(r)-by-default, such as Rust, Clojure, and Elixir (I haven't actually used concurrent Haskell).
Honestly, it's not anywhere near as bad as it used to be. The ecosystem(s) have embraced immutability everywhere that it's possible, so you don't have to worry quite as much about accidents.
.NET doesn't have an ImmutableList either (the ImmutableBag type is an unordered collection). This is because unlike with hash-tables you always need to lock the entire structure when mutating a List/Vector (with hashtables you only need to lock the specific bin/bucket).
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 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.
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.
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?).
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.
Anyway, the point is that it's possible within CLR constraints. There's just not much demand for it, and it's effectively a whole separate CPU arch for the compiler to support, in addition to x86/x64/ARM.
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.
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.