Introduction to 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 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:
public class Greeter {
    public static void main(String[] args) {
        System.out.print("Hello there");
        if (args.length > 0) {
            System.out.print(", " + args[0]);

Make sure your system has a Java compiler (you can get one here). Put the above code into the file and compile it like this:

$ javac

This produces 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.

While practicing, it’s a good idea to both compile and run with a single compound command:

$ javac && java Greeter Alice
Exercise: Why?
It’s not uncommon to make a change in your Java source code, then try to run it only with the java command. You have to re-run javac when you change the source code.

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:
import java.util.TreeMap;
import java.util.regex.Pattern;

public class WordCountApp {
    public static void main(String[] args) {
        var nonWord = Pattern.compile("[^\\p{L}']+");
        new BufferedReader(new InputStreamReader(
            .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:
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(;
        while (scanner.hasNext()) {
            var line = scanner.nextLine().toLowerCase();
            var matcher = wordPattern.matcher(line);
            while (matcher.find()) {
                var word =;
                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 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.
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:
public class VolumeChecker {
    public static void main(String[] args) {
        var c1 = new Cylinder(1, 4);
        var c2 = new Cylinder(1, 4);
        var c3 = c2;
        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:


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:


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.


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.


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)

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

Our AntiparallelClockHandsComputer class uses the classes:


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


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


The statements are:


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

OperatorsOperand TypesAssociativityArityDescription
++, --
+, -
R 1 increment and decrement
unary plus and minus
bitwise complement
logical not
*, /, % arithmetic L 2 multiplication, division, remainder
+, -
L 2 addition, subtraction
integral L 2 left shift
right shift with sign extension
right shift with zero extension
<, <=, >, >=
L 2 arithmetic comparison
has type
L 2 equal values
different values
refer to same object
refer to different objects
& integral
L 2 bitwise AND
boolean AND
^ integral
L 2 bitwise XOR
boolean XOR
| integral
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
*=, /=, %=, +=, –=
<<=, >>=, >>>=
&=, ^=, |=
R 2 assignment


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.


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


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.


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.
abstract class Animal {
    private String name;
    public Animal(String 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");
        Animal c = new Cow("Bessie");
        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.




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



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


and instead write:

if (employee != null) {
    if (employee.supervisor != null) {
        if (employee.supervisor.address != null) {
} else {


if (employee == null || employee.supervisor == null || employee.supervisor.address == null) {
} else {

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:
import java.util.Optional;
import java.util.Objects;

class Person {
    private String name;
    private Optional<Person> boss;

    private Person(String name, Optional<Person> boss) { = 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 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 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.
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)


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
Generator functions
I/O channels
reduce(identity, accumulator)
reduce(identity, accumulator, combiner)
collect(supplier, accumulator, combiner)

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:;;", "));;;

// Sum of salaries by department,
                                                 Collectors.summingInt(Employee::getSalary))); -> s.getGrade() >= PASS_THRESHOLD));


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.


Here are some useful facts:

More Notes

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