2021-04-11

Java sucks, yes it sucks

Java sucks.

This is a fact. One of the usual reaction is that it can’t be, because it is used everywhere. But widespread adoption doesn’t mean anything — lucky circumstances, inertia, conformism, money, lack of resourcefulness, bandwagon-like machanisms, or alike, help the language to keep its undeserved status.

The fact that a programming language sucks does not mean you can’t build useful and big things with it, especially if the language has “batteries included” — this is good and useful, but it is not a feature of the language (and other languages, especially the “modern” one, comes with batteries included).

I started writing about Java-suckness months ago, and a too long, unfinished article, saw the light. The writing process was scattered in that time frame, so it became a patchwork nobody could be interested in — not that I care too much, especially in this last year.

I collected and started commenting articles about why and how Java sucks, and also why it doesn’t — I commented these to reject their pro-java arguments or pseudoarguments.

The words you’ve just read are all that will remain of those comments: if you really like this topic, you can dig the net yourself and I’m sure you’ll find exactly what you expect.

Also, the following words are everything that was left about a long and pointless analysis of the why and when of the programming languages in the programming-related job world: pragmatically you pick whichever language does the job, whichever language your legacy code base is written in, whichever language your coworkers know and use, whichever language is chosen for whatever reason, like because there’s plenty of fresh and cheap programmers around, and so on.

So, I am not saying you must switch to another language: I’m trying to explain why Java survives, despite its demerits — which are, in my strong opinion, huge.

Please note that the very same flaws could be considered less important in underused languages, or languages usually used in specific context with a different programming “culture”.

Previously on this channel

The “culture” Java nurtures is this: you are ignorant and stupid, and you will stay like this. You can’t understand, so I will oversemplify the world for you.

I claim that this attitude makes people stupid; if you don’t push upward, gravity will make things go downward.

Let’s add things to the Java suckness level

Imports

The import mechanism is impractical and limited.

import org.my.framework.angelic.picture.draw.element.Path;
import org.my.framework.angelic.filesystem.native.file.basic.Path;

Ops. There’s a problem. You may think you can solve it like this:

import org.my.framework.angelic.picture.draw.element;
import org.my.framework.angelic.filesystem.native.file;


// use basic.Path
// use file.Path

But you can’t.

Can you rename type like the following?

import org.my.framework.angelic.picture.draw.element.Path as DrawPath;
import org.my.framework.angelic.filesystem.native.file.basic.Path as FilePath;

No.

Or maybe:

import org.my.framework.angelic.picture.draw.element as drawing;

// drawing.Path

import org.my.framework.angelic.filesystem.native.file.basic as fs;

// fs.Path

Neither.

No surprise if other JVM-targeting languages tried to fix this problem. Kotlin is an example. And Scala too. It’s not because they are more modern and/or recent. Certain ideas are obvious, old, well known. Also, there are bad experiences from languages older than Java. C and C++1 are great, but the #include mechanism is very primitive2, also when compared to the broken Java import.

Java import is even worse when you use * without knowing what’s brought in scope. Even if you know what * takes, you don’t know what * will take in the future — but you can stick to the version of the angelic framework you know and that works for you, of course (using Maven, or Gradle, or whatever, you can, and likely should or must, specify a version).

If you want to see what could be great for programming in the large, take a look at Ada. Regarding this, it is really on another level.

On filesystem structure

The module/package system imposes the hierarchy of source files and folders on the file system of a Java project. The logical hierarchy you have for your project must be reflected on the file system.

Is this bad or good? Maybe good, or at least not too bad, but I don’t like it. I prefer some degree of freedom: the “physical” layer shouldn’t necessarely mimic the “logical” layer. At the end, it could come in your way, especially if you don’t use an IDE.

Indeed, Java is a programming language for which an IDE is not just an useful tool which is cool to have, but a tool without which you won’t be able to program anymore.

No properties

Java has no properties. Conventionally you write your own getters and setters like getMyVariable() and setMyVariable(MyVarType value). Conventionally “they” decided that getters have prefix get and setters set, which makes sense, and usually plays well with the camel case convention.

Writing getters and setters for each instance variable is annoying and boring. An IDE can help fixing this problem with the language; the IDE can generate getters and setters, and hopefully it can keep the code in order — nonetheless your class will be polluted by trivial getters and setters.

Since Java is used so much and its flaws give headaches to every decent programmer, people had the idea to fix the language in several ways. In these days I’m using Lombok: there are annotations which can generate getters and setters for you (and other stuffs). Since they are annotations, you won’t see actually those getters and setters: your source code will stay clean.

