Java Notes

A collection of notes and references relating to Java.

Published: 9/23/2025

Favor composition over inheritance as a general rule of thumb.

Instead of “is-a”, think “has-a” or “uses-a”.

Use interfaces + composition for flexible contracts. More flexible, because you can swap out parts (via dependency injection, interfaces, etc.).

class Engine { void start() { ... } }
class Car {
    private Engine engine = new Engine();
    void drive() { engine.start(); ... }
}

Use inheritance only when:

  • You need polymorphic behavior across related types.
  • You are extending a framework or library class designed for it.
  • The parent class is stable and models an is-a relationship well.

In practice: Start with composition and interfaces. Then use inheritance only when you have a clear is-a relationship and a stable abstraction.

The super keyword is used to call the parent class’s constructor.

class Shape {
    int numberSides;
    
    public Shape(int numberSides) {
        this.numberSides = numberSides;
    }
}

class Triangle extends Shape {
    public Triangle() {
        super(3);
    }
}
ModifierClassSame packageSubclassGlobal
publicxxxx
protectedxxx
no modiferxx
privatex

We also have final and static modifiers.`

Use case example: static final double PI = 3.14159;. In this case, PI is a constant, and it is shared across all instances of the class.

  • Final classes cannot be extended
  • Final methods cannot be overridden.
  • Final variables cannot be reassigned.

In a nutshell, static variables and methods are shared across all instances of a class. They don’t rely on any internal state of the class, so they can be called without creating an instance of the class.

class Counter {
    // static variable (shared across all instances)
    static int staticCount = 0;

    // instance variable (separate for each object)
    int instanceCount = 0;

    // static method
    static void incrementStatic() {
        staticCount++;
        System.out.println("Static count: " + staticCount);
    }

    // non-static (instance) method
    void incrementInstance() {
        instanceCount++;
        System.out.println("Instance count: " + instanceCount);
    }
}

public class Main {
    public static void main(String[] args) {
        // Calling static method without creating an object
        Counter.incrementStatic();  // Static count: 1

        // Create two objects
        Counter c1 = new Counter();
        Counter c2 = new Counter();

        // Instance methods affect only that object's state
        c1.incrementInstance(); // Instance count: 1 (for c1)
        c1.incrementInstance(); // Instance count: 2 (for c1)
        c2.incrementInstance(); // Instance count: 1 (for c2)

        // Static variable is shared across all objects
        c1.incrementStatic();   // Static count: 2
        c2.incrementStatic();   // Static count: 3
    }
}
public class Main {
  public static void main(String[] args) {
    double result = Math.sqrt(16); 
    System.out.println(result); // 4.0
  }
}

An abstract class serves as a blueprint for other classes. May contain abstract or concrete methods, and cannot be instantiated directly.

Use for partial implementation and to enforce contracts. If you only need to enforce method signatures (no shared implementation), use an interface instead.

abstract class Animal {
    abstract void speak();
    
    void sleep() {
        System.out.println("Sleeping...");
    }
}

class Dog extends Animal {
    @Override
    void speak() {
        System.out.println("Woof!");
    }
}

class Cat extends Animal {
    @Override
    void speak() {
        System.out.println("Meow!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        dog.makeSound();  // "Woof!"
        dog.sleep();      // "Sleeping..."
    }
}

When an exception occurs, Java displays a message that includes:

  • the name of the exception and the line of the program where the exception occurred
  • a stack trace that includes:
    • The method running when the exception occurred
    • The method that invoked it
    • The method that invoked that method, and so on.
try {
    // Block of code to try
} catch (NullPointerException e) {
    System.err.println("Caught NullPointerException: " + e.getMessage());
} catch (ArithmeticException e) {
    System.err.println("Caught ArithmeticException: " + e.getMessage());
} catch (Exception e) {
    System.err.println("Caught some other exception: " + e.getMessage());
}

JDBC: Provides a standard Java API for database access; frameworks like Hibernate, JPA, and jOOQ build on JDBC to simplify querying and persistence.

Hibernate / JPA: Provide object-relational mapping (ORM), automatically translating Java objects to database tables and managing persistence.

jOOQ: Provides a type-safe SQL DSL, giving more control over SQL while still executing queries via JDBC.

Typical flows:

  • Code $\to$ jOOQ $\to$ JDBC $\to$ Database
  • Code $\to$ JPA/Hibernate $\to$ JDBC $\to$ Database