Java

Java is one of the most popular languages on the planet. It’s one of the official languages for the Android platform. Lots of jobs require Java.

Hello

Welcome to a technical introduction and overview of the Java language, targeted to intermediate programmers. If you feel a little less confident in your programming skills and would prefer to learn Java by example, start with this Basic Java Bootcamp. If you are a bit more advanced, feel good about programming, and especially if you are studying programming languages, read on!

We begin the usual way:

Hello.java
void main() {
    IO.println("Hello world");
}

To build Java programs from source code and run them locally, you will need to get a Java Development Kit, also known as a JDK. Make sure to get version 25 or later. Once installed, running the code is as simple as:

$ java Hello.java
Hello world

We’ll jump right to a terminal-based number guessing game with reading (IO.readlin), writing (IO.println), random numbers, while loops, if-statements, parsing strings, and error handling:

GuessingGame.java
void main() {
    var secret = new Random().nextInt(100) + 1;
    var message = "Welcome";

    while (true) {
        var input = IO.readln(message + ". Guess a number: ");
        int guess;

        try {
            guess = Integer.parseInt(input);
        } catch (NumberFormatException e) {
            message = "Not an integer";
            continue;
        }

        if (guess < secret) {
            message = "Too low";
        } else if (guess > secret) {
            message = "Too high";
        } else {
            IO.println("YOU WIN!");
            break;
        }
    }
}

How many more days until the next New Year’s Day?

NewYearCountdown.java
void main() {
    var today = LocalDate.now();
    var nextNewYearsDay = LocalDate.of(today.getYear() + 1, 1, 1);
    var days = ChronoUnit.DAYS.between(today, nextNewYearsDay);
    IO.println("Hello, today is " + today);
    IO.println("There are " + days + " days until New Year’s Day");
}

Hey, nice support for calendars and dates. How about times, too? The DateTimeFormatter has great tools for formatting dates and times. This program prints the times on a 12-hour clock in which the hour and minute hands are antiparallel (180° apart):

AntiparallelClockHandsComputer.java
void main() {
    for (var i = 0; i < 11; i++) {
        var time = LocalTime.MIDNIGHT.plusSeconds((int)((i + 0.5) * 43200 / 11));
        IO.println(time.format(DateTimeFormatter.ofPattern("hh:mm:ss")));
    }
}
$ java AntiparallelClockHandsComputer.java
12:32:43
01:38:10
02:43:38
03:49:05
04:54:32
06:00:00
07:05:27
08:10:54
09:16:21
10:21:49
11:27:16

While we’re on the subject of dates and times, it’s worth pointing out that Java has good support for locales. Try out this application:

Day.java
void main() {
    var input = IO.readln("Enter a date (YYYY-MM-DD): ");
    try {
        var date = LocalDate.parse(input);
        for (var language : List.of("en", "hi", "ar", "es", "ru", "zh", "pt", "ko")) {
            var locale = Locale.forLanguageTag(language);
            IO.println(date.getDayOfWeek().getDisplayName(TextStyle.FULL, locale));
        }
    } catch (Exception e) {
        System.err.println(e.getLocalizedMessage());
    }
}
$ java Day.java
Enter a date (YYYY-MM-DD): 2021-03-17
Wednesday
बुधवार
الأربعاء
miércoles
среда
星期三
quarta-feira
수요일

Language introductions need to feature Fibonacci numbers. Unlike Python, whose integer type is unbounded, Java’s int type has a very limited range. To get big integers, we use BigInteger:

TwoHundredFibs.java
void main() {
    var a = BigInteger.ZERO;
    var b = BigInteger.ONE;
    for (var i = 0; i < 200; i++) {
        IO.println(a);
        var c = a.add(b);
        a = b;
        b = c;
    }
}



Java is one of the most popular languages on the planet, ranking in the top 5 since its first public release in 1995. It is extremely popular in the enterprise computing space, and used often in banking and finance, but it can be found everywhere. Its standard library and ecosystem are huge. The community is extremely active and the language is constantly evolving. Java 25 was released in September of 2025 and has some amazing improvements over previous versions, and that’s the version we are covering in these notes. However, it’s always fun to see those Fireship 100 Seconds videos for programming languages, and the Java one is no exception. Keep in mind though, that Jeff’s Java video was way back in the pre-25 days, when Java programs were more verbose (and even the simplest of programs required explicit class declarations and the dreaded “public static void main string array args!”):

It’s time for us to go into some detail.

Program Structure

Java is designed for enterprise-level applications, so it not only full of great language features, it has a massive standard library. So massive that it needs a well-defined organization. All Java programs are made up of classes. The many thousands of standard classes are organized into packages. In our introductory examples, we have seen IO and Exception from the package java.lang, LocalDate from the package java.time, ChronoUnit from the package java.time.temporal, DateTimeFormatter from the package java.time.format, List and Locale from the package java.util, and BigInteger from the package java.math. Each of these packages come from the module java.base.

It is helpful to know that Java programs have both a logical and a physical structure.

Logical Structure

Logically, Java code and data are defined within classes that are grouped into packages, which are collected into modules. That’s it. There is no code outside of a class, no classes outside of a package, and no packages outside of a module. This is true! In fact:

This simple hierarchy keeps the overall structure easy to understand, while making it easy to manage the many thousands of classes in the standard library and the hundreds of thousands of third-party classes.

Physical Structure

Physically, your program is written in a collection of source code files. How do we make the connection between the logical structure and the physical structure?

In general, source code files look like this:

javastructure.png

A class can be defined not only with the keyword class but also with record, enum, or interface.

In any given file, at most one class definition can be public.

If the public class is called X, then the file must be called X.java (and the name is case sensitive—always follow convention and choose a capitalized name).

Each class definition (in source code) is compiled into a binary class file. The class “file” may be in memory or it may be written to the file system. Packages can be split over several source code files, but each class has to be wholly contained in a file.

javacompilation.png

Exercise: Stop and make sure you understand and can explain the difference between a source code class and a compiled class.

If you don’t put a package declaration in a file, its class(es) will go into the big global unnamed default package. This is fine for short programs and scripts, but in large applications, the use of the default package is discouraged.

If a file has code that is outside of a class declaration, the file is considered a compact source file. When compiled, an anonymous class will be generated and the top-level declarations of the file will be assumed to belong to that class. You should never rely on the name of the generated class, since it is never guaranteed. It will have a name; you just won’t know or care what it is.

To modularize a project, you designate a folder as your module’s root folder and create a file called module-info.java. It is structured like:

module com.example.game {
    exports com.example.game.api;
    requires java.desktop;
    requires java.logging;
}

Any packages in your project not explicitly exported are invisible to the outside world.

Modules are somewhat outside the scope of these introductory notes, so we won’t be featuring any runnable examples of modularized code here. Don’t let that stop you from further study. You’ll need to work with them when building professional-level applications.

Java’s Standard Library

The standard library comes with close to 5,000 classes organized into hundreds of packages, grouped into a few dozen modules.

Execution

Execution will start with a launchable main method in the class (or interface—a kind of class we’ll talk about later) that you specify. Basically, a launchable main method is called main and has either no parameters or a single String[] parameter, which holds the command line arguments:

Greeter.java
void main(String[] args) {
    IO.print("Hello there");
    if (args.length > 0) {
        IO.print(", " + args[0]);
    }
    IO.println(".");
}
$ java Greeter.java Alice
Hello there, Alice.
The main method can be static or non-static. If non-static, there are other rules that apply, which you can read about in JEP 512.

Execution ends when all nondaemon threads have finished. These notes don’t cover what a nondaemon thread is, but you can ask a friendly AI chatbot for the details. If you are writing a simple program and never mention threads, your program will run on a single non-daemon thread. How convenient!

An Example with Multiple Files

Let’s see what these “class” things are all about. It is time to explicitly define one. Here we have here a source file without a main:

Cylinder.java
/**
 * A mutable cylinder that can be widened or lengthened.
 */
public class Cylinder {
    private double radius;
    private double height;

    public Cylinder(double radius, double height) {
        this.radius = radius;
        this.height = height;
    }

    public double radius() {
        return radius;
    }

    public double height() {
        return height;
    }

    public double volume() {
        return capArea() * height;
    }

    public double surfaceArea() {
        return 2 * capArea() + sideArea();
    }

    private double capArea() {
        return Math.PI * radius * radius;
    }

    private double sideArea() {
        return 2.0 * Math.PI * radius * height;
    }

    public void widen(double factor) {
        radius *= factor;
    }

    public void lengthen(double factor) {
        height *= factor;
    }
}

You can give this small class file to a friend who doesn’t believe that a glass twice as wide as another glass holds four times as much water. Then your friend could use that class in their own program:

VolumeCheckerApp.java
void main() {
    var glass = new Cylinder(3, 5);
    IO.println("Volume is " + glass.volume());
    glass.widen(2);
    IO.println("After doubling the radius, volume is " + glass.volume());
}

To run this program, both files must be compiled. But to run the app, we only have to mention the file with the launchable main method:

$ java VolumeCheckerApp.java
Volume is 141.3716694115407
After doubling the radius, volume is 565.4866776461628

This will compile VolumeCheckerApp.java (if the source code was modified since the last compilation), then, because that file simply mentions the class Cylinder, the file Cylinder.java will also be compiled if needed. This works when the file name is the same as the class name.

Manual Compilation

As long as your classes are in the main file or in files with the same name as the class, you can compile and run them easily. The java command can always find them. All the compiled code stays in memory and the program just runs.

But if you place several classes in a single file, your main program won’t know where to look for them, so you will need to compile the file manually with the javac command. This will create actual .class files on your computer, which the java program can pick up. Here’s a complete example. The Geometry.java file contains three classes (remember that classes can be defined with class, record, enum, or interface):

Geometry.java
record Point(double x, double y, double z) {
    public static Point ORIGIN = new Point(0, 0, 0);
    public Point plus(Vector v) { return new Point(x + v.i(), y + v.j(), z + v.k()); }
    public Vector minus(Point p) { return new Vector(x - p.x, y - p.y, z - p.z);}
    public double distanceTo(Point p) { return p.minus(this).magnitude(); }
    public static Point mid(Point p, Point q) {
        return new Point((p.x + q.x) / 2, (p.y + q.y) / 2, (p.z + q.z) / 2);
    }
}

record Vector(double i, double j, double k) {
    public static Vector ZERO = new Vector(0, 0, 0);
    public double magnitude() { return Math.sqrt(i * i + j * j + k * k); }
    public Vector normalized() { return this.times(1.0 / magnitude());}
    public Vector plus(Vector v) { return new Vector(i + v.i, j + v.j, k + v.k); }
    public Vector minus(Vector v) { return new Vector(i - v.i, j - v.j, k - v.k); }
    public Vector times(double s) { return new Vector(i * s, j * s, k * s); }
    public double dot(Vector v) { return i * v.i + j * v.j + k * v.k; }
    public Vector cross(Vector v) {
        return new Vector(j * v.k - k * v.j, k * v.i - i * v.k, i * v.j - j * v.i);
    }
}

record Plane(double a, double b, double c, double d) {
    public static Plane from(Point p1, Point p2, Point p3) {
        var n = (p2.minus(p1)).cross(p3.minus(p1));
        var d = -(new Vector(p1.x(), p1.y(), p1.z()).dot(n));
        return new Plane(n.i(), n.j(), n.k(), d);
    }
    public Vector normal() { return new Vector(a, b, c); }
}

Here’s a illustrative program that simply exercises those three classes, without doing anything useful, but it looks like the beginning of something. When programs start to get big, it tends to be a good idea to put your main inside of an explicit class.

FlightSimulator.java
class FlightSimulator {

    Point currentPosition = new Point(0, 200, 0);
    Plane ground = Plane.from(
        new Point(0, 0, 0),
        new Point(100, 0, 0),
        new Point(0, 0, 100)
    );

    void main() {
        IO.println("This flight simulator is not finished!");
        var start = currentPosition;
        var end = new Point(100, 0, 100);
        var shortestPath = end.minus(start);
        var direction = shortestPath.normalized();
        var distance = shortestPath.magnitude();
        IO.println("We are at " + start);
        IO.println("We want to go to " + end + " on the ground " + ground);
        IO.println("The shortest path is " + shortestPath);
        IO.println("The direction of that path is " + direction);
        IO.println("The distance of that path is " + distance);
        IO.println("The normal vector of the ground is " + ground.normal());
    }
}

Now we just manually compile all the files together and launch the program by specifying the class holding the main method, rather than the file name.

javacandjava.png

You can do this on the command line in one shot:

$ javac Geometry.java FlightSimulator.java && java FlightSimulator

A nice thing about manual compilation with javac (which, by the way, you are free to run even when unnecessary), is that you can use the javap tool to inspect class files. Fun stuff. Not strictly necessary for casual Java programmers, but important for professionals and especially important for computer scientists.

Exercise: Make sure you have the source code for the Cylinder class from earlier in this section. Run javac Cylinder.java. Then run javap Cylinder. Then javap -c Cylinder. Try also javap -p -c -v Cylinder.
How might this really look in practice?

Rather than multiple classes in a file, you are more likely to see each class in its own file, with the classes grouped in to a geometry package, rather than a single file.

We’ll see an example much later in these notes. Our focus for now will be on the Java language rather than the application structure.

JShell

When learning or experimenting, use JShell. Let’s play around:

$ jshell
|  Welcome to JShell -- Version 25
|  For an introduction type: /help intro

jshell> 2 + 2    ARITHMETIC
$1 ==> 4

jshell> 2 > 2    BOOLEANS
$2 ==> false

jshell> 3 + 100 * Math.hypot(-4, 3)    OPERATOR PRECEDENCE
$3 ==> 503.0

jshell> "Hello".replace('e', 'u')    STRINGS AND CHARACTERS
$4 ==> "Hullo"

jshell> var greeting = "Good morning"    CREATING VARIABLES
greeting ==> "Good morning"

jshell> greeting.substring(5)    USING VARIABLES
$6 ==> "morning"

jshell> List.of(3, 5, 3, 2, 2, 1, 8, 1, 1)    LISTS
$7 ==> [3, 5, 3, 2, 2, 1, 8, 1, 1]

jshell> Map.of("dog", 3, "rat", 5, "pig", 99)    DICTIONARIES
$8 ==> {rat=5, pig=99, dog=3}

jshell> var poem = """         MULTI-LINE STRINGS
   ...>            I wonder
   ...>            about thunder
   ...>            And Java."""
poem ==> "I wonder\nabout thunder\nAnd Java."

jshell> int triple(int x) {    TOP-LEVEL SHELL METHODS
   ...>     return x * 3;
   ...> }
|  created method triple(int)

jshell> triple(30)    METHOD INVOCATION
$11 ==> 90

jshell> $11 * 2    USING SHELL VARIABLES (COOL)
$12 ==> 180

jshell> IntStream.range(1, 5).forEach(IO::println)    STREAMS
1
2
3
4

jshell> /exit
|  Goodbye

A Language Tour

We’ll be going through the language, not example-by-example, but by looking at concepts. (If you prefer whole-program examples, see the Basic Java Bootcamp.)

Our tour here, while technical, does not go super deep and is somewhat lacking in detailed examples. You can find much deeper coverage of the basics in these notes. If you have lots of time, check them out first!

The Type System

Let’s get technical.

Java gives us eight primitive types and five mechanisms for creating new types:

Primitive TypeDescriptionExample Values of the Type
booleanThe two values true and false
false
charCode units (WHAT?) in the range 0..65535 that are used to encode text
'π'
byteIntegers in the range -128 ... 127
(byte)89
shortIntegers in the range -32768 ... 32767
(short)89
intIntegers in the range -2147483648 ... 2147483647
89
longIntegers in the range -9223372036854775808 ... 9223372036854775807
89L
floatIEEE binary-32 floating point numbers
3.95f
doubleIEEE binary-64 floating point numbers
3.95
Type FormerDescriptionExample Type
classCompletely general classes, nothing special, just the basics—containing fields, methods, constructors, initializers, and perhaps inner classes
class Task {
    int id;
    Person assignee;
    String description;
    DateTime due;
}
[]Arrays (a special kind of class)
boolean[]
enumEnums (a special kind of class)
enum Confidence {
    LOW,
    MEDIUM,
    HIGH
}
recordRecord (just a shorthand way for making certain classes)
record Badge(int level, String name) {}
interfaceInterfaces (yeah, also a kind of class)
interface Savable {
    void save();
}
See anything missing?

If you come from a Python or JavaScript background, you might be asking “What, no string type?” or “What, no function type?” or “What, no unbounded integer type?”

Relax, Java has them: they’re just created with classes; we’ll see how soon.

Now that we have the big picture, let’s start using values of these types. But first, there’s something really important to understand, and that is....

Variables are Type-Constrained!

We all know that values have types. That is an axiom. That goes without saying. That is a fact of the universe. The value false has type boolean. All values have types. By definition! Types are how we group values.

But in Java, variables are constrained to hold values of a certain type:

jshell> var friends = 39
39

jshell> friends = 40
40

jshell> friends = true
|  Error: incompatible types: boolean cannot be converted to int
BUT WHY DO THIS?

Constraining variables to a certain type facilitates static typing, that is, the deduction of the type of expressions just by looking at the source code! If we can figure out the types of expressions at compile-time, we can tell that running a program will generate a type error and therefore we can report the error early and refuse to run the program.

This is generally thought to have two advantages: (1) it removes bugs due to type mismatches at run-time, and (2) it allows for more efficient code to be generated as there are no expensive run-time checks and in many cases allows machine code to be generated with powerful indexing instructions rather than string lookups.

Java compilers can do a ton of static type checking, but there are things that do require dynamic typing (type checking at run time), including arrays, nulls, and certain casts (all which we will see later).

Time for technical vocabulary

Java’s type system can be described as mostly static (the type of nearly every expression can be deduced at compile-time), quite strong (type mismatches generate errors rather than implicit conversions), and in the middle of the manifest (types must appear on every identifier) to inferential (identifier types can be inferred) spectrum.

Since variables have to have a type constraint, you can’t use var unless you know the type:

jshell> var payment
|  Error: cannot use 'var' on variable without initializer

You can, however, declare a variable without an initializer provided you specify the type:

jshell> int books
0

jshell> double price
0.0

jshell> boolean found
false

jshell> String message
null

Wait. Wait. What? Why wasn’t the default value of a string the empty string? What is that null thing? Hang on, things are about to get weird. But you knew Java was weird, right?

Can I be redundant?

If you want to write int limit = 10; or String warning = "Stay behind the yellow line"; you are free to do so. One person’s silly redundancy is another person’s explicitness.

Be nice to people that have a different preference than your own. This is not a hill to die on.

Primitive and Reference Types

All types that are not primitive types are called reference types. What’s the difference? Instances of primitive types are stored directly in variables. But for reference types, variables hold references (a.k.a. “pointers” or “links”) to the objects. In a sense, the “value” of a reference type is a reference to the object, not the object itself. We saw above the eight primitive types (boolean, byte, char, short, int, long, float, and double). Did you know there were reference types Boolean, Byte, Character, Short, Integer, and more? I am not kidding.

The distinction between primitive and reference types may be one of Java’s worst design mistakes. Brendan Eich, the designer of JavaScript, copied this distinction into JavaScript because of a “Make it look like Java” order from management, and when asked what decisions he would make differently if he could design JavaScript from scratch today, among the items in his list were, in his own words:

Distinguishing primitives and references is not helpful. In Python, for example, every variable holds a reference, without exception. Python’s designers had no need to distinguish them. Java successor languages (Scala, Clojure, Kotlin) also treat everything as reference types, though their compilers will always generate efficient code to avoid the overhead of references. There’s no need to expose this distinction to programmers at the language level.

References are fine. But guess what Java has. (Major cringe sound here.) It has the dreaded null reference. It is bad enough this exists, but Java goes even further and takes null references to even more abominable heights. In Java, null does not have its own type—it is a member of all reference types. So null is...a string...and also a cylinder...and also a date...and...🤦‍♀️. Sigh. Or maybe there are an infinite number of null values, one for each reference type? Sad. 😞

For something that is supposed to stand for nothing, to suggest that null is a valid value for a variable constrained to the type String is absurd. If you want to be a Java programmer, you will have to get used to this.

This example shows how variables of primitive types differ from variables of reference types:

void main() {
    float x;                    // declare a variable, it value is arbitrary
    x = 3;                      // value 3 stored in x
    float y = x;                // value 3 stored in y
    y = 5;                      // after this x is still 3

    Cylinder c1;                // just declares a variable
    c1 = new Cylinder(1, 1);    // now we have a cylinder created
    Cylinder c2 = c1;           // does NOT create a new cylinder
    c2.widen(2);                // exactly the same effect as c1.widen()
    c1 = null;                  // the cylinder still exists

    double d = c1.radius();     // OOPS!! Throws a NullPointerException

    double r = c2.radius();     // This is perfectly fine: c2 isn’t null
}

A picture is crucial here, as it shows how primitive type values appear directly in the boxes, and object values are always references (unless, ugh, they are that darn null):

referenceexample.png

The Billion Dollar Mistake 💸

Java has arguably done something wrong here. Is it really true that null is an acceptable value of type String? Or arrays can be null in addition to being empty? Why? What even is going on here? First of all, it’s confusing. How is no string a string? Mind blowing. Second, it’s deceitful. Java bills itself as statically typed, but if your program includes the expressions.toUpperCase() for a variable s constraiend to type String, the compiler is unable, in general, to guarantee that s will not be null at run time. Java is not as statically typed as you might think. Run time type errors are possible. Yep, a NullPointerException is thrown. AT RUN TIME. 🤢

What a disappointment! Who. Is. Responsible. For. This?

Answer: Sir Charles Anthony Richard (Tony) Hoare, FRS FREng is. But he said he is sorry. He calls this idea that null could belong to a reference types the Billion Dollar Mistake as it has caused about a billion dollars worth of damage due to lost productivity and who knows what else.

Don’t be too hard on Sir Tony

He also gave us Quicksort, Communicating Sequential Processes, Hoare Logic (for proving programs correct), and did a lot of work on structured programming, among other things. Also he said he was sorry. Stop judging people. We all make mistakes.

Assignment and Equality

Primitives and References are different in other ways, too.

For primitives, assignment (=) and equality testing (==) works as expected: assignment is a copy and equality compares values. But with references, remember that it is the reference, not the object contents, that are being copied and compared. Two distinct cylinder objects of the same exact size are not equal to each other:

jshell> /open Cylinder.java

jshell> var c1 = new Cylinder(3, 5)
c1 ==> Cylinder@37f8bb67

jshell> var c2 = new Cylinder(3, 5)
c2 ==> Cylinder@20ad9418

jshell> var c3 = c2
c3 ==> Cylinder@20ad9418

jshell> c1 == c2
$5 ==> false

jshell> c2 == c3
$6 ==> true

Remember, it is the references that are compared, not the objects themselves. Here we made two objects of the class Cylinder. Objects are created with the new operator. The first object is referenced by the variable c1 while the second is referenced by the variables c2 and c3. Never confuse the term variable with the term object. They are completely different things! Behold three variables but two objects:

cylindervarobj.png

Exercise: Check yourself to make absolutely sure you know the difference between variables and objects. Explain the difference to a friend.

So for these reference types:

So how do you copy object contents on assignment, or compare object contents in an equality test? You have to call a method to do it! And you have to write those methods yourself, or use methods someone wrote for you.

If you want to test equality by considering the content of the objects themselves, the convention is to call that method equals(), and have it conform to specific rules.

Unfortunately, implementing your own equals() is tricky

It’s so messy, we aren’t anywhere ready to talk about how messy it is, or even to begin writing such things! AAAAAAAAHHHHHHHHH.

Strings

Java strings are delimited with quotation marks (") if on a single line and triple-quotation marks (""") if multi-line, and are instances of the class java.lang.String. They are sequences of elements of type char, which are UTF-16 code units, not characters. 🤦‍♀️ At least they are immutable, yay! But like all objects, == works on references, not on the contents. Read the section on strings and characters in these notes for details.

Arrays

For every type T there is a type T[] of arrays of type T. Java arrays are fixed in size (they cannot grow or shrink) and they are mutable. Read the section on arrays in these notes for more.

Classes

Classes are pretty central to Java.

Let’s look deeper in the Cylinder class. It contains two fields (radius and height), one constructor, and eight methods (radius(), height(), volume(), surfaceArea(), capArea(), sideArea(), lengthen() and widen()). The purpose of this class is to instantiate objects. In fact, that’s what classes are for in programming language theory: classes are factories for creating objects that all share a similar structure and behavior. Classes classify objects (see where the name “class” comes from?). Objects created with this constructor will be instances of the class, and the methods will apply to these objects. Methods are marked private are for the internal use of the class only.

It is worth repeating: classes give us a way to instantiate objects with certain structure and behavior.

Watch out! Java overuses the term “class.”

Somewhat unfortunately, Java uses the term “class” to refer to a bundle of data and code, whether or not the intent of the code is to instantiate objects. It may or may not correlate with the traditional notion of a class.

In the case of the VolumeCheckerApp, the “class” generated by the compiler is simply some weird thing that happens to just house the executable code of an application. It’s certainly not a class in the traditional sense of the word, but only in the Java sense.

Why? It appears the Java designers just liked the fact that everything should be in a class, allowing a simple view of every program as a collection of classes. Perhaps they could have picked a better name for these compiled units. Who knows? Who cares? It’s not worth fretting about. It’s never going to change.

Immutability

An immutable object is one whose properties never change after construction. Immutable objects have several well-known advantages over mutable objects that a good rule of thumb is to use them when you can. So mark fields final when you can—the compiler will force you to set them in a constructor or initializer and ensure you never change them later.

Or, if you can, use a record. All fields are automatically final in a record. We’ll see records soon.

But beware that immutability with final fields and records is shallow. If your record contains a field that is a mutable list, say, you won’t be able to assign a new list to the field, but you will be able to update the contents of the list! Always Be Careful.

Static

Typically, each instance of a class gets its own fields and methods, but fields and methods marked static do not belong to any particular instance—they’re like globals. The former are called instance fields and instance methods. The latter are called class fields and class methods, but in Java land a lot of people call them “static fields” and “static methods.” Static members are referenced with the class name itself. In the Point class from earlier:

record Point(double x, double y, double z) {
    public static Point ORIGIN = new Point(0, 0, 0);
    public Point plus(Vector v) { return new Point(x + v.i(), y + v.j(), z + v.k()); }
    public Vector minus(Point p) { return new Vector(x - p.x, y - p.y, z - p.z);}
    public double distanceTo(Point p) { return p.minus(this).magnitude(); }
    public static Point mid(Point p, Point q) {
        return new Point((p.x + q.x) / 2, (p.y + q.y) / 2, (p.z + q.z) / 2);
    }
}

we can create points p and q:

var p = new Point(1, 2, 3);
var q = new Point(4, 5, 6);

and invoke the instance methods like so:

IO.println(p.plus(new Vector(5, 8, -2)));
IO.println(p.minus(q));
IO.println(p.distanceTo(q));

but access static fields and invoke static methods like so:

IO.println(Point.ORIGIN);
IO.println(Point.mid(p, q));

More Details!

Interested in going deeper into Java classes? We barely scratched the surface here. I have much more detailed notes on Java classes and also on classes in general.

Type Compatibility

This restriction that every variable has to have a type constraint sounds worse than it is. It turns out that is a variable is declared to have a type T, then it can be assigned a value that has a type compatible with T. What types are compatible with T? Well, it’s complicated, but roughly:

So these are okay:

Object a = "abc";                        // String is a subtype of Object
Object a = new int[]{3, 5, 8, 13};       // int[] is a subtype of Object

interface Reversible {}
class Sequence implements Reversible {}
Reversible r = new Sequence();           // Sequence is a subtype of Reversible

class Animal {}
class Dog extends Animal {}
Animal a = new Dog();                    // Dog is a subtype of Animal
The Liskov Substitution Principle

The idea we’re getting at here is S being a subtype of T means we can use an S wherever a T is expected. Barbara Liskov introduced the idea in the 1980s; it’s powerful as it a semantic (behavioral), rather than a syntactic (structural) idea.

You may lose information when assigning numbers of different types

Java will let you assign longs (with 64 bits of precision, to double variable (that have only 53 bits of precision). You will silently lose information. Java does not care.

We got ahead of ourselves with extends and implements, so let’s do those now.

Inheritance and Polymorphism

A class can extend at most one other class, called its superclass. A superclass can have zero or more subclasses. There are lots of lots of rules here, and lots of interesting stuff on access modifiers that are covered elsewhere. Let’s jump into an example with a superclass called Animal with three subclasses.

Animals.java
abstract class Animal {
    private String name;

    protected Animal(String name) {
        this.name = name;
    }

    public String speak() {
        return name + " says " + sound();
    }

    public abstract String sound();
}

class Cow extends Animal {
    public Cow(String name) {
        super(name);
    }

    @Override
    public String sound() {
        return "moooooo";
    }
}

class Horse extends Animal {
    public Horse(String name) {
        super(name);
    }

    @Override
    public String sound() {
        return "neigh";
    }
}

class Sheep extends Animal {
    public Sheep(String name) {
        super(name);
    }

    @Override
    public String sound() {
        return "baaaa";
    }
}

Superclass and subclasses are great when you have a collection of different types that are very similar in some respects, and slightly different in others. The superclass captures the commonality. Here all animals speak the same way, but their sounds are different. So we defined speak in the superclass and made sound an abstract method.

Lots of types can be compatible with the type of a variable, so if you see an expression like a.m() where a is a variable, and m is a method, the actual method that is called depends on the value bound to a at run time. This is called dynamic polymorphism.

ThreeAnimals.java
void main() {
    Animal h = new Horse("CJ");
    IO.println(h.speak());
    Animal c = new Cow("Bessie");
    IO.println(c.speak());
    IO.println(new Sheep("Little Lamb").speak());
}

Run like so:

$ javac Animals.java ThreeAnimals.java && java ThreeAnimals
Exercise: What if you tried to make a new subclass without implementing sound?
Exercise: What if you wanted all animals to have a “default sound”? Rewrite the example to do just that.

Interfaces

A Java interface is a type that bundles together behaviors and constants that you essentially mix in to other classes. You cannot instantiate interfaces directly: but you can make classes that implement interfaces. You normally use them to add behaviors to (seemingly unrelated) classes. Trivial example first:

interface Switcher {
  void turnOn();
  void turnOff();
}

class LightBulb implements Switcher {
  // ...
  public void turnOn() { ... }
  public void turnOff() { ... }
  // ...
}

Technical details:

interface Stack {
  String behavior = "LIFO";             // automatically public, static, and final
  void push(Object item);               // automatically public and abstract
  Object pop();                         // automatically public and abstract
  Object peek();                        // automatically public and abstract
  int size();                           // automatically public and abstract
  default empty() { return size == 0; } // automatically public and abstract
}

Multiple Inheritance

A class can implement zero or more interfaces. So you might wonder what happens if a class inherits conflicting members. Let’s try it in the shell:

jshell> interface A { default int f() { return 1;}}
|  created interface A

jshell> interface B { default int f() { return 2;}}
|  created interface B

jshell> class C implements A, B {}
|  Error:
|  types A and B are incompatible;
|    class C inherits unrelated defaults for f() from types A and B

Cool, Java says that’s an error! At compile time! That’s good: better than taking the first, or the last, or an arbitrary one (ugh, what could go wrong). If you want to avoid the error, just make class C would just define its own implementation of f and all will be fine.

Exercise: So two default methods generate a compile-time error if not overridden. But there are more scenarios to consider. Figure out what happens if (1) two fields conflict (2) two static methods conflict (3) pulling in conflicting data from the superclass and an interface.
Exercise: How does this approach compare to other languages you know that support multiple inheritance?

Modifiers

Here’s some useful reference information:

Class Modifiers
no modifieraccessible only within its own package
publicaccessible wherever its package is
abstractcannot be instantiated (may have abstract methods)
finalcannot be extended (subclassed)
staticmakes an inner-declared class really a top-level one
Member Modifiers
privateaccessible only within its class
no modifieraccessible only within its package
protectedaccessible only within its package and to its subclasses
publicaccessible wherever its class is
Field Modifiers
finalvalue may not be changed, once assigned
staticonly one instance of the field shared by all objects of the class
transientfield is not serialized
volatilevalue may change asynchronously compiler must avoid certain optimizations
Method Modifiers
finalmay not be overridden
staticmethod does not apply to a particular instance
abstracthas no body; subclasses will implement
synchronizedrequires locking the object before execution
nativeimplementation is not written in Java, but rather in some platform dependent way

The Class java.lang.Object

All reference types have Object as an ancestor, so they’ll all inherit these instance methods:

The equals, hashCode, and toString tend to get overridden a lot in practice. Use equals when you want to compare objects by content, not just by reference. Overriding equals can be a little tricky because the type of the argument is Object. So to do it properly, you use the instanceof operator that checks the class of the object that the reference refers to. The check can also give you a new variable whose static type is the one being checked. Here’s how you can do it:

class Cylinder {
    private double radius;
    private double height;

    // ...

    @Override public boolean equals(Object o) {
        return (o instanceof Cylinder that)
            && this.radius == that.radius
            && this.height == that.height;
    }
}

But be aware, be very aware, of this following guideline:

If you override equals, you must also override hashCode

This is because if you place objects in a set or map, their presence in the set or map is detected by hashCode, not equals. You must ensure that whenever x.equals(y) that x and y have the same hash code. Fortunately, you can override with:

class Cylinder {

    // ...

    @Override public int hashCode() {
        return Objects.hash(radius, height);
    }
}

Overriding these methods is only needed when you want “value semantics.” Usually, you will never need value-based comparison and you won’t be storing items in sets, or even printing their contents. But when you do, you will need these methods overridden. Fortunately, there is a way to get value-based equality, hash code generation, and printing auto-generated, provided you accept (as you probably should) immutability. You can use records.

Records

Defining immutable objects with value-base equality is so common that there’s a nice way to define them. Here’s an example:

record Point(double x, double y) {}

This gives you a fully immutable class called Point with a two-argument constructor, accessor methods x() and y() and properly overridden equals, hashCode, and toString(). This record is exactly the same as the following:

// Don't write this yourself, we're only showing what the record produces.

public class Point {
    private final double x;
    private final double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double x() {
        return x;
    }

    public double y() {
        return y;
    }

    @Override
    public boolean equals(Object o) {
        return (o instanceof Point other) && x == other.x && y == other.y;
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point[x=" + x + ", y=" + y + "]";
    }
}

You can add validation to the constructor and even add more methods if you desire. You can even add static fields, but not instance ones (ask a friend or chatbot about the difference between instance and static fields if you missed it). Let’s extend the point record type:

public record Point(double x, double y) {
    public static final Point ORIGIN = new Point(0, 0);

    public Point {
        if (Double.isNaN(x) || Double.isNaN(y)) {
            throw new IllegalArgumentException("Coordinates can not be NaN");
        }
    }

    public double distanceFromOrigin() {
        return Math.hypot(x, y);
    }

    public Point reflectionAboutOrigin() {
        return new Point(-x, -y);
    }

    public static Point midpointOf(Point p, Point q) {
        return new Point((p.x + q.x) / 2.0, (p.y + q.y) / 2.0);
    }
}

Everything else we saw before is still “auto-generated.”

Exercise: Write record definitions for vectors and quaternions.

Enums

Another shorthand way for making a common kind of class is the Enum. An enum is a class with a fixed set of instances. See these notes for more.

Generics

Okay, more on this static typing thing. So you remember, that as a (mostly) statically typed language, Java tries to guarantee that the types of all expressions are known by the compiler. For numbers, booleans, and strings, and for simple classes like our Cylinder above, this is easy. But how about the following?

jshell> class Pair {
   ...>     Object first;
   ...>     Object second;
   ...>     Pair(Object first, Object second) { this.first = first; this.second = second; }
   ...> }
|  created class Pair

jshell> var p = new Pair("hello", 5)
p ==> Pair@37f8bb67

jshell> Object message = p.first
message ==> "hello"

jshell> String greeting = p.first
|  Error:
|  incompatible types: java.lang.Object cannot be converted to java.lang.String
|  String greeting = p.first;
|                    ^-----^

Using Object for the type seems to work but IT IS SO UNSATISFYING!!! But while annoying, it makes sense, right? Static typing wants to check all the types at compile time so we don’t have to carry around expensive type information for expensive run-time type checks. So what right do we have to expect anything other than Object?

What if we could get the compiler to generate, on the fly, all the pair classes we need? We can!

jshell> class Pair<T, U> {
   ...>     T first;
   ...>     U second;
   ...>     Pair(T first, U second) { this.first = first; this.second = second; }
   ...> }
|  modified class Pair

jshell> var p = new Pair<String, Integer>("hello", 55)
p ==> Pair@439f5b3d

jshell> String s = p.first
s ==> "hello"

jshell> int x = p.second
x ==> 55

Here the class Pair is generic: it is not really a class, but more like a template. The real, concrete, classes would be things like Pair<String, Integer>, Pair<Boolean, Boolean>, and so on. The standard library makes use generics with concrete classes such as List<String> and List<BigInteger>. This is different from a dynamically typed language like Python, where there is a single, real, type called list.

More Java Weirdness

Can you believe this? Generic type parameters MAY NOT be primitives! Ugh! That’s right: you can NOT make a generic list of int values. 🤦‍♀️ So the Java Standard Library has a bunch of classes, namely Boolean, Character, Byte, Short, Integer, Long, Float, and Double which “wrap”, or “box” a primitive object. So you’ll need a List<Integer>.

Generally the compiler lets you move between, say, int values and Integer ones, but sometimes you can get tripped up. Usually it’s fine though. Just be aware of the distinction. Do you feel confident?

Collections

The Java standard library has a fairly rich collection of generic (of course!) collection classes and interfaces. The library is huge—HUGE—and you can learn about it in the official tutorial (and you might also want to check out the Overview), but here, for reference, is list of some very common interfaces:

Iterable
└── Collection
    ├── Set
    │   └── SortedSet
    │       └── NavigableSet
    ├── List
    └── Queue
        ├── Deque
        |   └──────────────┐
        └── BlockingQueue  │
            ├── BlockingDeque
            └── TransferQueue

Map
├── SortedMap
│   └── NavigableMap
|       └───────────────┐
└── ConcurrentMap       │
    └── ConcurrentNavigableMap

I have much more thorough additional notes on the Java collections in these notes and notes with good examples of lists and maps here. But do check out the official tutorial and the docs if you have time; it’s interesting how the library is structured, and how they implement such things as unmodifiable collections.

For now, here’s an example that uses a map to gather up all the words from standard input and compute and print their frequencies. DON’T PANIC: There’s a lot going on here, and the purpose of showing this code is to give you a feel of what Java programs can do, and not to be a tutorial.

TraditionalWordCountApp.java
void main() {
    var counts = new TreeMap<String, Integer>();
    var wordPattern = Pattern.compile("[\\p{L}'’]+");
    while (true) {
        var line = IO.readln();
        if (line == null) {
            break;
        }
        line = line.toLowerCase();
        var matcher = wordPattern.matcher(line);
        while (matcher.find()) {
            var word = matcher.group();
            counts.put(word, counts.getOrDefault(word, 0) + 1);
        }
    }
    for (var e : counts.entrySet()) {
        IO.println(e.getKey() + " " + e.getValue());
    }
}

Java is a large multiparadigm programming language, meaning it an do things in several ways. Here is an equivalent program to the one above (it does exactly the same thing) but works with the input and the map through a stream-based interface that should appeal to functional programming enthusiasts.

WordCountApp.java
void main() {
    Stream.generate(IO::readln)
        .takeWhile(line -> line != null)
        .flatMap(line -> Pattern.compile("[\\p{L}'’]+").matcher(line).results())
        .map(MatchResult::group)
        .collect(
            Collectors.groupingBy(
                String::toLowerCase, TreeMap::new, Collectors.counting()))
        .forEach((word, count) -> IO.println(word + " " + count));
}

Try this out on Emily Dickinson’s poems:

$ curl https://www.gutenberg.org/cache/epub/12242/pg12242.txt > emily.txt
$ java WordCountApp.java < emily.txt

If you thought the first word count program was crazy, this one must look super crazy. Naturally, there is a bit of a learning curve required to understand it! It’s got function values, optionals, and streams in it. Sounds like we can use it as a...segue!

What a Standard Library!

We just saw TreeMap from the package java.util, Pattern and MatchResult from the package java.util.regex, and Stream and Collectors from the package java.util.stream.

Relax, you do not have to memorize these facts. But some readers will.

Function Types

Having to bundle up all behavior into classes when only a single function is called for annoyed many Java programmers for the first 20 or so years of the language’s life. In 2014, Java caught up to the rest of the world and added what Lisp popularized in 1958 or so. About time, right? A couple examples:

var smallNumbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);

smallNumbers.stream()
    .filter(x -> x % 2 == 0)          // [2, 4, 6, 8]
    .mapToInt(x -> x * x)             // [4, 16, 36, 64]
    .reduce(1, (x, y) -> x * y) ;     // 147456

Here’s the deal: Any interface that has exactly one abstract method is called a functional interface, and you can instantiate it on the fly with a lambda expression. You can slap the annotation @FunctionalInterface on it if you like; it’s not required, but it’s nice.

LambdaDemo.java
@FunctionalInterface
interface IntToIntFunction {
    int apply(int x);
}

int twice(IntToIntFunction f, int x) {
    return f.apply(f.apply(x));
}

void main(String[] args) {
    IO.println(twice(x -> x * 3, 2));
}

That example was totally contrived (SORRY), since we pretty much reimplemented the built-in IntUnaryOperator. Speaking of which: the standard library has several dozen functional interfaces ready for you to use. Here are just a few:

Functional InterfaceThe Abstract Method
Function<T, R>R apply(T t)
BiFunction<T, U, R>R apply(T t, U u)
UnaryOperator<T>T apply(T t)
BinaryOperator<T>T apply(T t1, T t2)
Predicate<T>boolean test(T t)
BiPredicate<T, U>boolean test(T t, U u)
Supplier<T>T get()
Consumer<T>void accept(T t)
BiConsumer<T, U>void accept(T t, U u)
Runnablevoid run()
Comparator<T>int compare(T t1, T t2)
Callable<V>V call()

Check out the complete list of functional interfaces.

So we can write the famous twice function generically as:

public static <T> T twice(Function<T, T> f, T x) {
    return f.apply(f.apply(x));
}

Here’s an example with a consumer:

public static void powers(int base, int limit, Consumer consumer) {
    for (var power = 1; power <= limit; power *= base) {
        consumer.accept(power);
    }
}

You don’t have to always use lambda expressions for functions. You can also use method references, which are: pretty cool:

Instead of...You can write
x -> IO.println(x)IO::println
p -> p.getName()Person::getName
x -> new Widget(x)Widget::new

Optionals

Damn that billion dollar mistake. It infects Java at its core. It can never go truly away.

If, for example, we wanted each person to have a required name but an optional boss, writing person.boss.name could throw a NullPointerException, so we probably have to write:

if (person.boss != null) {
  // Do something with p.boss.name
}

These null checks have to go all over the place, and our code gets annoying and error prone when optional data is chained, as in person.boss.address.city.

But in 2014, Java added something to at least mitigate the problem and make the absence of something a little more explicit. But no one can force you to use this feature. While newer languages have this feature wired into the core of their being, Java was born too long ago and the billion dollar mistake was permanently, inextricably, baked in. Java programmers must see it in themselves to be professional and as a moral obligation diligently follow these principles:

Here’s an improved person class:

OptionalDemo.java
class Person {
    private String name;
    private Optional<Person> boss;

    private Person(String name, Optional<Person> boss) {
        this.name = Objects.requireNonNull(name);
        this.boss = boss;
    }

    public Person(String name) {
        this(name, Optional.empty());
    }

    public Person(String name, Person boss) {
        this(name, Optional.of(boss));
    }

    public String name() {
        return name;
    }

    public Optional<Person> boss() {
        return boss;
    }
}

void main(String[] args) {
    var alice = new Person("Alice");
    var bob = new Person("Bob", alice);

    bob.boss().ifPresent(p -> {
        // Here you would do something with the real person p
        assert p == alice;
    });
    alice.boss().ifPresent(_ -> {
        // This code will never be executed
        assert false;
    });

    assert alice.boss().orElse(bob) == bob;
    assert bob.boss().orElse(bob) == alice;

    assert bob.boss().filter(p -> p.name().startsWith("A")).isPresent();
    assert bob.boss().filter(p -> p.name().startsWith("B")).isEmpty();

    assert bob.boss().map(Person::name).orElse("").equals("Alice");
    assert alice.boss().map(Person::name).orElse("").equals("");
}

Optionals are like wrappers; they either wrap something or they don’t. A little reference:

MethodDescription
Optional.empty()An empty wrapper
Optional.of(x)Wrapped x
Optional.ofNullable(x)Wrapped x if x not null; else empty optional
o.isEmpty()false if o wraps a value; else true
o.isPresent()true if o wraps a value; else false
o.ifPresent(fT→void)calls f(x) if o wraps x; else does nothing
o.ifPresentOrElse(fT→void, runner)calls f(x) if o wraps x; else calls runner
o.get()x if o wraps x; else throw NoSuchElementException
o.or(fvoid→Opt<T>)o if o wraps a value; else f()
o.orElse(y)x if o wraps x; else y
o.orElse(fvoid→T)x if o wraps x; else f()
o.orElseThrow()x if o wraps x; else throw NoSuchElementException
o.map(fT→U)Wrapped f(x) if o wraps x; else empty optional
o.flatMap(fT→Opt<U>)f(x) if o wraps x; else empty optional
o.filter(p)o if o wraps x and p(x) is true; else empty optional

Streams

In addition to optionals, streams arrived in Java in 2014. You should go through this old but good tutorial which arrived at that time. The time of Java 8. It gets you started. Then you can go through the much longer dev.java Streams tutorials.

Done with the tutorial? Remember what streams are for?

Streams are intended for querying and transforming data.

Remember why they are cool? They provide an abstraction fo processing sequential data, regardless of how that sequence is implemented. Also, we can set up streams to be processed either sequentially or in parallel.

You start with a source, then apply zero or more intermediate operations, then one terminal operation. The intermediate operations don’t actually do anything right away (they’re “lazy”), they just set things to happen when the terminal operation is invoked. Once the terminal operation is done, the stream is consumed — “all used up.”

javastreamstates.png

SourcesIntermediate OperationsTerminal Operations
Arrays
Collections
Generator functions
I/O channels
etc.
filter(predicate)
map(function)
mapToInt(function)
mapToLong(function)
mapToDouble(function)
flatMap(function)
flatMapToInt(function)
flatMapToLong(function)
flatMapToDouble(function)
takeWhile(predicate)
dropWhile(predicate)
distinct()
sorted()
sorted(comparator)
peek(consumer)
limit(n)
skip(n)
forEach(consumer)
forEachOrdered(consumer)
toArray()
toArray(generator)
reduce(accumulator)
reduce(identity, accumulator)
reduce(identity, accumulator, combiner)
collect(supplier, accumulator, combiner)
collect(collector)
min(comparator)
max(comparator)
count()
anyMatch(predicate)
allMatch(predicate)
noneMatch(predicate)
findFirst()
findAny()

Examples of creating streams:

Lots of Standard Library operations produce streams:

By the way, those collectors are super flexible. But most of the time, you’ll just use a predefined one:

What’s a collection factory? Something like TreeSet::new, basically a constructor.

These are the collector examples from the Javadocs:

people.stream()
    .map(Person::getName)
    .collect(Collectors.toList());

people.stream()
    .map(Person::getName)
    .collect(Collectors.toCollection(TreeSet::new));

things.stream()
    .map(Object::toString)
    .collect(Collectors.joining(", "));

employees.stream()
    .collect(Collectors.summingInt(Employee::getSalary)));

employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment));

// Sum of salaries by department
employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.summingInt(Employee::getSalary)));

students.stream()
    .collect(Collectors.partitioningBy(
        s -> s.getGrade() >= PASS_THRESHOLD));

If you like this stuff, here are more tutorials.

Null Checking

Optionals are one way to avoid the Billion Dollar Mistake.

There are others. Here’s a great article.

Covariance, Contravariance, and Invariance

Oh hey, we’re not done with generics. It turns out that issues of subtyping get really theoretical here. How about just a very surface-level introduction to the basic ideas?

Here’s a class hierarchy to set the stage:

          Animal
        /        \
    Canine       Feline
    /    \       /    \
  Dog    Fox   Lion   Cat

We know:

But is this okay?

Suppose it were. Then we could write this:

// Assume this were possible
ArrayList<Animal> a = new ArrayList<Dog>();

// But then this would be, because a is a list of animals so sure add a cat
a.add(new Cat());

// BUT NO! The underlying list is a list of dogs. We should not add a cat.

Therefore, List<Canine> is not a subtype of List<Animal>.

But what if we wanted to write a function that accepted any kind of animal?

// This won’t work, because we cannot pass in a list of dogs
void putToBed(List <Animal> animals) { ... }

// So we need a better way! In Java we can do it!
void putToBed(List <? extends Animal> animals) { ... }

The question mark is called a wildcard and our parameter is what we call a bounded generic. It will accept a list of animals or a list of any subtype of animal. Of course, with a bounded generic with extends, you won’t be able to write to this list inside the method, but you can read from it and call animal methods on it.

Java also supports wildcards such as these:

List<? super Dog>   // accepts a List<Dog>, List<Canine>, List<Animal>, List<Object>, etc.

List<?>             // accepts any kind of list whatsoever
Exercise: Do you think that inside a function accepting a List<? super Dog> we could write a dog to this list? Do you think we could call the size() method? Do you think we can read a value from it?

PECS

More please. What if we had methods that accepted functions on or producing animals? Consider these:

void g(FunctionProducingCanine f) {
  // ...
  Canine c = f.apply(...)
  // ...
  // ...
}
void h(FunctionConsumingCanine f) {
  // ...
  Canine c = ...;
  f.apply(c);
  // ...
}

Let’s think about these.

Therefore we have this feeling of covariance on function return values (producers) and contravariance on function arguments (consumers). In Java-speak we say:

There you have it: Producer Extends, Consumer Super: PECS.

Can you believe it? Arrays are covariant!

One last thing on this topic, and is just awful. Did you know an array of dogs can be assigned to a variable constrained to be an array of animals? And the compiler will let you assign a cat through the animal array variable. It’s true, and it should make you doubt that Java is a 100% statically typed language. Because it is not. It is probably 99% statically typed, thanks to this glaring hole. And if you ever had to work with the truly bizarre toArray method, it probably made you cringe because it so messy.

At least, even though the compiler lets you assign a cat to the dog array, the language does not allow such a violation of the world’s natural order to occur at run time. Attempting this horror will cause an ArrayStoreException to be thrown at run time.

Relief.

Exceptions

There are dozens of throwable classes in the Java Core API. Throwables are either errors or exceptions. Exceptions are either runtime exceptions (a.k.a unchecked exceptions) or checked exceptions.

In many cases you will define your own exceptions, but if your situation can use a pre-existing one, Java has you covered. The most common pre-defined exceptions that you will have occasion to throw are:

Class Used when
IllegalArgumentException Arguments passed to a method don’t make sense.
IllegalStateException An object isn’t in the right state to execute this method.
NoSuchElementException You’re trying to remove an element from an empty collection, or looking for a specific item that isn’t there.
UnsupportedOperationException The requested operation isn’t implemented

Concurrency

One of Java’s great strengths is the degree to which is supports concurrent programming directly at the language level and in its standard library.

The topic is way to big to cover in depth here, so it is covered separately.

But how about a peek? Here is a Java implementation of the classic Dining Philosophers simulation:

DiningPhilosophers.java
// Classic set up with five philosophers and five chopsticks.
static String[] NAMES = {"Haack", "Hume", "Zhaozhou", "Kaṇāda", "Russell"};
static Object[] chopsticks =
    Stream.generate(Object::new).limit(NAMES.length).toArray(Object[]::new);

// We will require one seat to be empty to avoid deadlock. Philosophers
// must acquire the table semaphore in order to sit down and eat, and
// release it when they leave the table, which they do after each meal.
static Semaphore table = new Semaphore(NAMES.length - 1);

record Philosopher(int id, int numberOfMeals) implements Runnable {
    private void action(String action) {
        // For immediate actions, like picking up or putting down a chopstick.
        IO.println(NAMES[id] + " " + action);
    }

    private void action(String action, int maxMillis) throws InterruptedException {
        // For actions that take some time, like eating or thinking.
        // Sleeps between maxMillis / 2 and maxMillis milliseconds.
        action(action);
        Thread.sleep(ThreadLocalRandom.current().nextInt(maxMillis / 2, maxMillis + 1));
    }

    @Override
    public void run() {
        var left = chopsticks[id];
        var right = chopsticks[(id + 1) % chopsticks.length];
        try {
            for (var i = 0; i < numberOfMeals; i++) {
                table.acquire();
                action("has sat down and is now thinking", 8000);
                synchronized (left) {
                    action("picked up left chopstick");
                    synchronized (right) {
                        action("picked up right chopstick");
                        action("is eating", 5000);
                    }
                    action("put down right chopstick");
                }
                action("put down left chopstick");
                table.release();
                action("has left the table", 2000);
            }
            action("has gone home");
        } catch (InterruptedException _) {
            Thread.currentThread().interrupt();
        }
    }
}

void main() {
    for (var i = 0; i < NAMES.length; i++) {
        Thread.ofPlatform().start(new Philosopher(i, 3));
    }
}

Garbage Collection

Java assumes the existence of a tracing garbage collector (that is, the new operator in Java allocates memory for objects, but the programmer never explicitly deallocates memory for objects—the Java runtime automatically performs garbage collection on objects that are no longer referenced). However, programmers have a couple responsibilities when it comes to using memory and other resources efficiently:

Always take advantage of a language’s mechanisms for structured resource clean up. As a modern language, Java gives you this power in the form of a try statement. You are expected to use it for files, networking, and in any application that deals external resources.

A Bit of Reference Material

Here are some things you can easily look up, but having them in one place is nice.

Keywords

Did we say this was a technical overview? Let’s talk syntax. The official reference for the Java language can be found on the Oracle Java Specifications Page. Check out the nice one-page syntax of Java.

One very tiny piece of syntax is the choice of keywords.

The following keywords cannot ever be used as identifiers.

abstract
assert
boolean
break
byte
case
catch
char
class
const
continue
default
do
double
else
enum
extends
final
finally
float
for
goto
if
implements
import
instanceof
int
interface
long
native
new
package
private
protected
public
return
short
static
strictfp
super
switch
synchronized
this
throw
throws
transient
try
void
volatile
while

The words exports, opens, requires, uses, yield, module, permits, sealed, var, non-sealed, provides, to, when, open, record, transitive, with are known as contextual keywords because they are only keywords in certain contexts.

Note that false, true, and null, are not keywords, but rather are literals or identifiers with special meanings.

Statements

Here is a complete list of Java statements. If interested you may wish to read about all the statements in the official Java Language Specification. I also have a brief overview with lots of examples here.

Type of StatementStatement TypeWhen to Use
SimpleEmptyYou want to do nothing
Local Variable DeclarationTo bring a new variable into existence
AssignmentTo update the value in a variable
Increment or DecrementTo update the value in a variable by adding or subtracting 1
Method InvocationTo invoke a method just for its side effects
ConditionalIfTo do (zero or) one of a variety of different things, based on completely arbitrary conditions.
SwitchTo do (zero or) one of a variety of different things, based on the value of a single expression
IterationForTo iterate through a fixed range or collection
WhileTo iterate while some condition holds (the condition is checked before each iteration)
Do-whileTo iterate while some condition holds (the condition is checked after each iteration)
DisruptionBreakTo immediately terminate an entire loop
ContinueTo immediately terminate the current iteration of a loop
ReturnTo immediately return from a method
ThrowTo immediately exit the current try-block or method with an error
OtherBlockTo group a bunch of statements together so local variable declarations can have smaller scope.
LabeledTo give a name to a statement, either as documentation or to serve as a target of a break or continue.
SynchronizedTo ensure some code can be executed only by one thread at a time
TryTo define a small section of code for error-handling or resource management
Local Class DeclarationTo make a class used within the current block only
Instance CreationTo create an object without assigning it to a variable. Interestingly, this is sometimes useful: sometimes an object’s constructor will have side effects (like adding the newly created object to a global list)

Operators

Here are the Java operators, presented from highest to lowest precedence.

OperatorsOperand TypesAssociativityArityDescription
++, --
+, -
~
!
(typename)
arithmetic
arithmetic
integral
boolean
any
R 1 increment and decrement
unary plus and minus
bitwise complement
logical not
cast
*, /, % arithmetic L 2 multiplication, division, remainder
+, -
+
arithmetic
string
L 2 addition, subtraction
concatenation
<<
>>
>>>
integral L 2 left shift
right shift with sign extension
right shift with zero extension
<, <=, >, >=
instanceof
arithmetic
object/type
L 2 arithmetic comparison
has type
==
!=
==
!=
primitive
primitive
object
object
L 2 equal values
different values
refer to same object
refer to different objects
& integral
boolean
L 2 bitwise AND
boolean AND
^ integral
boolean
L 2 bitwise XOR
boolean XOR
| integral
boolean
L 2 bitwise OR
boolean OR
&& boolean L 2 short-circuit boolean AND
|| boolean L 2 short-circuit boolean OR
?: boolean/any/any R 3 if-then-else
=
*=, /=, %=, +=, –=
<<=, >>=, >>>=
&=, ^=, |=
variable/any
variable/arithmetic
variable/integral
variable/integral
R 2 assignment

Java In Practice

History and pragmatics are good to discuss in any language overview. Let’s briefly talk about good-to-know aspects of Java pragmatics.

Build Tools

Java is commonly used to build large, enterprise applications. In general, you will use a build system based on tools like Maven or Gradle.

Frameworks and Libraries

Java has a rich ecosystem of frameworks and libraries that can help you build applications more efficiently. Some popular frameworks include:

The Java Virtual Machine and Language Interoperability

However you build you program, you will get a big set of (compiled) class files (which can themselves be bundled into a big jar file, that is, a Java Archive), which are run on any device or operating system to which the Java interpreter, or more precisely, the Java Virtual Machine (JVM) has been implemented. By the way, hundreds of other languages can be compiled into class files, too, allowing you to write parts of your application in Java and parts in other languages.

Perhaps the most famous languages that have been ported to the JVM, and thus interoperate nicely with Java (and in most cases use the same standard library files as Java) are:

Kotlin has been chosen by Google to be the preferred language for developing Android applications, with Java being the alternative.

The JVM idea not only allows language interoperability, but allows Java to be implemented almost everywhere. A JVM can be:

Java Platforms

When you are building your Java projects, you’ll have access to tens of thousands of classes people have contributed to class repositories out there on the net, in addition to the thousands of classes that come standard with Java. Interestingly, the “standard” Java libraries actually vary by platform, of which there are three, Or four. Or five. Depends on how you count them.

Java SE

General purpose programming, the one these notes are all about.

Java ME

Lightweight APIs for resource-constrained, embedded systems and mobile devices.

Jakarta EE

The big stuff for large-scale distributed applications and support for databases, networking, middleware, scaling, messaging, mail, authentication, and more.

JavaFX

Graphics and media packages for creating and deploying rich client applications.

Java Card

For smart cards and similar small-memory devices such as SIM cards for telecom, EMV cards for banking, and secure identification devices.

FYI: SE = Standard Edition, ME = Micro Edition, EE = Enterprise Edition, FX = Effects.

Graphics

New graphics applications, both 2D and 3D, are generally implemented on the JavaFX Platform. You can download JavaFX and browse the documentation. Note that there is a fair amount of 3D support in the platform (see classes such as Mesh and PerspectiveCamera).

There is also a bit of graphics support within Java SE itself, in the module java.desktop. The support is less fully-featured than in JavaFX but still fun to learn. I have notes on that module here.

Networking

High-level support for enterprise-level, distributed applications can be found in the Jakarta EE platform. But there’s a fair amount of low-level, socket-based networking in Java SE, which is covered in these notes.

Unit Testing

While unit testing is a massive topic in its own right, it’s good to have a simple example. Let’s see a test for the animal classes we saw earlier:

AnimalSoundsTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class AnimalSoundsTest {

    @Test
    public void testCows() {
        var cow = new Cow("Bessie");
        assertEquals("moooooo", cow.sound());
        assertEquals("Bessie says moooooo", cow.speak());
    }

    @Test
    public void testHorses() {
        var horse = new Horse("CJ");
        assertEquals("neigh", horse.sound());
        assertEquals("CJ says neigh", horse.speak());
    }

    @Test
    public void testSheep() {
        var sheep = new Sheep("MacKenzie");
        assertEquals("baaaa", sheep.sound());
        assertEquals("MacKenzie says baaaa", sheep.speak());
    }
}

Java doesn’t come with built-in test runners (you do have an assert statement, which isn’t much) so most folks use a third-party library called JUnit.

In practice, Java applications are built with Maven, Gradle or some other build system, which makes it pretty automatic to connect JUnit to your project. But we’re going to do this manually. Go to the Maven Central page for the junit-platform-console-standalone module, click on the Browse link for a recent version, and download the jar file to your machine. You can place it anywhere you like. I renamed it to junit.jar and put it in my home folder.

Running tests using a main deep inside the jar, so running java only on this jar won’t find the classes we need to test. So we have to compile our files manually with javac. When compiling AnimalSoundsTest.java, we need to see the JUnit jar, so we have to tell the compiler about it. Similarly, when running the test runner, it needs to know where to find our compiled classes. Telling java and javac where to find files is done by specifying a classpath, that is, a set of folders and jar files, through the -cp option.

Getting right to it:

$ javac -cp ~/junit.jar:. Animals.java AnimalSoundsTest.java && java -jar ~/junit.jar execute -cp . -c AnimalSoundsTest

💚 Thanks for using JUnit! Support its development at https://junit.org/sponsoring

╷
├─ JUnit Platform Suite ✔
├─ JUnit Jupiter ✔
│  └─ AnimalSoundsTest ✔
│     ├─ testSheep() ✔
│     ├─ testCows() ✔
│     └─ testHorses() ✔
└─ JUnit Vintage ✔

Test run finished after 54 ms
[         4 containers found      ]
[         0 containers skipped    ]
[         4 containers started    ]
[         0 containers aborted    ]
[         4 containers successful ]
[         0 containers failed     ]
[         3 tests found           ]
[         0 tests skipped         ]
[         3 tests started         ]
[         0 tests aborted         ]
[         3 tests successful      ]
[         0 tests failed          ]
Classpaths and JARs

A JAR file is a zipped up collection of classes. A classpath is a colon-separated list of jars and folders that the java and javac commands will search to find the classes it needs. When we write ~junit.jar:. for a classpath, we are asking the tools to look for classes within the jar file and in the current folder.

Evolution

Just for fun, and for some historical perspective, here is how the Java SE platform has evolved over time:

Version Released Classes in
Core API
Notable New Stuff (Not complete)
JDK 1.0 1996‑Jan 210  
JDK 1.1 1997‑Feb 477 I/O design flaws fixed; inner classes; internationalization; AWT enhancements; JavaBeans API; JARs; RMI; Reflection; JDBC; JNI
J2SE 1.2 1998‑Dec 1524 Swing; Collections; Permissions, policies, certificates, signers; Java2D; Accessibility API; Extensions framework; RMI enhancements; Sound
J2SE 1.3 2000‑May 1840 JNDI; Java Sound; Swing and Java2D enhancements; Security enhancements; Networking enhancements; Improved serialization support; Input method framework; Timers
J2SE 1.4 2002‑Feb 2723 New IO library (NIO); Assertions; XML, crypto, SSE, authentication added to core API; Image IO; Java Print Service; Many Swing enhancements; Logging API; WebStart; Chained Exceptions; IPv6; Regexes
J2SE 5.0 2004‑Sep 3279 Generics; Better for statement; Annotations; Enumerations; Sophisticated concurrency packages; Autoboxing; Varargs; Static import; Unicode 4.0 (Wow!); Tons of library enhancements
Java SE 6 2006‑Dec 3777 Scripting languages can be embedded; XML and web services; JDBC 4; Many GUI enhancements (e.g. for tables and trees); monitoring and management, Programmatic access to compiler; Pluggable annotations; A few more security apis
Java SE 7 2011‑Jul 4024 Support for dynamically typed languages; Strings in switch; Better type inference (diamond); Simplified varargs; Binary integer literals; Multi-catch; Concurrency and collections updates; NIO.2; Elliptic-Curve Cryptography
Java SE 8 2014‑Mar 4240 Streams; Lambda expressions (closures); Interface default methods; Unsigned integer arithmetic; Repeating annotations; Date and Time API
Java SE 9 2017‑Sep 6005 Interface private methods; ImmutableSet; Optional.stream(); JShell
Java SE 10 2018‑Mar 6002 Optional.orElseThrow; Nicer ways to create unmodifiable collections
Java SE 11 2018‑Sep 4410 Local variable type inference; HTTP Client; New collection methods; New stream methods
Java SE 12 2019‑Mar 4432
Java SE 13 2019‑Sep 4403
Java SE 14 2020‑Mar 4420 Switch expressions and new Switch statement with ->
Java SE 15 2020‑Sep 4352 Text blocks
Java SE 16 2021‑Mar 4389 Records; Pattern Matching for instanceof
Java SE 17 2021‑Sep 4388 Sealed classes
Java SE 18 2022‑Mar 4404 Default charset is now UTF-8 everywhere
Java SE 19 2022‑Sep 4417
Java SE 20 2023‑Mar 4429
Java SE 21 2023‑Sep 4443 Record patterns; Pattern matching in switch
Java SE 22 2024‑Mar 4686 Unnamed variables and patterns
Java SE 23 2024‑Sep 4693
Java SE 24 2025‑Mar 4692
Java SE 25 2025‑Sep 4702 Compact source files, instance main methods, import module, flexible constructor bodies

You should check out Oracle’s writeup of many of the newer language features, and how they evolved from Java 9 to today. You can find the same information, presented slightly differently, at dev.java.

Long-Term Support Releases

Java 8, 11, 17, 21 and 25 are called LTS (long-term support) versions, meaning they will be maintained and supported for a long time.

Recall Practice

Here are some questions useful for your spaced repetition learning. Many of the answers are not found on this page. Some will have popped up in lecture. Others will require you to do your own research.

  1. What does Java’s Hello World program look like?
    void main() {
        IO.println("Hello, World!");
    }
  2. How do you read a line of text from standard input?
    var input = IO.readLine();
  3. What class do you use for unbounded integers?
    BigInteger
  4. How are Java programs organized?
    Code resides in classes, which reside in packages, which reside in modules.
  5. What is Java’s interactive environment called?
    JShell
  6. Java’s type system is divided into what two kinds of types?
    Primitive types and reference types.
  7. What does Brendan Eich think about the partitioning of types into primitive and reference types?
    He’s not a fan, having called it “trash” and “junk.”
  8. Name the eight primitive types of Java.
    byte, short, int, long, float, double, char, boolean.
  9. What are the five type formers?
    Array, class, interface, record, and enum.
  10. Java not only embraces the billion-dollar mistake, but makes it much worse than it has to be. How?
    It adds a null value into each reference type, meaning that you can say “this variable is constrained to hold strings” and assign the variable null, which stands for no string at all. Nulls have to be checked for at run time, weakening Java’s claim to be safe and statically typed.
  11. What trips up a lot of Java programmers when it comes to testing equality?
    The == operator checks for reference equality, not value equality.
  12. What are the main components of a class in Java?
    Fields, methods, constructors, and nested classes. (There are also initializers, but they were not covered in these notes.)
  13. Why is it said that Java overuses the term “class”?
    Because it uses the term to refer not only to traditional object blueprints/factories, but also to arbitrary code bundles.
  14. What keywords does Java use for subclassing and for implementing interfaces?
    The keyword for subclassing is extends, and the keyword for implementing interfaces is implements.
  15. What benefits are obtained by using Java records?
    An extremely compact syntax, shallow immutability, autogenerated accessor methods, autogenerated equals(), hashCode, and toString() methods.
  16. What are Java’s parametric types called?
    Generics
  17. What is the purpose of Java’s Optional type?
    To represent a value that may or may not be present, without using null.
  18. Does using Java optionals prevent the billion-dollar mistake?
    No, it does not. It just moves the problem to a different place. The optional itself can still be null.
  19. What is a stream in Java?
    A stream is a sequence of elements that can be processed in parallel or sequentially. It provides a high-level abstraction for working with sequences of data.
  20. When studying streams, it makes sense to group operations into three parts. Which ones?
    The three parts are: Source (where the data comes from), Intermediate Operations (which transform the data), and Terminal Operations (which produce a result or side effect).
  21. What is the PECS acronym?
    PECS stands for "Producer Extends, Consumer Super." It is a guideline for using wildcards in Java generics, indicating that if a generic type is a producer of a value, it should use the extends wildcard, and if it is a consumer, it should use the super wildcard.
  22. What kind of Java object is featured in concurrent applications?
    Thread
  23. What are some languages that have been targeted to the Java Virtual Machine?
    Groovy, Scala, Clojure, Kotlin, and many others.
  24. What are the five Java Platforms?
    Java SE (Standard Edition), Java ME (Micro Edition), Jakarta EE (Enterprise Edition), JavaFX, and Java Card.
  25. What is the JavaFX platform used for?
    JavaFX is used for creating rich client applications, including both 2D and 3D graphics.
  26. What is the Java SE module that contains the graphics API?
    The java.desktop module.
  27. What is the main Java testing framework called?
    JUnit
  28. Java debuted in 1995. What version of Java SE was released in 2025?
    Java SE 25

Summary

We’ve covered:

  • The look-and-feel of basic Java programs
  • Program structure: code in classes in packages in modules
  • JShell
  • The Type System
  • Classes
  • Records
  • Generics
  • Optionals
  • Streams
  • Java in Practice