2021-10-04

What is wrong with Java


This is part of a series of posts in which I am documenting what is wrong with certain popular programming languages that I am (more or less) familiar with.  The aim of these posts is to support a future post in which I will be describing what the ideal programming language would look like for me.  

I will be amending and revising these texts over time.

What is wrong with Java:

  • The garbage collector.
  • Curly braces.
  • No user-defined value types.
    • Note: Since version 14, Java supports records, but they are still allocated on the heap and passed by reference. So, an array of 1000 records which would be a single memory block in C# is 1001 memory blocks in Java.
  • No conditional compilation.
    • Cannot even externally supply the value of a constant.
  • Generics are decent, but still lacking.
    • Type erasure allows unsafe constructs which may result in "heap pollution".
    • Type erasure makes it impossible to disambiguate entities based on their generic parameters, thus making it impossible to overload based on generics.  This forces us to give artificially different names to entities that would ideally share the same name.
    • Working with generics inevitably requires either littering the code with `@SuppressWarnings( "unchecked" )`, or entirely disabling the "unchecked" warning, which opens up another can of worms.
  • No C#-style properties.
  • No operator overloading.
  • No namespaces.
    • Packages are ill-conceived and lame.
  • No C#-style assemblies.
    • Individual class files scattered all over the place are cumbersome to work with.
    • The filesystem/jar-file duality is very cumbersome to work with.
    • Packages are unrelated to packaging.
    • Jar files only deal with packaging; they offer no support for specifying what is exported and what is kept private.
    • Modules were added as an after-thought, and the unit of publication is still the package.
  • Class loaders are lame.
    • They, as well as many other language features, are a relic from the java web applet era.
    • They are very cumbersome to work with.
    • They unnecessarily impose a significant performance penalty by doing a lot of work on a per-class basis instead of a per-module basis.
  • Lame access rules.
    • Everything that is package-private is also protected. (Duh?)
    • Inner classes have access to private members of their enclosing class; this might be okay; however, private members of inner classes are also accessible by their enclosing class, which is definitely wrong.
  • Member initializers have no access to constructor parameters.
  • Member initializers execute between the invocation of the super constructor and the statement that immediately follows the invocation of the super constructor, which technically makes sense, but these jumps in the flow of execution are completely counter-intuitive to the novice programmer.
  • The syntax for invoking the super constructor suggests that one might be able to insert statements before the call to super, but this is not the case. (The deviation from the C++ syntax would be justifiable if the new syntax had something to offer, but it does not.) The language falls short of doing the one sensible thing which would be allowed by this syntax, which would be to allow code before the call to super, as long as this code does not try to access `this`, for example assertions on the constructor parameters before passing them to super, but no, you cannot do that.
  • No optional parameters to functions. (No default parameter values.)
  • No named parameters to functions.
  • No compiler intrinsics like __FILE__ and __LINE__.
  • No member literals and not even a 'nameof' operator.
  • No nullable/non-nullable semantics for reference types. (C# 8 does a fairly decent job at that.)
  • No assignment expressions. (`while( (line = next()) != null )`)
  • No nesting of methods within methods.
  • No redefining of names (as with the `new` keyword of C#)
  • The long history of the language inevitably means that there are some bad choices of yore which interfere with newly introduced features. For example:
    • The ability to use the same name for a field and a function never really offered anything of value, but it did necessitate the introduction of the cumbersome double-colon operator when function references were added to the language.
  • Checked exceptions were a good idea in principle, but turned out to be too cumbersome in practice. With the advent of lambdas, they represent nothing but hindrance.
  • Collecting a stack trace (and therefore also throwing an exception) might not be as slow as it is in C#, but it is still unnecessarily slow, and prohibitively slow for some purposes.
  • The built-in collection model is very outdated and lame.
    • Arrays do not implement any of the collection interfaces so they always need special handling.
    • The `Iterator` interface is lame.
      • The `hasNext()` and `next()` methods are unusable in a for-loop.  (A for-each loop can be used with an `Iterable`, but then you have no access to the `Iterator`.)
      • A filtering iterator cannot be implemented without cumbersome look-ahead logic and then it is impossible to use it for removing items from the collection because looking ahead means that you are always past the item you want to delete.
    • Lack of unmodifiable collection interfaces means no compile-time readonlyness. 
      • Every single collection instance looks mutable, since it is implementing an interface that has mutation methods, but quite often is secretly immutable, meaning that if you make the mistake of invoking any of the mutation methods, you will be slapped with a runtime exception.
  • Fluent collections (collection streams) are lame.
    • They are unnecessarily verbose
      • They require every single call chain to begin with a quite superfluous-looking `stream()` operation
      • They almost always have to be ended with an equally superfluous-looking `collect()` operation.
    • They are not particularly extensible because they are entirely based on a single interface (`Stream`). Their only point of extensibility is at the very end of each call chain, by means of custom-written collectors.
    • Collectors are convoluted, so writing one is not trivial.
    • Collection streams work by means of incredibly complex logic behind the scenes, so:
      • They are very difficult to debug.
      • They are noticeably slower than C#-style fluent collection operations even before we consider the collection step at the end.
    • The collection step, which is in most cases necessary, is tantamount to making an unnecessary safety copy of the information produced by the collection stream chain.
    • Collection streams are unnecessarily convoluted by having built-in support for the ill-conceived idea that the mechanism used for fluent collection operations should also be usable for parallel collection operations.
  • Various standard classes are implemented in lame ways. For example:
    • All input-output stream classes suffer a performance handicap due to unnecessarily and ill-conceivedly trying to be thread-safe.
    • Input-output functionality is often achievable not via interfaces, but instead via abstract classes with an unnecessarily verbose set of methods, which makes extending them a tedious and error prone endeavor.  (E.g. java.io.Writer, java.io.StreamWriter.)
    • The java.util.concurrent.BlockingQueue interface does not offer any means of block-waiting while the queue is empty. (Presumably due to the ill-conceived notion that it should support multiple consumers.)
    • There is no way to attempt parsing a number and obtain an indication as to whether the parsing succeeded or not, without:
      • Suffering the performance penalty of an exception being thrown 
      • Having to write code that catches the exception and takes notice that the parsing failed.  
(And funnily enough, even though the Java runtime makes liberal use of checked exceptions everywhere, the parse-failed exception is unchecked.)
  • The for-each loop does not do anything about closeable iterators. (The for-each loop of C# properly disposes disposable enumerators.)
  • The language runtime if full of always-on error checks instead of using assertions.
  • The inner workings of the language runtime are convoluted, and its performance is hindered, by unrequired operations such as "access checking", "bytecode verification", "protection domains", and even some optional "security manager". (The security manager is finally being deprecated as of Java 17.)
  • No compiler-enforced function purity.
  • No compiler-enforced immutability.
  • Still no string interpolation in 2021.
Note: the above list of disadvantages is kind of long, because I am intimately familiar with the language.

Feedback is more than welcome: you'd be doing me a favor. However, be aware that blogger sometimes eats comments, so be sure to save your text before submitting it. If blogger eats your comment, please e-mail it to me.

No comments:

Post a Comment