Jim Cheung

Learning Java

(this note is based on Learning Java 3rd ed, java 5/7 was not out yet at the time. 4ed was released on july 2013)

Chapter 1, A Modern Language

Chapter 2, A First Application

constructor doesn't have return type

public Hello() {


import java.awt.event; // import java.awt.* will not import event		

static member can be accessed even if no instances of the class exist.


call Thread's start() to begin execution. once Thread starts, it continues to run until it completes its work, we interrupt it, or we stop the application.

Thread always expects to execute a method called run() to perform the action of the thread.

Runnable Inerface force you to implement the run() method.


the synchronized modifier tells Java to acquire a lock for the object that contains the method before executing that method. Only one method in the object can have the lock at any given time, which means that only one synchronized method in that object can be running at a time. When the method is done, it releases the lock on the class.

Chapter 3, Tools of the Trade

The main() method must have the right method signature. it must be public, static method that takes an array of String objects are argument and does not return any value (void)

System Properties

system property is passed to the inerpreter on the command line using -D option followed by name=value.

static System.getProperty() method to access.


the CLASSPATH environment variable.

or pass -classpath /path/ to javac


useful tool to print description of a compiled class.

you don't even need to know where the class is if it's in your classpath.

% javap java.util.stack				

The Java Compiler

javac allows one public class per file and insists the file must have the same name as the class. A single file can contain multiple classes, as long as only one of the classes is public.

use -d to specify where class files to store.


% jar -cvf jarFile path // create
% jar -tvf jarFile path // list
% jar -xvf jarFile path // extract 		

The Default Security Manager

% java -Djava.security.manager EvilClass


% policytool		

run with policy file

% java -Djava.security.manager -Djava.security.policy=EvilClass.policy EvilClass		

Chapter 4, The Java Language

Javadoc Comments

block comment beginning with /** indicates a special doc comment.


Primitive Types

Reference Types

Primitive types are passed by value Reference types are always passed by reference

except arrays and interfaces.

from Java 5.0, Generic types and parameterized types are added.

Statements and Expressions


Variable declarations are limited in scope to their enclosing code block

    int i = 5;
i = 6; // compile-time error, no such variable i		

The null value

The expression null can be assigned to any reference type. A null reference can't be used to reference anything and attempting to do so generates a NullPointerException at runtime.

value null is not considered an instance of any object.

String s = null;
if (s instanceof String)
    // false				

Stack Traces

try {
    // complext, deeply nested task
} catch (Exception e) {

Checked and Unchecked Exceptions

void readFile(String s) throws IOException, InterruptedException {

the throws clause tells the compiler that a method is a possible source of that type of checked exception and that anyone calling that method must be prepared to deal with it.

In contrast, exceptions that are subclasses of either java.lang.RuntimeException or java.lang.Error are unchecked. unchecked exceptions behave the same as other exceptions. We are free to catch them if we wish.

checked exceptions are intended to cover application-level problems, unchecked exceptions are intended for system-level problems.

Performance Issues of Exceptions

guarding against an exception being thrown (using a try) is free. It doesn't add any overhead to the execution of your code. However, throwing an exception is not free. When an exception is thrown, Java has to locate the appropriate try/catch block and perform other time-consuming activities at runtime.

do some test code instead of throwing exceptions frequently.


assert false;
assert (array.length > min);
assert a > 0 : a
assert foo != null : "foo is null!"		

Enable and disable assertions

% java -ea MyApplication
% java -ea:com.example.MyClass ... MyApplication
% java -ea:com.exmaple.MyClass -da:com.example.MyClass2 MyApplication		


int [] arrayOfInts; // preferred
int arrayOfInts []; // C-style		

array creation

double [] someNumbers = new double [20];
Component [] widgets = new Component [12];

int [] primes = { 1, 2, 3, 4 };
String [] verbs = { "run", "jump", someWord.toString() };

the following is equivalent:

Button [] threeButtons = new Button [3];
Button [] threeButtons = { null, null, null };

Using Arrays

int num = someArray.length
for (int i : keyPad)

array throws ArrayIndexOutOfBoundsException

Anonymous Arrays

setPets ( new Animal [] { pokey, boojum, simon } );

Multidimensional Arrays

ChessPiece [][] chessBoard;
chessBoard = new CheckPiece [8][8];
chessBoard[0][0] = new ChessPiece.Rook;
chessBoard[1][0] = new ChessPiece.Pawn;

Chapter 5, Objects in Java


Variables and method declarations can appear in any order, but variable initializers can't make forward references to other variables that appear later.

instance variables and static variables, every object instance has its own instance variables; static variables (class variables) are shared among all instance of an object.

static float gravAccel = 9.80;

MyClass.gravAccel = 8.76; // change for any current or future instances

static final float EARTH_G = 9.80;

constants versus enumerations

static int SIMPLE = 0, ONE_SPRING = 1, TWO_SPRING = 2;

public enum PredulumTypes { Simple, OneSpring, TwoSpring };


Now Java has variable-length argument lists for methods. (prior to Java 5.0, number and type of method arguments are fixed).

use this to access instance variables.

this.xPos = xPos; // instance var = local var

local variables must be initialized before they can be used.

Object Creation

A constructor is a special method with the same name as its class and no return type. Constructors can be overloaded but are not inherited like other methods.

constructor overloading

A consructor can refer to another constructor in the same class or the immediate superclass using special forms of the this and super references.

If a constructor calls another constructor, it must do so as its first statement.

class Car {
    String model;
    int doors;
    Car (String model, int doors) {
        this.model = model;
        this.doors = doors;
    Car (String model) {
        this(model, 4); // must be the first statement

initializer blocks

A block of code directly within the scope of a class, it's executed once at the time of the object is constructed.

Use static initializer block to set up static class members.

class ColorWheel {
    static Hashtable colors = new Hashtable();
    static {
        colors.put("Red", Color.red);
        colors.put("Green", Color.green);

Object Destruction

Java uses garbage collection to remove objects that are no longer needed. You can prompt the garbage collector to make a clean sweep explicitly by invoking the System.gc() method.


Before an object is removed by garbage collection, it's finalize() method is invoked to give it a last opportunity to clean up its act and free other kinds of resources it may be holding.

The finalize() methods of superclasses are not invoked automatically. use super.finalize() to invoke it.

Weak and Soft References

Weak References are eligible for garbage collection immediately.

A Soft Reference is similar to a weak reference, but it tells the garbage collector to be less aggressive about reclaiming its contents.

java.lang.ref package contains the WeakReference and SoftReference wrappers, as well as a facility called ReferenceQueue that allows your application to receive a list of references that have been collected.


An enumeration is an object type in the Java language that is limited to an explicit set of values.

enum Weekday { Sunday, Monday, ... }

setDay( Weekday.Sunday );

Customizing Enumerations

public enum Weekday
    Sunday(8), Monday(0), Tuesday(1) ... ;
    int fun;
    Weekday (int fun) { this.fun = fun; }
    public int getFun() { return fun; }

Chapter 6, Relationships Among Classes

Overriding Methods

Any difference in the number or type or arguments produces two overloaded methods instead of a single, overridden method.

@Override annotation tells the compiler that the method it marks is intended to override a method in the superclass.

overridden methods and dynamic binding

Overloaded methods are selected by the compiler at compile time. Overridden methods, on the other hand, are selected dynamically at runtime.

Static method binding

Static method can be shadowed by another static method in sub-class, as long as the original method was not declared final. But you can't have a static method and instance method with the same signature in the same class hierarchy.

final methods and performance

A profiling runtime can determine which methods are not being overridden, treating them as if they were final until it becomes necessary to do otherwise. The final keyword should not for performance considerations.

compiler optimizations

The Java compiler is smart enough to remove codes that won't be called.

static final boolean DEBUG = false;
final void debug(String message) {
    if (DEBUG) {

In this case, body of debug() method will be optimized away, the method call would be removed entirely.

Exceptions and overridden methods

New method can redefine the throws clause, this technique is called covariant typing of the throws clause.

Return types and overridden methods

As of Java 5.0, when you override a method, you may change the return type.


You never change the object pointed to by a reference by casting it; you change only the compiler's (or runtime system's) notion of it.

use Generics to type objects.

Abstract Methods and Classes

you can't directly use a class that contains an abstract method; you must instead create a subclass that implements the abstract method's body.

abstract void vaporMethod(String name);

In Java, a class that contains one or more abstract methods must be explicitly declared as an abstract class

abstract class vaporClass {
    abstract void vaporMethod(String name);


interface Driveable {
    boolean startEngine();
    void stopEngine();

Interface as Callbacks

The callback occurs when the called object subsequently invokes one of the methods. In C or C++, this is prime territory for function pointers; Java uses interfaces instead.

Interface Variables

An interface can contain constants (static final variables).

interface Scaleable {
    static final int BIG = 0, MEDIUM = 1, SMALL = 2;
    void setScale(int size);

In Java 5.0, it is better to use an enumeration or to put your constants in their own class and then use the new static import syntax to remove the hassle of referring to them.

enum SizeConstants { BIG, MEDIUM, SMALL }

// usage
static import mypackage.SizeConstants;

Flag interfaces

Sometimes completely empty interfaces serve as a marker that a class has a special property. The java.io.Serializeable interface is a good example.


An interface can extend another interface.

interface DynamicallyScaleable extends Scaleable {
    void changeScale(int size);

extends more

interface DynamicallyScaleable extends Scaleable, SomethingElseable {

Packages and Compilation Units

For most of us, a compilation unit is just a file with a .java extension, but theoretically in an integrated development environment, it could be an arbitrary entry.

The package statement must appear as the first statement in a compilation unit. There can be only one package statement, and it applies to the entire file.

Package Names

Package namespace is flat, not hierarchical.

Importing Classes

Other than the potential for naming conflicts, there's no penalty for importing many classes. Java doesn't carry extra baggage into the compiled class files.

Static Imports

import static java.lang.Math.*;

// usage
double circumference = 2 * PI * radius;

Visibility of Variables and Methods

Interfaces and Visibility

Interfaces behave like classes within packages. And interface can be declared public to make it visible outside its package. Under the default visibility, an interface is visibile only inside its package.

Like classes, only one public interface can be declared in a compilation unit.

Inner Classes

In Java, anonymous inner classes take the place of closures in other language, giving the effect of handling state and behavior independently classes.

Inner classes are pure syntactic sugar; they are not supported by the VM but are instead mapped to regular Java classes by the compiler.

Inner classes are essentially nested classes.

Class Animal {
    Class Brain {
    void performBehavior() { ... }

The Brain class "sees" all of the methods and variables of the Animal class directly in its scope.

From within Brain we can invoke the method performBehavior().

An Brain object always lives within a single instance of Animal. We'll call the object that contains any instance of Brain its enclosing instance

A Brain object cannot live outside of an enclosing instance of an Animal object. Brain always requires an enclosing instance of Animal to "hold" it.

Animal monkey = new Animal();
Animal.Brain monkeyBrain = monkey.new Brain();

Inner Classes as Adapters

A particularly important use of inner classes is to make adaptor classes. An adaptor class is a "helper" class that ties one class to another in a very specific way.

For example we want a iterator to loop employees, instead of extends and implements a new class for iterator, use inner class and an access method.

public class EmployeeList {
    private Employee [] employees = ... ;
    class Iterator implements java.util.Iterator {
    Iterator getIterator() {
        return new Iterator();

Inner classes can have constructors, variables and initializers. They are real classes.

Inner Classes within Methods

class Animal {
    void performBehavior() {
        class Brain {

The body of Brain can see anything in the scope of performBehavior()

limitation on inner classes in methods

Method invocations have limited lifetimes, but instance of inner class lives on as long as it is referenced. This means that any of the method's local variables or arguments that are referenced by the inner class must be declared final.

void performBehavior ( final boolean nocturnal ) {
    class Brain() {
        void sleep() {
            if ( nocturnal ) { ... }

Static inner classes

class Animal {
    static class MigrationPattern {

// usage
Animal.MigrationPattern stl = new Animal.MigrationPattern();

Animal acts like a mini package, use import to use void full qualified name

import Animal.MigrationPattern;

Anonymous inner classes

Anonymous inner classes are an extension of the syntax of the new operation.

Iterator getIterator() {
    return new Iterator() {
        // body of anonymous inner class
    }; // <-- it's a statement

Inner classes are best used when you want to implement a few lines of code, but the verbiage and conspicuousness of declaring a separate class detracts from the task at hand.

new Thread() {
    public void run() { performBehavior(); }

Anonymous adapter classes are a perfect fit for event handling.

addMouseListener ( new MouseInputAdapter() {
    public void mouseClicked(MouseEvent e) { handleClicks(e); }
} );

scope of "this" reference

Inner class has multiple this reference, you can specify which this you want by prefixing it with the name of the class.

class Brain {
    int animalSize = Animal.this.size;
    Animal ourAnimal = Animal.this;

How do inner classes really work?

class Animal {
    class Brain {

comipler generates Animal.class and Animal$Brain.class

The dollar sign is a valid character in class names but is intended for use only by automated tools.

to check

% javap 'Animal$Brain' # or Animal.Brain with Java 5.0

to pack

% jar cvf animal.jar Animal*class

Chapter 7, Working with Objects and Classes

The Object Class

Every object has a toString() method. PrintStream objects use toString() to print data.

MyObj myObject = new MyObj();
System.out.println( myObject );

Equality and Equivalence

equals() determines whether two objects are equivalent.

String userName = "Joe";
if (userName.equals( suspectName ))
    arrest ( userName );

Using equals() is not the same as:

if (userName == suspectName) // wrong

This statement tests whether the two reference variables, userName and suspectName, refer to the same object. It is a test for identity, not equality.

In order to override (not overload) equals(), the method must specify its arguments to be an Object

class Sneakers extends Shoes {
    public boolean equals ( Object arg ) {


The hashCode() method returns an integer that is a hashcode for the object. A hashcode is like an signature or checksum for an object. The hashcode should always be different for instances of the class that contain different data, but should be the same for instances that compares "equal" with the equals() method.

The default implementation of hashCode() assigns each object instance a unique number.

Cloning Objects

To make itself cloneable, an object must implement the java.lang.Cloneable interface.

clone() is a protected method. If we want to an object cloneable by everyone, override its clone() method and make it public.

import java.util.HashMap;

public class Sheep implements Cloneable {
    HashMap flock = new HashMap();
    public Object clone() {
        try {
            return super.clone();
        } catch ( CloneNotSupportedException e ) {
            throw new Error("This should never happen!");

// usage
Sheep one = new Sheep();
Sheep anotherOne = (Sheep)one.clone();

The cast is neccessary here because the return type of clone() is Object. In Java 5.0, we could do

public Sheep clone() {
    return (Sheep)super.clone();

Sheep anotherOne = one.clone();

The Class Class

Classes in Java source code are represented at runtime by instances of the java.lang.Class class. There's a Class object for every object type you use; this Class object is responsible for producing instances of that type.

We get the Class associated with a particular object with the getClass() method

String myString = "foo";
Class StringClass = myString.getClass();

We can also get the class reference using the .class notation

Class StringClass = String.class;

The .class reference looks like a static field that exists in every class. However, it is really resolved by the compiler;

String s = "Boofa!";
Class stringClass = s.getClass();
system.out.println( stringClass.getName() ); // get name

try {
    String s2 = (String)stringClass.newInstance();
catch (InstantiationException e) { .. }
catch (IllegalAccessException e) { .. }

newInstance() return an Object, so we have to cast it. But the Class class is a generic class, we can parameterize it to be more specific Java type.

Class<String> stringClass = String.class;
try {
    String s2 = stringClass.newInstance(); // no cast necessary
catch ...

forName() is a static method of Class that returns a Class object given its name as a String

try {
    Class sneakersClass = Class.forName("Sneakers");
} catch ( ClassNotFoundException e ) { .. } 


Java Reflection API is supported by the classes in the java.lang.reflect package.

The three primary features of a class are its fields (variables), methods and constructors. They are represented by java.lang.reflect.Field, java.lang.reflect.Method and java.lang.reflect.Constructor API.

(a lots of details, read the book again)


Annotations allow you to add metadata to Java classes, methods, and fields.

The apt Tool

The Java 5.0 SDK ships a command-line Annotation Processing Tool, apt, that is a sort of frontend to the javac compiler. apt uses pluggable annotation proccessors to process the annotations in source files before the code is compiled by javac.

Chapter 8, Generics

Generics are also referred to as parameterized types. In other languages, generics are sometimes referred to as templates.

A generic class requires one or more type parameters wherever we refer to the class type and uses them to customize itself.

List<String> listOfStrings;
List<Date> dates;
list<java.math.BigDecimal> decimals;

Completing the type by supplying its type parameter is called instanitiating the type. It is also sometimes called invoking the type.'


The compiler has erased all of the angle bracket syntax and replaced the type variable in our List class with a type that can work at runtime with any allowed type: (in book's case), Object.

We can't hava a class that implements two different generic List instantiations because they are really the same type at runtime and there is no way to tell them apart.

Raw Types

Every generic has a raw type. The class of List<Date> and List<String> share the plain old Java class List. List is called the raw type of the generic class.

Java 5.0 compiler will warn you if raw type is used

// non-generic code using the raw type
List list = new ArrayList();
list.add("foo"); // warning 

use -Xlint:unchecked to get more details

% javac -Xlint:unchecked MyClass.java

We can place limitations or bounds on the parameter types.

class Bounded< E extends Date > {
    public void addElement( E element ) { ... }

Date is called the upper bound of the type.

Why isn't a List<Date> a List<Object>?

If we could assign DateList to an ObjectList variable, we would have to be able to use Object methods to insert elements of types other than Date into it. And Java generics have no runtime representation.

Writing Generic Classes

One or more type variables are declared in the angle bracket (<>) type declaration and used throughout the body and instance methods of the class.

class Trap<T> {
    T trapped;
    public void snare(T trapped) { this.trapped = trapped; }
    public T release() { return trapped; }

Subclassing Generic

A nongeneric subclass must extend a particular instantiation of the parent type, filling in the required parameters to make it concrete:

class DateList extends ArrayList<Date> { ... }

A generic subtype of a generic class may extend either a concrete instanitation of the class or it may share a type variable that is "passes up" to the parent upon instaniation.

class AdjustableTrap<T> extends Trap<T> { ... }

Exceptions and Generics

Type variables may be used to define the type of exceptions thrown by methods, but to do so we need to introduce the concept of bounds.

<T extends Throwable>

No generic Throwables

A type variable can be used to specify the type of Throwable in the throws clause of a method. However, we can not use generics to create new types of exceptions. No Generic subtypes of Throwable are allowed.

Generic Throwable would require try/catch blocks that can differentiate instantiations of Throwable. Since there is no runtime representation of generics, this isn't possible with erasure.


Bounds use the extends keyword and some new syntax to limit the parameter types that may be applied to a generic type.

class EmployeeList<T extends Employee> { ... }

We could further require that the Employee type implement one or more interfaces using the special & syntax

class EmployeeList<T extends Employee & Ranked & Printable> { ... }

The order of the & interface bounds is not significant, but only one class type can be specified and if there is one, it must come first.

Ensure and Bounds

The type after erasure used for the parameter type of a generic class is the leftmost bound.

Insert a somewhat gratuitous additional type Object as the leftmost bound in order to get back our old API.

class List<E extends Object & Listable> { ... }

Inserting Object doesn't change the actual bounds of the generic class but does change the erased signature.


The ? wildcard by itself is called the unbounded wildcard and denotes that any type instantiation is acceptable.

List<?> anyInstantiationOfList = new ArrayList<Date>();

Bounded Wildcards

A bounded wildcard uses the extends keyword to limit the range of assignable types.

List<? extends Date> dateInstantiations = new ArrayList<Date>();

May extend interfaces as well

Trap<? extends Catchable & Releaseable> trap;

Lower bounds

Wildcard instantiations actually allow another type of bound called a lower bound as well. A lower bound is specified with the keyword super and requires that intantiations be of a certain type or any of its supertypes, up to Object.

List< ? super MyDate > listOfAssignableFromMyDate;
listOfAssignableFromMyDate = new ArrayList<MyDate>();
listOfAssignableFromMyDate = new ArrayList<Date>();
listOfAssignableFromMyDate = new ArrayList<Object>();

Lower bounds are useful for cases where we want to be sure that a particular container instantiation can hold a particular element type, without limiting it to just the specific type of the element.

Only the wildcard instantiation syntax can use the super keyword to refer to lower bounds.

(gave up, read later)

Chapter 9: Threads

The Thread Class and Runnable Interface

All execution in Java is associated with a Thread object, beginning with a "main" thread that is started by the Java VM to launch your application. A new thread is born when we create an instance of the java.lang.Thread class.

Every thread begins its life by executing the run() method in a Runnable object.

public interface Runnable {
    abstract public void run();

A new born thread remains idle until we call its start() method. The thread then wakes up and proceeds to execute the run() method of its target object. start() can be called only once in the lifetime of a thread.

class Animation implements Runnable {
    boolean animate = true;
    public void run() {
        while (animate) {
    Thread my thread;
    Animation (String name) {
        myThread = new Thread(this);


class Animation extends Thread {
    boolean animate = true;
    public void run() {
        while (animate) {

better, using an adapter

class Animation {
    public void startAnimating() {
        Thread myThread = new Thread ( new Runnable() {
            public void run() { drawFrames(); }
    private void drawFrames() {

without saving a reference

new Thread() {
    public void run() { drawFrames(); }

Controlling Threads

stop(), suspend() and resume() are deprecated.

The sleep() method

tell a thread to sit idle or sleep for a fixed period of time.

try {
    Thread.sleep(1000); // milliseconds, current thread
} catch ( InterruptedException e) { ... }

The join() method

If you need to coordinate your activities with another thread by waiting for it to complete its task, use join(). Calling join() causes the caller to block until target thread completes.

This is a very coarse form of thread synchronization. A more general and powerful mechanism is using wait() and nofity() and higher-level API in java.util.concurrent package.

The interrupt() method

When a thread is interrupted, its interrupt status flag is set, whether the thread is idle or not. The thread can test this status with isInterrupted(). Another form isInterrupted(boolean) accepts a Boolean value indicating whether or not to clear the interrupt status.

Death of a Thread

A thread continues to execute until one of following happens:

The setDaemon() method can be used to mark a thread as a daemon thread that should be killed and discarded when no other nondaemon application thread remain.

class Devil extends Thread {
    Devil() {
    public void run() { ... }


In Java, every object has an associated lock. To be more specific, every class and every instance of a class has its own lock. The synchronized keyword marks places where a thread must acquire the lock before proceeding.

class SpeechSynthesizer {
    synchronized void say (String word) { ... }

In addition to synchronizing entire methods, the synchronized keyword can be used in a special construct to guard arbitrary blocks of code.

synchronized ( myObject ) {

below is equivalent

synchronized void myMethod() { ... }
void myMethod() {
    synchronized (this ) { ... }

Accessing variables

Do individual variable types need to be synchronized? Normally, the answer is no. Almost all operations on primitives and object reference types in Java happen atomically: they are handled by the VM in one step, with noo opportunity for two threads to collide.

But double and long types are not guaranteed to be handled atomically. Both of them represent 64-bit values. The problem has to do with how the Java VM's stack handles them. For now, to be strict, you should synchronize access to double and long instance variables through accessor methods or use the volatile keyword or an atomic wrapper class.

(something about memory barrier). The volatile keyword indicates to the VM that the value may be changed by extenal threads and effectively synchronizes access to it automatically.

Java 5.0 added java.util.concurrent.atomic that provides synchronized wrapper classes for all primitive types and references.

Reentrant locking

The locks acquired by Java upon entering a synchronized method or block of code are reentrant, meaning that the thread holding onto the lock may acquire the same lock again any number of times and never blocks waiting for itself.

The wait() and notify() methods

Every object in Java is a subclass of Object, so every object inherits wait() and notify() methods.

By calling wait(), the thread release lock and goes to sleep until the other thread notify() on the target object. The first thread won't wake up from the wait() unless another thread calls notify().

An overloaded version of wait() allows us to specify a timeout period. If another thread doesn't call notify() in the specified period, the waiting thread automatically wakes up.

class MyThing {
    synchronized void waiterMethod() {
        // do some stuff
        wait(); // wait for notifier
        // continue doing stuff
    synchronized void notifierMethod() {
        // do some stuff
        notify(); // notify waiter
        // continue doing stuff
    synchronized void relatedMethod() {
        // do some related stuff
  1. waiter thread comes first, hold the lock. notifier and related threads are waiting for the lock.
  2. waiter executed wait() (released lock and went to sleep)
  3. (assume notifier is the next) notifier acquires lock.
  4. notifier executes notify() (waiter wakes up and rejoins related in vying for the lock)
  5. We'll let you choose your own ending for the story.. (WTF??)

For each notify() call, the runtime system wakes up only one thread that is asleep in a wait() call. If multiple threads are waiting, Java picks a thread on an arbitrary basis. The Object class also provides a notifyAll() call to wake up all waiting threads.

wait conditions

we want our waiter thread to sit in a loop

while (condition != true)

This test is called the wait condition. It's important to use a loop on the wait condition to be sure that the thread has been awakened for the right reason.

(the rest of the chapter is also a good read, including modern ways to create threads, but no notes were taken)

(afer this chapter, all core java language: syntax and build-in features have been covered. rest of the book is about API and libraries)