I Do Not Hate Java …
I Do Not Hate Java …
People may have the impression from some of my other posts that I “hate” Java, either as a programming language or as a development ecosystem or both.
I don’t.
Java is a relatively simple programming language with plenty of already written, easily available, and well tested code. That allows a competent developer to write software for clustered and distributed environments without needing to understand every detail of how it works once deployed.
That’s not to say it’s simple, but it’s close to as simple as possible without being more simple than possible. Much of the difficulty learning Java comes from the sheer number of frameworks and libraries. Those coming from a .NET world are often initially overwhelmed.
Newish additions to the set of particularly useful tools include WSO2’s MSF4J, a webservices framework for Java using OSGi that starts up the same service in a tenth the time of SpringBoot and a fifth of Express.js, making atomic microservices far more practicable; Payara Micro, a Glassfish based implementation of the Eclipse MicroProfile that provides much of the meat of Java EE in a 70 MB runtime; Java EE 8 and Glassfish 5; Apache River 3.0.0, a long overdue update to JINI 2.1 that adds some modularity (though in Apache’s defense, there wasn’t much wrong with JINI 2.1 in the first place); and, of course, Java 9, although it will take a bit of work to make some programs compatible.
The modularity of Jigsaw just in the base speeds up the language significantly, particularly at startup, while also enhancing reliability.
The number of technologies within those umbrellas gives an indication to people not familiar with the Java ecosystem of the wealth of technologies available and the quality of many of them.
To be an expert in Java is difficult, more difficult in many ways than in languages more generally considered difficult. But most developers in Java don’t need that level of expertise. Developers at that level are generally working on more difficult to solve problems, which are almost by definition niche, rather than the applications people use in everyday situations.
The amount of code written in Java combined with the accompanying necessity for problem or industry specific technologies to at minimum support Java makes it naturally the best integration platform for most systems. With many of the best Java technologies focused around integration, it becomes a no brainer.
Many of those technologies aren’t discussed much in online forums because they’re not intended to be noticeable, and they simply work. The ‘squeaky wheel’ effect doesn’t discriminate between technologies within an ecosystem and technologies in different ecosystems.
And any tool becomes more prominent when it’s missing or broken.
Without a lot of integration work we’d have no useful systems. Few people were interested in having computers at home until it became possible to write emails, chat, watch movies, etc., all of which require a lot of integration.
It’s not the most interesting set of problems to developers nor to onlookers, who by and large take it for granted, and somewhat unfortunately constitute most of management, but it’s nearly always necessary in introducing anything new. Some of the core code in the systems we rely on predates Java, but plenty of components in those systems are written in Java.
There are things I find annoying and lose patience with in writing applications in Java, there are a couple that are language specific, the main one being that the syntax hides the object nature of the language rather than using it, but more often it has little or nothing to do with the language itself.
In perspective, nearly all developers will admit Java is much better and more pleasant to work with than COBOL, BASIC or Pascal, no matter what their preference in terms of languages, environments and ecosystems happens to be. The kind of masochism required to do much in C++ is a perversion best left to itself.
The number one cause of integration problems found in a study of code on GitHub was Google’s V8 JavaScript engine, apparently due to its flakiness in maintaining network connections, which is a bit odd for technology that originated in a web browser. Don’t expect your Electron non-app to talk much though, or at least not with any consistency.
The degree of excitement in the JavaScript world when they get a technology that’s been in just about everything else for ages is much more annoying. Hello AngularJSF, nice to meet you, again.
As Oscar Wilde said of a lecture, I’ve not heard discussion of Node.js. I’ve overheard it.
It remains a single threaded language that happens to have asynchronous I/O, and network I/O at least doesn’t appear to work very well. When the only way to get JavaScript apps to work together is to put a Java app between them, it’s a non-starter as an integration tool.
Next year it might catch up to Windows 1.0, i.e. Electron non-apps will “sort of” appear to work for longer than 5 minutes.
In any case, getting back to Java, most commonly problems have to do with decisions made in choice of frameworks and libraries for a specific project, when there are better options within the Java ecosystem for that project. But as I noted above, the sheer number of options can make it difficult to assess all the possibilities.
When you find yourself trying to ‘fix’ problems with an application, and discover it’s running in 18 different Tomcat Java SE web containers within a JBoss Java EE container. In that situation, it’s not choice but non-choice that causes problems.
There’s not much any language can do about stupidity.
Avital Ronell observed that ‘stupidity is our most common experience of the infinite, while intelligence is always limited’. In the case above, intelligence appears to have been cut off and tossed in the dumpster out back before a line of code was written.
It does give the Pastafarians ample evidence for the existence of flying spaghetti monsters, though.
There’s complexity in some Java technologies for certain aspects of what they’re intended for. At the same time, using them when they’re not really needed can be more convenient than using something unfamiliar or requires additional work to integrate.
The Java Collections Framework is a good example. It seems like overkill to require three classes for each collection type: the collection type itself, a comparator to sort it, and an iterator to navigate it. While the syntax changes introduced in Java 5 do make that easier, using the Eclipse collections is still easier and faster, assuming you already know them.
In writing a highly concurrent clustered or distributed application the way the JCF works is often the simplest way to accomplish what’s required.
The need to support both that kind of application and more common application types is what drives technologies such as JDBC and JPA to use the JCF. It then becomes easier to use the JCF for most Java applications, because related technologies already use it.
Conversely, some frameworks and libraries try to oversimplify things, causing problems when attempting more than they’re capable of, and creating the need for additional complexity in other areas of a given system.
The ‘keep it simple’ mantra is too simple. It usually just results in over-complexity elsewhere in the application.
Other problems are not at restricted to Java, in some cases Java at least has a few solutions to them.
Most remote APIs hide the fact that remote calls are remote calls. Most forms of RPC or RMI are prime examples.
If a local call within the VM doesn’t return a response, your application is probably in far more trouble than that lack of response. Remote calls, on the other hand, quite often don’t return a response, due to various types of issues in the network environment.
Hiding the fact that the call is remote makes it less likely that a developer under time constraints (and who isn’t?) will account for non-responses, and the result is a runtime exception that ends the application.
JINI (now Apache River) is one of the few technologies that use remote calls extensively while making it obvious when remote calls need non-responses considered.
Few people realize the prevalence of technologies like JINI because they work reliably, and code written to use them generally works reliably as well. For instance, the ‘automagic’ integration of devices like cell phones in newer cars is entirely dependent on JINI. That it works invisibly, with few people ever considering the actual difficulties involved, is the sign of a truly good tool.
Unfortunately, languages aren’t immune to a similar effect. When Java doesn’t have a feature it suddenly becomes important, unless the only language that previously had it was Smalltalk, Haskell or LISP. Things like closures, lambdas, recursive list functions, atomic functions, etc. didn’t appear to excite anyone until they realized that Java lacked them.
Since the JVM runs code written in languages other than Java, it’s not easy to implement those types of features. Providing support for them in the runtime would break those languages, such as Clojure, Scala, Ceylon, NetREXX and Groovy, to name a few.
Some of the contortions javac performs to make things like lambdas work are rather astonishing, but a bit ridiculous. They also make debugging a program more difficult. Considering most of them accomplish nothing more than shortening the code by a few lines, it seems nonsensical to add it just because it’s suddenly fashionable.
The current trend of using Scala to improve concurrency (applications such as Apache Storm and Spark are good examples) carries that into a different JVM language in an inverse way. The parallelism implied by the language constructs in Scala, or the reliability implied by the very strong typing in Ceylon, are largely obliterated by the conversion to JVM bytecode.
The result, at least in the case of Scala, looks remarkably like Java bytecode when the Java code uses the Java streaming API and the Java concurrent package. Syntax, language structures, and code restrictions don’t buy you much when the runtime doesn’t use, follow or obey them.
Adding features makes the bytecode unnecessarily complicated, with a related issue where stack traces used in debugging no longer resemble the code as written, and they’re often one of the only things a developer has when a problem only shows up in production, or only at the company’s largest customer.
Conversely, in a language with apparent advantages in terms of syntax and language structure, those are most often removed by the pre-compiler to run in an environment designed for a language lacking them. Both languages have the same issue where stack traces don’t resemble the code, though.
For reasons like those above, and the difficulty thinking in terms of one language while writing a VM, interpreter or compiler in another, the few languages where, aside from an initial bootstrapper, everything is written in the language itself, either originated many such features, or adding them wasn’t as difficult. As soon as you save code in those languages, you’ve implicitly changed the state machine that runs that code to support that code.
Lambdas, closures, proto-typing, reflection, introspection, recursion, strong typing with safe dynamic casts, traits (sometimes called mixins in other languages) and polymorphism all existed in either LISP, Smalltalk or Haskell prior to Java, JavaScript, Clojure, Scala or most of the other languages that have some of them.
Further, when the interpreter/compiler is written in the language it interprets/compiles, the resulting assembler implicitly follows the restrictions and inherits the structures of the language, with the result that the actual code that runs on the processor benefits from the restrictions preventing of the use of dangerous instructions and from the power the structures and metaphors provide.
It’s unsurprising that the prototype for the JIT compiler in Java was written by Sun in a Smalltalk VM, afterwards open sourced as ‘Strongtalk’. Similarly, IBM’s proof of concept that a VM based language can scale is a VM for Squeak/Pharo open source Smalltalk, which can scale to 1024 CPU’s close to linearly (IBM didn’t have a bigger machine to see how far it can scale).
Languages of that type are initially more difficult to write and take far longer, but once the base is written, adding things becomes much easier, because it doesn’t need to be added in multiple places in different languages, in a semantically equivalent way.
The difficulties involved with a language like Java that had to be overcome and that continue to require significant work to upgrade, even more so with such a large base of applications that depend on the language and/or runtime, requires enormous effort by some brilliant people.
The biggest problem remaining is the propagation of events globally, and the generic nature of most events, particularly in UI frameworks but not restricted to them, means that far too many objects take time consuming them, only to find they’re irrelevant.
A really obvious example is the Dali JPA library in Eclipse. Although Dali does a better job than other JPA frameworks at reverse engineering a complex database, it took over 300 CPU hours with 16 GB allocated to the JVM to reveng an ERP database, due to the number of events it sends out and the number of consumers / responders to them. At one point there were over 850,000 unprocessed events in the queue.
JavaScript has the same issue, although many libs implement the stopPropagation() and a few also implement the stopImmediatePropagation() API’s (jQuery is an example). Few JS programmers actually call them though.
Back to Java again (by now you’ve probably figure out that I pretty much do hate JavaScript, or EczemaScript as I usually call it). It looks and works as if it were written in a few weeks by a company in desperation for their new product to work. Then again, it was.
That there are some phenomenal libraries, frameworks and applications in Java, and that most Java programs do what they do very well, is a fortunate circumstance.
It can be difficult to solve many requirements common in business and user applications in LISP or Haskell in comparison to Java. Just as the difficulties encountered in converting well-known SQL solutions to common requirements in databases that use map/reduce functions or object graph iteration, so many people having already solved common issues in the language makes it far easier.
While map/reduce and object graph databases, as with esoteric languages like LISP, Haskell, or Prolog, have use cases where they’re either necessary or simply better, those use cases are relatively rare.
And it’s still better than COBOL …