A Brief History of Java

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.

Eight tips for teaching programming to novices

Title slide: Eight tips fro teaching programming to novices

I recently gave a talk, titled “Eight tips for teaching programming to novices“, which presented my recommendations of programming teaching practices I posted here some time ago.

In this talk, I did not only present and explain the tips themselves, but I presented concrete examples of what these tips mean, and how you could enact them with actual programming projects or assignments.

Currently, I do not have a recording of the talk, just the slides, which you can find here.

The slides by themselves do not really show how to put things into practice. If you are interested in this talk for a group of teachers somewhere, contact me and I’d be happy to present the talk again elsewhere.

Announcing: Kotlin in BlueJ

I am very happy to announce that we – the Programming Education Tools group here at King’s – have started a collaboration with JetBrains to bring Kotlin to BlueJ.

BlueJ is one of the most popular IDEs for teaching programming and currently supports Java and Stride as programming languages.

Kotlin is a modern general purpose programming language developed and maintained by JetBrains, running on the JVM and fully interoperable with Java.

King’s and JetBrains have started a collaboration to implement support for Kotlin in BlueJ, with both organisations contributing developer time to the project. We expect beta versions to become available for testing in the Spring, and full support being released later this year.

If you are interested in keeping up to date with progress, check back here at a later time, or – if you are a teacher – sign up to the Blueroom, where we will keep members up to date with developments.

Guideline 9: Programming for the many, not the few

Decorative image showing "Guideline No 9"

Over the last week or so, I published here eight guidelines for teaching programming. This last one is not so much a guideline, as a guiding thought. The first eight were reasonably concrete tips about how to (or not to) approach the teaching of programming to novices. This one just illuminates one aspect of our teaching that may be helpful to hold in the back of your mind to guide your general approach to the content you decide to teach. It has to do with how initial programming teaching has changed over the last 20 years.

When I started working in teaching programming to beginners, the target audience we had in mind were first year university students. This is where programming learning really happened.

It is not now.

Today, almost every learner encounters some form of programming long before university, at some time during their school age. The migration of initial programming teaching from universities to schools creates a big shift. The difference is not primarily that learners are younger, but that they have different goals.

When we started to move computer science teaching into schools – initially into the last few years of a curriculum – the content was almost universally derived from introductory university courses. Mostly, teaching at school was a direct copy of teaching at university.

Now that teaching of computing has migrated down the years, is taking place sometimes in primary school, and in earnest in early secondary school, this makes no sense anymore.

The goal of the teaching of programming in a university computer science degree is to educate potential future software engineers. The learners had chosen a career in computing, and professional software skills are an essential part of that education.

In early secondary school, we teach programming to all pupils. Not just the ones who go on to work in a software profession, but all of them.

We should be very aware that the goal is not to train software engineers. Most of your class will never work as a professional software developer.

When we shift from training computing professionals to educating the entire population, we need to adjust our objectives. Learning professional software engineering practices is no longer the main goal. Gaining a deep understanding of the power and limitations of computing systems in general is. Grasping how computing impacts our life, the possibilities and dangers of modern computing systems, the potential for creativity, for science, for social change.

Understanding how the world can be modelled digitally, the transformative power that results from this, the limitations and dangers, the joy, the potential – that is the goal.

At secondary school, we are trying to create educated citizens in a digital world, not software engineering professionals.

Keeping this thought in mind when planning your teaching helps to provide guidance in judging the importance and value of different elements of content in the curriculum. It can help you decide what to emphasise, and what to leave as challenges for the keen few.

That said, programming is still best door to enter the magical world of computer science. It gives us concrete and immediate experiences of the power and joy that can be had by creating something out of nothing. It is wonder-ful in very sense of the word, and lays the foundation for all else that comes afterwards.

Guideline 8: Use A Spiral Approach

A decorative image showing the test "Guidelines for Teaching Programming" and the number 8.

One common mistake (and yes, I think it’s a mistake!) that I often see in teaching (and especially in textbooks) is to try to be exhaustive too early. When it is time to mention, for example, data types, you have to say all there is to say about data types. Or if it is a textbook: since this is the chapter with data types, it has to have a table of all the types and all their operations.

Since types are needed early, this lesson/chapter comes early. Perhaps a pupil only needs to use a number and a string, but suddenly they hear everything about floats, booleans, chars, type conversions, widening casts, and whatever else there is to say about the intricacies of type systems.

The problem is that learners do not know which bits of the information is relevant right now, and which isn’t. It’s just stuff.

The result: The relevant information is lost, the budding programmer is overwhelmed, learning is rote (and therefore boring), and no one but the author is happy.

The thing to do instead: Use a spiral approach. When encountering any concept for the first time, say only as much as is needed to get the task done. Get learners to get a result on screen without overloading them with detail. Then return to the topic again (and again) over the next weeks, each time deepening the discussion and understanding.

There are several benefits to it: First, you avoid adding cognitive load for no (immediate) gain. Let pupils concentrate on what is needed. Second, you get to the fun parts more easily and quickly. And thirdly, linear instruction doesn’t work in programming anyway. Constructs in any modern language have circular dependencies, and you cannot fully explain everything in a single sequence.

By revisiting concepts multiple times, you can get to interesting results quickly, deepen understanding as learners develop, and discuss influences of specific language constructs on others, pointing out the (circular) dependencies of one language design element on the others.

Guideline 7: Creativity (or: Open & Closed)

Decorative image showing the text "Guidelines for Teaching Programming" and the number 7

Many teachers think that the main goal of programming instruction in early secondary school teaching is to make pupils understand computational thinking, algorithms and the writing of programs.

I think this is wrong.

The main goal, at this level, is to get students interested.

What I most want from early programming instruction is to get pupils excited, having experienced the creative potential and expressive power of computing, and wanting to know more.

The best way to achieve this is to support creativity in the learning of programming. Supporting creativity means finding ways to let pupils take control, to let them add their own ideas and extensions, to allow creative experimentation and expression.

Allowing creativity is not always easy: You should use creative examples, such as stories, games, art, animations or simulations. These are areas of expression that allow pupils to shape the content. You need a programming environment that supports this (more on this later).

But also: you need to structure your projects to make this possible. The way to do this is to use what I call the Open/Closed principle: The assignment you give your students should be simultaneously open and closed.

What I mean by this is this: The task should include a core component with the expected minimum achievement that is well specified and carefully scaffolded. This is the Closed part: it allows the less secure and less adventurous students to concentrate on the core material in a guided environment, and supports them to succeed.

But the task should also include an Open part: a challenge to extend the project in various directions of interest to the learner, to invent, to experiment, to stretch. This allows the keen learners to take control, to make the project truly theirs, to excel and to show what they can do.

An example might be a game where you specify core functionality that everyone is expected to implement, but challenge students to add their own extensions and personalisations.

I will come back to how exactly to do this later. But for now, the takeaway is: The most important goal is to let pupils become creative, so that they experience the power and joy that writing programs can bring, the joy that we have all experienced, but that is so impossible to express in words.

Guideline 6: Illustrate the process

Decorative image showing the text "Guidelines for Teaching Programming" and the number 6

One common pattern of programming teaching – especially in books and other written material, but also in in-person teaching – is the focus on the product, rather than the process.

The usual structure is: (1) Here is a problem. (2) Here is a program (involving some new construct) to solve the problem. (3) Here is a discussion/explanation of the solution and the new construct.

The problem with this is that the solution appears – apparently in one step out of the ether – in its entirety.

In novice learners, this creates the impression that this is how experts work: If you know how to program, you will be able to think for a moment, and then just write down the solution.

They struggle with coming up with a working program, and even if they have an idea, they make mistakes, run into compiler errors, have bugs in their code, and think “I am just not a good programmer”.

The fact that we all work like this – write some code, discover mistakes, correct, change our minds, slowly build – must be one of the best kept secrets in programming.

For you as a teacher this means: Don’t just teach your pupils what the correct program looks like, teach them how to get there.

Guideline 5: Show program structure

Decorative image showing the text "Guidelines for Teaching Programming" and the number 5

In much of programming teaching at school level, the discussion revolves around syntax and statements. I realise that – with young learners – mastering a loop is initially a challenge. But the statement level (and especially syntax) is neither the difficult, nor interesting, nor important part of programming.

Every pupil will learn to write an assignment and an if statement. This is not where the difficulty lies, nor where the important concepts, which we want our students to understand, are illustrated.

The most important part of learning to program is abstraction, and its incarnations in structuring a problem, conquering complexity and guiding our thinking.

In object-oriented languages, this is embodied by classes, objects and methods. In procedural approaches, it manifests in procedures and functions, scope of data and separation of concerns.

It is important to visualise this structure early and often. In the best case, use a programming environment that provides the structure visualisation. Use an environment that de-emphasises syntax, ideally removing the need to memorise where it can.

If you don’t have an environment like this, do it yourself: draw diagrams on the whiteboard. Do what works, but TALK ABOUT STRUCTURE.

The challenge of learning about statements will become meaningless. The challenge of designing program structures will remain – it is an intrinsic part of being a programmer and computer scientist. Make this the centre of teaching your students good programming practice.

Guideline 4: Don’t use “Hello World”

Decorative image showing the text "Guidelines for Teaching Programming" and the number 4

I am now skipping two of the guidelines from the paper, because they are specific to object orientation, and I would like to first concentrate on the general advice. (I might come back to the OO advice later.) Therefore, our Guideline 4 here is actually number 6 in the paper. It tells us not to start with “Hello World” as a first example.

The main problem with “Hello World” (or the only marginally more interesting “Hello <NAME>”) is that it is just so boring!

If you want to show a 15-year old why they should be interested in programming as a discipline, why on earth would you do THIS? Felienne Hermans, in a recent article, described her experience in teaching beginners where a pupil commented “If I want Hello world on the screen, I can type that in Word.” (1) And that student is quite right!

The problem is not restricted to Hello World. When I was a student, we were told to write a program that prints out all the prime numbers from 1 to 100. This is a boring example. There is so much wrong with it.

Firstly, it’s Maths. Maths immediately puts off 80% of your class.

Secondly, if you really wanted to know the prime numbers up to 100, what would you do? No, you would not write a program for it. You would google it. It would take you about three seconds and bang! – there they are. Using a computer to solve this problem is entirely artificial. It makes nobody understand why you would want to learn to program.

Thirdly, the program is finished when it prints exactly what I knew it would print all along! To test my program, I would work out the complete answer myself, and then confirm that the program does it. You learn absolutely nothing from running the program. You might learn the program statements, but not why you would want to write them. It excites no one.

And fourthly, imagine a pupil is really quick, finishes the exercise after five minutes, while everyone else is just getting started. What do they do then? How can they stretch, extend, be creative? Add their own ideas, and grow? Print out the primes to 200?

The problems is that, once the program has run, there is absolutely no reason to ever run it again. A program that you do not want to run twice is not an interesting program.

So my advice is: use examples of programs where you actually need a computer to do the task! Games and simulations are such examples. Visualisation of data, or graphical animations. Use examples that give learners immediate ideas what they want to add!

Of course, you need an environment that supports this, and makes it easy enough to do this from your first hour of teaching. But these exist. Use them!

Every time I see another “What is your name? Hello Michael” program, I could tear my hair out in frustration at how far we have not come.


(1) Felienne Hermans: The Story of Building Hedy: A Programming Language with Cognitive Science in Mind, Informatics in Education, Volume 23, Issue 4 (2024), pp. 791–822

Guideline 3: Read Code

Decorative image showing the text "Guidelines for Teaching Programming" and the number 3

The third guideline of teaching programming is to read code, lots of it, and early.

I have already talked about this in Guideline 2, which was about not starting with a blank screen. While Guideline 2 was aimed primarily at avoiding the empty-screen paralysis (and discussed code reading as a side issue), this one is explicitly about the importance of code reading as a skill, and not limited to the first example.

Many programming courses concentrate solely on code writing, and assume that – somehow – code reading will follow. Either it is assumed that this is an easy skill that exists automatically, or that it is acquired by osmosis somehow. More often teachers do not think about this much at all.

Code reading is, in fact, a valuable and separate skill that should be practiced. And it should be practiced early, since it is of great value to learning to write code.

For the first few projects (spanning weeks, if not months), start each project by handing out to students a partial, half written application, and make the goal to fix/complete/extend. In this way, the first task required is to read and understand the existing code, before working out what to extend and write. Discuss and require code reading explicitly.

A large amount of peripheral learning happens when reading well written code. We get benefits from an apprenticeship approach, where learners can copy the master’s practices at many levels, and learn by example.

This approach is also much more realistic as preparation for actual work: No new programmer joining a software company will, as their first task, be asked to write a new system from scratch. It is always the reading and extending of existing software.

Of course, this requires teachers to show students code that is worth reading, that embodies good practice, from which also techniques can be gleaned that are not the current focus of discussion. It requires teachers to use a significant number of well-written examples that exemplify good practice. Not all teachers will always be able to write such examples, but resources can be shared. Look for resources that support this approach.