Introduction to Java

Java is one of the most popular languages on the planet. Let’s learn some Java programming.

Getting Started

A Java program is nothing more than a collection of classes, optionally grouped into packages. Here is a simple program made up of one class that is not part of a package:

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 compiler (such as the JDK from Oracle. Put the above code into the file Greeter.java and compile it like this:

$ javac Greeter.java

This would produce the file Greeter.class, which you then run with a Java interpreter, which can be:

To run from the command line:

$ java Greeter Alice

This runs the public static void method called main in Greeter.class, passing it the command line arguments as an array.

You can run these class files on any device or operating system to which the Java interpreter, or more precisely, the Java Virtual Machine 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.

Oh by the way, while practicing, it might be a good idea to both compile and run with the single compound command:

$ javac Greeter.java && java Greeter Alice
Exercise: Why?

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

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

WordCountApp.java
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.stream.Collectors;

public class WordCountApp {
    public static void main(String[] args) {
        var nonWord = Pattern.compile("[^\\p{L}']+");
        new BufferedReader(new InputStreamReader(System.in))
            .lines()
            .flatMap(line -> nonWord.splitAsStream(line.toLowerCase()))
            .filter(word -> !word.isEmpty())
            .collect(Collectors.groupingBy(w->w, TreeMap::new, Collectors.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.SortedMap;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

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?

The types for parameters and method return value must be written explicitly. Local variables can be introduced simply with var, but you can spell out the type if you want to.
Exercise: Write TraditionalWordCountApp.java without using var. See why var is so nice? Or is it? Do you think making the types for local variables explicit makes the code more readable? Or less?

Classes and Objects

We’ve mentioned that a Java program is a collection of classes. Most classes exists so that you can make objects (also known as instances) of that class.

Cylinder.java
public class Cylinder {
    private double radius;
    private double height;
    public Cylinder(double r, double h) {radius = r; height = h;}
    public double getRadius() {return radius;}
    public double getHeight() {return height;}
    public double getVolume() {return capArea() * height;}
    public double getSurfaceArea() {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;}
}

The class in this file contains two fields (radius and height), one constructor, and eight methods (getRadius, getHeight, getVolume, getSurfaceArea, capArea, sideArea, lengthen and widen). Compile this file to produce the file Cylinder.class. 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:

VolumeChecker.java
public class VolumeChecker {
    public static void main(String[] args) {
        var c1 = new Cylinder(1, 4);
        var c2 = new Cylinder(1, 4);
        var c3 = c2;
        c2.widen(2);
        System.out.println("Volume of first glass is " + c1.getVolume());
        System.out.println("Volume of second glass is " + c2.getVolume());
        System.out.println("Volume of third glass is " + c3.getVolume());
    }
}

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! See how we have three variables but two objects:

cylindervarobj.png

The new operator in Java allocates memory for the cylinder objects, but the programmer never explicitly deallocates memory for objects. The Java runtime automatically performs garbage collection on objects that are no longer referenced.

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

Program Structure

Logically, Java programs are made up of classes that are grouped into packages. Physically, your program is written in a collection of source code files. Almost every Java compiler in the world forces this organization:

javastructure.png

Each class definition is compiled into a .class file. 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.

JShell

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

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

jshell> "Hello".startsWith("Hell")
$1 ==> true

jshell> 9 * 4 - 2 ^ 20
$2 ==> 54

jshell> int triple(int x) {
   ...>     return x * 3;
   ...> }
|  created method triple(int)

jshell> triple(30)
$4 ==> 90

jshell> $4 * 2
$5 ==> 180

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

jshell> /exit
|  Goodbye
$

Standard Libraries

People have written millions of classes since Java was created, but a few thousand of them have been collected into one of Java’s standard, or core, APIs. The APIs vary by platform, of which there are three. Java ME (embedded systems, with our without displays), Java SE) (typical stuff), and Java EE (for the big stuff like databases, networking, middleware, scaling, messaging, mail, authentication).

Here’s an example program that uses six of the classes from Java SE:

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

/**
 * An application which displays each of the times on a twelve-hour clock at which the
 * hour and second hands make an angle of 180 degrees.
 */
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")));
        }
    }
}

Our AntiparallelClockHandsComputer class uses the classes:

    java.lang.String
    java.lang.System
    java.lang.Math
    java.text.SimpleDateFormat
    java.util.Date;
    java.util.GregorianCalendar

The import statements at the top of the source file allow us to avoid specifying the full name of these classes throughout our code. Note that import statements are never necessary, they are simply a convenience. (Also note that classes from java.lang don’t have to be mentioned in import statements.)

Evolution

Java has evolved over time. Java SE has evolved like this:

Version Released Packages in
Core API
Classes in
Core API
What’s New
JDK 1.0 1995 8 210  
JDK 1.1 1997 22 477 I/O design flaws fixed; inner classes; internationalization; AWT enhancements; JavaBeans API; JARs; RMI; reflection; JDBC; JNI; ...
J2SE 1.2 1999 59 1524 Swing; collections; Permissions, policies, certificates, signers; Java2D; accessibility API; extensions framework; RMI enhancements; reference objects; sound; ...
J2SE 1.3 2000 76 1840 JNDI; Java Sound; Swing and Java2D enhancements; Security enhancements; Networking enhancements; Improved serialization support; Input method framework; timers; ... (Sun released the HotSpot VM during this time.)
J2SE 1.4 2002 135 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; JDBC 3.0; Chained Exceptions; IPv6; Unicode 3.0; Regexes; ...
J2SE 5.0 2004 166 3279 Generics; better for statement; annotations; enumerations; sophisticated concurrency packages; autoboxing; varargs; static import; Unicode 4.0 (uh-oh); tons of library enhancements; ...
Java SE 6 2006 202 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; improved desktop deployments; a few more security apis; ...
Java SE 7 2011 209 4024 Support for dynamically typed languages; strings in switch; better type inference (diamond); simplified varargs; binary integer literals; multi-catch; concurrency and collections updates; Unicode 6.0; NIO.2; SCTP, SDP, TLS 1.2; Elliptic-Curve Cryptography; XRender pipeline; Nimbus L&F; Gervill sound; Enhanced MBeans; ...
Java SE 8 2014 217 4240 Streams; lambda expressions (closures); interface default methods; unsigned integer arithmetic; repeating annotations; Date and Time API; statically linked JNI libraries; ...
Java SE 11 2018 222 4843 Local variable type inference; HTTP Client; New collection methods; New stream methods; ...

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 SE 11.

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, and var are not keywords, but rather are literals or identifiers with special meanings.

Statements

The statements are:

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

Types

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 (you generally have to express the type of every identifier) to inferential (identifier types can mostly be inferred) spectrum.

There are eight and only eight primitive types, wired into the language:

An infinite number of types can be created by one of the following mechanisms:

Primitive and Reference Types

All types that are not primitive types are called reference types.

Here is how primitive and reference types differ. The value of a primitive type is the primitive entity (like true or 759 or 4322.95 or '@'). The value of a reference type is either null or a reference to an object, not the object itself.

referenceexample.png

Primitive entities can just be written down; objects are created using the new operator.

Important:

Remember that variables of non-primitive types contain references; therefore, the "=" and "==" operators DO NOT copy and test the equality of object values. (There may be clone() and equals() methods available, or you can define these if needed.)

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 = 4;                          // 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
c2 = null;                      // Now the object is unhooked. It is
                                // "garbage" and the runtime system
                                // will eventually reclaim the storage.

An interesting note: variables which are fields of a class are automatically initialized in Java: for primitive types they are automatically initialized to 0 for numbers and false for booleans; for objects they are initialized to null.

The Billion Dollar Mistake

WAIT A MINUTE! DID WE JUST READ THAT THE VALUE OF ANY REFERENCE TYPE COULD HAVE THE VALUE null? What the...?! So null is an acceptable value of type String? Or arrays can be null in addition to being empty? Like what even is going on here? So if I have a variable s of type String and I invoke s.toUpperCase() it’s like possible that s could be null? Well I thought Java was like this awesome static and strongly typed language! Um, so, Java will in this case give a run time type error? Yep, it will throw a NullPointerException.

WHO IS RESPONSIBLE FOR THIS ABOMINATION?

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!

Variables have Types

Sorry for that rant in the last section. Back to Java.

In virtually every programming language, values have types.

In Java, variables are given types as well! The type of a variable, parameter, or method return value, must be specified at compile time. The compiler will check that all values assigned to the variable must be compatible with the declared type of the variable. 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();
Remember Java is statically typed. The compiler will try to check the type compatibility of every single expression in your program before running it. Virtually all type errors will be caught at compile time.

There are some exceptions, but not many.

Inheritance and Polymorphism

Classes can extend other classes. 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 Animal with three subclasses. All animals speak the same way, but their sounds are different. So sound is made an abstract method.

AnimalDemo.java
abstract class Animal {
    private String name;
    public 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"; }
}

public class AnimalDemo {
    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

TODO

Generics

Generics popup a lot in statically typed languages. Dynamically typed languages like Python will probably just have a type called list, Java distinguishes these from each other:

The types with variables and wildcards aren’t exactly classes; they are type expressions that you can associate with a variable (or parameter or method return).

TODO

Optionals

Damn that billion dollar mistake. What can we do about it? Should we avoid writing:

System.out.println(employee.supervisor.address.city);

and instead write:

if (employee != null) {
    if (employee.supervisor != null) {
        if (employee.supervisor.address != null) {
            System.out.println(employee.supervisor.address.city);
        }
    }
} else {
    System.out.println("---");
}

or

if (employee == null || employee.supervisor == null || employee.supervisor.address == null) {
    System.out.println("---");
} else {
  System.out.println(employee.supervisor.address.city);
}

Ideally you should enforce that a field of an object can never be null at construction time (and ideally make the field readonly). But if a field can be missing, you should use an optional rather than letting it get null. For any type T, there is a type Optional<T>. A somewhat contrived example:

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 getName() {return name;}
    public Optional<Person> getBoss() {return boss;}
}

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

        bob.getBoss().ifPresent(p -> {assert p == alice;});
        alice.getBoss().ifPresent(p -> {assert false;});
        assert alice.getBoss().orElse(bob) == bob;
        assert bob.getBoss().orElse(bob) == alice;

        Optional<Person> b = bob.getBoss();
        assert b.filter(p -> p.getName().startsWith("A")) == b;
        assert !b.filter(p -> p.getName().startsWith("B")).isPresent();
        assert b.map(Person::getName).orElse("").equals("Alice");
        assert alice.getBoss().map(Person::getName).orElse("").equals("");
    }
}

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

Optional.empty()An empty “wrapper”
Optional.of(x)A wrapper for x
Optional.ofNullable(x)A wrapper for x if x not null; else empty optional
o.isPresent()true if o wraps a value; else false
o.ifPresent(f)calls f(x) if o wraps x; else does nothing
o.orElse(y)x if o wraps x; else y
o.map(f)f(x) if o wraps x; else empty optional
o.flatMap(f)like map but won’t keep overwrapping if chained
o.filter(p)o if o wraps x and p(x) is true; else empty optional

Function Types

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

There are some built-in functional interfaces in the standard library:

Functional InterfaceThe Abstract Method
Supplier<T>T get()
Consumer<T>void accept(T t)
Predicate<T>boolean test(T t)
Function<T, R>R apply(T t)
BiFunction<T, U, R>R apply(T t, U u)

Streams

You should go through this great tutorial.

Done with the tutorial? Okay, here’s a little review of what streams are, and what they are for:

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.”

SourcesIntermediate OperationsTerminal Operations
Arrays
Collections
Generator functions
I/O channels
etc.
filter(p)
map(f)
mapToInt(f)
mapToLong(f)
mapToDouble(f)
flatMap(f)
flatMapToInt(f)
flatMapToLong(f)
flatMapToDouble(f)
distinct()
sorted()
sorted(comparator)
peek()
limit()
skip()
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(pred)
allMatch(pred)
noneMatch(pred)
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));

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, please do so. 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

Like most languages, Java is managed, meaning its runtime system has a tracing garbage collector. However, programmers have a couple responsibilites when it comes to using memory and other resources efficiently:

Java In Practice

Java is commonly used to build large, enterprise applications. Tools like Maven and Gradle help here. I have a different page of notes for this.

Summary

Here are some useful facts:

More Notes

That was a trivial introduction. There is so much more to learn. Try: