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. Let’s learn some Java programming.

Getting Started

A Java program is a a collection of one or more classes. Here is a simple program:

Greeter.java
public class Greeter {
    public static void main(String[] args) {
        System.out.print("Hello there");
        if (args.length > 0) {
            System.out.print(", " + args[0]);
        }
        System.out.println(".");
    }
}

Make sure your system has a Java Development Kit, or “JDK” (you can get one here or here). Put the above code into the file Greeter.java and run it like this (note the command line argument):

$ java Greeter.java Alice
Hello there, Alice.

Did you notice Java is, um, more verbose than Python?

In the program above, we created our own class called Greeter and used two classes called System and String from the standard library. These classes come from the package called java.lang (Java classes are optionally grouped in to packages), which is so special that you never have to mention it! But if you use classes from any other package, you do have to mention the package:

NewYearCountdown.java
public class NewYearCountdown {
    public static void main(String[] args) {
        var today = java.time.LocalDate.now();
        var nextNewYearsDay = java.time.LocalDate.of(today.getYear() + 1, 1, 1);
        var days = java.time.temporal.ChronoUnit.DAYS.between(today, nextNewYearsDay);
        System.out.println("Hello, today is " + today);
        System.out.printf("There are %d days until New Year’s Day%n", days);
    }
}
Exercise: Identify the two not-from-java.lang classes and their packages.
Class LocalTime from package java.time and class ChronoUnit from package java.time.temporal

Putting the package names on every class can make the code really verbose, so Java allows the import statement at the top of the file:

AntiparallelClockHandsComputer.java
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public class AntiparallelClockHandsComputer {
    public static void main(String[] args) {
        for (var i = 0; i < 11; i++) {
            var time = LocalTime.MIDNIGHT.plusSeconds(Math.round((0.5 + i) * 43200.0 / 11));
            System.out.println(time.format(DateTimeFormatter.ofPattern("hh:mm:ss")));
        }
    }
}
Import statements are never necessary, they are simply a convenience.

They really don’t import anything, they simply allow you to leave off package names. That is all.

Here are couple more programs. Both do exactly the same thing, but are written in different styles. The first is all flowy and streamy:

WordCountApp.java
import java.util.Scanner;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.regex.MatchResult;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.counting;

public class WordCountApp {
    private static final Scanner scanner = new Scanner(System.in);
    public static void main(String[] args) {
        scanner
            .findAll(Pattern.compile("[\\p{L}'’]+"))
            .map(MatchResult::group)
            .collect(groupingBy(String::toLowerCase, TreeMap::new, counting()))
            .forEach((word, count) -> System.out.printf("%s %d\n", word, count));
    }
}

and the second is kind of, you know, pretty imperative and uses a lot of variables:

TraditionalWordCountApp.java
import java.util.Scanner;
import java.util.TreeMap;
import java.util.regex.Pattern;

public class TraditionalWordCountApp {
    public static void main(String[] args) {
        var counts = new TreeMap<String, Integer>();
        var wordPattern = Pattern.compile("[\\p{L}'’]+");
        var scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            var line = scanner.nextLine().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()) {
            System.out.printf("%s %d\n", e.getKey(), e.getValue());
        }
    }
}
Did you notice?

Local variables tend to be introduced with var, but there are explicit type names attached to fields, parameters, and methods. It turns out you can use the type name for local variable if you want to.

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

Program Structure

Logically, Java programs are made up of classes that are grouped into packages. But physically, your program is written in a collection of source code files. (You didn’t expect every program to be made up of just one file, did you?) Almost every Java compiler in the world forces this organization:

javastructure.png

In any given file, at most one class definition can be public (written public class A).

If the public class is called X, then the file must be called X.java (and the name is case sensitive).

Execution will start at a method with signature public static void main(String[] args) in some designated class.

Execution ends when all non-daemon threads have finished.

Each class definition is compiled into a .class file, generally using the javac command. Packages can be split over several source code files, but each class has to be wholly contained in a file.

javacompilation.png

If you don’t give a package declaration in a file, its class(es) will go in the default package. This is considered unprofessional, but it’s fine when you are learning or experimenting.

A multi-file example

That was too much detail. Let’s see an example:

Cylinder.java
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 write the following:

VolumeCheckerApp.java
public class VolumeCheckerApp {
    public static void main(String[] args) {
        var glass = new Cylinder(3, 5);
        System.out.println("Volume is " + glass.volume());
        glass.widen(2);
        System.out.println("After doubling the radius, volume is " + glass.volume());
    }
}

To run this, we have to first compile each class with javac then we can run by specifying the class name with the main to use as our entry point:

javacandjava.png

The straightforward way to do this is:

$ javac Cylinder.java && javac VolumeCheckerApp.java && java VolumeCheckerApp

But we can also do just this:

$ javac VolumeCheckerApp.java && java VolumeCheckerApp

Why? because javac will check to see if any dependent .java files have changed since the last compilation. When you say javac VolumeCheckerApp.java, the compiler will check to see if you changed Cylinder.java since the last time Cylinder.class was generated.

Avoid running the java command by itself

If you run the command java VolumeCheckerApp on a line by itself, it will use whatever dependent class files it can find WITHOUT checking for source code changes! Do yourself a favor and get in the habit of always running javac and java together, in the same command, all the time.

Of course, this warning does not apply if you are using a professional build system (such as Maven or Gradle) or you are managing your program in an IDE like Eclipse or IDEA, as these systems manage all the dependencies and source code changes automatically for you.

The single-file program short-cut

If your program is just a single file, you can run java on the source code file itself: it generates a class file in memory. It is just a convenience. Remember this will not work for multi-file programs.

Why so many classes?

Let’s look deeper in the Cylinder class. It contains two fields (radius and height), one constructor, and eight methods (getRadius, getHeight, getVolume, getSurfaceArea, capArea, sideArea, lengthen and widen). The purpose of this class is to instantiate objects. Objects created with this constructor will be instances of the class, and the methods will apply to these objects. A couple of the methods are marked private, these are for the internal use of the class only.

But the VolumeCheckerApp class is not used to instantiate objects, but rather to bundle up methods that would be called functions in most programming languages. We don’t create app objects. The methods of this class are not invoked on any particular object: they are just functions. Java uses the word static for this.

The class concept is overloaded in Java

Java forces the use of the class construct for both object factories and bundles of related functions.

Oh well.

JShell

If you want to play around, Java has a REPL, called JShell. Let’s play around:

$ jshell
|  Welcome to JShell -- Version 17
|  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(System.out::println)    STREAMS
1
2
3
4

jshell> /exit
|  Goodbye

JShell actually is manufacturing some classes behind the scenes to hold all your stuff; it just mercifully hides all that complexity from you so you can experiment and learn stuff. When it comes time for you to write a real-life app, it’s back to explicit classes!

Types

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

Primitive TypeDescriptionExample Values of the Type
booleanThe two values true and falsefalse
charCode units 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 ... 214748364789
longIntegers in the range -9223372036854775808 ... 922337203685477580789L
floatIEEE binary-32 floating point numbers3.95f
doubleIEEE binary-64 floating point numbers3.95
Type FormerDescriptionExample Type
classClasses
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 the values and the types. But first, a surprise....

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, determining the type of expressions just by looking at the source code! In other words we don’t have to run the program to know an expression’s type: type checking is done at compile time!

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 static (the type of nearly every expression can be deduced at compile-time), 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 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.

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 instances of reference types live outside of variables; 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. And (cringe sound here) there is a special null reference. Now this which means 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. 😞

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

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.getRadius();      // OOPS!! Throws a NullPointerException

double r = c2.getRadius();      // 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 💸

This null this is just so wrong. Like, 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? I thought this “static typing” thing was supposed to help me!!! But I have a variable s of type String and I invoke s.toUpperCase() Java says it’s possible that s could be null? Well I thought Java was like this awesome static and strongly typed language and prevent me from saying stuff like this! Oh it doesn’t? We get a run time type error? 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

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. Consider:

jshell> /open Cylinder.java

jshell> var c1 = new Cylinder(1, 4)
c1 ==> Cylinder@37f8bb67

jshell> var c2 = new Cylinder(1, 4)
c2 ==> Cylinder@20ad9418

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

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

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

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.

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:

double x = 3;
Object a = "abc";
Object a = new int[]{3, 5, 8, 13};

interface Reversable {}
class Sequence implements Reversable {}
Reversable r = new Sequence();

class Animal {}
class Dog extends Animal {}
Animal a = new Dog();
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

Classes can extend other classes. 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. But here’s the basic idea.

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. The right method is dispatched by examining the class of the object at run time.

Here’s a superclass called Animal with three subclasses. All animals speak the same way, but their sounds are different. So sound is made an abstract method.

AnimalSoundsApp.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";
    }
}

class AnimalSoundsApp {
    public static void main(String[] args) {
        Animal h = new Horse("CJ");
        System.out.println(h.speak());
        Animal c = new Cow("Bessie");
        System.out.println(c.speak());
        System.out.println(new Sheep("Little Lamb").speak());
    }
}
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. Example:

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);
    }
}

Remember that overriding these methods is only needed when you want “value semantics.” Often 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. Generally you have to write these methods on your own but not always! You can often 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().

You can add validation to the constructor and even add more methods if you desire:

Location.java
public record Location(double latitude, double longitude) {
    public Location {
        if (Math.abs(latitude) > 90 || Math.abs(longitude) > 180) {
            throw new IllegalArgumentException("Value out of range");
        }
    }

    public boolean inNorthernHemisphere() {
        return latitude > 0;
    }
}

Writing this class without the record syntax requires a ridiculous amount of code. There’s a bit more on records in these notes (including a look at how records are implemented behind the scenes).

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 statically typed language, Java needs to guarantee that the types of all expressions are known by the compiler. For numbers, booleans, and strings, and 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?

Well, if we could just 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. 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 checkout 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.

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);
}

public class LambdaDemo {
    private static int twice(IntToIntFunction f, int x) {
      return f.apply(f.apply(x));
    }

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

That example was totally contrived (SORRY), since we pretty much reimplemented the built-in IntUnaryOperator. Which is a nice segue into the next observation: 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 in Java 17.

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 -> System.out.println(x)System.out::println
p -> p.getName()Person::getName
x -> new Widget(x)Widget::new

Optionals

Damn that billion dollar mistake.

Let’s make a Person class, where a person has a required name, but an optional boss. For a given person p, writing p.boss.name could throw a NullPointerException, so we probably have to write:

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

The null checks have to go all over the place, and they get really annoying when optional data is chained, as in person.boss.address.city. What we WANT, though, is:

Here’s how we could define the person class:

OptionalDemo.java
import java.util.Optional;
import java.util.Objects;

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;
    }
}

public class OptionalDemo {
    public static 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(p -> {
            // 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

You should go through this great tutorial.

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). But 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

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:

Some Language Details

The official reference for the language can be found on the Oracle Java Specifications Page. Check out the nice one-page syntax of Java.

Interesting in a few technical items before you dive into the official spec? Here are a few:

Keywords

Keywords are words that cannot be used as identifiers.

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

Note that false, true, and null, var, and yield 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

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

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:

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:

Java ME

embedded systems, with our without displays

Java SE

typical stuff

Java EE

the big stuff like databases, networking, mipleware, scaling, messaging, mail, authentication

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 collecitons
Java SE 11 2018‑Sep 4410 Local variable type inference; HTTP Client; New collection methods; New stream methods
Java SE 12 2019‑Mar 4432 JVM Constants API
Java SE 13 2019‑Sep 4403 (No language changes)
Java SE 14 2020‑Mar 4420 JFR event streaming; Non-volatile mapped byte buffers; Switch expressions
Java SE 15 2020‑Sep 4352 Hidden classes; 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 (No language changes); Default charset is now UTF-8 everywhere
Java SE 19 2022‑Sep ? Pattern matching in switches

Check out the full list of classes from Java 17.

Also of interest is Oracle’s writeup of many of the newer language features.

Long-Term Support Releases

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

More Notes

That was a trivial introduction without much depth. There is so much more to learn. We completely skipped reflection and concurrency, barely hinted at type erasure, and hardly gave any examples. Go deeper with these, perhaps:

Then continue with whatever other good stuff you find out there.

Summary

We’ve covered:

  • Java programs are made up entirely of classes grouped into packages
  • Java uses classes for both object factories and bundling operations
  • JShell is a neeat little REPL for experimentation
  • Java has 8 primitive types and 5 type formers, which make new reference types
  • A class can extend at most one superclass but may implement multiple interfaces
  • Java is staticially typed, but its covariant arrays and non-reified generics are kinda sad
  • Java has optionals but their use is not required so they don’t hide the billion-dollar mistake
  • Functions and streams give Java a modern feel and have decent type inference, but they are still verbose