DEV Community

Sergiy Yevtushenko
Sergiy Yevtushenko

Posted on

We should write Java code differently

For the last few years, I'm writing articles which describe a new, more functional way to write Java code. But the question of why we should use this new coding style remains largely unanswered. This article is an attempt to fill this gap.

Just like any other language, Java evolves over the time. So does the style in which Java code is written. Code written around Y2K is significantly different from code written after 2004-2006, when Java5 and then Java6 were released. Generics and annotations are so widespread now, that it's hard to even imagine Java code without them.

Then came Java8 with lambdas, Stream<T> and Optional<T>. Those functional elements should be revolutionizing Java code, but largely they don't. In a sense, they definitely affected how we write Java code, but there were no revolution. Rather slow evolution. Why? Let's try to find the answer.

I think that there were two main reasons.

The first reason is that even Java authors felt uncertainty how new functional elements fit into existing Java ecosystem. To see this uncertainty, it's enough to read Optional<T> JavaDoc:

API Note: Optional is primarily intended for use as a method return type where there is a clear need to represent "no result," and where using null is likely to cause errors.

API also shows the same: presence of get() method (which may throw NPE) as well as a couple of orElseThrow() methods are clear reverences to traditional imperative Java coding style.

The second reason is that existing Java code, especially libraries and frameworks, was incompatible with functional approaches - null and business exceptions were the idiomatic Java code.

Fast-forward to present time: Java 17 released few weeks ago, Java 11 is quickly getting wide adoption, replacing Java 8, which was ubiquitous a couple of years ago. Yet, our code looks almost the same as 7 years ago, when Java 8 was released.

Perhaps it's worth to step back and answer another important question: do we need to change the way in which we're writing Java code at all? It served us good enough for long time, we have skills, guides, best practices and tons of books which teach us how to write code in this style. Do we actually need to change that?

I believe the answer to this question could be derived from the answer to another question: do we need to improve the development performance?

I bet we do. Business pushes developers to deliver apps faster. Ideally, projects we're working on should be written, tested and deployed before business even realizes what actually need to be implemented. Just kidding, of course, but delivery date "yesterday" is a dream of many business people.

So, we definitely need to improve development performance. Every single framework, IDE, methodology, design approach, etc., etc., focuses on improving the speed at which software (of course, with necessary quality standards) is implemented and deployed. Nevertheless, despite all these, there is no visible development performance breakthroughs.

Of course, there are many elements which define the pace at which software is delivered. This article focuses only on development performance.

From my perspective, most attempts to improve development performance are assuming that writing less code (and less code in general) automatically means better performance. Popular libraries and frameworks like Spring, Lombok, Feign - all trying to reduce amount of code. Even Kotlin was created with obsession on brevity as opposed to Java "verbosity". History did prove this assumption wrong many times (Perl and APL, perhaps, most notable examples), nevertheless it's still alive and drives most efforts.

Any developer knows that writing code is a tiny portion of the development activities. Most of the time we're reading code. Is reading less code more productive? The first intent is to say yes, but in practice, the amount of code and its readability are barely related. Reading and writing of the same code often has different "impedance" in the form of mental overhead.

Probably the best examples of this difference in the "impedance" are regular expressions. Regular expressions are quite compact and in most cases rather easy to write, especially using countless dedicated tools. But reading regular expressions usually is a pain and consumes much more time. Why? The reason is the lost context. When we're writing regular expression, we know the context: what we want to match, which cases should be considered, how possible input may look like, and so on and so forth. The expression itself is a compressed representation of this context. But when we're reading them, the context is lost or, to be precise, squeezed and packed using very compact syntax. And attempt to "decompress" it from the regular expression is a quite time-consuming task. In some cases, rewriting from scratch takes significantly less time than an attempt to understand existing code.

The example above gives one important hint: reducing the amount of code is meaningful only to the point where context remains preserved. As soon as reducing code causes loss of context, it starts to be counterproductive and harms development performance.

So, if code size is not so relevant, then how we really can improve productivity?

Obviously, by preserving and/or restoring lost context. But when and why, context is getting lost?

Context Eaters

Context Eaters are coding practices or approaches which results to the context loss. Idiomatic Java code has several such context eaters. Popular frameworks often add their context eaters. Let's take a look at the two most ubiquitous context eaters.

Nullable Variables

Yes, you read it correctly. Nullable variables hide part of the context - cases when variable value might be missing. Look at this code example:

String value = service.method(parameter);
Enter fullscreen mode Exit fullscreen mode

Just by looking at this code, you can't tell if value can be null or not. In other words, part of the context is lost. To restore it, one needs to take a look into the code of the service.method() and analyze it. Navigation to that method, reading its code, returning - all these are a distraction from the current task. And the constant need to keep in mind that a variable might be null, causes a mental overhead. Experienced developers are good at keeping such things in mind, but this does not mean that this mental overhead does not affect their development performance.

Let's sum up:

Nullable variables are context eaters, development performance killers and source of run-time errors.

Exceptions

Idiomatic Java uses business exceptions for the error propagation and handling. There are two types of exceptions - checked and unchecked. Use of checked exceptions, usually discouraged and often considered an antipattern because they cause deep code coupling. Although the initial intent of introduction of checked exceptions, by the way, was preserving the context. And compiler even helps to preserve it. Nevertheless, over the time, we've switched to unchecked exceptions. Unchecked exceptions were designed for the technical errors - accessing null variable, attempt to access value outside the array bounds, etc.

Think about this for a moment: we're using technical unchecked exceptions for the business error handling and propagation.

Use of the language feature outside the area it was designed for, results in loss of context and issues similar to ones described for nullable variables. Even reasons are the same - unchecked exceptions require navigation and reading code (often quite deep in the call chain). They also require switching back and forth between current task and error handling. And just like nullable variables, exceptions can be a source of run time errors if not processed correctly.

Summary:

Business exceptions are context eaters, development performance killers and source of bugs.

Frameworks as Context Eaters

Since frameworks are usually specific to a particular project, issues caused by them are also project-specific. Nevertheless, if you got the idea of context loss/preservation, you might notice that popular frameworks like Spring and others, which use class path scan, "convention over configuration" idiom and other "magic", intentionally remove large part of the context and replace it with implicit knowledge of the default setup (i.e. mental overhead). With this approach, the application gets broken into a set of loosely related classes. Without IDE support, it's even hard to navigate between components, so disconnected they are. Besides loss of huge part of context, there is another significant problem, which negatively impacts productivity: significant number of errors are shifted from compile time to run-time. Consequences are devastating:

  • more tests are necessary. Famous contextLoads() test is a clear sign of this problem
  • software support and maintenance requires significantly more time and efforts

So, by reducing typing for a few lines of code, we're getting a lot of headache and decreased development performance. This is the real price of the "magic"

Pragmatic Functional Java Way

The Pragmatic Functional Java is an attempt to solve some problems mentioned above. While initial intent was to just preserve context by encoding special states into variable type, practical use did show a number of other benefits of taken approach:

  • significantly reduced navigation
  • a number of errors are shifted from run-time to compile time which, in turn, improved reliability and reduced number of necessary tests
  • removed significant portion of boilerplate and even type declarations - less typing, less code to read, business logic is less cluttered with technical details
  • sensibly less mental overhead and need to keep in mind technical things not related to current task

Top comments (9)

Collapse
 
palexdev profile image
Alessadro Parisi

The article is pretty interesting but it would have been nice to see some code examples

You said that frameworks often are context eaters, and Spring is one of them so you are discouraging to rely too much on it, did I get it wrong? But then how do you properly handle dependencies without too much boiler plate code?

Collapse
 
siy profile image
Sergiy Yevtushenko • Edited

Some code examples are provided in the Introduction To Pragmatic Functional Java article and several others from my blog: here and here. Finally, there is a (WIP) demo project at GitHub.

As for Spring. There are many other DI containers out there, Spring is definitely not the best of them (I'd say worst, but this is mostly irrelevant). Some DI containers, for example, Guice, use explicit configuration and this preserves much more context than Spring does. Haven't tried Micronaut, but givent that it uses compile-time annotation processing, it should address at least some Spring pain points.

Collapse
 
palexdev profile image
Alessadro Parisi

Thank you very much @siy

Collapse
 
philtroy profile image
philtroy

Hi

I think that Kotlin does help, if you don't take it to an extreme. It handles the null value issue much better than Java does, and some of the constructs allow you to focus more on the code. Another of the constructs, automatically determining the type of a variable, is also extremely helpful in facilitating not losing context.

However, some of the constructs do take you out of the context of the code so one needs to be careful.

At the risk of being controversial, there is another coding practice that I think takes people out of context when reading code, the practice of putting the starting brace { at the end of one line (with an if or while) and the end brace at the start of another line. I think that having the braces line up makes it a lot easier to follow the start and end of a block. What would make it better is if the IDEs showed braces more intelligently, i.e. as one continuous vertical brace spanning the whole block, without necessitating the need for unneeded vertical spaces. (See attached image.)

Phil

Collapse
 
siy profile image
Sergiy Yevtushenko

PFJ uses the same idea as Kotlin (type separation) but does not require "double" type system.

Type inference (especially how it is implemented in Kotlin) can hide part of context.

Finally, code formatting does not matter much as long as it's consistent. If bracket placing causes problems, most likely you have too deeply nested code.

Collapse
 
philtroy profile image
philtroy

Hi

I'll take a look at PFJ hopefully this week.

With respect to your type inference comment, can you give an example? But I will say in advance that in many cases it makes it a lot easier to read the code when types are automatically inferred.

And my finally (for today at least), code formatting does matter to me. I remember back in 1973 when I was taking an assembler programming course at Penn State University, purposefully not formatting the assembler code well. It was very hard to follow, more so than it would have been just because it was in assembler. And yes, bracket placing is bothersome to me for a few reasons:

  • Many times it introduces a blank line (at the end brace) that distracts from code reabilitiy.
  • Having the braces the way I suggested very clearly delineates visually the block of code (to me at least), regardless of how deep the nesting is.

Thanks for your comments!

Phil

Thread Thread
 
siy profile image
Sergiy Yevtushenko

Here is the detailed explanation with examples: 4comprehension.com/kotlin-type-inf...

And yes, in most cases type inference makes code cleaner without loosing context.

As for formatting. Formatting affects readability, but it should be extremely weird to cause loss of context. And as long as context is preserved, getting used to particular formatting style is just a question of time. Developer may still hate formatting, but once he/she is get used to it, formatting is no longer an issue.

Collapse
 
angius profile image
Angius

The best way to write Java is either Kotlin or C#, if you ask me.

Collapse
 
siy profile image
Sergiy Yevtushenko

None of them have any sensible advantages over Java for the enterprise software development. Moreover, idiomatic coding styles for them suffer from very similar issues.