Preface: The following text is a Chapter of the upcoming 7th edition of our textbook, Objects First With Java.
In the seventh edition, we faced the problem that – as Java is evolving – there are several language constructs that we did not want to discuss in detail in the book – because we do not recommend using them – but that nonetheless we wanted our students to know about, because they will encounter them in other programmers’ code.
Thus, we took that opportunity to insert a chapter recapping a brief history of the Java language, so that students do not only understand what is in the language, but also why it is there.
This is of general interest to anyone interested in language design, and also gives us the opportunity to discuss Stale Java – language constructs that we do not use anymore.
I hope you enjoy this extract of our forthcoming book.
David J. Barnes & Michael Kölling
Objects First with Java
A Practical Introduction using BlueJ
Seventh Edition
Chapter 16
This chapter is a bit different from the others you have seen so far. We will not introduce any new concepts, but we will present a mixed bag of constructs and syntax examples that we have left out of our presentation so far. More importantly, we will discuss why we have left them out, why we think you should probably not use them, and why they are in Java in the first place.
To understand the rationale for these constructs and their interactions with other parts of the language, it will be interesting to discuss the evolution of programming languages in general. These thoughts on language design are perhaps not crucial to writing Java programs, but they will give you a much better understanding of why Java is the way it is, and what some of the challenges are in designing a programming language.
This chapter, therefore, is a chapter of background information and foundation. It does not help you immediately with writing your programs. If you, however, have an interest in designing your own language one day, or just wish to have an informed opinion about the quality of a language, this discussion should be interesting.
There is also one practical aspect to the material in this chapter: We will present several constructs intentionally left out of the rest of the book, because you do not need them to write good code. You may, however, come across some of these constructs in reading other people’s code, so there is a clear advantage in being at least somewhat familiar with this material.
We call these constructs Stale Java—constructs that are legal Java, but we generally avoid using them in our own code. They are presented throughout this chapter in a sequence of tables.
The structure of this chapter is arranged roughly chronologically, along the lifespan of the Java language. The principles discussed, however, apply to the design and evolution of all programming languages.
1 Java 1.0 – The Birth of a Language
Java was first released in January 1996, designed by James Gosling (and initially named Oak, but renamed Java before release due to trademark issues). It was designed to be a real-world production language, and its guiding principles included simplicity and consistency.
1.1 Research languages vs development languages
New programming languages can roughly be separated into two groups: research languages that introduce novel features and allow users to experiment with those, and development languages that consolidate a coherent set of features—usually tried and tested in other languages—into a consistent whole and aim to be widely adopted and used in earnest in industry.
Research languages are the ones driving forward programming language research, but they rarely get things perfectly right in the first attempt, and are often more influential in shaping other languages than in their own adoption in industry. They can be adventurous and different.
Development languages copy, adapt and use proven concepts from other languages, shape them into a consistent and useful unit, and aim at being widely adopted and long-lived. They have to be pragmatic and take real-world constraints into account.
This separation is somewhat artificial (many developers of research languages were likely hoping for widespread adoption, and creators of development languages often find it hard to stop experimenting), but it serves as a useful approximation.
Java is firmly in the group of development languages. It was meant, from the start, to be widely adopted by industry, and this goal has a distinct influence on priorities. Two of the constraints this introduces are:
■ The language should look (superficially) familiar to practicing programmers. Programmers usually have a favored language and are reluctant to change. A lot of time has been invested in learning the language, and re-gaining the same level of familiarity and competence is a significant investment. If a programmer looks at a new language and it appears easy to understand, they are more likely to adopt it.
■ Later language changes should be backwards compatible with previous versions. If a language has been widely adopted in industry, and then a new version of the language introduces a change that invalidates existing code (i.e., causes it not to compile anymore), this would create serious problems in companies who have adopted the language. It can be extremely time intensive and costly to adapt existing code for new language versions, and in itself provides no immediate benefit. Commercial companies are very wary of cost without benefit, and such changes may be a reason not to use a language. Java, therefore, has always promised full backwards compatibility: it has given a guarantee to developers that any new Java version will always correctly compile all existing Java code written for previous versions.
We shall see how these two principles shape a lot of the decision making in the language design, and how the details of many language constructs are directly dictated by these goals.
1.2 Superficial familiarity, ease of use and copying from C
Java was designed to be simple to use and understand: it was intended to have a small number of clear, orthogonal constructs, little redundancy and obvious interactions of its language elements. In other words: for every problem, there should ideally be one obvious way to write the solution.
But Java is also pragmatic: it makes compromises if it helps it progress in the real world. One of these compromises was to use C-style syntax.
C and C++ were two of the dominant languages used in production at the time of Java’s design, and many professional programmers were familiar with them. The decision to base the syntax and most statements directly on the syntax of the C/C++ family of languages was intended to entice programmers to try Java. “Look”, it was saying, “you know half the language already. This will be easy to learn.”
With the C-style syntax came the loops, = as the assignment symbol and == for equality, curly brackets and semicolons, and so on. With it came also a number of language constructs that Java may not have implemented were it not for the goal to look like C. These include the following.
Table 1
Stale Java: Inherited from C
Java construct | Comment |
Prefix/postfix increment operators
Examples: i++; ++i; n = ++i + i++; |
The four lines of Java code
i = i+1; i += 1; i++; ++i; all have the same effect. This goes clearly against the goal to avoid redundancy in the language design. The reason they all exist is efficiency: at the time C was designed, using these constructs could result in more efficient code. In modern systems, optimizers will generally generate efficient code in any case, and the advantage has disappeared. Writing a post- or pre-increment within another expression will no longer lead to faster execution, and often makes code harder to read. We generally recommend against using this style. |
Omission of visibility modifier
Example: String name; (as instance variable) |
In all our code presented here, instance variables declarations start with a visibility modifier:
private String name; In Java, it is legal to omit the visibility modifier entirely. This has defined semantics, but it is entirely illogical why this should not be expressed via a keyword (i.e., a package visibility modifier). The reason this is supported is only to allow code that looks more like standard C, which is no longer a reason at all. Avoid using this variant. The same counts for visibility modifiers for classes. Java allows them to be omitted. Don’t. |
Loops
Examples: do { … } while (condition); break; continue; |
The syntax of all loops in C has been directly copied to Java. From there, we get the for-loop, while-loop and do-while-loop.
It is not clear whether a language designed from scratch in the 1990s would have used those loop constructs. The do-while-loop is rarely used, and allowing break and continue statements in the middle of a loop can reduce clarity and readability. We generally recommend to avoid both. |
Another concession to the languages at the time was the decision to use C’s primitive types. Numbers and booleans were not implemented as objects, but as the same primitives that were used in C and C++. Apart from familiarity, a strong argument was efficiency: there was concern that the language would not perform well enough otherwise. This decision would have implications later.
2 The Challenge of Retrofitting New Constructs
Java, at its first release, was neither complete nor perfect. This did not come as a surprise; the language designers decided that it was worth getting a first usable version of the language out, and to add further improvements later down the line.
New language constructs or libraries were added with every release—we shall discuss some of the more important ones here.
2.1 Collections and generic types
Java 1.0 included only very few and simple implementations of data structures in its standard library. Java 1.2 added a comprehensive collection framework. This is when the sets, maps and lists first appeared that we are still heavily using now. Generic types, however, were still missing in the language. Every collection had only Object as an element type, and objects had to be cast back to their actual dynamic type manually every time they were retrieved from a collection.
One of Java’s flagship attributes was that the language was intended to be type-safe: all data was to be statically typed, and the system aimed at ensuring type correctness at compile time. Losing type information every time an object was added to a collection was clearly a big hole in this.
To fix this issue, in Java 5 the language added generic types. Instead of writing
ArrayList notes = new ArrayList();
we could now write
ArrayList<Note> notes = new ArrayList<Note>();
This construct is clearly better. The system can now ensure that objects added to a collection are actually of the desired type, and the type is still known when elements are retrieved from the collection. Type safety is restored.
A problem, however, arises from the goal mentioned earlier: full backwards compatibility. Remember that the Java developers promised that all existing Java code would still be valid in future versions. That means that the Java 5 compiler also had to accept code written with the old collections. Java now allows both variants, even though the new version is clearly better. In this case, the designers decided to add a warning to the compiler when the old style is used, but it is still valid. There is, however, no good reason to use it. Thus, the old style collections make it onto our Stale Java list (Table 2).
Table 2
Stale Java: Untyped collections
Java construct | Comment |
Untyped collections
Example: List a = new ArrayList(); |
Declarations of collections without a declared element type are valid in Java. There is no reason why these should be used. Use the generic version of the collection classes instead. |
Note Java has a confusing relationship with version numbers. The official versions were initially Java 1.0, Java 1.1, Java 1.2, and so on. This changed after version 1.4: the next release was not called 1.5, but Java 5. Internally, however, the version was still identified as 1.5. This continued until Java 8: Java 1.8 and Java 8 are essentially the same, with “Java 8” being the marketing name, and “1.8” being the official version number. This was finally cleaned up from Java 9, where the name and version number are now the same, and are incremented in whole numbers.
2.2 Autoboxing
An unintended side effect of the decisions to implement numbers as primitive types, not as objects, and the fact that collections hold objects, was that collections could not directly hold numbers.
As we have seen earlier, this problem was solved by introducing wrapper classes. It became apparent fairly quickly, however, that repeatedly writing code to wrap and unwrap primitives was tedious and made our code look messy. In reaction, the Java designers decided (in Java 5) to add auto-boxing.
Auto-boxing made it possible to write code like this:
myList.add(5);
int n = myList.get(0);
This code is now legal and allows us to enter primitive types into a collection. At least it appears like it—behind the scenes, we are still inserting objects.
There are several issues with this. The odd inconsistent naming is only one of them. (If they are called wrapper classes, why is the mechanism not called auto-wrapping?)
While the lines of code above make it possible to ignore the fact that wrapping happens, this simplification is still not possible in the declaration of the collection. That is, in declaring our list, we still have to write
List<Integer> myList;
and not
List<int> myList;
Thus, the whole auto-boxing mechanism only manages to handle primitives some of the time, not everywhere. To be able to use this construct correctly, a programmer must understand both the fact that the list actually holds the wrapper type, and the auto-boxing rules. While the code is simpler, the concepts that must be learned by a novice actually increase in number. The complexity of understanding what is really happening here goes up, not down.
Perhaps it is not so bad that use of auto-boxing requires this understanding. There is, after all, also a big performance difference between dealing with primitive types and dealing with objects, so it is a good thing if a programmer is aware of the performance implications.
This situation illustrates, however, a fundamental problem that we will encounter several times in this chapter: It is almost impossible to retrofit a new construct into an existing language while maintaining backwards compatibility, and do so seamlessly and with the same elegance as one might have achieved had it been in the original design. Almost always, compromises have to be made, and unintended side effects arise. We shall discuss some of those below.
To our list of Stale Java constructs, however, we can add explicit wrapping and unwrapping (Table 3).
Table 3
Stale Java: Explicit wrapping
Java construct | Comment |
Explicit wrapping and unwrapping
Examples: Integer n = new Integer(3); int n = l.get(0).intValue(); |
Explicit wrapping of primitives in their wrapper classes and unwrapping to retrieve the original value is no longer necessary. Auto-boxing and unboxing achieves the same and results in simpler looking code. |
2.3 The problem with keywords
One of the problems with retrofitting new constructs while maintaining backwards compatibility is that it is essentially impossible to introduce new keywords. Java encountered this challenge multiple times, and the Java developers have opted for different work-arounds each time. Let us look at a few examples.
Java 5 introduced the for-each loop. This loop works very well with the collection framework introduced earlier, and it was a welcome addition to the language. In an ideal world, this loop would have used the keyword foreach:
foreach (Student st : studentList) { ... }
Doing so would have required introducing a new keyword: foreach. The fundamental problem we encounter here is that the introduction of any new keyword fundamentally clashes with the promise of backwards compatibility. Imagine a class that uses the name foreach as a variable name. In Java 1.4, this would have worked perfectly well. But variable names are not allowed to be the same as a keyword, so had Java 5 introduced this keyword, the code would have no longer compiled.
In other words: new keywords are always in danger of breaking some existing code, since the keyword may have already been used as an identifier in an existing program.
So, what to do about this?
In the case of the for-each loop, the designers decided to reuse a keyword that already existed: for. As a result, we now have two different loops with the same keyword. This works, because the loops can be distinguished by the shape of their header—not ideal, but workable.
The same problem occurred repeatedly later. Another instance was in dealing with overriding of methods. At some point, programmers realized that a method declaration could inadvertently override an inherited method and, in the process, break existing code. If you, for example, declare a new method in your class, and by chance this method signature already exists in a superclass, but you are not aware of it, you are altering existing behavior without meaning to do so.
Therefore, at some point the Java designers decided that it would be better if programmers explicitly declared when they intended to override a method to make sure it is not accidental. In an ideal world, Java would now have a new keyword, and a method header might look like this
override public String process(Data data) { ... }
But we know what happened: we cannot easily add new keywords, so @Override was introduced as an annotation instead, with the effect that responsibility for checking correct use was shifted to the IDE instead of the Java compiler. Another quirk in the Java language, caused by pragmatism to maintain compatibility.
We can see a third instance of the same problem with the introduction of the var keyword in Java 10. It allows us to write variable declarations such as
var myMap = new HashMap<String, Integer>(); var number = 1.0;
Using this construct, the type of the variable being declared is inferred statically from the initial value. The variable is still statically typed—we just don’t have to write down the type ourselves.
It is actually incorrect to speak of the “var keyword”. For the reason discussed above, var is not a keyword, even though it looks like one. The Java language specification actually says that var is a “reserved type name”. This is a concept that did not previously exist, and it has the benefit that it does not invalidate existing code that uses var as an identifier.
As a side effect, it now allows us to write lines such as
var var = var();
as legal Java code.
2.4 Side effects and unintended consequences
The introduction of the var type also brings us to another observation about a maturing language: while a language develops and grows, its complexity increases. But this increase in complexity is not linear, it is exponential. Complexity comes not only from having to learn each new construct, but from the interaction of each innovation with each existing construct. And these interactions may be subtle, unintended, and lead to odd effects. Let us look at a few examples.
Double type inference
Java is statically typed. Every identifier that the compiler handles has a declared data type which helps the compiler ensure type correctness.
In early Java versions, all types had to be explicitly written by the programmer. Later, this requirement was softened in some selected cases: Java 7 introduced the diamond operator, which allows us to omit repeating an element type on the right-hand side of a variable initialization:
Set<Point> points = new HashSet<>();
In this case, the type is still statically fixed. The semantics are exactly the same as writing the full type on the right-hand side; the mechanism merely allows us not to write it out. The Java compiler uses type inference to decide the type (that is: it works out the actual type by looking at the left hand side of the assignment).
Java 10 introduced a further case of type inference: the var type discussed in the previous section. Again, the system now allows us to omit writing the static type, instead working it out from context.
Both of these mechanisms seem benign, and sometimes useful, on their own. But oddities can arise when we look at interactions of these different ideas.
We have two cases of type inference, one working right-to-left (the left side is decided by looking at the right), the other left-to-right. So, what happens if we put both together? Consider:
var collection = new ArrayList<>();
What now is the element type of this list? How can it be worked out? Is this code legal?
The answer is that current Java systems actually allow this code, and the answer to the question what actually happens is obscure.
This declaration is clearly bad code, and we should never write it. A language should strive for clarity and avoidance of surprises: in an ideal world, a reasonable competent programmer should be able to look at a line of code and be able to predict its effects, without being surprised by obscure behavior.
Java, in this case, breaks down in this rule. Through a combination of two benign constructs, it allows code to be written that is obscure and hard to understand.
Overall, we question the value of the var type in general. It saves a little bit of typing, and in turn introduces additional uncertainty and reading effort. Our recommendation is not to use it. Therefore, it gets added to our Stale Java set (Table 4).
Table 4
Stale Java: the var type
Java construct | Comment |
Using the var type for type inference
Examples: var s = new HashSet<String>(); var num = getValue(); |
Leaving out the type declaration on the left side of the assignment is legal, but brings little benefit. It saves a bit of typing, but saving typing is not the main goal of programming. Some people would argue that it makes the code neater, and therefore easier to read. We have doubts about the value of this benefit. It imposes, however, another task on the reader who must infer the type to make sense of the code. This is especially true if the right-hand side is, for example, a method call, so that the type is not immediately visible. Our judgement: the disadvantages outweigh the advantages. |
Equality of numbers
Another example of straightforward constructs interacting in non-obvious ways can be seen by considering this question: Can the condition in the following if statement ever be false?
if ((n > m) || (n < m) || (n == m)) { ... }
Assume that the code above compiles and runs. Is there a possible definition of the variables n and m so that the condition is false? If the code compiles, we know they must be numbers (otherwise we could not use the greater-than operator). And if they are numbers, n must be either greater or smaller or the same, surely?
The answer is: Yes, this condition can be false. Consider these declarations:
Integer n = new Integer(1); Integer m = new Integer(1);
Given these declarations, the body of the if-statement would not execute.
So what is going on here? The greater-than and less-than comparisons perform auto-unboxing (since the operator is not defined on the object type), but the == operator does not. It compares identity.
The effect is awful: we look at two entirely sensible constructs (object identity and auto-unboxing), and the result is that we can write code that seems to break fundamental rules of mathematics and logic. The golden rule of programming language design that a language should never surprise its programmer is certainly broken here.
Again, we see how retrofitting constructs into the language can interact with existing semantics in unexpected ways, leading to undesirable effects. In general, we should avoid ever doing calculations with wrapper objects. In fact, we should almost always avoid use of wrapper objects and auto-boxing in the first place. They are very rarely needed (Table 5).
Table 5
Stale Java: wrapper types
Java construct | Comment |
Wrapper classes, especially arithmetic operations on wrapped objects
Example: new Integer(n) Integer n = 3; |
Wrapper classes are seldom useful. They are used to insert primitive types into collections. However, collections of primitives are rare, and are often not the best solution. Think twice before using them.
But especially: Never perform mathematical operations on wrapped types. |
The equals and hashCode methods
A last example of unfortunate side effects is the fact that the collections framework’s hash functions (used, for example in HashMap and HashSet) use both the element object’s equals and hashCode methods. As a result, this operation will not work correctly if the equals and hashCode methods have not both been defined to depend on exactly the same attributes.
In other words, if a programmer overrides equals, but neglects to override hashCode accordingly, the resulting object is in danger of breaking a collection if another unsuspecting programmer inserts it into a HashSet. Yet Java simply has no mechanism to express this dependency so that the compiler could reject a class if this rule is broken. This error can be very hard to debug, because it leads to surprising behavior at a point in the code far removed from the actual source of the problem.
In all these cases, we can see how an evolving language becomes more and more complex over time, and how the complexity increases with every possible interaction of two constructs.
So, is Java designed badly? No. Languages must evolve and develop, and the challenges of growing an existing language that is in active use are intrinsic. Each language is an artifact of its own history.
3 The Pressure of Hardware Development
So why can we not just leave the language alone once it has been designed and avoid all the problems that come with adding constructs later? The answer is that users demand improvements, and rightly so.
There are several reasons why a language should be adapted over time, but perhaps the most obvious is changing hardware. When Java was initially designed in the 1990s, most processors had a single core. Most programs, therefore, were written as sequential programs, intended to be executed on a single core.
Today, even mid-range laptops have multi-core processors. Sequential programs (of the kind we have been writing in this book) make use only of a single core at a time. In other words, if your processor has eight cores, and your program uses exactly one of them, seven eights of your processor investment is wasted. This is fine for most types of programs, but not in applications where we really care about performance.
To write really efficient programs, we would like to write concurrent code—code that can use multiple cores simultaneously. (This topic is beyond the scope of this book; we mention it here only because it has influenced the evolution of the Java language.)
For various reasons, concurrent programs are easier to write if we use a functional programming style. Java, initially, did not support functional programming. When multi-core systems became standard, it became advantageous to add functional programming mechanisms to Java to enable easier writing of concurrent programs. So, Java 8 added the concepts of lambdas and streams we saw in Chapter 5.
Having these constructs available, many of the existing library classes could now be improved to make use these new capabilities. Thus, Java 8 also included significantly extended and improved class libraries, and especially improved version of the collections.
Again, let us briefly examine some of the consequences of these changes.
3.1 Extending libraries
Extending the Java libraries with new functionality provided by functional programming constructs, such as lambdas and streams, has several consequences.
Adding new methods to library classes is easy. Existing subclasses will inherit the new methods, and this does not pose a problem. The existing subclasses will not make use of these new methods, but they could do so in future versions of a software system. In any case, the original code compiles, and all is well.
The same, however, is not true for interfaces. When the Java designers add a new method to an interface in the standard library, every existing software system using that interface will break. Since existing classes implementing that interface will now miss a method implementation, they will no longer compile. Therefore, Java could not introduce new methods to interfaces and still maintain backwards compatibility. On the other hand, the whole introduction of functional programming constructs is futile if they cannot be used in some important, widely used interfaces from the standard library.
What to do?
The Java designers came up with a solution: From Java 8 onwards, Java allowed the implementation of methods in interfaces (“default methods”). This addition to the language solved the problem (interfaces can introduce a new method, and an implementing subclass that does not have this method still compiles), but this went against an important prior principle: interfaces, previously, just provided type inheritance, not implementation inheritance. The fact that interfaces do not contain code was important to avoid certain types of problems.
Again, Java development was pragmatic: a change was made to a previously cleaner, simpler rule in order to move the language forward. As a result, however, it is now quite difficult to really explain the rationale for—and difference between—interfaces and abstract classes. Both can provide partial implementations with abstract methods. They have very similar functionality, but different syntax. In a new language created from scratch, this would have been designed differently.
For our Stale Java list, this means we should make a decision and put one construct onto the list. Whenever there is redundancy—two constructs achieving the same thing—in the name of clarity and simplicity we should prefer one single style and avoid the other. In this case, default methods in interfaces should be avoided, unless you have the task to retrospectively extend an existing framework (Table 16.6).
Table 6
Stale Java: Default methods in interfaces
Java construct | Comment |
Default methods in interfaces
Example: public interface Filter { ... default void apply() { ... ; } } |
Avoid method implementations in interfaces. Keep interfaces fully abstract; when you wish to provide an implementation for one or more of its methods, use an abstract class instead. |
3.2 Method references vs inner classes
The introduction in Java 8 of functional programming elements, including lambdas and method references, made it possible, for the first time in Java, to treat code like data. A segment of implementation—a method or a sequence of statements—could be stored in a variable and passed as a parameter.
There are situations in programming where this capability is needed to write elegant code. The most prominent example is the implementation of graphical user interfaces (GUIs), which we discuss in the a later chapter. In GUI development, we need to associate an action (code) with an object (say, a button), so that something happens when a user clicks the button.
Before Java had method references, it used a clunky mechanism to do this: an object was associated with the button; this object had a single method implementing the action. This object needed a class to define it, which was used exactly once to instantiate this one object. Since the mechanism involved a lot of overhead, Java introduced a construct to make this a little easier: inner classes, and especially anonymous inner classes.
We shall not discuss these constructs here (you can search for them on the web if you are curious) beyond saying that today there is little reason for their use. Anonymous inner classes especially were a syntactical oddity in Java that made code hard to read and clunky to write. Table 7 adds them to our list.
Table 7
Stale Java: Anonymous inner classes
Java construct | Comment |
Anonymous inner classes
Example: item.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { saveAs(); } });
|
The functionality provided by anonymous inner classes is better provided by method references or lambdas. There is no need to use anonymous inner classes anymore.
Named inner classes still have valid use cases in some situations, but these are rare and their use should be limited. |
4 More Recent Language Changes
More recent language versions added a host of other new constructs. Examples include a new switch statement and switch expressions added in Java 14, text blocks added in Java 15, and records added in Java 16. Java 17 added the concept of sealed classes.
Some of these provided new functionality that did not previously exist (sealed classes, records), but others, such as the new switch statement syntax and switch expressions provided new, more flexible syntax as an alternative to an existing construct, in effect making the previous version obsolete.
The syntax we are using for switch statements in this book looks like this:
switch (input.toLowerCase()) { case "yes", "y" -> deleteFile(); case "no", "n" -> println("Cancelled"); default -> println("Unrecognised"); }
Previous versions of the switch statement had a slightly different syntax:
switch (input.toLowerCase()) { case "yes": case "y": deleteFile (); break; case "no": case "n": println("Cancelled"); break; default: println("Unrecognised"); break; }
As always, both versions are now available in Java. The new syntax is more elegant, and less error prone. The old style had a “fall through” behavior, where execution continued into the next case option if no break statement was included. This was needed to allow multiple cases invoking the same action, but led to regular errors when programmers forgot to include break statements. The new syntax is more concise and safer.
At the same time, a switch was now allowed as an expression (e.g., on the right-hand side of an assignment), providing a useful and shorter variant of a previous idiom. Consider this example:
state = switch(Math.signum(value)) { case 1 -> "positive"; case -1 -> "negative"; default -> "zero"; };
Previously, this code would look like this:
switch(Math.signum(value)) { case 1: state = "positive"; break; case -1: state = "negative"; break; default: state = "zero"; break; }
The new version, again, is more concise, safer and more elegant.
The old switch syntax is shown here only so that you can recognize it when you encounter it in legacy code; there is no longer any need to use it yourself, and we add it to our list in Table 8.
Table 8
Stale Java: Old-style switch syntax
Java construct | Comment |
Old-style switch
Example: switch (input){ case "y": saveAndExit(); break; case "n": exit(); break; default: error("invalid command"); break; }
|
The old-style syntax for switch statements, recognizable by the use of a colon after the case labels, is more verbose and less safe than the new syntax.
The most common error in practice was omitting the break statement after a case implementation. This does not lead to a syntax errors, but simply continues execution into the next case. This was often unintended and can lead to runtime errors. The new switch syntax should be preferred. |
5 The future, and what it may bring
The development of the Java language has not stopped. The Java development team continues to make improvements to the language, and if you become a professional Java programmer, you will have to keep up to date with ongoing developments.
One last language change we shall briefly discuss here are value classes.
At the time of writing, value classes are currently implemented only in experimental (preview) releases of the Java system and have not yet been officially added to the language. The code name for the project to design and implement this extension is Valhalla (you can search on the web for this name if you like to read more).
Value classes, and a related construct named primitive classes, aim to remove a very fundamental principle of Java: that every object is stored by reference. These constructs would store objects inline (that is: directly in another object).
The reason for this is, again, performance and hardware evolution. Currently, objects are stored indirectly and have to be loaded from heap memory. When Java was developed in the mid-1990s, the performance time of a memory fetch and an arithmetic operation was roughly the same. On modern hardware, this is no longer true: memory fetch operations are now vastly slower (often by a factor of 500 or more) than arithmetic operations, leading to load operations via references slowing down performance.
The new model would allow objects being stored on the stack, with little overhead. This has the potential to lead to significant performance improvements for data-heavy applications (such as data analytics or modeling).
The implications, however, are considerable. We have mentioned before the unintended consequences and unfortunate interactions of new constructs with old: primitive classes would mean that objects lose identity, and equality can be checked only based on state. Assignment would duplicate objects. The semantics of polymorphism are affected.
On the plus side, this construct would merge object types with primitive types (e.g., collections of primitive types would be possible), on the other hand much of what we assume about object structures in Java would no longer be true. We would have to learn entirely new object semantics for primitive classes. The language would be more powerful, and more complex.
Valhalla is a difficult and sensitive project. This can also be seen in its timeline: It has been planned since 2014, initially intended for release in Java 10, and then repeatedly delayed.
We mention it here as an example of the difficulty of evolving a language, and to encourage you to keep an eye on modern developments in Java. By the time you read this, there will be new ideas, new proposals, and new additions to the language.
6 Simplicity, and the evolution of languages
All the changes made to the Java language were made to make Java better: to make it more flexible, more powerful, more efficient.
An overarching goal in any language change is to strive for simplicity: One should always search for the simplest solution. Some changes necessitate making the language more complex—that is unavoidable—but the designers were still looking for the simplest solution that meets the goal.
A problem with this is that simplicity is an ambiguous term—it means different things to different people.
Professional programmers often think about simplicity as the elegance with which they are able to write their code. For every given problem, they want a construct that allows them to express the solution in elegant, simple terms. Having more constructs in the language, more flexibility, more options, supports writing simpler code.
Beginners (and teachers), on the other hand, are interested in a different form of simplicity: we want a language that is small and has little redundancy. For every task, there should be one (and ideally only one) clear and obvious way to do it. Choices are not helpful: having to make choices before understanding all subtle implications is a problem, not an opportunity.
Thus, for professionals, simplicity means more constructs, for beginners it means fewer constructs.
A side effect of language evolution is therefore that every language, over time, becomes less suitable as a first language to learn. Older languages are more complicated, have more quirks and odd corner cases, and just more stuff.
This is not a fault of Java specifically, or the design team of the Java language. Every language goes through this evolution. A language either grows and adapts, or the whole language becomes stale. Once that happens, it will only be used to maintain legacy code, and developers will look to newer languages for new, exiting projects.
Java, so far, has managed very well to stay in the game. But one day, we may need a new language for introductory teaching.
7 Summary
In this chapter, we have shown some Java constructs that we recommend not to use, or to use sparingly. The reason we present them here is that you may very well encounter them when reading other people’s code, and a good programmer should be able to recognize and interpret them.
It is also helpful to understand why these constructs have fallen out of fashion, so that we can make an informed choice whether their use in a particular case can or cannot be justified.
We have framed the discussion of these constructs along a presentation of some milestones in Java’s history. Having a sense for the way languages develop (and why) will give you a better understanding of the features and characteristics of languages, and will make you better equipped to participating in debates of relative merits of particular languages for specific tasks.