Before this epiphany, or in those cases when you can’t use libraries like Lombok, you must stick to the use of an advanced IDE: without it, anyway, programming in Java is more painful than in other languages. In Java, the IDE is not just helpful: it is essential. To program in Java effectively, you need the constant presence of an automated assistant.

Tricks like those of Lombok help you to keep the source code clean.

Strong typing my beeeep

Why do types exist? Why should we care?

A string can be many things, and many things fit into a string. Your name and your password, for example. Your age and your postcode can be both numbers. For strings you are going to use String, and for numbers maybe int. The age can’t be negative, so you likely want an unsigned int, which Java hasn’t, as already said (Java and its silly war against unsigned integers), so we must be ok with int.

And we are ok with String, except that sometimes we would like to have a proper type for a name and a proper different type for the password. A proper type for an age, and a different proper type for a postcode.

You can’t. Java has nothing like typedef in C, and imagine, nothing like Ada3, of course. Other strongly typed languages allow to create new types. Aliases for existing types would be fine, if we can’t have more.

All you can do is creating classes. Deriving a class from String, it could work — let’s be happy with what we have! But you can’t. So no, no happiness. You can forge your own class to hold the String value, though.

That wouldn’t be just a new type, a String in disguise: it would be a full class.

There’s a difference, but do Java programmers care? They don’t. And they shouldn’t bother of all the signed/unsigned thing, neither. It’s technical stuff… Let a Java programmer be a Java programmer…

In C# it would be something like

using NameType = System.String;
using Age = System.UInt16;

Similarly C and “old” C++ can use typedef; modern C++ (since C++11) has using, maybe inspired by C# (for the syntax, not for the idea, which wasn’t invented by C#).

I mention again C# for two reasons:

  1. it can be seen as a sort of competitor of Java;
  2. it’s a Microsoft product and I think Microsoft is evil (Gates made serious damages to the digital world with his “creature(s)”, and now he’s working hard on the analogic world level) and produces bad software.

And yet, I think C# is a far superior programming language than Java.

No partial or similar helpers

This is minor, but it piles on the stack of clues which scream “Java sucks”.

Java does not allow to separate implementation of methods of a class. Everything must stay inside a class which must be in a file named after the class name.

Sometimes it can be convenient to put code in a separate file, even when the code belongs logically to the class (contained in a file).

I suppose Java solution is to create other classes (maybe called Something + Impl…), but that sucks.

Ada, which is my “model” when thinking about “programming in the large”, has the separate keyword. C# has partial. As Microsoft docs point out, it can be useful for:

  • “concurrent” programming on the same project
  • automatically generated code4

But also, just to keep the code of a method, maybe a longer than usual one5, in its own file, plain in sight.

No operator overloading

This isn’t horrible, that is, it isn’t a main point — other better language has this “limit” too; yet, operator overloading in an OO language isn’t a bad idea, I think (OO-as-it-is-known might be a bad idea itself, though). Each operator could be seen as a shortcut for a method (there are languages that do so); that is, x.plus(y) would be the real, canonical way, while x + y is just a shorter, more clear way of writing it (the familiar infix notation).

Java has no operator overload by the user, but at least one operator (as far as I know), +, is intrinsecally overloaded. In fact, you can do 1 + 2, obtaining 3, and "a" + "b", obtaining "ab".

Hence we can say it another way: Java has operator overloading, but it is not available to the programmer. Because Java programmers must be treated as if they were so dumb to abuse that power for sure (in fact they don’t get signed/unsigned math correctly neither…!)

No actual namespaces

Java scoping facilities are basic. About namespacing, it’s just package, and nothing else. Packages and classes provide namespaces, and that’s all.

Another minor point that makes the stack taller.

What about Uniform Access Principle?

If you like the UAP, apparently you are fine with Java. In fact, getters and setters are the only way to access attributes and you don’t know if a getter just read an attribute, like this:

public ReturnType getValue() {
    return this.value;
}

or if it does something different:

// this doesn't make sense, ...
private ReturnType computeValue() {
   ReturnType localVal = this.value + Service.valueChange();
   this.value = localVal - Service.ReadFromSpace();
   return localVal;
}

public ReturnType getValue() {
    return computeValue();
}

If attributes are all private, as they should be6, you can read or write them only through methods. The access is uniform, and you can’t tell what’s behind; thus, I suppose we can say that Java doesn’t hinder the UAP, or that Java “tolerates” the UAP by a combination of limits (no properties) and common best practices (you shall never have a public attribute).

Objects are always by reference

This is a serious one.

