10 min read
Java and Education
Some thoughts on Java and how important good education and a good teacher is.

I had my last exam couple weeks ago, completing my first year of university. Funnily enough, my last exam was the final for my module on Java. Thus, this post is kinda going to be dedicated to my lecturer and Java.
I had never properly learned how OOP worked or how the ideas behind it contributed to good code. I had never written humongous coding projects until recently . And in my head, I had this idea that Java was bad, and since Java == OOP
, I was begrudging against this year's module on Java—more preciely, programming practices and applications.
The module was an introduction to programming, so the first five weeks were mostly boring—conditinals, loops, objects, classes—which strengthened this idea that Java, and thus OOP, were bad. Also, at the time, I had started watching cppcons, where I came across Mike Acton's talk on data-oriented design . This also further deepend my thoughts about Java: something that I need to force upon myself until the year is over and never write any Java or OOP code.
Contradiction!
Until our lecturer put up this code snippet and asked us to give definitions for a
and b
such that the programme would print "YES"
.
1if (a <= b && b <= a && a != b) {2System.out.println("YES");3} else {4System.out.println("NO");5}
Mathematically speaking, if and , then , but we see that we also need to satisfy , which seems to be a contradiction.
This question problem stumped me really hard, and we, as an entire class, tried to figure out an answer that would work: everyone was trying something on their own computers. I, on the other hand, tried to figure out a way using floating point numbers, since they are really imprecise and one cannot be sure that some equation is valid with them .
I've tried various things with floating point magic, but nothing worked. The answer, however, relies in wrapper classes and object references. In Java, there are two types of "types": primitive and object types . Object types contain a reference to the actual object in the heap. Therefore, we might have the same value in different addresses, which means they have different identities. The ==
sign checks for identity, so even if we have the same value in different addresses, the equality sign would not be true. This explains how a != b
evaluates to true—having different "objects"—but how can we compare values with <=
, it's not like that we have operator overload in Java.
Since int
, float
, etc. are primitive types, they hold their value, without needing to perform a look-up in the memory. An inherent limitation with them is that we cannot use them to instantiate generic types , so we cannot create variables such as ArrayList<int>
or HashSet<boolean>
; we need to use wrapper classes. Each primitive type has a wrapper class associated with it: Integer
for int
, Double
for double
, etc. The tricky thing is, we cannot do arithmetic with wrapper classes , needing us to convert them—unbox them—to use their "value." Before Java 1.4, we had to unbox the wrapper objects ourselves with Integer.intValue()
, but with Java 5, the unboxing as well as boxing is done by the compiler automatically.1 Meaning that we can write Integer a = 37;
, which would box the primitive int value of 37 into an Integer
object.
In order to compare values of wrapper objects, we need to use the respective primitive types: we need to unbox a wrapper object to compare values with <=
. Coming back to our if statement (a <= b && b <= a && a != b)
, the first two comparisons auto-unbox, but the last one doesn't. Allowing us to define a
and b
as follows:
1Integer a = 7;2Integer b = new Integer(7);
a <= b
: Auto-unboxes and uses the values asint
which are equal.b <= a
: Auto-unboxes and compares the values asint
which are equal.a != b
: Since we are are creating two different objects, their addresses will be different, and thus not equal.
Language design
When Java was first introduced, the creators of the language decided to have different types that we have today , because primitive types are a lot more performant . However, when generic types were introduced in Java 5, they had to change wrapper classes for primitive types to allow auto-boxing, which is especially useful for collections.
Did someone mention functional programming?
Another interesting example to discuss would be the time when Java extended the standard libraries with new functionality provided by functional programming constructs, such as lambdas and streams, which had quite bit of a consequence.
Adding new methods to library classes is easy. Existing subclasses will inherit the new methods, and this does not pose a problem. The existing code will not make use of these new methods, but they could do so in the future. In any case, the original code compiles, and all is well.
The problem arises when we consider interfaces. When Java designers add a new method to an interface in the standard library, all existing classes that implement it will break—because they all have a missing method. Everything would have been fine if Java developers slapped a "BREAKING CHANGE!!!!" sticker on the new version and called it a day. But they had promised, from the start, that they would maintain backwards compatibility—so every code written in an older version of Java would still compile in the newer version.
Now what?
Well, Java designers decided to solve the problem by allowing implementations of methods in interfaces from Java 8 and onwards. While this seem to be fine in solving the problem, it went against an important prior principle: interfaces, previously, just provided "type" inheritance, not implementation knowledge. This solution went against the elegance of the Java system.
It's now become really difficult to explain the rationale for interfaces and abstract classes. Both can provide partial implementations with abstract methods. There seems to be no practical difference between them. In fact, it gets quite complicated in some cases. In Java, a class can't inherent multiple times from any class, but it can with interfaces. So, I can write
1public class Ba implements A, B { }
Or even
1public class Ca extends A implements B { }
But not
1public class Na extends A, B { }
What's even more stupider, I can even write this:
1public class Da extends A implements B, C, D, E { }
This made sense before, because an interface didn't had any implementation detail in it, and there were no way for them to be overriden, because the class implementing the interface provided that implementation. But now, a developer must take care when implementing multiple interfaces, god forbid they might override some default method.
All in all, Java designers were pragmatic: a change was made to a previously cleaner, simpler rule in favour of moving the language forward. However, in a new language created from the start, this would've been designed quite differently.
Mature languages
Java is a really old language. The first version was released in the 90s—nearly 30 years ago—and the language has come a long way, with new paradigms affecting the language as well as maintaining backwards compatibility to this day. This has resulted in some funky constructs being introduced to the language, such as default methods, wrapper classes, the "var" type, inner classes, etc.2
As a language such as Java matures, it starts to contain these "stale" constructs—syntax that are legal tender, but are mostly avoided in writing good code. As Java programmers, we must deal with these constructs, and as more constructs are added to the language, we will get an ever increasing amount of stale Java: redundant ways to practically do the same thing.
How it all ties back together
As always, we strive to write good code. We strive to write simple, elegant, and neat code.3 And, Java designers know this. They looked for the simplest solution in adding those constructs as the language grew bigger. The problem comes when we look at the meaning of "simple." For an experienced programmer, it moslty means being able to write the shortest or simplest code and being able to write better code as one learns how the language works under the hood.
This isn't the case for people starting to learn a new language. A student might want to just express, as needed, when to box or unbox with .intValue()
. Because understanding auto-boxing and object identities is more "complex." Having more choices as a beginner, without understanding the subtle nuances, doesn't lead to simple code.
As a language matures and as a language gets more constructs, the less suitable it becomes as a first language or as a new language to learn . This is also why a lot of universities and teaching organisations are moving away from Java, in favour of Kotlin, as a first language. Because it's not as mature as Java, and it has all of the experiences and failures to learn from. The ==
doesn't mean identity equality in Kotlin, it means semantic equality, and constructs in Kotlin are a lot more intuitive and "simpler."
All in all, these ideas, these discussions were the reason I started to like Java, because the history it has. Java has allowed me to be interested in language design, and how even the simplest of changes can affect the language as a whole and the direction that it is going.
This, to me, showcases really well how a good teacher/educator can do to people. If an educator is really enthusiastic about some concept, some idea, that most certainly transfers to the students and listeners. And, that is exactly what makes a good teacher. For instance, I wrote my other post on skip lists thanks to my lecturer from my module on data structures, just because he mentioned the randomized aspect of the algorithm in such an interesting way.
I might've hated Java for the entire year, but I was always interested to go and listen to the lectures, and I haven't been dissinterested in writing Java code, it's always interesting—obviously, there are some problematic areas, but every language has them.
Footnotes
-
https://stackoverflow.com/questions/3571352/how-to-convert-integer-to-int ↩
-
A blog explaining some of Java's design choices and the reasons behind them by the one and only Michael Kölling: https://blogs.kcl.ac.uk/proged/2025/05/02/a-brief-history-of-java/ ↩
-
Honestly, "elegant" code is a lie disseminated by big code. Writing elegant code defeats the purpose of writing software altogether: We're not writing code for developers , and the end-user won't see any of the awesome constructs that you've used in your code. As long as your code is understandable with maybe a single or two comments, then that's good enough. I might write another post about this. :eyes: ↩
§