
Welcome to a technical introduction and overview of the Java language, targeted to intermediate programmers. If you feel a little less confident in your programming skills and would prefer to learn Java by example, start with this Basic Java Bootcamp. If you are a bit more advanced, feel good about programming, and especially if you are studying programming languages, read on!
We begin the usual way:
void main() {
IO.println("Hello world");
}
To build Java programs from source code and run them locally, you will need to get a Java Development Kit, also known as a JDK. Make sure to get version 25 or later. Once installed, running the code is as simple as:
$ java Hello.java Hello world
We’ll jump right to a terminal-based number guessing game with reading (IO.readlin), writing (IO.println), random numbers, while loops, if-statements, parsing strings, and error handling:
void main() {
var secret = new Random().nextInt(100) + 1;
var message = "Welcome";
while (true) {
var input = IO.readln(message + ". Guess a number: ");
int guess;
try {
guess = Integer.parseInt(input);
} catch (NumberFormatException e) {
message = "Not an integer";
continue;
}
if (guess < secret) {
message = "Too low";
} else if (guess > secret) {
message = "Too high";
} else {
IO.println("YOU WIN!");
break;
}
}
}
How many more days until the next New Year’s Day?
void main() {
var today = LocalDate.now();
var nextNewYearsDay = LocalDate.of(today.getYear() + 1, 1, 1);
var days = ChronoUnit.DAYS.between(today, nextNewYearsDay);
IO.println("Hello, today is " + today);
IO.println("There are " + days + " days until New Year’s Day");
}
Hey, nice support for calendars and dates. How about times, too? The DateTimeFormatter has great tools for formatting dates and times. This program prints the times on a 12-hour clock in which the hour and minute hands are antiparallel (180° apart):
void main() {
for (var i = 0; i < 11; i++) {
var time = LocalTime.MIDNIGHT.plusSeconds((int)((i + 0.5) * 43200 / 11));
IO.println(time.format(DateTimeFormatter.ofPattern("hh:mm:ss")));
}
}
$ java AntiparallelClockHandsComputer.java 12:32:43 01:38:10 02:43:38 03:49:05 04:54:32 06:00:00 07:05:27 08:10:54 09:16:21 10:21:49 11:27:16
While we’re on the subject of dates and times, it’s worth pointing out that Java has good support for locales. Try out this application:
void main() {
var input = IO.readln("Enter a date (YYYY-MM-DD): ");
try {
var date = LocalDate.parse(input);
for (var language : List.of("en", "hi", "ar", "es", "ru", "zh", "pt", "ko")) {
var locale = Locale.forLanguageTag(language);
IO.println(date.getDayOfWeek().getDisplayName(TextStyle.FULL, locale));
}
} catch (Exception e) {
System.err.println(e.getLocalizedMessage());
}
}
$ java Day.java Enter a date (YYYY-MM-DD): 2021-03-17 Wednesday बुधवार الأربعاء miércoles среда 星期三 quarta-feira 수요일
Language introductions need to feature Fibonacci numbers. Unlike Python, whose integer type is unbounded, Java’s int type has a very limited range. To get big integers, we use BigInteger:
void main() {
var a = BigInteger.ZERO;
var b = BigInteger.ONE;
for (var i = 0; i < 200; i++) {
IO.println(a);
var c = a.add(b);
a = b;
b = c;
}
}
Java is one of the most popular languages on the planet, ranking in the top 5 since its first public release in 1995. It is extremely popular in the enterprise computing space, and used often in banking and finance, but it can be found everywhere. Its standard library and ecosystem are huge. The community is extremely active and the language is constantly evolving. Java 25 was released in September of 2025 and has some amazing improvements over previous versions, and that’s the version we are covering in these notes. However, it’s always fun to see those Fireship 100 Seconds videos for programming languages, and the Java one is no exception. Keep in mind though, that Jeff’s Java video was way back in the pre-25 days, when Java programs were more verbose (and even the simplest of programs required explicit class declarations and the dreaded “public static void main string array args!”):
It’s time for us to go into some detail.
Java is designed for enterprise-level applications, so it not only full of great language features, it has a massive standard library. So massive that it needs a well-defined organization. All Java programs are made up of classes. The many thousands of standard classes are organized into packages. In our introductory examples, we have seen IO and Exception from the package java.lang, LocalDate from the package java.time, ChronoUnit from the package java.time.temporal, DateTimeFormatter from the package java.time.format, List and Locale from the package java.util, and BigInteger from the package java.math. Each of these packages come from the module java.base.
It is helpful to know that Java programs have both a logical and a physical structure.
Logically, Java code and data are defined within classes that are grouped into packages, which are collected into modules. That’s it. There is no code outside of a class, no classes outside of a package, and no packages outside of a module. This is true! In fact:
This simple hierarchy keeps the overall structure easy to understand, while making it easy to manage the many thousands of classes in the standard library and the hundreds of thousands of third-party classes.
Physically, your program is written in a collection of source code files. How do we make the connection between the logical structure and the physical structure?
In general, source code files look like this:

A class can be defined not only with the keyword class but also with record, enum, or interface.
In any given file, at most one class definition can be public.
If the public class is called X, then the file must be called X.java (and the name is case sensitive—always follow convention and choose a capitalized name).
Each class definition (in source code) is compiled into a binary class file. The class “file” may be in memory or it may be written to the file system. Packages can be split over several source code files, but each class has to be wholly contained in a file.

If you don’t put a package declaration in a file, its class(es) will go into the big global unnamed default package. This is fine for short programs and scripts, but in large applications, the use of the default package is discouraged.
If a file has code that is outside of a class declaration, the file is considered a compact source file. When compiled, an anonymous class will be generated and the top-level declarations of the file will be assumed to belong to that class. You should never rely on the name of the generated class, since it is never guaranteed. It will have a name; you just won’t know or care what it is.
To modularize a project, you designate a folder as your module’s root folder and create a file called module-info.java. It is structured like:
module com.example.game {
exports com.example.game.api;
requires java.desktop;
requires java.logging;
}
Any packages in your project not explicitly exported are invisible to the outside world.
Modules are somewhat outside the scope of these introductory notes, so we won’t be featuring any runnable examples of modularized code here. Don’t let that stop you from further study. You’ll need to work with them when building professional-level applications.
Java’s Standard LibraryThe standard library comes with close to 5,000 classes organized into hundreds of packages, grouped into a few dozen modules.
Execution will start with a launchable main method in the class (or interface—a kind of class we’ll talk about later) that you specify. Basically, a launchable main method is called main and has either no parameters or a single String[] parameter, which holds the command line arguments:
void main(String[] args) {
IO.print("Hello there");
if (args.length > 0) {
IO.print(", " + args[0]);
}
IO.println(".");
}
$ java Greeter.java Alice Hello there, Alice.The main method can be static or non-static. If non-static, there are other rules that apply, which you can read about in JEP 512.
Execution ends when all nondaemon threads have finished. These notes don’t cover what a nondaemon thread is, but you can ask a friendly AI chatbot for the details. If you are writing a simple program and never mention threads, your program will run on a single non-daemon thread. How convenient!
main:
/**
* A mutable cylinder that can be widened or lengthened.
*/
public class Cylinder {
private double radius;
private double height;
public Cylinder(double radius, double height) {
this.radius = radius;
this.height = height;
}
public double radius() {
return radius;
}
public double height() {
return height;
}
public double volume() {
return capArea() * height;
}
public double surfaceArea() {
return 2 * capArea() + sideArea();
}
private double capArea() {
return Math.PI * radius * radius;
}
private double sideArea() {
return 2.0 * Math.PI * radius * height;
}
public void widen(double factor) {
radius *= factor;
}
public void lengthen(double factor) {
height *= factor;
}
}
You can give this small class file to a friend who doesn’t believe that a glass twice as wide as another glass holds four times as much water. Then your friend could use that class in their own program:
void main() {
var glass = new Cylinder(3, 5);
IO.println("Volume is " + glass.volume());
glass.widen(2);
IO.println("After doubling the radius, volume is " + glass.volume());
}
To run this program, both files must be compiled. But to run the app, we only have to mention the file with the launchable main method:
$ java VolumeCheckerApp.java Volume is 141.3716694115407 After doubling the radius, volume is 565.4866776461628
This will compile VolumeCheckerApp.java (if the source code was modified since the last compilation), then, because that file simply mentions the class Cylinder, the file Cylinder.java will also be compiled if needed. This works when the file name is the same as the class name.
As long as your classes are in the main file or in files with the same name as the class, you can compile and run them easily. The java command can always find them. All the compiled code stays in memory and the program just runs.
But if you place several classes in a single file, your main program won’t know where to look for them, so you will need to compile the file manually with the javac command. This will create actual .class files on your computer, which the java program can pick up. Here’s a complete example. The Geometry.java file contains three classes (remember that classes can be defined with class, record, enum, or interface):
record Point(double x, double y, double z) {
public static Point ORIGIN = new Point(0, 0, 0);
public Point plus(Vector v) { return new Point(x + v.i(), y + v.j(), z + v.k()); }
public Vector minus(Point p) { return new Vector(x - p.x, y - p.y, z - p.z);}
public double distanceTo(Point p) { return p.minus(this).magnitude(); }
public static Point mid(Point p, Point q) {
return new Point((p.x + q.x) / 2, (p.y + q.y) / 2, (p.z + q.z) / 2);
}
}
record Vector(double i, double j, double k) {
public static Vector ZERO = new Vector(0, 0, 0);
public double magnitude() { return Math.sqrt(i * i + j * j + k * k); }
public Vector normalized() { return this.times(1.0 / magnitude());}
public Vector plus(Vector v) { return new Vector(i + v.i, j + v.j, k + v.k); }
public Vector minus(Vector v) { return new Vector(i - v.i, j - v.j, k - v.k); }
public Vector times(double s) { return new Vector(i * s, j * s, k * s); }
public double dot(Vector v) { return i * v.i + j * v.j + k * v.k; }
public Vector cross(Vector v) {
return new Vector(j * v.k - k * v.j, k * v.i - i * v.k, i * v.j - j * v.i);
}
}
record Plane(double a, double b, double c, double d) {
public static Plane from(Point p1, Point p2, Point p3) {
var n = (p2.minus(p1)).cross(p3.minus(p1));
var d = -(new Vector(p1.x(), p1.y(), p1.z()).dot(n));
return new Plane(n.i(), n.j(), n.k(), d);
}
public Vector normal() { return new Vector(a, b, c); }
}
Here’s a illustrative program that simply exercises those three classes, without doing anything useful, but it looks like the beginning of something. When programs start to get big, it tends to be a good idea to put your main inside of an explicit class.
class FlightSimulator {
Point currentPosition = new Point(0, 200, 0);
Plane ground = Plane.from(
new Point(0, 0, 0),
new Point(100, 0, 0),
new Point(0, 0, 100)
);
void main() {
IO.println("This flight simulator is not finished!");
var start = currentPosition;
var end = new Point(100, 0, 100);
var shortestPath = end.minus(start);
var direction = shortestPath.normalized();
var distance = shortestPath.magnitude();
IO.println("We are at " + start);
IO.println("We want to go to " + end + " on the ground " + ground);
IO.println("The shortest path is " + shortestPath);
IO.println("The direction of that path is " + direction);
IO.println("The distance of that path is " + distance);
IO.println("The normal vector of the ground is " + ground.normal());
}
}
Now we just manually compile all the files together and launch the program by specifying the class holding the main method, rather than the file name.

You can do this on the command line in one shot:
$ javac Geometry.java FlightSimulator.java && java FlightSimulator
A nice thing about manual compilation with javac (which, by the way, you are free to run even when unnecessary), is that you can use the javap tool to inspect class files. Fun stuff. Not strictly necessary for casual Java programmers, but important for professionals and especially important for computer scientists.
javac Cylinder.java. Then run javap Cylinder. Then javap -c Cylinder. Try also javap -p -c -v Cylinder.
How might this really look in practice?Rather than multiple classes in a file, you are more likely to see each class in its own file, with the classes grouped in to a geometry package, rather than a single file.
We’ll see an example much later in these notes. Our focus for now will be on the Java language rather than the application structure.
When learning or experimenting, use JShell. Let’s play around:
$ jshell | Welcome to JShell -- Version 25 | For an introduction type: /help intro jshell> 2 + 2 ARITHMETIC $1 ==> 4 jshell> 2 > 2 BOOLEANS $2 ==> false jshell> 3 + 100 * Math.hypot(-4, 3) OPERATOR PRECEDENCE $3 ==> 503.0 jshell> "Hello".replace('e', 'u') STRINGS AND CHARACTERS $4 ==> "Hullo" jshell> var greeting = "Good morning" CREATING VARIABLES greeting ==> "Good morning" jshell> greeting.substring(5) USING VARIABLES $6 ==> "morning" jshell> List.of(3, 5, 3, 2, 2, 1, 8, 1, 1) LISTS $7 ==> [3, 5, 3, 2, 2, 1, 8, 1, 1] jshell> Map.of("dog", 3, "rat", 5, "pig", 99) DICTIONARIES $8 ==> {rat=5, pig=99, dog=3} jshell> var poem = """ MULTI-LINE STRINGS ...> I wonder ...> about thunder ...> And Java.""" poem ==> "I wonder\nabout thunder\nAnd Java." jshell> int triple(int x) { TOP-LEVEL SHELL METHODS ...> return x * 3; ...> } | created method triple(int) jshell> triple(30) METHOD INVOCATION $11 ==> 90 jshell> $11 * 2 USING SHELL VARIABLES (COOL) $12 ==> 180 jshell> IntStream.range(1, 5).forEach(IO::println) STREAMS 1 2 3 4 jshell> /exit | Goodbye
We’ll be going through the language, not example-by-example, but by looking at concepts. (If you prefer whole-program examples, see the Basic Java Bootcamp.)
Our tour here, while technical, does not go super deep and is somewhat lacking in detailed examples. You can find much deeper coverage of the basics in these notes. If you have lots of time, check them out first!
Let’s get technical.
Java gives us eight primitive types and five mechanisms for creating new types:
| Primitive Type | Description | Example Values of the Type |
|---|---|---|
boolean | The two values true and false | |
char | Code units (WHAT?) in the range 0..65535 that are used to encode text | |
byte | Integers in the range -128 ... 127 | |
short | Integers in the range -32768 ... 32767 | |
int | Integers in the range -2147483648 ... 2147483647 | |
long | Integers in the range -9223372036854775808 ... 9223372036854775807 | |
float | IEEE binary-32 floating point numbers | |
double | IEEE binary-64 floating point numbers | |
| Type Former | Description | Example Type |
class | Completely general classes, nothing special, just the basics—containing fields, methods, constructors, initializers, and perhaps inner classes | |
[] | Arrays (a special kind of class) | |
enum | Enums (a special kind of class) | |
record | Record (just a shorthand way for making certain classes) | |
interface | Interfaces (yeah, also a kind of class) | |
See anything missing?If you come from a Python or JavaScript background, you might be asking “What, no string type?” or “What, no function type?” or “What, no unbounded integer type?”
Relax, Java has them: they’re just created with classes; we’ll see how soon.
Now that we have the big picture, let’s start using values of these types. But first, there’s something really important to understand, and that is....
We all know that values have types. That is an axiom. That goes without saying. That is a fact of the universe. The value false has type boolean. All values have types. By definition! Types are how we group values.
But in Java, variables are constrained to hold values of a certain type:
jshell> var friends = 39
39
jshell> friends = 40
40
jshell> friends = true
| Error: incompatible types: boolean cannot be converted to int
BUT WHY DO THIS?Constraining variables to a certain type facilitates static typing, that is, the deduction of the type of expressions just by looking at the source code! If we can figure out the types of expressions at compile-time, we can tell that running a program will generate a type error and therefore we can report the error early and refuse to run the program.
This is generally thought to have two advantages: (1) it removes bugs due to type mismatches at run-time, and (2) it allows for more efficient code to be generated as there are no expensive run-time checks and in many cases allows machine code to be generated with powerful indexing instructions rather than string lookups.
Java compilers can do a ton of static type checking, but there are things that do require dynamic typing (type checking at run time), including arrays, nulls, and certain casts (all which we will see later).
Time for technical vocabularyJava’s type system can be described as mostly static (the type of nearly every expression can be deduced at compile-time), quite strong (type mismatches generate errors rather than implicit conversions), and in the middle of the manifest (types must appear on every identifier) to inferential (identifier types can be inferred) spectrum.
Since variables have to have a type constraint, you can’t use var unless you know the type:
jshell> var payment
| Error: cannot use 'var' on variable without initializer
You can, however, declare a variable without an initializer provided you specify the type:
jshell> int books 0 jshell> double price 0.0 jshell> boolean found false jshell> String message null
Wait. Wait. What? Why wasn’t the default value of a string the empty string? What is that null thing? Hang on, things are about to get weird. But you knew Java was weird, right?
Can I be redundant?If you want to write
int limit = 10;orString warning = "Stay behind the yellow line";you are free to do so. One person’s silly redundancy is another person’s explicitness.Be nice to people that have a different preference than your own. This is not a hill to die on.
All types that are not primitive types are called reference types. What’s the difference? Instances of primitive types are stored directly in variables. But for reference types, variables hold references (a.k.a. “pointers” or “links”) to the objects. In a sense, the “value” of a reference type is a reference to the object, not the object itself. We saw above the eight primitive types (boolean, byte, char, short, int, long, float, and double). Did you know there were reference types Boolean, Byte, Character, Short, Integer, and more? I am not kidding.
The distinction between primitive and reference types may be one of Java’s worst design mistakes. Brendan Eich, the designer of JavaScript, copied this distinction into JavaScript because of a “Make it look like Java” order from management, and when asked what decisions he would make differently if he could design JavaScript from scratch today, among the items in his list were, in his own words:
Distinguishing primitives and references is not helpful. In Python, for example, every variable holds a reference, without exception. Python’s designers had no need to distinguish them. Java successor languages (Scala, Clojure, Kotlin) also treat everything as reference types, though their compilers will always generate efficient code to avoid the overhead of references. There’s no need to expose this distinction to programmers at the language level.
References are fine. But guess what Java has. (Major cringe sound here.) It has the dreaded null reference. It is bad enough this exists, but Java goes even further and takes null references to even more abominable heights. In Java, null does not have its own type—it is a member of all reference types. So null is...a string...and also a cylinder...and also a date...and...🤦♀️. Sigh. Or maybe there are an infinite number of null values, one for each reference type? Sad. 😞
For something that is supposed to stand for nothing, to suggest that null is a valid value for a variable constrained to the type String is absurd. If you want to be a Java programmer, you will have to get used to this.
This example shows how variables of primitive types differ from variables of reference types:
void main() {
float x; // declare a variable, it value is arbitrary
x = 3; // value 3 stored in x
float y = x; // value 3 stored in y
y = 5; // after this x is still 3
Cylinder c1; // just declares a variable
c1 = new Cylinder(1, 1); // now we have a cylinder created
Cylinder c2 = c1; // does NOT create a new cylinder
c2.widen(2); // exactly the same effect as c1.widen()
c1 = null; // the cylinder still exists
double d = c1.radius(); // OOPS!! Throws a NullPointerException
double r = c2.radius(); // This is perfectly fine: c2 isn’t null
}
A picture is crucial here, as it shows how primitive type values appear directly in the boxes, and object values are always references (unless, ugh, they are that darn null):

Java has arguably done something wrong here. Is it really true that null is an acceptable value of type String? Or arrays can be null in addition to being empty? Why? What even is going on here? First of all, it’s confusing. How is no string a string? Mind blowing. Second, it’s deceitful. Java bills itself as statically typed, but if your program includes the expressions.toUpperCase() for a variable s constraiend to type String, the compiler is unable, in general, to guarantee that s will not be null at run time. Java is not as statically typed as you might think. Run time type errors are possible. Yep, a NullPointerException is thrown. AT RUN TIME. 🤢
What a disappointment! Who. Is. Responsible. For. This?
Answer: Sir Charles Anthony Richard (Tony) Hoare, FRS FREng is. But he said he is sorry. He calls this idea that null could belong to a reference types the Billion Dollar Mistake as it has caused about a billion dollars worth of damage due to lost productivity and who knows what else.
Don’t be too hard on Sir TonyHe 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.
Primitives and References are different in other ways, too.
For primitives, assignment (=) and equality testing (==) works as expected: assignment is a copy and equality compares values. But with references, remember that it is the reference, not the object contents, that are being copied and compared. Two distinct cylinder objects of the same exact size are not equal to each other:
jshell> /open Cylinder.java jshell> var c1 = new Cylinder(3, 5) c1 ==> Cylinder@37f8bb67 jshell> var c2 = new Cylinder(3, 5) c2 ==> Cylinder@20ad9418 jshell> var c3 = c2 c3 ==> Cylinder@20ad9418 jshell> c1 == c2 $5 ==> false jshell> c2 == c3 $6 ==> true
Remember, it is the references that are compared, not the objects themselves. Here we made two objects of the class Cylinder. Objects are created with the new operator. The first object is referenced by the variable c1 while the second is referenced by the variables c2 and c3. Never confuse the term variable with the term object. They are completely different things! Behold three variables but two objects:

So for these reference types:
= assigns references, it does NOT copy object contents== operator always and only compares references, never object contents. It is concerned with identity, not equality. It is like the === operator in JavaScript and the is operator in Python.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 trickyIt’s so messy, we aren’t anywhere ready to talk about how messy it is, or even to begin writing such things! AAAAAAAAHHHHHHHHH.
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.
For every type T there is a type T[] of arrays of type T. Java arrays are fixed in size (they cannot grow or shrink) and they are mutable. Read the section on arrays in these notes for more.
Classes are pretty central to Java.
Let’s look deeper in the Cylinder class. It contains two fields (radius and height), one constructor, and eight methods (radius(), height(), volume(), surfaceArea(), capArea(), sideArea(), lengthen() and widen()). The purpose of this class is to instantiate objects. In fact, that’s what classes are for in programming language theory: classes are factories for creating objects that all share a similar structure and behavior. Classes classify objects (see where the name “class” comes from?). Objects created with this constructor will be instances of the class, and the methods will apply to these objects. Methods are marked private are for the internal use of the class only.
It is worth repeating: classes give us a way to instantiate objects with certain structure and behavior.
Watch out! Java overuses the term “class.”Somewhat unfortunately, Java uses the term “class” to refer to a bundle of data and code, whether or not the intent of the code is to instantiate objects. It may or may not correlate with the traditional notion of a class.
In the case of the
VolumeCheckerApp, the “class” generated by the compiler is simply some weird thing that happens to just house the executable code of an application. It’s certainly not a class in the traditional sense of the word, but only in the Java sense.Why? It appears the Java designers just liked the fact that everything should be in a class, allowing a simple view of every program as a collection of classes. Perhaps they could have picked a better name for these compiled units. Who knows? Who cares? It’s not worth fretting about. It’s never going to change.
An immutable object is one whose properties never change after construction. Immutable objects have several well-known advantages over mutable objects that a good rule of thumb is to use them when you can. So mark fields final when you can—the compiler will force you to set them in a constructor or initializer and ensure you never change them later.
Or, if you can, use a record. All fields are automatically final in a record. We’ll see records soon.
But beware that immutability with final fields and records is shallow. If your record contains a field that is a mutable list, say, you won’t be able to assign a new list to the field, but you will be able to update the contents of the list! Always Be Careful.
Typically, each instance of a class gets its own fields and methods, but fields and methods marked static do not belong to any particular instance—they’re like globals. The former are called instance fields and instance methods. The latter are called class fields and class methods, but in Java land a lot of people call them “static fields” and “static methods.” Static members are referenced with the class name itself. In the Point class from earlier:
record Point(double x, double y, double z) {
public static Point ORIGIN = new Point(0, 0, 0);
public Point plus(Vector v) { return new Point(x + v.i(), y + v.j(), z + v.k()); }
public Vector minus(Point p) { return new Vector(x - p.x, y - p.y, z - p.z);}
public double distanceTo(Point p) { return p.minus(this).magnitude(); }
public static Point mid(Point p, Point q) {
return new Point((p.x + q.x) / 2, (p.y + q.y) / 2, (p.z + q.z) / 2);
}
}
we can create points p and q:
var p = new Point(1, 2, 3);
var q = new Point(4, 5, 6);
and invoke the instance methods like so:
IO.println(p.plus(new Vector(5, 8, -2)));
IO.println(p.minus(q));
IO.println(p.distanceTo(q));
but access static fields and invoke static methods like so:
IO.println(Point.ORIGIN);
IO.println(Point.mid(p, q));
Interested in going deeper into Java classes? We barely scratched the surface here. I have much more detailed notes on Java classes and also on classes in general.
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:
extends or implements)So these are okay:
Object a = "abc"; // String is a subtype of Object
Object a = new int[]{3, 5, 8, 13}; // int[] is a subtype of Object
interface Reversible {}
class Sequence implements Reversible {}
Reversible r = new Sequence(); // Sequence is a subtype of Reversible
class Animal {}
class Dog extends Animal {}
Animal a = new Dog(); // Dog is a subtype of Animal
The Liskov Substitution PrincipleThe 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 typesJava 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.
A class can extend at most one other class, called its superclass. A superclass can have zero or more subclasses. There are lots of lots of rules here, and lots of interesting stuff on access modifiers that are covered elsewhere. Let’s jump into an example with a superclass called Animal with three subclasses.
abstract class Animal {
private String name;
protected Animal(String name) {
this.name = name;
}
public String speak() {
return name + " says " + sound();
}
public abstract String sound();
}
class Cow extends Animal {
public Cow(String name) {
super(name);
}
@Override
public String sound() {
return "moooooo";
}
}
class Horse extends Animal {
public Horse(String name) {
super(name);
}
@Override
public String sound() {
return "neigh";
}
}
class Sheep extends Animal {
public Sheep(String name) {
super(name);
}
@Override
public String sound() {
return "baaaa";
}
}
Superclass and subclasses are great when you have a collection of different types that are very similar in some respects, and slightly different in others. The superclass captures the commonality. Here all animals speak the same way, but their sounds are different. So we defined speak in the superclass and made sound an abstract method.
Lots of types can be compatible with the type of a variable, so if you see an expression like a.m() where a is a variable, and m is a method, the actual method that is called depends on the value bound to a at run time. This is called dynamic polymorphism.
void main() {
Animal h = new Horse("CJ");
IO.println(h.speak());
Animal c = new Cow("Bessie");
IO.println(c.speak());
IO.println(new Sheep("Little Lamb").speak());
}
Run like so:
$ javac Animals.java ThreeAnimals.java && java ThreeAnimals
sound?
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:
new!) and because of this, they exist to be implemented by other classes, so you cannot make them final.public specifier; the compiler essentially adds it.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
}
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.
Here’s some useful reference information:
| Class Modifiers | |
|---|---|
| no modifier | accessible only within its own package |
public | accessible wherever its package is |
abstract | cannot be instantiated (may have abstract methods) |
final | cannot be extended (subclassed) |
static | makes an inner-declared class really a top-level one |
| Member Modifiers | |
private | accessible only within its class |
| no modifier | accessible only within its package |
protected | accessible only within its package and to its subclasses |
public | accessible wherever its class is |
| Field Modifiers | |
final | value may not be changed, once assigned |
static | only one instance of the field shared by all objects of the class |
transient | field is not serialized |
volatile | value may change asynchronously compiler must avoid certain optimizations |
| Method Modifiers | |
final | may not be overridden |
static | method does not apply to a particular instance |
abstract | has no body; subclasses will implement |
synchronized | requires locking the object before execution |
native | implementation is not written in Java, but rather in some platform dependent way |
All reference types have Object as an ancestor, so they’ll all inherit these instance methods:
protected Object clone()boolean equals(Object obj)protected void finalize()Class<?> getClass()int hashCode()void notify()void notifyAll()String toString()void wait()void wait(long timeoutMillis)void wait(long timeoutMillis, int nanos)The equals, hashCode, and toString tend to get overridden a lot in practice. Use equals when you want to compare objects by content, not just by reference. Overriding equals can be a little tricky because the type of the argument is Object. So to do it properly, you use the instanceof operator that checks the class of the object that the reference refers to. The check can also give you a new variable whose static type is the one being checked. Here’s how you can do it:
class Cylinder {
private double radius;
private double height;
// ...
@Override public boolean equals(Object o) {
return (o instanceof Cylinder that)
&& this.radius == that.radius
&& this.height == that.height;
}
}
But be aware, be very aware, of this following guideline:
This is because if you place objects in a set or map, their presence in the set or map is detected by hashCode, not equals. You must ensure that whenever x.equals(y) that x and y have the same hash code. Fortunately, you can override with:
class Cylinder {
// ...
@Override public int hashCode() {
return Objects.hash(radius, height);
}
}
Overriding these methods is only needed when you want “value semantics.” Usually, you will never need value-based comparison and you won’t be storing items in sets, or even printing their contents. But when you do, you will need these methods overridden. Fortunately, there is a way to get value-based equality, hash code generation, and printing auto-generated, provided you accept (as you probably should) immutability. You can use records.
Defining immutable objects with value-base equality is so common that there’s a nice way to define them. Here’s an example:
record Point(double x, double y) {}
This gives you a fully immutable class called Point with a two-argument constructor, accessor methods x() and y() and properly overridden equals, hashCode, and toString(). This record is exactly the same as the following:
// Don't write this yourself, we're only showing what the record produces.
public class Point {
private final double x;
private final double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double x() {
return x;
}
public double y() {
return y;
}
@Override
public boolean equals(Object o) {
return (o instanceof Point other) && x == other.x && y == other.y;
}
@Override
public int hashCode() {
return java.util.Objects.hash(x, y);
}
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
You can add validation to the constructor and even add more methods if you desire. You can even add static fields, but not instance ones (ask a friend or chatbot about the difference between instance and static fields if you missed it). Let’s extend the point record type:
public record Point(double x, double y) {
public static final Point ORIGIN = new Point(0, 0);
public Point {
if (Double.isNaN(x) || Double.isNaN(y)) {
throw new IllegalArgumentException("Coordinates can not be NaN");
}
}
public double distanceFromOrigin() {
return Math.hypot(x, y);
}
public Point reflectionAboutOrigin() {
return new Point(-x, -y);
}
public static Point midpointOf(Point p, Point q) {
return new Point((p.x + q.x) / 2.0, (p.y + q.y) / 2.0);
}
}
Everything else we saw before is still “auto-generated.”
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.
Okay, more on this static typing thing. So you remember, that as a (mostly) statically typed language, Java tries to guarantee that the types of all expressions are known by the compiler. For numbers, booleans, and strings, and for simple classes like our Cylinder above, this is easy. But how about the following?
jshell> class Pair {
...> Object first;
...> Object second;
...> Pair(Object first, Object second) { this.first = first; this.second = second; }
...> }
| created class Pair
jshell> var p = new Pair("hello", 5)
p ==> Pair@37f8bb67
jshell> Object message = p.first
message ==> "hello"
jshell> String greeting = p.first
| Error:
| incompatible types: java.lang.Object cannot be converted to java.lang.String
| String greeting = p.first;
| ^-----^
Using Object for the type seems to work but IT IS SO UNSATISFYING!!! But while annoying, it makes sense, right? Static typing wants to check all the types at compile time so we don’t have to carry around expensive type information for expensive run-time type checks. So what right do we have to expect anything other than Object?
What if we could get the compiler to generate, on the fly, all the pair classes we need? We can!
jshell> class Pair<T, U> {
...> T first;
...> U second;
...> Pair(T first, U second) { this.first = first; this.second = second; }
...> }
| modified class Pair
jshell> var p = new Pair<String, Integer>("hello", 55)
p ==> Pair@439f5b3d
jshell> String s = p.first
s ==> "hello"
jshell> int x = p.second
x ==> 55
Here the class Pair is generic: it is not really a class, but more like a template. The real, concrete, classes would be things like Pair<String, Integer>, Pair<Boolean, Boolean>, and so on. The standard library makes use generics with concrete classes such as List<String> and List<BigInteger>. This is different from a dynamically typed language like Python, where there is a single, real, type called list.
More Java WeirdnessCan you believe this? Generic type parameters MAY NOT be primitives! Ugh! That’s right: you can NOT make a generic list of
intvalues. 🤦♀️ So the Java Standard Library has a bunch of classes, namelyBoolean,Character,Byte,Short,Integer,Long,Float, andDoublewhich “wrap”, or “box” a primitive object. So you’ll need aList<Integer>.Generally the compiler lets you move between, say,
intvalues andIntegerones, but sometimes you can get tripped up. Usually it’s fine though. Just be aware of the distinction. Do you feel confident?
The Java standard library has a fairly rich collection of generic (of course!) collection classes and interfaces. The library is huge—HUGE—and you can learn about it in the official tutorial (and you might also want to check out the Overview), but here, for reference, is list of some very common interfaces:
Iterable
└── Collection
├── Set
│ └── SortedSet
│ └── NavigableSet
├── List
└── Queue
├── Deque
| └──────────────┐
└── BlockingQueue │
├── BlockingDeque
└── TransferQueue
Map
├── SortedMap
│ └── NavigableMap
| └───────────────┐
└── ConcurrentMap │
└── ConcurrentNavigableMap
I have much more thorough additional notes on the Java collections in these notes and notes with good examples of lists and maps here. But do check out the official tutorial and the docs if you have time; it’s interesting how the library is structured, and how they implement such things as unmodifiable collections.
For now, here’s an example that uses a map to gather up all the words from standard input and compute and print their frequencies. DON’T PANIC: There’s a lot going on here, and the purpose of showing this code is to give you a feel of what Java programs can do, and not to be a tutorial.
void main() {
var counts = new TreeMap<String, Integer>();
var wordPattern = Pattern.compile("[\\p{L}'’]+");
while (true) {
var line = IO.readln();
if (line == null) {
break;
}
line = line.toLowerCase();
var matcher = wordPattern.matcher(line);
while (matcher.find()) {
var word = matcher.group();
counts.put(word, counts.getOrDefault(word, 0) + 1);
}
}
for (var e : counts.entrySet()) {
IO.println(e.getKey() + " " + e.getValue());
}
}
Java is a large multiparadigm programming language, meaning it an do things in several ways. Here is an equivalent program to the one above (it does exactly the same thing) but works with the input and the map through a stream-based interface that should appeal to functional programming enthusiasts.
void main() {
Stream.generate(IO::readln)
.takeWhile(line -> line != null)
.flatMap(line -> Pattern.compile("[\\p{L}'’]+").matcher(line).results())
.map(MatchResult::group)
.collect(
Collectors.groupingBy(
String::toLowerCase, TreeMap::new, Collectors.counting()))
.forEach((word, count) -> IO.println(word + " " + count));
}
Try this out on Emily Dickinson’s poems:
$ curl https://www.gutenberg.org/cache/epub/12242/pg12242.txt > emily.txt $ java WordCountApp.java < emily.txt
If you thought the first word count program was crazy, this one must look super crazy. Naturally, there is a bit of a learning curve required to understand it! It’s got function values, optionals, and streams in it. Sounds like we can use it as a...segue!
What a Standard Library!We just saw
TreeMapfrom the packagejava.util,PatternandMatchResultfrom the packagejava.util.regex, andStreamandCollectorsfrom the packagejava.util.stream.Relax, you do not have to memorize these facts. But some readers will.
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.
@FunctionalInterface
interface IntToIntFunction {
int apply(int x);
}
int twice(IntToIntFunction f, int x) {
return f.apply(f.apply(x));
}
void main(String[] args) {
IO.println(twice(x -> x * 3, 2));
}
That example was totally contrived (SORRY), since we pretty much reimplemented the built-in IntUnaryOperator. Speaking of which: the standard library has several dozen functional interfaces ready for you to use. Here are just a few:
| Functional Interface | The 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) |
| Runnable | void run() |
| Comparator<T> | int compare(T t1, T t2) |
| Callable<V> | V call() |
Check out the complete list of functional interfaces.
So we can write the famous twice function generically as:
public static <T> T twice(Function<T, T> f, T x) {
return f.apply(f.apply(x));
}
Here’s an example with a consumer:
public static void powers(int base, int limit, Consumer consumer) {
for (var power = 1; power <= limit; power *= base) {
consumer.accept(power);
}
}
You don’t have to always use lambda expressions for functions. You can also use method references, which are: pretty cool:
| Instead of... | You can write |
|---|---|
| x -> IO.println(x) | IO::println |
| p -> p.getName() | Person::getName |
| x -> new Widget(x) | Widget::new |
Damn that billion dollar mistake. It infects Java at its core. It can never go truly away.
If, for example, we wanted each person to have a required name but an optional boss, writing person.boss.name could throw a NullPointerException, so we probably have to write:
if (person.boss != null) {
// Do something with p.boss.name
}
These null checks have to go all over the place, and our code gets annoying and error prone when optional data is chained, as in person.boss.address.city.
But in 2014, Java added something to at least mitigate the problem and make the absence of something a little more explicit. But no one can force you to use this feature. While newer languages have this feature wired into the core of their being, Java was born too long ago and the billion dollar mistake was permanently, inextricably, baked in. Java programmers must see it in themselves to be professional and as a moral obligation diligently follow these principles:
null values creep in. What is an optional? For any type T, there is a type Optional<T>.Here’s an improved person class:
class Person {
private String name;
private Optional<Person> boss;
private Person(String name, Optional<Person> boss) {
this.name = Objects.requireNonNull(name);
this.boss = boss;
}
public Person(String name) {
this(name, Optional.empty());
}
public Person(String name, Person boss) {
this(name, Optional.of(boss));
}
public String name() {
return name;
}
public Optional<Person> boss() {
return boss;
}
}
void main(String[] args) {
var alice = new Person("Alice");
var bob = new Person("Bob", alice);
bob.boss().ifPresent(p -> {
// Here you would do something with the real person p
assert p == alice;
});
alice.boss().ifPresent(_ -> {
// This code will never be executed
assert false;
});
assert alice.boss().orElse(bob) == bob;
assert bob.boss().orElse(bob) == alice;
assert bob.boss().filter(p -> p.name().startsWith("A")).isPresent();
assert bob.boss().filter(p -> p.name().startsWith("B")).isEmpty();
assert bob.boss().map(Person::name).orElse("").equals("Alice");
assert alice.boss().map(Person::name).orElse("").equals("");
}
Optionals are like wrappers; they either wrap something or they don’t. A little reference:
| Method | Description |
|---|---|
| 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 |
In addition to optionals, streams arrived in Java in 2014. You should go through this old but good tutorial which arrived at that time. The time of Java 8. It gets you started. Then you can go through the much longer dev.java Streams tutorials.
Done with the tutorial? Remember what streams are for?
Streams are intended for querying and transforming data.
Remember why they are cool? They provide an abstraction fo processing sequential data, regardless of how that sequence is implemented. Also, we can set up streams to be processed either sequentially or in parallel.
You start with a source, then apply zero or more intermediate operations, then one terminal operation. The intermediate operations don’t actually do anything right away (they’re “lazy”), they just set things to happen when the terminal operation is invoked. Once the terminal operation is done, the stream is consumed — “all used up.”

| Sources | Intermediate Operations | Terminal 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:
Stream.of("dog", "rat", "bat", "cat")
Stream.of("dog")
Stream.empty()
Stream.iterate(8, i -> i * 10) // infinite stream, so you probably need a limit() too
Stream.generate(Math::random) // infinite stream, so you probably need a limit() too
Stream.concat(stream1, stream2)
IntStream.range(1, 100)
Lots of Standard Library operations produce streams:
"π😊".codePoints()"π😊".chars()new Random().ints()new Random().ints(30)Arrays.stream(a)bufferedReader.lines()Files.lines(fileName)Files.lines(fileName, charset)Files.list(directoryName)someArbitraryCollectionObject.stream()pattern.splitAsStream()By the way, those collectors are super flexible. But most of the time, you’ll just use a predefined one:
Collectors.toList()Collectors.toSet()Collectors.groupingBy(func)Collectors.averagingInt(func) // also for Long and Double
Collectors.summarizingInt(func) // also for Long and Double
Collectors.joining()Collectors.joining(delimiter)Collectors.joining(delimiter, prefix, suffix)Collectors.partitioningBy(predicate)Collectors.toMap(keyMapper, valueMapper)Collectors.toCollection(collectionFactory)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.
Optionals are one way to avoid the Billion Dollar Mistake.
There are others. Here’s a great article.
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:
Animal a = new Canine(); is fine.Canine c = new Dog(); is fine.But is this okay?
ArrayList<Animal> animals = new ArrayList<Canine>(); 🤷♀️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>.
List<Animal> were are subtype of List<Canine> we would have contravariance.
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
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?
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.
g be passed a function returning an Animal? No, bc it has to be assigned to a canine.g be passed a function returning a Dog? Yes.h be passed a function accepting an Animal? Yes.h be passed a function accepting a Dog? No, bc it gets passed a canine.Therefore we have this feeling of covariance on function return values (producers) and contravariance on function arguments (consumers). In Java-speak we say:
? extends T when you have producers? super T when you have consumersThere you have it: Producer Extends, Consumer Super: PECS.
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.
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 |
One of Java’s great strengths is the degree to which is supports concurrent programming directly at the language level and in its standard library.
The topic is way to big to cover in depth here, so it is covered separately.
But how about a peek? Here is a Java implementation of the classic Dining Philosophers simulation:
// Classic set up with five philosophers and five chopsticks.
static String[] NAMES = {"Haack", "Hume", "Zhaozhou", "Kaṇāda", "Russell"};
static Object[] chopsticks =
Stream.generate(Object::new).limit(NAMES.length).toArray(Object[]::new);
// We will require one seat to be empty to avoid deadlock. Philosophers
// must acquire the table semaphore in order to sit down and eat, and
// release it when they leave the table, which they do after each meal.
static Semaphore table = new Semaphore(NAMES.length - 1);
record Philosopher(int id, int numberOfMeals) implements Runnable {
private void action(String action) {
// For immediate actions, like picking up or putting down a chopstick.
IO.println(NAMES[id] + " " + action);
}
private void action(String action, int maxMillis) throws InterruptedException {
// For actions that take some time, like eating or thinking.
// Sleeps between maxMillis / 2 and maxMillis milliseconds.
action(action);
Thread.sleep(ThreadLocalRandom.current().nextInt(maxMillis / 2, maxMillis + 1));
}
@Override
public void run() {
var left = chopsticks[id];
var right = chopsticks[(id + 1) % chopsticks.length];
try {
for (var i = 0; i < numberOfMeals; i++) {
table.acquire();
action("has sat down and is now thinking", 8000);
synchronized (left) {
action("picked up left chopstick");
synchronized (right) {
action("picked up right chopstick");
action("is eating", 5000);
}
action("put down right chopstick");
}
action("put down left chopstick");
table.release();
action("has left the table", 2000);
}
action("has gone home");
} catch (InterruptedException _) {
Thread.currentThread().interrupt();
}
}
}
void main() {
for (var i = 0; i < NAMES.length; i++) {
Thread.ofPlatform().start(new Philosopher(i, 3));
}
}
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:
null — a lot of Java programmers are naive and they hold onto objects way too long, wasting tons of memory and either slowing down their programs or making them run out of memory.close() or dispose() method on them to free up O.S. resources. The Java garbage collector will only free up the memory in the Java virtual machine used by the Java object that refers to the O.S. resource. Fortunately, most resources are wrapped in Java objects of classes that implement the Closeable interface, which the try statement handles automatically. Here’s an example:
try (var socket = listener.accept()) {
var out = new PrintWriter(socket.getOutputStream(), true);
out.println(new Date().toString());
}
Always take advantage of a language’s mechanisms for structured resource clean up. As a modern language, Java gives you this power in the form of a try statement. You are expected to use it for files, networking, and in any application that deals external resources.
Here are some things you can easily look up, but having them in one place is nice.
Did we say this was a technical overview? Let’s talk syntax. The official reference for the Java language can be found on the Oracle Java Specifications Page. Check out the nice one-page syntax of Java.
One very tiny piece of syntax is the choice of keywords.
The following keywords cannot ever be used as identifiers.
abstractassertbooleanbreakbytecasecatchcharclassconstcontinuedefaultdodoubleelseenumextendsfinalfinallyfloatforgotoifimplementsimportinstanceofintinterfacelongnativenewpackageprivateprotectedpublicreturnshortstaticstrictfpsuperswitchsynchronizedthisthrowthrowstransienttryvoidvolatilewhileThe words exports, opens, requires, uses, yield, module, permits, sealed, var, non-sealed, provides, to, when, open, record, transitive, with are known as contextual keywords because they are only keywords in certain contexts.
Note that false, true, and null, are not keywords, but rather are literals or identifiers with special meanings.
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 Statement | Statement Type | When to Use |
|---|---|---|
| Simple | Empty | You want to do nothing |
| Local Variable Declaration | To bring a new variable into existence | |
| Assignment | To update the value in a variable | |
| Increment or Decrement | To update the value in a variable by adding or subtracting 1 | |
| Method Invocation | To invoke a method just for its side effects | |
| Conditional | If | To do (zero or) one of a variety of different things, based on completely arbitrary conditions. |
| Switch | To do (zero or) one of a variety of different things, based on the value of a single expression | |
| Iteration | For | To iterate through a fixed range or collection |
| While | To iterate while some condition holds (the condition is checked before each iteration) | |
| Do-while | To iterate while some condition holds (the condition is checked after each iteration) | |
| Disruption | Break | To immediately terminate an entire loop |
| Continue | To immediately terminate the current iteration of a loop | |
| Return | To immediately return from a method | |
| Throw | To immediately exit the current try-block or method with an error | |
| Other | Block | To group a bunch of statements together so local variable declarations can have smaller scope. |
| Labeled | To give a name to a statement, either as documentation or to serve as a target of a break or continue. | |
| Synchronized | To ensure some code can be executed only by one thread at a time | |
| Try | To define a small section of code for error-handling or resource management | |
| Local Class Declaration | To make a class used within the current block only | |
| Instance Creation | To 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) |
Here are the Java operators, presented from highest to lowest precedence.
| Operators | Operand Types | Associativity | Arity | Description |
|---|---|---|---|---|
++, --+, -~!(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 |
History and pragmatics are good to discuss in any language overview. Let’s briefly talk about good-to-know aspects of Java pragmatics.
Java is commonly used to build large, enterprise applications. In general, you will use a build system based on tools like Maven or Gradle.
Java has a rich ecosystem of frameworks and libraries that can help you build applications more efficiently. Some popular frameworks include:
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, Or four. Or five. Depends on how you count them.
General purpose programming, the one these notes are all about.
Lightweight APIs for resource-constrained, embedded systems and mobile devices.
The big stuff for large-scale distributed applications and support for databases, networking, middleware, scaling, messaging, mail, authentication, and more.
Graphics and media packages for creating and deploying rich client applications.
For smart cards and similar small-memory devices such as SIM cards for telecom, EMV cards for banking, and secure identification devices.
FYI: SE = Standard Edition, ME = Micro Edition, EE = Enterprise Edition, FX = Effects.
New graphics applications, both 2D and 3D, are generally implemented on the JavaFX Platform. You can download JavaFX and browse the documentation. Note that there is a fair amount of 3D support in the platform (see classes such as Mesh and PerspectiveCamera).
There is also a bit of graphics support within Java SE itself, in the module java.desktop. The support is less fully-featured than in JavaFX but still fun to learn. I have notes on that module here.
High-level support for enterprise-level, distributed applications can be found in the Jakarta EE platform. But there’s a fair amount of low-level, socket-based networking in Java SE, which is covered in these notes.
While unit testing is a massive topic in its own right, it’s good to have a simple example. Let’s see a test for the animal classes we saw earlier:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class AnimalSoundsTest {
@Test
public void testCows() {
var cow = new Cow("Bessie");
assertEquals("moooooo", cow.sound());
assertEquals("Bessie says moooooo", cow.speak());
}
@Test
public void testHorses() {
var horse = new Horse("CJ");
assertEquals("neigh", horse.sound());
assertEquals("CJ says neigh", horse.speak());
}
@Test
public void testSheep() {
var sheep = new Sheep("MacKenzie");
assertEquals("baaaa", sheep.sound());
assertEquals("MacKenzie says baaaa", sheep.speak());
}
}
Java doesn’t come with built-in test runners (you do have an assert statement, which isn’t much) so most folks use a third-party library called JUnit.
In practice, Java applications are built with Maven, Gradle or some other build system, which makes it pretty automatic to connect JUnit to your project. But we’re going to do this manually. Go to the Maven Central page for the junit-platform-console-standalone module, click on the Browse link for a recent version, and download the jar file to your machine. You can place it anywhere you like. I renamed it to junit.jar and put it in my home folder.
Running tests using a main deep inside the jar, so running java only on this jar won’t find the classes we need to test. So we have to compile our files manually with javac. When compiling AnimalSoundsTest.java, we need to see the JUnit jar, so we have to tell the compiler about it. Similarly, when running the test runner, it needs to know where to find our compiled classes. Telling java and javac where to find files is done by specifying a classpath, that is, a set of folders and jar files, through the -cp option.
Getting right to it:
$ javac -cp ~/junit.jar:. Animals.java AnimalSoundsTest.java && java -jar ~/junit.jar execute -cp . -c AnimalSoundsTest 💚 Thanks for using JUnit! Support its development at https://junit.org/sponsoring ╷ ├─ JUnit Platform Suite ✔ ├─ JUnit Jupiter ✔ │ └─ AnimalSoundsTest ✔ │ ├─ testSheep() ✔ │ ├─ testCows() ✔ │ └─ testHorses() ✔ └─ JUnit Vintage ✔ Test run finished after 54 ms [ 4 containers found ] [ 0 containers skipped ] [ 4 containers started ] [ 0 containers aborted ] [ 4 containers successful ] [ 0 containers failed ] [ 3 tests found ] [ 0 tests skipped ] [ 3 tests started ] [ 0 tests aborted ] [ 3 tests successful ] [ 0 tests failed ]
Classpaths and JARsA JAR file is a zipped up collection of classes. A classpath is a colon-separated list of jars and folders that the
javaandjavaccommands will search to find the classes it needs. When we write~junit.jar:.for a classpath, we are asking the tools to look for classes within the jar file and in the current folder.
Just for fun, and for some historical perspective, here is how the Java SE platform has evolved over time:
| Version | Released | Classes in Core API |
Notable New Stuff (Not complete) |
|---|---|---|---|
| JDK 1.0 | 1996‑Jan | 210 | |
| JDK 1.1 | 1997‑Feb | 477 | I/O design flaws fixed; inner classes; internationalization; AWT enhancements; JavaBeans API; JARs; RMI; Reflection; JDBC; JNI |
| J2SE 1.2 | 1998‑Dec | 1524 | Swing; Collections; Permissions, policies, certificates, signers; Java2D; Accessibility API; Extensions framework; RMI enhancements; Sound |
| J2SE 1.3 | 2000‑May | 1840 | JNDI; Java Sound; Swing and Java2D enhancements; Security enhancements; Networking enhancements; Improved serialization support; Input method framework; Timers |
| J2SE 1.4 | 2002‑Feb | 2723 | New IO library (NIO); Assertions; XML, crypto, SSE, authentication added to core API; Image IO; Java Print Service; Many Swing enhancements; Logging API; WebStart; Chained Exceptions; IPv6; Regexes |
| J2SE 5.0 | 2004‑Sep | 3279 | Generics; Better for statement; Annotations; Enumerations; Sophisticated concurrency packages; Autoboxing; Varargs; Static import; Unicode 4.0 (Wow!); Tons of library enhancements |
| Java SE 6 | 2006‑Dec | 3777 | Scripting languages can be embedded; XML and web services; JDBC 4; Many GUI enhancements (e.g. for tables and trees); monitoring and management, Programmatic access to compiler; Pluggable annotations; A few more security apis |
| Java SE 7 | 2011‑Jul | 4024 | Support for dynamically typed languages; Strings in switch; Better type inference (diamond); Simplified varargs; Binary integer literals; Multi-catch; Concurrency and collections updates; NIO.2; Elliptic-Curve Cryptography |
| Java SE 8 | 2014‑Mar | 4240 | Streams; Lambda expressions (closures); Interface default methods; Unsigned integer arithmetic; Repeating annotations; Date and Time API |
| Java SE 9 | 2017‑Sep | 6005 | Interface private methods; ImmutableSet; Optional.stream(); JShell |
| Java SE 10 | 2018‑Mar | 6002 | Optional.orElseThrow; Nicer ways to create unmodifiable collections |
| Java SE 11 | 2018‑Sep | 4410 | Local variable type inference; HTTP Client; New collection methods; New stream methods |
| Java SE 12 | 2019‑Mar | 4432 | |
| Java SE 13 | 2019‑Sep | 4403 | |
| Java SE 14 | 2020‑Mar | 4420 | Switch expressions and new Switch statement with -> |
| Java SE 15 | 2020‑Sep | 4352 | Text blocks |
| Java SE 16 | 2021‑Mar | 4389 | Records; Pattern Matching for instanceof |
| Java SE 17 | 2021‑Sep | 4388 | Sealed classes |
| Java SE 18 | 2022‑Mar | 4404 | Default charset is now UTF-8 everywhere |
| Java SE 19 | 2022‑Sep | 4417 | |
| Java SE 20 | 2023‑Mar | 4429 | |
| Java SE 21 | 2023‑Sep | 4443 | Record patterns; Pattern matching in switch |
| Java SE 22 | 2024‑Mar | 4686 | Unnamed variables and patterns |
| Java SE 23 | 2024‑Sep | 4693 | |
| Java SE 24 | 2025‑Mar | 4692 | |
| Java SE 25 | 2025‑Sep | 4702 | Compact source files, instance main methods, import module, flexible constructor bodies |
You should check out Oracle’s writeup of many of the newer language features, and how they evolved from Java 9 to today. You can find the same information, presented slightly differently, at dev.java.
Long-Term Support ReleasesJava 8, 11, 17, 21 and 25 are called LTS (long-term support) versions, meaning they will be maintained and supported for a long time.
Here are some questions useful for your spaced repetition learning. Many of the answers are not found on this page. Some will have popped up in lecture. Others will require you to do your own research.
void main() {
IO.println("Hello, World!");
}var input = IO.readLine();
BigIntegernull value into each reference type, meaning that you can say “this variable is constrained to hold strings” and assign the variable null, which stands for no string at all. Nulls have to be checked for at run time, weakening Java’s claim to be safe and statically typed.== operator checks for reference equality, not value equality.extends, and the keyword for implementing interfaces is implements.equals(), hashCode, and toString() methods.Optional type? null.null.Threadjava.desktop module.We’ve covered: