A Java program is a a collection of one or more classes. Here is a simple program:
public class Greeter { public static void main(String[] args) { System.out.print("Hello there"); if (args.length > 0) { System.out.print(", " + args[0]); } System.out.println("."); } }
Make sure your system has a Java Development Kit, or “JDK” (you can get one here or here). Put the above code into the file Greeter.java
and run it like this (note the command line argument):
$ java Greeter.java Alice Hello there, Alice.
Did you notice Java is, um, more verbose than Python?
140 characters:
print("hello, world!")280 characters:
public class HelloTweetApp {
— Neckbeard Hacker (@NeckbeardHacker) November 10, 2017
public static void main(String [] argv) {
System.out.println("hello, world!");
}
}
In the program above, we created our own class called Greeter
and used two classes called System
and String
from the standard library. These classes come from the package called java.lang
(Java classes are optionally grouped in to packages), which is so special that you never have to mention it! But if you use classes from any other package, you do have to mention the package:
public class NewYearCountdown { public static void main(String[] args) { var today = java.time.LocalDate.now(); var nextNewYearsDay = java.time.LocalDate.of(today.getYear() + 1, 1, 1); var days = java.time.temporal.ChronoUnit.DAYS.between(today, nextNewYearsDay); System.out.println("Hello, today is " + today); System.out.printf("There are %d days until New Year’s Day%n", days); } }
LocalTime
from package java.time
and class ChronoUnit
from package java.time.temporal
Putting the package names on every class can make the code really verbose, so Java allows the import
statement at the top of the file:
import java.time.LocalTime; import java.time.format.DateTimeFormatter; public class AntiparallelClockHandsComputer { public static void main(String[] args) { for (var i = 0; i < 11; i++) { var time = LocalTime.MIDNIGHT.plusSeconds(Math.round((0.5 + i) * 43200.0 / 11)); System.out.println(time.format(DateTimeFormatter.ofPattern("hh:mm:ss"))); } } }
Import statements are never necessary, they are simply a convenience.They really don’t import anything, they simply allow you to leave off package names. That is all.
Here are couple more programs. Both do exactly the same thing, but are written in different styles. The first is all flowy and streamy:
import java.util.Scanner; import java.util.TreeMap; import java.util.regex.Pattern; import java.util.regex.MatchResult; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.counting; public class WordCountApp { private static final Scanner scanner = new Scanner(System.in); public static void main(String[] args) { scanner .findAll(Pattern.compile("[\\p{L}'’]+")) .map(MatchResult::group) .collect(groupingBy(String::toLowerCase, TreeMap::new, counting())) .forEach((word, count) -> System.out.printf("%s %d\n", word, count)); } }
and the second is kind of, you know, pretty imperative and uses a lot of variables:
import java.util.Scanner; import java.util.TreeMap; import java.util.regex.Pattern; public class TraditionalWordCountApp { public static void main(String[] args) { var counts = new TreeMap<String, Integer>(); var wordPattern = Pattern.compile("[\\p{L}'’]+"); var scanner = new Scanner(System.in); while (scanner.hasNext()) { var line = scanner.nextLine().toLowerCase(); var matcher = wordPattern.matcher(line); while (matcher.find()) { var word = matcher.group(); counts.put(word, counts.getOrDefault(word, 0) + 1); } } for (var e : counts.entrySet()) { System.out.printf("%s %d\n", e.getKey(), e.getValue()); } } }
Did you notice?Local variables tend to be introduced with var, but there are explicit type names attached to fields, parameters, and methods. It turns out you can use the type name for local variable if you want to.
Try this out on Emily Dickinson’s poems:
$ curl https://www.gutenberg.org/cache/epub/12242/pg12242.txt > emily.txt $ java WordCountApp.java < emily.txt
Logically, Java programs are made up of classes that are grouped into packages. But physically, your program is written in a collection of source code files. (You didn’t expect every program to be made up of just one file, did you?) Almost every Java compiler in the world forces this organization:
In any given file, at most one class definition can be public
(written public class A
).
If the public class is called X, then the file must be called X.java
(and the name is case sensitive).
Execution will start at a method with signature public static void main(String[] args)
in some designated class.
Execution ends when all non-daemon threads have finished.
Each class definition is compiled into a .class
file, generally using the javac
command. Packages can be split over several source code files, but each class has to be wholly contained in a file.
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.
That was too much detail. Let’s see an example:
public class Cylinder { private double radius; private double height; public Cylinder(double radius, double height) { this.radius = radius; this.height = height; } public double radius() { return radius; } public double height() { return height; } public double volume() { return capArea() * height; } public double surfaceArea() { return 2 * capArea() + sideArea(); } private double capArea() { return Math.PI * radius * radius; } private double sideArea() { return 2.0 * Math.PI * radius * height; } public void widen(double factor) { radius *= factor; } public void lengthen(double factor) { height *= factor; } }
You can give this small class file to a friend who doesn’t believe that a glass twice as wide as another glass holds four times as much water. Then your friend could write the following:
public class VolumeCheckerApp { public static void main(String[] args) { var glass = new Cylinder(3, 5); System.out.println("Volume is " + glass.volume()); glass.widen(2); System.out.println("After doubling the radius, volume is " + glass.volume()); } }
To run this, we have to first compile each class with javac
then we can run by specifying the class name with the main
to use as our entry point:
The straightforward way to do this is:
$ javac Cylinder.java && javac VolumeCheckerApp.java && java VolumeCheckerApp
But we can also do just this:
$ javac VolumeCheckerApp.java && java VolumeCheckerApp
Why? because javac
will check to see if any dependent .java
files have changed since the last compilation. When you say javac VolumeCheckerApp.java
, the compiler will check to see if you changed Cylinder.java since the last time Cylinder.class was generated.
Avoid running the java command by itselfIf you run the command
java VolumeCheckerApp
on a line by itself, it will use whatever dependent class files it can find WITHOUT checking for source code changes! Do yourself a favor and get in the habit of always runningjavac
andjava
together, in the same command, all the time.Of course, this warning does not apply if you are using a professional build system (such as Maven or Gradle) or you are managing your program in an IDE like Eclipse or IDEA, as these systems manage all the dependencies and source code changes automatically for you.
The single-file program short-cutIf your program is just a single file, you can run
java
on the source code file itself: it generates a class file in memory. It is just a convenience. Remember this will not work for multi-file programs.
Let’s look deeper in the Cylinder class. It contains two fields (radius
and height
), one constructor, and eight methods (getRadius
, getHeight
, getVolume
, getSurfaceArea
, capArea
, sideArea
, lengthen
and widen
). The purpose of this class is to instantiate objects. Objects created with this constructor will be instances of the class, and the methods will apply to these objects. A couple of the methods are marked private
, these are for the internal use of the class only.
But the VolumeCheckerApp class is not used to instantiate objects, but rather to bundle up methods that would be called functions in most programming languages. We don’t create app objects. The methods of this class are not invoked on any particular object: they are just functions. Java uses the word static
for this.
The class concept is overloaded in JavaJava forces the use of the class construct for both object factories and bundles of related functions.
Oh well.
If you want to play around, Java has a REPL, called JShell. Let’s play around:
$ jshell | Welcome to JShell -- Version 17 | For an introduction type: /help intro jshell> 2 + 2 ARITHMETIC $1 ==> 4 jshell> 2 > 2 BOOLEANS $2 ==> false jshell> 3 + 100 * Math.hypot(-4, 3) OPERATOR PRECEDENCE $3 ==> 503.0 jshell> "Hello".replace('e', 'u') STRINGS AND CHARACTERS $4 ==> "Hullo" jshell> var greeting = "Good morning" CREATING VARIABLES greeting ==> "Good morning" jshell> greeting.substring(5) USING VARIABLES $6 ==> "morning" jshell> List.of(3, 5, 3, 2, 2, 1, 8, 1, 1) LISTS $7 ==> [3, 5, 3, 2, 2, 1, 8, 1, 1] jshell> Map.of("dog", 3, "rat", 5, "pig", 99) DICTIONARIES $8 ==> {rat=5, pig=99, dog=3} jshell> var poem = """ MULTI-LINE STRINGS ...> I wonder ...> about thunder ...> And Java.""" poem ==> "I wonder\nabout thunder\nAnd Java." jshell> int triple(int x) { TOP-LEVEL SHELL METHODS ...> return x * 3; ...> } | created method triple(int) jshell> triple(30) METHOD INVOCATION $11 ==> 90 jshell> $11 * 2 USING SHELL VARIABLES (COOL) $12 ==> 180 jshell> IntStream.range(1, 5).forEach(System.out::println) STREAMS 1 2 3 4 jshell> /exit | Goodbye
JShell actually is manufacturing some classes behind the scenes to hold all your stuff; it just mercifully hides all that complexity from you so you can experiment and learn stuff. When it comes time for you to write a real-life app, it’s back to explicit classes!
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 | false |
char | Code units in the range 0..65535 that are used to encode text | 'π' |
byte | Integers in the range -128 ... 127 | (byte)89 |
short | Integers in the range -32768 ... 32767 | (short)89 |
int | Integers in the range -2147483648 ... 2147483647 | 89 |
long | Integers in the range -9223372036854775808 ... 9223372036854775807 | 89L |
float | IEEE binary-32 floating point numbers | 3.95f |
double | IEEE binary-64 floating point numbers | 3.95 |
Type Former | Description | Example Type |
class | Classes |
|
[] | Arrays (a special kind of class) | boolean[] |
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 the values and the types. But first, a surprise....
We all know that values have types. That is an axiom. That goes without saying. That is a fact of the universe. The value false
has type boolean
. All values have types. By definition! Types are how we group values.
But in Java, variables are constrained to hold values of a certain type:
jshell> var friends = 39
39
jshell> friends = 40
40
jshell> friends = true
| Error: incompatible types: boolean cannot be converted to int
BUT WHY DO THIS?Constraining variables to a certain type facilitates static typing, that is, determining the type of expressions just by looking at the source code! In other words we don’t have to run the program to know an expression’s type: type checking is done at compile time!
This is generally thought to have two advantages: (1) it removes bugs due to type mismatches at run-time, and (2) it allows for more efficient code to be generated as there are no expensive run-time checks and in many cases allows machine code to be generated with powerful indexing instructions rather than string lookups.
Java compilers can do a ton of static type checking, but there are things that do require dynamic typing (type checking at run time), including arrays, nulls, and certain casts (all which we will see later).
Time for technical vocabularyJava’s type system can be described as static (the type of nearly every expression can be deduced at compile-time), strong (type mismatches generate errors rather than implicit conversions), and in the middle of the manifest (types must appear on every identifier) to inferential (identifier types can be inferred) spectrum.
Since variables have to have a type constraint, you can’t use var
unless you know the type:
jshell> var payment
| Error: cannot use 'var' on variable without initializer
You can, however, declare a variable without an initializer provided you specify the type:
jshell> int books 0 jshell> double price 0.0 jshell> boolean found false jshell> String message null
Wait why wasn’t the default value of a string the empty string? What is that null
thing? Hang on, things are about to get weird. But you knew Java was weird, right?
Can I be redundant?If you want to write
int limit = 10;
orString warning = "Stay behind the yellow line";
you are free to do so. One person’s silly redundancy is another person’s explicitness.
All types that are not primitive types are called reference types. What’s the difference? Instances of primitive types are stored directly in variables. But instances of reference types live outside of variables; variables hold references (a.k.a. “pointers” or “links”) to the objects. In a sense, the “value” of a reference type is a reference to the object, not the object itself. And (cringe sound here) there is a special null
reference. Now this which means null
does not have its own type: it is a member of all reference types. So null
is...a string...and also a Cylinder...and also a Date...and...🤦♀️. Sigh. 😞
This example shows how variables of primitive types differ from variables of reference types:
float x; // declare a variable, it value is arbitrary x = 3; // value 3 stored in x float y = x; // value 3 stored in y y = 5; // after this x is still 3 Cylinder c1; // just declares a variable c1 = new Cylinder(1, 1); // now we have a cylinder created Cylinder c2 = c1; // does NOT create a new cylinder c2.widen(2); // exactly the same effect as c1.widen() c1 = null; // the cylinder still exists double d = c1.getRadius(); // OOPS!! Throws a NullPointerException double r = c2.getRadius(); // This is perfectly fine: c2 isn’t null
A picture is crucial here, as it shows how primitive type values appear directly in the boxes, and object values are always references (unless, ugh, they are that darn null
):
This null
this is just so wrong. Like, null
is an acceptable value of type String
? Or arrays can be null
in addition to being empty? Why? What even is going on here? I thought this “static typing” thing was supposed to help me!!! But I have a variable s
of type String
and I invoke s.toUpperCase()
Java says it’s possible that s
could be null
? Well I thought Java was like this awesome static and strongly typed language and prevent me from saying stuff like this! Oh it doesn’t? We get a run time type error? Yep, a NullPointerException
is thrown. AT RUN TIME. 🤢
What a disappointment! Who. Is. Responsible. For. This?
Answer: Sir Charles Anthony Richard (Tony) Hoare, FRS FREng is. But he said he is sorry. He calls this idea that null
could belong to a reference types the Billion Dollar Mistake as it has caused about a billion dollars worth of damage due to lost productivity and who knows what else.
Don’t be too hard on Sir 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.
For primitives, assignment (=
) and equality testing (==
) works as expected: assignment is a copy and equality compares values. But with references, remember that it is the reference, not the object contents, that are being copied and compared. Consider:
jshell> /open Cylinder.java jshell> var c1 = new Cylinder(1, 4) c1 ==> Cylinder@37f8bb67 jshell> var c2 = new Cylinder(1, 4) c2 ==> Cylinder@20ad9418 jshell> var c3 = c2 c3 ==> Cylinder@20ad9418 jshell> c1 == c2 $5 ==> false jshell> c2 == c3 $6 ==> true
Here we made two objects of the class Cylinder
. Objects are created with the new
operator. The first object is referenced by the variable c1
while the second is referenced by the variables c2
and c3
. Never confuse the term variable with the term object. They are completely different things! Behold three variables but two objects:
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.
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:
double x = 3; Object a = "abc"; Object a = new int[]{3, 5, 8, 13}; interface Reversable {} class Sequence implements Reversable {} Reversable r = new Sequence(); class Animal {} class Dog extends Animal {} Animal a = new Dog();
The Liskov Substitution 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.
Classes can extend other classes. A class can extend at most one other class, called its superclass. A superclass can have zero or more subclasses. There are lots of lots of rules here, and lots of interesting stuff on access modifiers that are covered elsewhere. But here’s the basic idea.
Lots of types can be compatible with the type of a variable, so if you see an expression like a.m()
where a
is a variable, and m
is a method, the actual method that is called depends on the value bound to a
at run time. This is called dynamic polymorphism. The right method is dispatched by examining the class of the object at run time.
Here’s a superclass called Animal
with three subclasses. All animals speak the same way, but their sounds are different. So sound
is made an abstract method.
abstract class Animal { private String name; protected Animal(String name) { this.name = name; } public String speak() { return name + " says " + sound(); } public abstract String sound(); } class Cow extends Animal { public Cow(String name) { super(name); } @Override public String sound() { return "moooooo"; } } class Horse extends Animal { public Horse(String name) { super(name); } @Override public String sound() { return "neigh"; } } class Sheep extends Animal { public Sheep(String name) { super(name); } @Override public String sound() { return "baaaa"; } } class AnimalSoundsApp { public static void main(String[] args) { Animal h = new Horse("CJ"); System.out.println(h.speak()); Animal c = new Cow("Bessie"); System.out.println(c.speak()); System.out.println(new Sheep("Little Lamb").speak()); } }
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 implemeted 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
. Example:
class Cylinder { private double radius; private double height; // ... @Override public boolean equals(Object o) { return (o instanceof Cylinder that) && this.radius == that.radius && this.height == that.height; } }
But be aware, be very aware, of this following guideline:
This is because if you place objects in a set or map, their presence in the set or map is detected by hashCode
, not equals
. You must ensure that whenever x.equals(y)
that x
and y
have the same hash code. Fortunately, you can override with:
class Cylinder { // ... @Override public int hashCode() { return Objects.hash(radius, height); } }
Remember that overriding these methods is only needed when you want “value semantics.” Often you will never need value-based comparison and you won’t be storing items in sets, or even printing their contents. But when you do, you will need these methods overridden. Generally you have to write these methods on your own but not always! You can often use records.
Defining immutable objects with value-base equality is so common that there’s a nice way to define them. Here’s an example:
record Point(double x, double y) {}
This gives you a fully immutable class called Point
with a two-argument constructor, accessor methods x()
and y()
and properly overridden equals
, hashCode
, and toString()
.
You can add validation to the constructor and even add more methods if you desire:
public record Location(double latitude, double longitude) { public Location { if (Math.abs(latitude) > 90 || Math.abs(longitude) > 180) { throw new IllegalArgumentException("Value out of range"); } } public boolean inNorthernHemisphere() { return latitude > 0; } }
Writing this class without the record syntax requires a ridiculous amount of code. There’s a bit more on records in these notes (including a look at how records are implemented behind the scenes).
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 statically typed language, Java needs to guarantee that the types of all expressions are known by the compiler. For numbers, booleans, and strings, and simple classes like our Cylinder
above, this is easy. But how about the following?
jshell> class Pair {
...> Object first;
...> Object second;
...> Pair(Object first, Object second) { this.first = first; this.second = second; }
...> }
| created class Pair
jshell> var p = new Pair("hello", 5)
p ==> Pair@37f8bb67
jshell> Object message = p.first
message ==> "hello"
jshell> String greeting = p.first
| Error:
| incompatible types: java.lang.Object cannot be converted to java.lang.String
| String greeting = p.first;
| ^-----^
Using Object
for the type seems to work but IT IS SO UNSATISFYING!!! But while annoying, it makes sense, right? Static typing wants to check all the types at compile time so we don’t have to carry around expensive type information for expensive run-time type checks. So what right do we have to expect anything other than Object
?
Well, if we could just get the compiler to generate, on the fly, all the pair classes we need? We can!
jshell> class Pair<T, U> { ...> T first; ...> U second; ...> Pair(T first, U second) { this.first = first; this.second = second; } ...> } | modified class Pair jshell> var p = new Pair<String, Integer>("hello", 55) p ==> Pair@439f5b3d jshell> String s = p.first s ==> "hello" jshell> int x = p.second x ==> 55
Here the class Pair
is generic: it is not really a class, but more like a template. The real, concrete, classes would be things like Pair<String, Integer>
, Pair<Boolean, Boolean>
, and so on. This is different from a dynamically typed language like Python, where there is a single, real, type called list
.
More Java WeirdnessCan you believe this? Generic type parameters MAY NOT be primitives! Ugh! That’s right: you can NOT make a generic list of
int
values. 🤦♀️ So the Java Standard Library has a bunch of classes, namelyBoolean
,Character
,Byte
,Short
,Integer
,Long
,Float
, andDouble
which “wrap”, or “box” a primitive object. So you’ll need aList<Integer>
.Generally the compiler lets you move between, say,
int
values andInteger
ones, 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 checkout the official tutorial and the docs if you have time; it’s interesting how the library is structured, and how they implement such things as unmodifiable collections.
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); } public class LambdaDemo { private static int twice(IntToIntFunction f, int x) { return f.apply(f.apply(x)); } public static void main(String[] args) { System.out.println(twice(x -> x * 3, 2)); } }
That example was totally contrived (SORRY), since we pretty much reimplemented the built-in IntUnaryOperator
. Which is a nice segue into the next observation: the standard library has several dozen functional interfaces ready for you to use. Here are just a few:
Functional 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 in Java 17.
So we can write the famous twice function generically as:
public static <T> T twice(Function<T, T> f, T x) { return f.apply(f.apply(x)); }
Here’s an example with a consumer:
public static void powers(int base, int limit, Consumerconsumer) { for (var power = 1; power <= limit; power *= base) { consumer.accept(power); } }
You don’t have to always use lambda expressions for functions. You can also use method references, which are: pretty cool:
Instead of... | You can write |
---|---|
x -> System.out.println(x) | System.out::println |
p -> p.getName() | Person::getName |
x -> new Widget(x) | Widget::new |
Damn that billion dollar mistake.
Let’s make a Person
class, where a person has a required name, but an optional boss. For a given person p, writing p.boss.name
could throw a NullPointerException
, so we probably have to write:
if (p.boss != null) { // Do something with p.boss.name }
The null checks have to go all over the place, and they get really annoying when optional data is chained, as in person.boss.address.city
. What we WANT, though, is:
Optional<
T>
.Here’s how we could define the person class:
import java.util.Optional; import java.util.Objects; class Person { private String name; private Optional<Person> boss; private Person(String name, Optional<Person> boss) { this.name = Objects.requireNonNull(name); this.boss = boss; } public Person(String name) { this(name, Optional.empty()); } public Person(String name, Person boss) { this(name, Optional.of(boss)); } public String name() { return name; } public Optional<Person> boss() { return boss; } } public class OptionalDemo { public static void main(String[] args) { var alice = new Person("Alice"); var bob = new Person("Bob", alice); bob.boss().ifPresent(p -> { // Here you would do something with the real person p assert p == alice; }); alice.boss().ifPresent(p -> { // This code will never be executed assert false; }); assert alice.boss().orElse(bob) == bob; assert bob.boss().orElse(bob) == alice; assert bob.boss().filter(p -> p.name().startsWith("A")).isPresent(); assert bob.boss().filter(p -> p.name().startsWith("B")).isEmpty(); assert bob.boss().map(Person::name).orElse("").equals("Alice"); assert alice.boss().map(Person::name).orElse("").equals(""); } }
Optionals are like wrappers; they either wrap something or they don’t. A little reference:
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 |
You should go through this great tutorial.
Done with the tutorial? Remember what streams are for?
Streams are intended for querying and transforming data.
Remember why they are cool? They provide an abstraction fo processing sequential data, regardless of how that sequence is implemented. Also, we can set up streams to be processed either sequentially or in parallel.
You start with a source, then apply zero or more intermediate operations, then one terminal operation. The intermediate operations don’t actually do anything right away (they’re “lazy”), they just set things to happen when the terminal operation is invoked. Once the terminal operation is done, the stream is consumed — “all used up.”
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 dogsvoid 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). But 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 |
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()); }
The official reference for the language can be found on the Oracle Java Specifications Page. Check out the nice one-page syntax of Java.
Interesting in a few technical items before you dive into the official spec? Here are a few:
Keywords are words that cannot be used as identifiers.
abstract continue for new switch assert default if package synchronized boolean do goto private this break double implements protected throw byte else import public throws case enum instanceof return transient catch extends int short try char final interface static void class finally long strictfp volatile const float native super while
Note that false
, true
, and null
, var
, and yield
are not keywords, but rather are literals or identifiers with special meanings.
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 |
Java is commonly used to build large, enterprise applications. In general, you will use a build system based on tools like Maven or Gradle.
However you build you program, you will get a big set of (compiled) class files (which can themselves be bundled into a big jar file, that is, a Java Archive), which are run on any device or operating system to which the Java interpreter, or more precisely, the Java Virtual Machine (JVM) has been implemented. By the way, hundreds of other languages can be compiled into class files, too, allowing you to write parts of your application in Java and parts in other languages.
Perhaps the most famous languages that have been ported to the JVM, and thus interoperate nicely with Java (and in most cases use the same standard library files as Java) are:
Kotlin has been chosen by Google to be the preferred language for developing Android applications, with Java being the alternative.
The JVM idea not only allows language interoperability, but allows Java to be implemented almost everywhere. A JVM can be:
java
or java.exe
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:
embedded systems, with our without displays
typical stuff
the big stuff like databases, networking, mipleware, scaling, messaging, mail, authentication
Just for fun, and for some historical perspective, here is how the Java SE platform has evolved over time:
Version | Released | Classes in Core API |
Notable New Stuff (Not complete) |
---|---|---|---|
JDK 1.0 | 1996‑Jan | 210 | |
JDK 1.1 | 1997‑Feb | 477 | I/O design flaws fixed; inner classes; internationalization; AWT enhancements; JavaBeans API; JARs; RMI; Reflection; JDBC; JNI |
J2SE 1.2 | 1998‑Dec | 1524 | Swing; Collections; Permissions, policies, certificates, signers; Java2D; Accessibility API; Extensions framework; RMI enhancements; Sound |
J2SE 1.3 | 2000‑May | 1840 | JNDI; Java Sound; Swing and Java2D enhancements; Security enhancements; Networking enhancements; Improved serialization support; Input method framework; Timers |
J2SE 1.4 | 2002‑Feb | 2723 | New IO library (NIO); Assertions; XML, crypto, SSE, authentication added to core API; Image IO; Java Print Service; Many Swing enhancements; Logging API; WebStart; Chained Exceptions; IPv6; Regexes |
J2SE 5.0 | 2004‑Sep | 3279 | Generics; Better for statement; Annotations; Enumerations; Sophisticated concurrency packages; Autoboxing; Varargs; Static import; Unicode 4.0 (Wow!); Tons of library enhancements |
Java SE 6 | 2006‑Dec | 3777 | Scripting languages can be embedded; XML and web services; JDBC 4; Many GUI enhancements (e.g. for tables and trees); monitoring and management, Programmatic access to compiler; Pluggable annotations; A few more security apis |
Java SE 7 | 2011‑Jul | 4024 | Support for dynamically typed languages; Strings in switch; Better type inference (diamond); Simplified varargs; Binary integer literals; Multi-catch; Concurrency and collections updates; NIO.2; Elliptic-Curve Cryptography |
Java SE 8 | 2014‑Mar | 4240 | Streams; Lambda expressions (closures); Interface default methods; Unsigned integer arithmetic; Repeating annotations; Date and Time API |
Java SE 9 | 2017‑Sep | 6005 | Interface private methods; ImmutableSet; Optional.stream(); JShell |
Java SE 10 | 2018‑Mar | 6002 | Optional.orElseThrow; Nicer ways to create unmodifiable collecitons |
Java SE 11 | 2018‑Sep | 4410 | Local variable type inference; HTTP Client; New collection methods; New stream methods |
Java SE 12 | 2019‑Mar | 4432 | JVM Constants API |
Java SE 13 | 2019‑Sep | 4403 | (No language changes) |
Java SE 14 | 2020‑Mar | 4420 | JFR event streaming; Non-volatile mapped byte buffers; Switch expressions |
Java SE 15 | 2020‑Sep | 4352 | Hidden classes; Text blocks |
Java SE 16 | 2021‑Mar | 4389 | Records; Pattern Matching for instanceof |
Java SE 17 | 2021‑Sep | 4388 | Sealed classes |
Java SE 18 | 2022‑Mar | 4404 | (No language changes); Default charset is now UTF-8 everywhere |
Java SE 19 | 2022‑Sep | ? | Pattern matching in switches |
Check out the full list of classes from Java 17.
Also of interest is Oracle’s writeup of many of the newer language features.
Long-Term Support ReleasesJava 8, 11, and 17 are called LTS (long-term support) versions, meaning they will be maintained and supported for a long time.
That was a trivial introduction without much depth. There is so much more to learn. We completely skipped reflection and concurrency, barely hinted at type erasure, and hardly gave any examples. Go deeper with these, perhaps:
Then continue with whatever other good stuff you find out there.
We’ve covered: