Java Teaches Bad Habits
Java is a popular programming language, but it teaches bad habits and bad OOP. Here are my reasons and opinions.
Interface Inflation
Because Java was designed without lambda functions/closures, hundreds of unnecessary single-method interfaces had to be added. E.g. all kinds of XyzListeners
. If the language had lambdas from the start, these interfaces could have been replaced by simple function signatures. Instead they now haunt the standard APIs.
Naming Things In Java Is More Than Just ‘Hard’
Here are some examples that make me want to cry:
AbstractCookieValueMethodArgumentResolver
AbstractEncoderMethodReturnValueHandler
RequestMappingInfoHandlerMethodMappingNamingStrategy
TimeoutDeferredResultProcessingInterceptor
TransactionalApplicationListenerMethodAdapter
These are straight from the spring framework. These kind of naming patterns seem to be very common in Java in general. I rarely see similar patterns in other languages. Why is that?
I think there are multiple reasons to this:
- Unique Names and Packages
- General Verbosity
Unique Names and Packages
First of all, package names are horribly long. Everything starts with the domain. This makes the navigation in the project tree annoying too. But the big problem is:
In other languages we would have something like Server.Context.Factory
. We would be able to nest classes or namespaces inside other classes or namespaces. And then we would be able to import Server.Context.Factory
as Factory
or give it an alias in the case that there is another imported type Factory
. In Java though, you can not import two types with the same name. This means there are basically two options. Either you give the classes a more unique name, or you have to refer to the class using the full package qualifier. Now remember that package names are generally really long. This creates pressure to create long unique class names.
Technically, you could use nested static classes. This is actually really useful. E.g. using User.Id
instead of UserId
. One drawback and limit of this approach though is that a nested static class has to be in the same file as the outer class. So you can’t have dozens of nested classes in the same outer class because it makes the file unreadable.
General Verbosity
Java is extremely verbose in general compared to other languages (e.g. crystal, ruby, kotlin). The verbosity kicks in on every level, e.g. the language constructs, the APIs, the build process (XML), package names. Death by a thousand cuts.
A lot of classes are only necessary because of shortcomings of the language. Those classes dont even exist in other languages because they are just not needed. Suffixes like Handler
, Holder
, Resolver
, … are often indicators of that. In addition, many architectural design decisions are only necessary in Java.
Many statements are overly verbose and complex too. Just look at the Stream-API:
var interestingCustomers = customers
.stream()
.filter(c -> c.getMoney() > 100000)
.collect(Collectors.toList()); // in later java versions: .toList()
Compare that to ruby/crystal:
interesting_customers = customers.select { |c| c.money > 100_000 }
And these are the simpler examples.
The java standard libraries are old, and the age is visible. Collection handling is ugly, even when using Streams and e.g. Guava. Is it Collections.xyz
or Lists.xyz
? Or is it in our own CollectionUtils
? Or was it CollectionsHelper
? Or ListUtils
? When the standard library lacks fundamental everyday-features, everyone adds patchwork libraries to fill the gaps. Everyone is reinventing the missing wheels in every project, but with different names. Same problem in Javascript/Typescript. At least in JS/TS it is possible to monkeypatch some functions into the existing Arrays and Maps. No need for ArrayHelper/Utils/Whatever.
Then there are builders and fluent-interfaces. These are complete abominations too. Not just are they hard to use, they require a whole bunch of additional strangely-named classes, hierarchies and boilerplate code. Here is - i think (im not even sure) - one of those beauties related to builders/fluent interfaces:
public final class ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry
extends AbstractInterceptUrlConfigurer.AbstractInterceptUrlRegistry<
ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry,
ExpressionUrlAuthorizationConfigurer.AuthorizedUrl
> {
...
}
Other verbosity annoyances are:
- Writing constructors (1)
- Getters and setters (1)
- Repeating types as well as lots of modifiers
- Statements not being expressions
- Lack of default arguments and named parameters (causing lots of boilerplate overloads; and often overload-variants are missing)
- Variables captured in closures required to be effectively final
- Having to use
equals
instead of overloading==
- No operator overloading, which makes types like
BigDecimal
annoying to use - Lack of Map-literals
- Boxing/Unboxing
- Probably a lot more …
(1) Lombok and records help, but both have their own limitations (naming of getters, ability to use inheritance and call super)
And of course, there is the lack of null-safety. You can try to make things null-safe by using @NonNull
from Lombok (different than @NotNull
). But this means you have to add annotations everywhere. More boilerplate. Plus, if you start using @NonNull
in e.g. a method, then you have to be consistent and mark everything that should not be nullable as @NonNull
. You can’t just add @NonNull
to one parameter that you are certain should never be null. The existence of one @NonNull
implies that all other parameters without this annotation are supposed to be nullable.
And in addition there is Optional
. Lots of different incomplete solutions for one problem.
In languages like crystal, everything is non-nullable by default. Declaring something as nullable is just:
class Point
getter x : Int32? # same as Int32 | Nil
end
Bloated Dependency Injection
Dependency injection is fine, usually. Just not in Java. I am not a programming-language historian, so i might be wrong here. But my impression is this: One of the main reasons for the push for heavy dependency injection in Java was that testing was difficult or even impossible without it. You could not easily swap out a component for a fake-component in tests, unless you used very general dependency-injection solutions.
The problem is that this turned into a “we need DI for everything”-thinking. Whats with all the crazy and confusing annotations? Where the hell are my instances being injected from? Often it is really hard to follow the crazy DI injection flow. I wish people would just instantiate the simple stuff with new
and pass in parameters. Do people even inject different objects? Often that is not the case. YAGNI.
Date And Time API Labyrinths
Dates and Times were always a mess in Java. Here are some of the classes related to time handling:
Date
Time
Instant
Timestamp
LocalDate
LocalTime
LocalDateTime
ZonedDateTime
OffsetTime
OffsetDateTime
TimeZone
ZoneId
ZoneOffset
Period
Duration
Calendar
Clock
TemporalField
TemporalUnit
TemporalAmount
TemporalQuery
TemporalAdjuster
TemporalAccessor
These are just the classes. Each of those classes has a bunch of methods.
Documentation
Is the java documentation good? In my experience: no. This has multiple reasons.
- Unnecessary Complexity: Since Java code is generally more verbose and unnecessarily complicated, it naturally needs more documentation from the start. In other languages, you can often just look at the implementation to figure out what a method does. In Java you usually go down a rabbit-hole of crazy
AbstractBaseFilterChainInterceptorSupervisorFactory
(can you tell from the name whether this is a real class or i just made it up?). - Difficult Concepts: You will often find classes that are hard to understand. The name does not really give much away. In addition, the class is embedded in an even weirder class-hierarchy. These classes typically exist to work around some shortcommings of java, to make things injectable or to adhere to some interface. These classes are often so abstract and nebulous, that they are hard to even describe - and therefore hard to understand and document.
- Fake Documentation: A lot of the Javadoc documentation is kinda fake. It just repeats parameters and return types as well as class-hierarchies. That is not real documentation. What is the purpose of this class? What exactly does parameter
config
do? What (unchecked) exceptions will be thrown, and when?
Existing Libraries
As a popular and old language, Java has a lot of libraries. This is true. Unfortunately, a lot of these libraries are … no that great.
One example is WebClient:
var result = client.post()
.uri(new URI("https://example.com/post"))
.body(BodyInserters.fromFormData(values))
.retrieve()
.bodyToMono(Result.class)
.block();
Of course, a builder is involved. And you have a new strange terminology like retrieve/exchange
and Flux/Mono
.
Compare this to crystal:
response = HTTP::Client.post("http://example.com/messages", body: "Hello!")
response.status_code #=> 200
response.body #=> "..."
How many hidden and unintentional bugs fit in the former example and how many in the latter?
New Features
Java has been adding lots of features like the var
keyword, new methods to String and other standard classes, default methods for interfaces (kinda like mixins), record-classes, pattern-matching, multiline string literals, … These features might look great for someone who wasn’t used to them. But in my opinion, it’s just lipstick on a pig. And why wait for these features to be introduced step-by-step and only half-complete when you can use more modern languages like kotlin?
Even the new and shiny features are poisoned (often due to backwards-compatibility) by having lots of caveats and limitations. Take switch-expressions: Are null-values allowed in the switch? How many different variations are there? Plus, all of these different variations will now probably linger in java libraries for the next 20 years.
Conclusion
The intention of this post is not to degrade other peoples work. I know that a lot of smart people are working on all these things, be it the language itself or libraries and applications. And i don’t want to pretend like i am the super-smart developer who has it all figured out. But i need to point out that - in my opinion - there are a lot of really deep flaws in Java that lead people (me included) to write really bad code. A messy language makes you write messy code and come up with messy solutions.
I even think that the main pushback against OOP that we can observe in developer communities from time to time, comes from witnessing Java code.
I do not have a simple solution to these problems that exist in Java. Maybe just use Kotlin instead if possible?