Teaching Java is Getting Simpler

.png

I just talked to a professor who is using “Big Java”, my college textbook for beginning programmers. The professor had useful suggestions for improvement in the ebook. Since I had the chance, I asked “Are you planning to switch to Java 25 so that your students can use the simple onramp features? Should I cover them in the next edition?” Blank stares ensued.

Since I obsessively track all Java developments for accurate and timely updates of my “Core Java” book for professional programmers, I pay close attention to these changes. But of course, teachers in the trenches have other things to obsess about. If you are among them, perhaps you will find this article helpful.

Sayonara to public static void main(String[] args)

For the last thirty years, ever since Java 1.0, the “Hello, World” program in Java looked like this:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

So many questions. What is public? What is static? Why String[] args? What with the oddball System.out? Nowhere else in Java do you use a public lowercase constant. (Not actually constant—you can change it with System.setOut.)

Python programmers gloat. For them, it's just

print('Hello, World!')

That's potentially nicer. But not as nice as you think, once you get into Python for a bit. Why is print available without an import, but other library functions must be imported? And prefixed with a module name. Why is there some code that is in functions, and other code that is naked at the top level? What about the enigmatic

if __name__ == "__main__"

In modern Java (starting with Java 21 in preview mode, and finalized in Java 25), you can do better:

void main() {
    IO.println("Hello, World!");
}

The main function no longer needs to be public static, and the String[] argument is optional.

The Unnamed Class

You don't even need a class. Just put void main() { ... } in a Java file.

You can add other functions in the same way:

void greet(String subject) {
    IO.println("Hello, " + subject + "!");
}

They all become methods of an unnamed class.

Later, when you teach classes, the transition is simple. With class NamedClass { ... }, what used to be top-level, turns into methods.

An unnamed class can also have top-level variables that can be accessed by all functions. You probably only want to teach this for constants:

final String PREFIX = "Hello";

void greet(String subject) {
    IO.println(PREFIX + ", " + subject + "!");
}

The java.base Module

An unnamed class automatically imports the entire java.base module. This includes all collections, classes used for input and output (such as Scanner, PrintWriter, Path), and many others. Students don't have to worry about the intricacies of import statements.

As students gain proficiency and implement their own classes, they can get the same convenience with the statement

import module java.base;

Of course, they can still use traditional package imports.

A Note on Method Invocation

Look at the print statement

IO.println("Hello, World!");

Why not just println? Java aims to be both simple and regular. When you use a “function” (i.e. static method) that has been defined outside your program, you specify where it comes from: IO.println, Integer.parseInt, Math.pow, and so on.

If you call a function in your own code, you don't use a prefix. After all, the class is unnamed:

greet("Sailor");

Of course, as in Python, students need to learn about methods that are invoked on an object:

String tail = str.substring(1);

Here again, Java is more regular than Python. In Python, the length of a string is obtained from a global function (without an import), but in Java, it's a method:

len(str) ## Python
str.length() // Java

Launching Programs

If your students use the command line, invoking a Java program has become simpler. There is no need to compile the program. Simply call

java MyProgram.java

The program automatically compiles and runs, just like with Python.

This also works for programs that consist of multiple source files. Just put them in the same folder and launch the file with the main method.

Records

Java records describe immutable data, usually with some methods. Here is a typical example:

record Point(int x, int y) {
    double distance(Point other) { return Math.hypot(x - other.x, y - other.y); }
    public Point translate(int dx, int dy) { return new Point(x + dx, y + dy); }
}

A constructor, as well as component accessors, toString, equals, and hashCode methods, are generated automatically.

Point p = new Point(4, 0);
Point q = new Point(0, 3);
double dist = p.distance(q); // 5
Point r = p.translate(1, 2) // r.x() is 5, r.y() is 2

Python programmers often use ad-hoc dictionaries to describe such values; in this case, with keys 'x' and 'y'.

The Java approach is better. You get a named type, and it is easy to add methods, as in the example above.

When you teach a Java course for beginners, should you embrace records?

It can be a good idea. There is a fair amount of accidental complexity with constructors, component accessors, toString, equals, and hashCode methods. Why not get that for free?

It also seems like a good idea to treat immutable data as the normal case.

As you saw in the example, methods are valuable in an immutable context. What we used to implement as a mutator method becomes a method that yields the new value, such as translate in the Point record.

A downside is added complexity. Do you want to teach record, then class, in the first semester?

It is worth questioning the first example of object-oriented programming. In my college texts, I use a BankAccount, with a mutable balance that is affected by deposit and withdraw methods. A bit dull perhaps, but effective for demonstrating state.

I felt it was important to have mutable state in the first example of a class. I remember how disappointed I was when I saw, as the first example of some tutorial, a Loan class with static methods for the total payoff, the years to payoff, and so on. That did not feel like OO!

But why are we teaching mutable state for beginners? When I wrote that textbook, I was motivated by the classic definition of the nature of objects: behavior, state, identity. But now we know that state can be hard to manage, and identity isn't always a blessing. Behavior is really the key concept.

I could see the benefits of a first semester Java course that uses records and interfaces, and leaves classes to the end, or even the second semester.

Pattern Matching

I never taught the classic switch statement to beginning students. The breakand fallthrough behavior is confusing, and I want my students to worry about more important topics than deciding between switch and if/else. (In my textbook, I cover switch as a “special topic” to support those instructors who feel that students should at least have seen it.)

Recent versions of Java has brought improvements to an entirely different kind of switch, used for pattern matching.

Imagine a hierarchy of geometric shapes with an interface Shape and records Circle and Rectangle. You can process them as follows:

double area = switch (shape) {
    case Circle(var _, var radius)
        -> Math.PI * radius * radius;
    case Rectangle(Point(var x1, var y1), Point(var x2, var y2))
        -> Math.abs((x2 - x1) * (y2 - y1));
};

We don't care about the center of the circle, hence the underscore. The radius variable is initalized with ((Circle) shape).radius(), and x1, y1, x2, y2 are obtained from two levels of accessors.

Right now, this “deconstruction” works for records, but it will soon be extended to classes.

This form of switch doesn't have the complexity of fallthrough. Each case is separate. The fallthrough form still exists, but you don't have to teach it. I wouldn't.

The instanceof operator also supports patterns:

if (shape instanceof Circle c) { ... } 
if (shape instanceof Circle(var _, var radius)) { ... }

But you don't have to teach instanceof to beginners. You can just use switch:

switch (shape) {
    case Circle c -> { ... }; // The variable c has type Circle, no cast needed
    default -> {};
}

Notebooks

For Python programmers, the Jupyter notebook interface provides an intuitive onramp for beginning programmers. Code snippets are placed in cells, and the output is displayed as text, tables, or images.

.png

It has always been possible to use a Java kernel with Jupyter notebooks, but the installation was a bit complex. The Jupyter Java Anywhere initiative makes this much easier.

Here are the instructions for Google Colab:

  1. Visit Google Colab
  2. Open a new notebook
  3. Paste these lines in the first cell and run it:
    !pip install jbang 
    import jbang
    jbang.exec("trust add https://github.com/jupyter-java")
    print(jbang.exec("install-kernel@jupyter-java --java 21 --enable-preview").stdout)
    
  4. Select Runtime → Switch runtime type → Runtime type: java (JJava/j!)
  5. Make a new cell and code away in Java:
    var countdown = new ArrayList<Integer>();
    for (int i = 0; i < 10; i++) { countdown.add(0, i); }
    countdown
    
  6. Execute the cell to see the result:
    [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
    
  7. You can easily import libraries:
    %maven org.dflib:dflib-jupyter:1.2.0
    

    Then you can use Java for graphing and data processing:

    import org.dflib.*;
    DataFrame df = DataFrame.foldByRow("name", "year").of(
                    "C++", 1985,
                    "Python", 1989,
                    "Java", 1995);
    df
    
    import org.dflib.echarts.*;
    EChartHtml chart = ECharts
            .chart()
            .xAxis("name")
            .series(SeriesOpts.ofBar(), "year")
            .plot(df);
    chart
    

Google Colab can be slow. If you prefer a local notebook, use JTaccuino. It is an attractive JavaFX application that runs on Windows, Mac OS X and Linux.

.png

Conclusion

Java has many advantages as a teaching language. Most importantly, it has compile-time typing. Many programming errors are reported before the program runs, when they are much easier to diagnose and fix than in a running program.

However, Java, as an industrial-strength language that is 30 years old, also has its share of complexity and baggage. In the last couple of years, the Java designers at Oracle have worked at reducing some of that accidental complexity, in order to “pave the onramp”. In particular:

All these features will be available in Java 25, to be released September 2025. That is a “long term support” release. They are currently in Java 24, some marked as “preview features”.

If you are currently teaching Java, now would be a good time to consider how you can bring some of those features to your students so they no longer have to see public static void main.

If you have moved away from teaching Java, but are missing compile-time typing, and the great IDEs, educational tools, and libraries that the Java world has to offer, have another look!

Comments

With a Mastodon account (or any account on the fediverse), please visit this link to add a comment.

Thanks to Carl Schwann for the code for loading the comments.

Not on the fediverse yet? Comment below with Talkyard.