In Java there aren’t pointers. Except that indeed pointers are everywhere, but the syntax doesn’t make it blatant: objects (instances of a class) aren’t values but actual references (pointers), so that the following code is aliasing a:

                Baz a = new Baz();
                Baz b = a;
                a.setValue(5);
                b.setValue(10);
                System.out.println(a.getValue());

The output is 10: Baz b = a does not copy a: it assigns the pointer a to the pointer b. The fact that there are pointers behind the curtains is obviously ok: it’s just a detail of the implementation. But Java tries to put it under the rug, except when huge problems pop up if you are na├»vely believing that objects are values and you ignore the truth about what a and b really hold.

It’s a horror. And it becomes worse when you pass an object to a function/method: the callee can modify the object using its mutator methods (e.g., setters), and there’s no way to establish a contract between caller and callee.

The next section expands on this huge flaw.

Methods can do whatever on parameters

When you call a method passing an object as parameter, you can’t stop the callee from modifying the object, that is, from calling methods which modify the object — of course you can’t stop the callee from assigning to public non-final members.

Objects are references, not values, so you are passing references as parameters.

There’s no way to signal an error at compile time; there’s no way to make it an error/exception thrown at runtime. There’s no const to say that a method does not modify an object state, there’s no const (or in) marker for function/method parameters.

Maybe there is a way to fix the problem… but I don’t even care to check, unless it is something like const or in or alike.

Java sucks to the point that I can’t establish a “contract” between a caller and the callee. I can’t say: “look, this is the reference to object a, which it is a constant for you”… that is, the callee can’t call mutator methods of an object given as parameter (like obj.setValue(10)).

And this is bad because a variable holding an object is a hidden pointer (or reference, if you prefer).

To avoid problems and keep your objects safe, you must deep copy the object and pass the copy. Then, you can stop worrying about what the callee does with its parameters. Unless the called mutator method has also side effects outside the instance!

Copying complex objects containing other objects — which are references and not values — can be tricky (deep copy or cloning opposed to shallow copy). If only objects had always a copy constructor which would be called automatically! If only objects would be value, not pointers! If only Java wouldn’t hide this truth! If only I could classify methods as const or mutator and establish a contract between caller and callee!

If only…

Cloning7 (deep copying) can be a costly operation. But if you don’t trust the callee, it’s the only thing you can do.8 Except that it won’t save you when methods have side effects outside the object.9

And there’s anyway still a problem even if you give just getters in a wrapping “safety” class: in fact…

Escaping the jail

This is actually the third part of the problem behind the fact that in Java instances of a class are indeed references, and there’s no way to mark methods as mutators and to benefit from contracts between a caller and the callee.

Let us consider this:

@Getter // this implies the use of Lombok or similar
class Something {
    private long id;

    // ...
}

Suppose you have an instance of Something and you call a rogue function:

    // ...
    rogueMethod(whatever, something);

The rogueMethod() must just read the id, it shouldn’t modify it, but for its purposes it increments it:

    // ... inside rogueMethod()
    long something_id = something.getId();
    something_id += 1;
    doSomethingWithNextId(something_id);
    somethingElse(something_id, whatever);
    // ... more jibberish

This is fine. Nothing dangerous happens. Now let’s see another case: we have Long instead of long. The rogueMethod() will contain something like:

    Long some_id = something.getId();
    some_id += 1;

And this is still fine — note that Long is a class, but a special one, and it behaves like long would (this means +, +=, … are overloaded…).

We can check it like this:

    Long id2 = something.getId();
    if (some_id == id2) {
        System.out.println("It smells!");
    }

Wait… are we comparing references, or values? Just to be sure, replace the line containing some_id == id2 with

    if (some_id.longValue() == id2.longValue()) {

But Long is special, so, let’s forget it and play with our fake long class:

@Data
class MyLong {
        long value;

        public MyLong(long v) {
                this.value = v;
        }

        
        public void incBy(long i) {
                this.value += i;
        }
}

Now our Something class will use MyLong and the rogueMethod() will contain:

    // ... inside rogueMethod()
    MyLong someId = something.getId();
    someId.incBy(1);
    doSomethingWithNextId(someId);
    somethingElse(someId, whatever);
    // ... more jibberish

Now let’s add a check like this:

    if (someId.getValue() == something.getId().getValue()) {
        System.out.println("smell!");
    }

This is inside the callee. The caller can compare the original value with the value held after the call and it will print the smell word anyway.

It’s because the getter method gets the reference to the object. And once it has the reference, calling a mutator on that reference will change the very same MyLong instance contained in the Something instance.

So, in this case, you can see that if you give a getXXX() method, and if XXX is a non-final object, … you’ve opened your class to a potential world of trouble. There’s no way to check if a callee misbehaves.

In order to close properly this “chapter”, we need to stress that this is a serious flaw, likely the most serious one.

Are lambdas proper closures?

Java’s lambdas are half-closure. The mix between lambdas (half-)closures and the objects which are, indeed, references, doesn’t look exactly a safe harbour. Unless you put limits, like: you can bind only on effectively final variables.

The following example compiles, and it is problematic — of course you can always say “don’t do it and you’re fine”.

That is, do not introduce bugs, and your software won’t have bugs!

The problem here is that you are closing on a reference and there are no signs to remember you that fact: c is a value, and this value is indeed a reference, not an object…

import java.util.function.Function;
// ...

       public static Function<Integer, Integer> doSome(Baz c) {
                c.setValue(11);
                return (a) -> { 
                        int t = c.getValue(); 
                        c.setValue(t + a); 
                        return t; };
        }

Baz is a class with an usual getter and setter for value. The method doSome() returns a lambda/closure which binds the reference passed as parameter to doSome().

Note:

  • I had to use Function<Integer, Integer> because you can’t make lambdas with primitive types: they need to be references; objects, as said, are references/pointers — this is hidden, except in error messages (by javac or runtime, e.g., null pointer exception…)

Now, if the parameter to doSome() is a copy of a Baz object, the GC can’t collect the object until the lambda is in scope. Basically we’ve built a function with an internal state kept by an object which is unreachable elsewhere.

var y = doSome(new Baz(b));

y is the lambda which has access to the copy of b (Baz has a copy constructor, and b is another instance of Baz).10

This is the happy case. If we pass b

var y = doSome(b);

we have access to b, and also we have a lambda, y, which can modify b, but we don’t need to know it, since we are just client code using doSome(). Of course this is bad, or maybe not… anyway, the main problem is, again, that we don’t know what doSome() does with our reference b: we don’t have control over how doSome() is allowed to use that reference… We, as client code, don’t even know how y do whatever it is designed to do. In our case we have the code, and we know that y.apply(10) is going to mess with our b.

But when you are using libraries you don’t have the code of… you want the callee be bound to rules. Java can’t help you with it: you need to trust it, or be defensive while programming.

To make things ok, there must be discipline on both sides. Odd for a language designed by experts for non-experts (citation from a pro-Java article).

Hell of frameworks…

This is complicated to summarize, and questionable. In a note11 I try to explain why frameworks can be a problem, in my opinion. But I’ve found another interesting, short point of view in the book “The Go Programming Language” (Alan A.A. Donovan, Brian W. Kernighan):

Furthermore, although frameworks are convenient in the early phases of a project, their additional complexity can make longer-term maintenance harder.

Enough said, even if it doesn’t apply always to every project and for any framework.

Miscellanea of other Java-sucks stuffs

No List Literals

It’s pretty incredible that it isn’t, or wasn’t, so obvious and the language had (or has?) this hole.

Concurrency

I think too many languages have “concurrency done wrong”, or at least they haven’t embraced a more high-level way of allowing or helping programmers to write concurrent, correct code. There are enough experiences to show what can be done: Ada, Erlang, Go, Raku… Just to name the one I’ve met, but I’m sure there are more.12

In Java I’ve just played a bit with CompletableFuture. It’s a start, but still far from what I’d like once I’ve played with Erlang, Raku, Go, and Ada.

Culture

In a pro-Java article I’ve found this:

The idea was to make a language which prevented novice mistakes and abuse. Java was basically a language designed by experts for non-experts.

Mission failed. Novices make all sort of mistakes and abuse. You can’t avoid that. O, well, there’s a way to reduce the risk: ① help the novice to become expert, ② do not treat people like dumb who can’t learn (e.g., to handle signed and unsigned digital arithmetic properly), or they’ll become dumber.

Java is like an arrogant silly adult which treats children as children who can’t learn and grow, by design. That’s the best way to make the worst programmer: shield them from the hard fact that programming isn’t that easy, after all.

In general poor design also happens, and the Java language culture fosters it.

In some articles I’ve even found that there are users confusing the compiler with the IDE. Since in Java you almost can’t do anything without an IDE, it is understandable.

Conclusion (for now)

Java sucks. The more I use it, the more I confirm it. People might disagree because they swiftly put together a working Product using tons of cool frameworks, some clicks, some copy-pasting, few digging into Stackoverflow… And everything automagically seems to work.

But it’s a fallacy to believe that this easiness tells us that the Java programming language does not suck.

It sucks, that’s a fact.


  1. I hated/hate C++, indeed. With C++11 it started to be acceptable: it seemed they took a good path, but now it seems they are overdoing something, with all that new standards which sometimes fixed bad decisions taken in the previous, fresh standard. C++ isn’t an easy language, and because of that the committee needs to be very much more careful before publishing a standard with features which need to be deprecated or corrected in the next iteration. It seems like they are in a rush, maybe to show that the language is alive and kicking and following the stream towards modernity (i.e., how current modernity is perceived…). But if they don’t “settle”, developer won’t adopt easily a new standard: they will stick with an old, stable one for which a stable compiler exists.↩︎

  2. C++20 introduced modules. C++20 is a 2020 standard. Better late than never, but yet… It’s odd that they woke up so late. Why? I don’t know. Do usual C++ programmer feel the need for modules only now? Do they hope C++ will have a larger user base thanks to the features they are adding?↩︎

  3. Talking about the type system here. Ada can’t be beated easily on this ground. Curiously, however, Ada’s interfaces were inspired by Java, so we can say that Ada has something of Java…↩︎

  4. This is my encounter with the partial in C#: I had a project in which a big part of code was generated.↩︎

  5. A learned one could say that if the method code is so long to need its own file, it is too long and it must exist a way to make it differently, maybe decomposing it so to achieve the desired shortness per method. This can be by the book, but it doesn’t fit necessarily well with every situation. Anyhow, every single method of a class can be of the “right length”, and altogether all the methods make a lot of code in the class, and you maybe would like to split it up.↩︎

  6. One can wonder what a public Type attribute is for. This question on Stackoverflow has answers. Not totally satisfactory to me, but better than nothing.↩︎

  7. Object has a clone() method you can override. And you need to say that your class implements Cloneable.↩︎

  8. You can imagine solutions which are worse than the hole in the Java language. For instance, you can have two “version” of the same class: one with private setters, and another one with public setters. This is, of course, a horror.↩︎

  9. Imagine a class with a method that modifies a state of the object and then dumps certain values held by the instance into a database table. Not all clients should be allowed to call that method. In Java, you can’t avoid this explicitly. You can’t establish a contract saying that the client shan’t call that method, or similar methods, or otherwise an error (hopefully at compile-time) will be given.↩︎

  10. var is the Java 10 (and later) thing that allows you to use type inference. I suppose you’ve noticed that the compiler always know the correct type of a variable. So it can internally infer it and check if you wrote the correct type for y (in our case). Now this can be exploited in the language (it isn’t always so “easy”, but for practical purposes this explanation is what it is). Of course Java isn’t the first language to have this. There are languages which are born with type inference (especially functional languages); other languages, like C++ with auto and Java with var, added it later.↩︎

  11. Frameworks… Sometimes the “leaders” for certain patterns are smart explorers who created a whole world (a “framework”) to solve a problem or make common needs satisfied. These frameworks can do their magic well enough, but in their way. Once established, everyone does the same thing in the same way, without even questioning if there are better ways, if there are other possibilities, alternatives. One is forced to enter the mind or minds of whoever wrote the framework. Since then, everybody start to believe that that’s the way. It becomes the common pratice, even when it doesn’t fit that well. You don’t want to reinvent the wheel, that is, you don’t want to spend time solving the problem B to solve the problem A, when problem B is already solved. Maybe B isn’t solved well, you would like to have something different, but it would be a big project of its own; and there are deadlines, costs… so, B is solved: you won’t write your solution for B. If you are lucky you can pick among B1, B2, B3… Which one is better? It doesn’t matter: the real question is: which one is more used and known? Sometimes it is a matter of support (maybe just by a community). You now depend on that framework, and you don’t even need to understand how it works — it would be a waste of time. ¶ So, you picked B and… Can you switch from B1 to B2 seamlessly? Maybe not. And then you’re a captive of the chosen solution: you won’t change it, unless you really, really need to. ¶ This is how things get stuck. Add the “everybody use this” trait, the tons of examples, documents, supports, and so on, that this widespread usage implies, and you should see how even shit can become the “enterprise solution” everybody’s using and buzz-talking about. This isn’t to say that you should write always everything from scratch; but you should ponder your gears and your dependency carefully, and understand them in depth. Which for certain frameworks can be rather hard and time consuming. Then you use B (or B1, B2, …) without an actual knowledge of it. Then it can also happen that you become a slave of a framework. Let us suppose its authors start changing things, deprecating API or whatever. You could stick to the version you began with. But then, will you find help for the older version? Will new-found bugs be fixed? Will you be forced to upgrade? Successful frameworks maybe are careful and don’t trash their clients with sudden incompatible changes. But, anyway, there is a problem.↩︎

  12. See also Communicating sequential processes↩︎

No comments:

Post a comment