Back to Java: Notes from a Developer Returning to the Language

Why Return to Java?

Java never really went away. It just got quieter in my daily work as I moved toward JavaScript, TypeScript, and frontend-leaning stacks. But modern Java especially post-JDK 17 is a genuinely different language from what I remember. Records, sealed classes, pattern matching, virtual threads. It felt worth a proper revisit rather than pretending I knew it.

These are rough notes from the first few days getting back up to speed.

Day 1 Tooling and Hello World

The starting point was obvious: install JDK 21 and get something running.

# Verify the installation
java --version
javac --version

I used SDKMAN to manage the JDK install, which made switching versions trivial later. For the editor I went with IntelliJ IDEA Community for Java.

The classic first program:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

Two commands to understand from the start:

  • javac HelloWorld.java compiles source to bytecode (HelloWorld.class)
  • java HelloWorld runs the bytecode on the JVM

The distinction matters: Java ships bytecode, not native binaries. The JVM interprets (and JIT-compiles) that bytecode at runtime. This is still the foundation of why Java runs everywhere.

Day 2 Primitive Types, Casting, and the Memory Model

Java has 8 primitive types. The ones that come up constantly:

TypeSizeDefaultNotes
int32-bit0Most common integer type
long64-bit0LUse L suffix for literals
double64-bit0.0Default floating-point type
boolean1-bitfalsetrue or false only
byte 8-bit-127 to 127Holds char and value less than 127
char16-bit’�‘Unsigned Unicode character

Type Casting

Widening (smaller → larger) is implicit. Narrowing requires an explicit cast and can lose data:

int x = 42;
long y = x;          // widening  implicit, safe

double d = 9.99;
int truncated = (int) d;  // narrowing  explicit, loses .99

Stack vs Heap

This tripped me up when I first learned Java years ago worth locking in early:

  • Stack stores primitive values and references. Each method call gets its own stack frame. Memory is reclaimed automatically when the frame pops.
  • Heap stores objects. Managed by the garbage collector. Lives longer than a single method call.
int a = 5;              // 'a' lives on the stack
String s = "hello";     // 's' (the reference) is on the stack; the String object is on the heap

Primitives are always on the stack. Objects are always on the heap. The reference to an object sits on the stack.

Day 3 Variables, Constants, and String Basics

Variables and Constants

Java is statically typed declare the type upfront or use var (type inferred by the compiler, not runtime):

int count = 0;
var name = "Aderemi";   // compiler infers String

Constants use final. The convention is UPPER_SNAKE_CASE:

final int MAX_RETRIES = 3;
final String API_VERSION = "v2";

Once assigned, a final variable cannot be reassigned. For class-level constants, pair it with static:

public static final double TAX_RATE = 0.075;

String Basics

String in Java is immutable operations always return a new String, they do not modify in place. The most-used methods:

String s = "Hello, Java";

s.length();               // 11
s.charAt(7);              // 'J'
s.substring(7);           // "Java"
s.substring(7, 11);       // "Java" (end index is exclusive)
s.contains("Java");       // true
s.equals("Hello, Java");  // true  always use equals(), not ==

The == vs equals() distinction is critical. == compares references (memory addresses). equals() compares content. Two String objects with identical text will fail a == check if they were allocated separately:

String a = new String("test");
String b = new String("test");

a == b;        // false  different objects
a.equals(b);   // true  same content

String literals from the pool are an exception (the JVM interns them), but relying on that is fragile. Always use equals().

Day 4 Operators

Arithmetic

Standard arithmetic works exactly as expected, with one gotcha: integer division truncates toward zero rather than rounding.

int a = 10, b = 3;

a + b;   // 13
a - b;   // 7
a * b;   // 30
a / b;   // 3  not 3.33, truncation!
a % b;   // 1  remainder

To get a decimal result from integer division, cast at least one operand to double first:

(double) a / b;   // 3.3333...

Compound assignment and increment/decrement work the same as in C-derived languages:

int x = 5;
x += 3;   // x is now 8
x++;      // x is now 9 (post-increment: returns old value, then increments)
++x;      // x is now 10 (pre-increment: increments first, then returns)

The prefix/postfix distinction only matters when the expression value is used in the same statement. Inside a standalone x++ or ++x it makes no difference.

Comparison and Logical

Comparison operators return boolean:

5 == 5    // true
5 != 3    // true
5 > 3     // true
5 <= 5    // true

Logical operators for combining booleans the important detail is short-circuit evaluation:

true && false    // false  evaluates right side only if left is true
true || false    // true   evaluates right side only if left is false
!true            // false

Because of short-circuiting, && and || are safe to use when the right-hand operand has a side effect (like a method call) you only want to run conditionally. Bitwise & and | do not short-circuit they always evaluate both sides.

Bitwise

Less common in day-to-day application code but useful when working with flags, permissions, or low-level data:

OperatorNameExampleResult
&AND5 & 31
|OR5 | 37
^XOR5 ^ 36
~NOT (unary)~5-6
<<Left shift1 << 38
>>Right shift8 >> 14
>>>Unsigned right shift8 >>> 14

The difference between >> and >>> only shows up with negative numbers. >> preserves the sign bit (arithmetic shift); >>> fills with zeros regardless (logical shift).

Ternary

A compact inline conditional. Readable when short, painful when nested:

int age = 20;
String label = age >= 18 ? "adult" : "minor";

Don’t chain ternaries. Use an if/else block the moment it takes more than one line to read.

Operator Precedence

From highest to lowest, condensed to what actually matters in practice:

PriorityOperators
1++, -- (postfix), method calls
2++, -- (prefix), !, ~, unary -
3*, /, %
4+, -
5<<, >>, >>>
6<, >, <=, >=
7==, !=
8&
9^
10|
11&&
12||
13?: (ternary)
14=, +=, -=, etc.

The practical rule: use parentheses to make intent explicit. Don’t rely on memorising every precedence level. a + b * c is fine. a | b && c is a bug waiting to happen parenthesise it.

Day 5 Control Flow

if / else if / else

Nothing unusual here. The body can be a single statement without braces, but always use braces the dangling-else ambiguity is not worth the saved characters:

int score = 74;

if (score >= 90) {
    System.out.println("A");
} else if (score >= 80) {
    System.out.println("B");
} else if (score >= 70) {
    System.out.println("C");
} else {
    System.out.println("F");
}

Nested Conditions

Nesting works, but more than two levels deep is usually a sign to extract a method or restructure with early returns:

if (user != null) {
    if (user.isActive()) {
        if (user.hasPermission("admin")) {
            // do the thing
        }
    }
}

Cleaner as guard clauses:

if (user == null) return;
if (!user.isActive()) return;
if (!user.hasPermission("admin")) return;
// do the thing

switch Statement (Traditional)

The traditional switch is still valid and widely found in existing codebases. One thing to know immediately: fall-through is the default. Without break, execution continues into the next case:

int day = 3;

switch (day) {
    case 1:
        System.out.println("Monday");
        break;
    case 2:
        System.out.println("Tuesday");
        break;
    case 3:
        System.out.println("Wednesday");
        break;
    default:
        System.out.println("Other");
}

Forgetting break is one of the classic Java bugs. The compiler won’t warn you.

switch Expression (Java 14+)

The newer arrow-syntax switch expression eliminates fall-through entirely and can return a value directly. This is the form to prefer in modern code:

int day = 3;

String name = switch (day) {
    case 1 -> "Monday";
    case 2 -> "Tuesday";
    case 3 -> "Wednesday";
    case 4 -> "Thursday";
    case 5 -> "Friday";
    default -> "Weekend";
};

Each arm is isolated no fall-through, no break required. Multiple labels can share an arm:

String type = switch (day) {
    case 1, 2, 3, 4, 5 -> "Weekday";
    case 6, 7 -> "Weekend";
    default -> "Unknown";
};

For arms that need more than one statement, use a block with yield to produce the value:

String label = switch (score) {
    case 10 -> "Perfect";
    default -> {
        String result = score > 5 ? "Good" : "Low";
        yield result + " (" + score + ")";
    }
};

yield is the switch-expression equivalent of return it sets the value produced by that arm. Using return here would exit the enclosing method entirely, which is almost never what you want inside a switch expression.

Day 6 Loops

for Loop

The classic counted loop. Three-part header: initializer, condition, update:

for (int i = 0; i < 5; i++) {
    System.out.println(i);   // 0 1 2 3 4
}

The enhanced for-each loop is cleaner for iterating over arrays and collections when you don’t need the index:

int[] numbers = {10, 20, 30, 40};

for (int n : numbers) {
    System.out.println(n);
}

while Loop

Checks the condition before each iteration. If the condition is false at the start, the body never runs:

int count = 0;

while (count < 3) {
    System.out.println(count);
    count++;
}

do-while Loop

Checks the condition after each iteration the body always runs at least once. Less common, but useful for input prompts or retry logic:

int n;

do {
    n = getUserInput();
} while (n < 0);

break and continue

break exits the loop immediately. continue skips the rest of the current iteration and jumps to the next:

for (int i = 0; i < 10; i++) {
    if (i == 5) break;         // stop at 5
    if (i % 2 == 0) continue;  // skip even numbers
    System.out.println(i);     // prints 1, 3
}

Labelled Loops

Java supports labels on loops, which lets break and continue target an outer loop directly. I had never actually used this in practice before revisiting the language:

outer:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (j == 1) continue outer;   // skip to next iteration of outer loop
        System.out.println(i + "," + j);
    }
}
// prints: 0,0  |  1,0  |  2,0

break outer would exit the outer loop entirely rather than just the inner one. It’s not something you reach for often, but when you need to escape a nested loop cleanly it’s better than a sentinel variable.

Day 7 Methods

Declaration Syntax

A Java method declaration has four required parts: access modifier, return type, name, and parameter list. The body follows in braces:

accessModifier returnType methodName(ParameterType paramName) {
    // body
    return value;
}

A concrete example:

public int add(int a, int b) {
    return a + b;
}

Parameters and Return Types

Parameters are declared with their type. Java passes primitives by value (a copy) and objects by reference (a copy of the reference not the object itself):

public String greet(String name) {
    return "Hello, " + name + "!";
}

Multiple parameters, multiple types:

public double calculateArea(double width, double height) {
    return width * height;
}

void Methods

When a method produces no return value, the return type is void. A bare return; is valid to exit early but is not required at the end:

public void printSeparator(int length) {
    for (int i = 0; i < length; i++) {
        System.out.print("-");
    }
    System.out.println();
}

Method Signature

The method signature is the name plus the parameter type list return type and parameter names are not part of it. This is what the compiler uses to distinguish overloaded methods:

// Different signatures  same name, different parameters
public int multiply(int a, int b)           { return a * b; }
public double multiply(double a, double b)  { return a * b; }
public int multiply(int a, int b, int c)    { return a * b * c; }

Overloading on return type alone is not allowed. int foo() and double foo() are indistinguishable from the call site the compiler rejects it.

Calculator Utility Class

Putting Day 7 together into a small utility class. No instance needed, so everything is static. Division and modulo include guards without them a zero denominator would throw an unchecked ArithmeticException at runtime with no helpful message:

public class Calculator {

    public static int add(int a, int b) {
        return a + b;
    }

    public static int subtract(int a, int b) {
        return a - b;
    }

    public static int multiply(int a, int b) {
        return a * b;
    }

    public static double divide(double a, double b) {
        if (b == 0) {
            throw new ArithmeticException("Division by zero");
        }
        return a / b;
    }

    public static double power(double base, int exponent) {
        double result = 1;
        for (int i = 0; i < Math.abs(exponent); i++) {
            result *= base;
        }
        return exponent < 0 ? 1.0 / result : result;
    }

    public static int modulo(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException("Modulo by zero");
        }
        return a % b;
    }
}

Usage:

public class Main {
    public static void main(String[] args) {
        System.out.println(Calculator.add(10, 5));         // 15
        System.out.println(Calculator.subtract(10, 5));    // 5
        System.out.println(Calculator.multiply(4, 6));     // 24
        System.out.println(Calculator.divide(10.0, 3.0));  // 3.3333...
        System.out.println(Calculator.power(2, 8));        // 256.0
        System.out.println(Calculator.modulo(17, 5));      // 2
    }
}

The power method handles negative exponents power(2, -3) returns 0.125. Rolling it manually rather than reaching for Math.pow immediately was a useful exercise for combining a loop, a conditional, and a return value in one method.

Day 8 Classes and Objects

Defining a Class

A class is a blueprint. An object is an instance of that blueprint, allocated on the heap at runtime.

public class Person {
    // instance fields  each object gets its own copy
    String name;
    int age;

    // instance method
    void greet() {
        System.out.println("Hi, I'm " + name);
    }
}

Creating Instances with new

new allocates heap memory, initialises the fields, runs the constructor, and returns a reference:

Person alice = new Person();
alice.name = "Alice";
alice.age = 30;
alice.greet();   // Hi, I'm Alice

Person bob = new Person();
bob.name = "Bob";
bob.age = 25;

alice and bob are independent objects. Changing alice.name has no effect on bob.name.

Instance vs. Class Members

Member typeKeywordLives onOne copy per
Instance field(none)heap, per objectobject
Instance method(none)method areaclass (shared, but this differs)
Class fieldstaticmethod areaclass
Class methodstaticmethod areaclass
public class Counter {
    static int total = 0;   // shared across all instances
    int count = 0;          // unique to each instance

    void increment() {
        count++;
        total++;
    }
}

Counter a = new Counter();
Counter b = new Counter();
a.increment();
a.increment();
b.increment();

System.out.println(a.count);   // 2
System.out.println(b.count);   // 1
System.out.println(Counter.total);  // 3  access via class name, not instance

Access static members through the class name, not through an instance reference. The compiler allows both, but accessing via an instance is misleading it looks like the value belongs to that object.

Day 9 Constructors

Default Constructor

If you write no constructor, the compiler generates a no-argument one that sets all fields to their zero values (0, false, null). The moment you write any constructor yourself, the compiler stops generating the default:

public class Point {
    int x;
    int y;
    // compiler generates: Point() {}
}

Point p = new Point();   // x=0, y=0

Parameterised Constructor

public class Point {
    int x;
    int y;

    public Point(int x, int y) {
        this.x = x;   // 'this' distinguishes the field from the parameter
        this.y = y;
    }
}

Point p = new Point(3, 7);

this refers to the current object. When a parameter name shadows a field name, this.fieldName is the only unambiguous way to reach the field.

Constructor Chaining with this()

this() calls another constructor in the same class. It must be the first statement in the constructor body:

public class Rectangle {
    int width;
    int height;
    String color;

    public Rectangle(int width, int height, String color) {
        this.width = width;
        this.height = height;
        this.color = color;
    }

    public Rectangle(int width, int height) {
        this(width, height, "black");   // delegates to the three-arg constructor
    }

    public Rectangle() {
        this(1, 1);   // delegates to the two-arg constructor
    }
}

Chaining keeps initialisation logic in one place. If the default color ever changes, only one constructor needs updating.

Object Lifecycle

  1. new triggers class loading if the class hasn’t been loaded yet.
  2. Memory is allocated on the heap and zero-initialised.
  3. Instance initialisers and field assignments run top-to-bottom.
  4. The constructor body runs.
  5. The reference is returned to the caller.
  6. When no more references to the object exist, it becomes eligible for garbage collection. The GC reclaims the memory at an unspecified later time.

Java gives you no destructor. Resource cleanup (files, connections) goes in a try-with-resources block, not in object death.

Day 10 Encapsulation

The Problem with Public Fields

With public fields, any code anywhere can reach in and corrupt your object’s state:

public class BankAccount {
    public double balance;   // anyone can set this to -99999
}

account.balance = -99999;   // no validation, no audit trail

Private Fields + Public Getters/Setters

Encapsulation means keeping fields private and exposing controlled access through methods:

public class BankAccount {
    private double balance;
    private String owner;

    public BankAccount(String owner, double initialBalance) {
        this.owner = owner;
        if (initialBalance < 0) throw new IllegalArgumentException("Balance cannot be negative");
        this.balance = initialBalance;
    }

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Withdrawal must be positive");
        if (amount > balance) throw new IllegalStateException("Insufficient funds");
        balance -= amount;
    }
}

The class now owns every path into balance. Invalid states are rejected at the boundary rather than silently accepted.

Why It Matters

  • Invariants you can guarantee balance >= 0 everywhere in the codebase.
  • Change isolation if the internal representation changes (say, storing balance in pence as a long), callers using the public API don’t need to change.
  • Auditability validation and logging live in one place.

A getter with no corresponding setter is a read-only field from the caller’s perspective. That’s not a missing setter; it’s a deliberate design decision.

Day 11 Access Modifiers in Depth

Four levels, from most to least restrictive:

ModifierSame classSame packageSubclass (other pkg)Any class
privateyes
package-privateyesyes
protectedyesyesyes
publicyesyesyesyes

Package-private is the default when no modifier is written. It is not a keyword omitting the modifier gives you package-private access.

public class Example {
    private int a;      // only this class
    int b;              // package-private  any class in this package
    protected int c;    // package + subclasses in other packages
    public int d;       // everywhere
}

Design Boundaries

Start with private. Open up only as far as actually needed:

  • private implementation details that should never leak.
  • package-private collaborating classes within the same logical module.
  • protected extension points for subclasses; use sparingly. Every protected member is part of your API for subclassers.
  • public your stable external contract. Changing a public member is a breaking change.

A class itself can be public or package-private. A package-private class is invisible outside its package, which is useful for implementation classes you don’t want to expose.

Day 12 The static Keyword

Static Fields

A static field belongs to the class, not to any instance. All instances share the same value:

public class IdGenerator {
    private static int nextId = 1;
    private int id;

    public IdGenerator() {
        this.id = nextId++;
    }

    public int getId() { return id; }
}

IdGenerator a = new IdGenerator();   // id = 1
IdGenerator b = new IdGenerator();   // id = 2

Constants are always static final:

public static final double PI = 3.14159265358979;

Static Methods

Static methods have no this. They can only access static fields and other static methods directly:

public class MathUtils {
    public static int clamp(int value, int min, int max) {
        return Math.max(min, Math.min(max, value));
    }
}

MathUtils.clamp(150, 0, 100);   // 100

Calling a static method through an instance compiles but is a code smell the compiler resolves it to the class anyway.

Static Initialiser Blocks

Run once, when the class is first loaded. Useful when a static field needs complex setup that can’t fit in a field initialiser:

public class Config {
    public static final Map<String, String> DEFAULTS;

    static {
        DEFAULTS = new HashMap<>();
        DEFAULTS.put("timeout", "30");
        DEFAULTS.put("retries", "3");
    }
}

Math and Arrays as Utility Class Examples

Both are final classes with only static members the classic utility class pattern:

Math.abs(-7);          // 7
Math.max(3, 9);        // 9
Math.pow(2, 10);       // 1024.0
Math.sqrt(144);        // 12.0
Math.round(3.6);       // 4
Math.random();         // [0.0, 1.0)

int[] nums = {5, 2, 8, 1, 9};
Arrays.sort(nums);                        // [1, 2, 5, 8, 9]
Arrays.binarySearch(nums, 8);             // 3  (index after sort)
int[] copy = Arrays.copyOf(nums, 3);      // [1, 2, 5]
Arrays.fill(copy, 0);                     // [0, 0, 0]
System.out.println(Arrays.toString(nums)); // [1, 2, 5, 8, 9]

Day 13 Inheritance

extends

A subclass inherits all non-private members of its superclass:

public class Animal {
    String name;

    public Animal(String name) {
        this.name = name;
    }

    public void eat() {
        System.out.println(name + " is eating.");
    }
}

public class Dog extends Animal {
    String breed;

    public Dog(String name, String breed) {
        super(name);   // must be the first statement
        this.breed = breed;
    }

    public void bark() {
        System.out.println(name + " says: Woof!");
    }
}

super for Fields and Constructors

super(...) calls the parent constructor. If the parent has no no-arg constructor, the subclass must explicitly call a parent constructor or the code won’t compile.

super.methodName() calls the parent’s version of a method from within an overriding method:

public class Animal {
    public void describe() {
        System.out.println("I am an animal named " + name);
    }
}

public class Dog extends Animal {
    @Override
    public void describe() {
        super.describe();   // runs Animal.describe() first
        System.out.println("I am a " + breed);
    }
}

What Inheritance Gives You

  • Code reuse without duplication.
  • Substitutability a Dog can be used anywhere an Animal is expected.
  • Java enforces single inheritance for classes. A class can extend exactly one superclass. Use interfaces (Day 18) when you need multiple type contracts.

Every class that doesn’t explicitly extend anything implicitly extends java.lang.Object. That’s where toString(), equals(), and hashCode() come from.

Day 14 Method Overriding

@Override

A subclass can replace an inherited method by declaring a method with the same signature. @Override is technically optional but should always be present it lets the compiler catch typos in the method name or signature:

public class Animal {
    public String sound() {
        return "...";
    }
}

public class Cat extends Animal {
    @Override
    public String sound() {
        return "Meow";
    }
}

Dynamic Dispatch

The JVM resolves the method at runtime based on the actual type of the object, not the declared type of the variable. This is the mechanism that makes polymorphism work:

Animal a = new Cat();   // declared as Animal, actually a Cat
a.sound();              // "Meow"  Cat.sound() is called, not Animal.sound()

The decision of which sound() to call happens at runtime, every call, based on what a actually points to.

Covariant Return Types

An overriding method may narrow the return type to a subtype of the parent’s return type:

public class AnimalFactory {
    public Animal create() {
        return new Animal("generic");
    }
}

public class DogFactory extends AnimalFactory {
    @Override
    public Dog create() {   // Dog is a subtype of Animal  legal
        return new Dog("Rex", "Labrador");
    }
}

Animal Hierarchy Example

public class Animal {
    protected String name;

    public Animal(String name) { this.name = name; }

    public String sound() { return "..."; }

    public void describe() {
        System.out.println(name + " says: " + sound());
    }
}

public class Dog extends Animal {
    public Dog(String name) { super(name); }

    @Override
    public String sound() { return "Woof"; }
}

public class Cat extends Animal {
    public Cat(String name) { super(name); }

    @Override
    public String sound() { return "Meow"; }
}

public class Duck extends Animal {
    public Duck(String name) { super(name); }

    @Override
    public String sound() { return "Quack"; }
}
Animal[] animals = { new Dog("Rex"), new Cat("Whiskers"), new Duck("Donald") };

for (Animal a : animals) {
    a.describe();
}
// Rex says: Woof
// Whiskers says: Meow
// Donald says: Quack

describe() is defined once in Animal. The correct sound() is selected at runtime for each object. Adding a new animal type requires no changes to describe() or the loop.

Day 15 Polymorphism

Compile-Time Polymorphism (Overloading)

The compiler picks the right method based on the static types of the arguments. Resolution happens before the program runs:

public class Printer {
    public void print(int n)    { System.out.println("int: " + n); }
    public void print(double d) { System.out.println("double: " + d); }
    public void print(String s) { System.out.println("String: " + s); }
}

Printer p = new Printer();
p.print(42);       // int: 42
p.print(3.14);     // double: 3.14
p.print("hello");  // String: hello

Runtime Polymorphism (Overriding)

The JVM picks the right method based on the actual type of the object. Resolution happens at runtime (see Day 14 for the mechanism):

Animal a = new Dog("Rex");
a.sound();   // "Woof"  resolved at runtime to Dog.sound()

Upcasting

Assigning a subtype to a supertype variable. Always safe, always implicit:

Dog dog = new Dog("Rex");
Animal animal = dog;   // upcast  implicit, safe

The variable animal now has a narrower view of the object it can only see Animal methods. The object itself is still a Dog.

Downcasting

Recovering the subtype from a supertype variable. Requires an explicit cast. Fails at runtime with ClassCastException if the object is not actually that type:

Animal animal = new Dog("Rex");
Dog dog = (Dog) animal;   // downcast  explicit, safe here because the object IS a Dog
dog.bark();

Animal cat = new Cat("Whiskers");
Dog bad = (Dog) cat;   // compiles, but throws ClassCastException at runtime

Always check with instanceof before downcasting to untrusted references (see Day 16).

Day 16 instanceof and Safe Casting

Classic instanceof

Returns true if the object is an instance of the given type (including subtypes):

Animal a = new Dog("Rex");

if (a instanceof Dog) {
    Dog d = (Dog) a;
    d.bark();
}

Two lines of boilerplate: the check and the cast. Easy to forget the check or use the wrong variable after it.

Pattern Matching instanceof (Java 16+)

The check and the cast collapse into one expression. The variable d is only in scope inside the if block where the check passed:

Animal a = new Dog("Rex");

if (a instanceof Dog d) {
    d.bark();   // d is already typed as Dog, no explicit cast needed
}

It also works in expressions with &&:

if (a instanceof Dog d && d.breed.equals("Labrador")) {
    System.out.println("Good boy, " + d.name);
}

The right side of && can safely use d because && short-circuits if the instanceof check failed, the right side never runs.

instanceof with null

null instanceof AnyType always returns false. No null check needed before an instanceof test:

Animal a = null;
System.out.println(a instanceof Dog);   // false, no NullPointerException

When to Use Downcasting

Downcasting is a signal that the abstraction might be incomplete. If you find yourself checking instanceof Dog in multiple places, consider whether sound() or a similar method in the base class would eliminate the need. That said, legitimate uses exist: visitor patterns, serialisation, and framework interop all sometimes require type inspection.

Day 17 Abstract Classes

The abstract Keyword

An abstract class cannot be instantiated directly. It exists to be extended:

public abstract class Shape {
    String color;

    public Shape(String color) {
        this.color = color;
    }

    public abstract double area();       // no body  subclasses must implement
    public abstract double perimeter();  // no body

    public void describe() {
        System.out.printf("%s: area=%.2f, perimeter=%.2f%n",
            getClass().getSimpleName(), area(), perimeter());
    }
}

Concrete subclasses must implement all abstract methods or be abstract themselves:

public class Circle extends Shape {
    double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override public double area()      { return Math.PI * radius * radius; }
    @Override public double perimeter() { return 2 * Math.PI * radius; }
}

public class Rectangle extends Shape {
    double width, height;

    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }

    @Override public double area()      { return width * height; }
    @Override public double perimeter() { return 2 * (width + height); }
}
Shape[] shapes = { new Circle("red", 5), new Rectangle("blue", 4, 6) };
for (Shape s : shapes) {
    s.describe();
}
// Circle: area=78.54, perimeter=31.42
// Rectangle: area=24.00, perimeter=20.00

Abstract Class vs. Concrete Class

Use an abstract class when:

  • The concept makes no sense without specialisation (Shape with no defined geometry is meaningless).
  • You want to share implementation (fields, concrete methods) alongside abstract contracts.
  • The subclasses share state that belongs on the base.

Don’t make a class abstract just because you want to force subclassing. If the class is complete and useful on its own, leave it concrete.

Day 18 Interfaces

Defining and Implementing

An interface declares a contract what a type can do, not how it does it. All methods are implicitly public abstract unless marked otherwise:

public interface Drawable {
    void draw();
    void resize(double factor);
}

public interface Printable {
    void print();
}

A class implements an interface with implements. It must provide a body for every abstract method:

public class Circle implements Drawable, Printable {
    double radius;

    public Circle(double radius) { this.radius = radius; }

    @Override public void draw()   { System.out.println("Drawing circle r=" + radius); }
    @Override public void resize(double factor) { radius *= factor; }
    @Override public void print()  { System.out.println("Circle(r=" + radius + ")"); }
}

Default Methods (Java 8+)

default methods have a body inside the interface. They let you add new behaviour to an interface without breaking all existing implementors:

public interface Drawable {
    void draw();
    void resize(double factor);

    default void drawAndDescribe() {
        draw();
        System.out.println("Drawn.");
    }
}

Implementors inherit drawAndDescribe() for free. They may still override it if they need different behaviour.

Static Interface Methods

Utility methods that belong conceptually to the interface but don’t need an instance:

public interface Validator {
    boolean validate(String input);

    static Validator nonEmpty() {
        return input -> input != null && !input.isEmpty();
    }
}

Validator v = Validator.nonEmpty();
v.validate("hello");   // true

Interface Fields

All fields in an interface are implicitly public static final. Interfaces cannot hold instance state.

Day 19 Multiple Interfaces, Interface Segregation, and Abstract Class vs. Interface

Multiple Interface Implementation

A class can implement as many interfaces as needed. This is the mechanism Java uses in place of multiple inheritance:

public interface Flyable  { void fly(); }
public interface Swimmable { void swim(); }
public interface Runnable  { void run(); }

public class Duck implements Flyable, Swimmable, Runnable {
    @Override public void fly()  { System.out.println("Duck flying"); }
    @Override public void swim() { System.out.println("Duck swimming"); }
    @Override public void run()  { System.out.println("Duck running"); }
}

Default Method Conflict

If two interfaces define a default method with the same signature, the implementing class must override it:

interface A { default void hello() { System.out.println("A"); } }
interface B { default void hello() { System.out.println("B"); } }

class C implements A, B {
    @Override
    public void hello() {
        A.super.hello();   // explicitly choose A's version, or write your own
    }
}

Interface Segregation

Prefer narrow, focused interfaces over wide ones. A class shouldn’t be forced to implement methods it doesn’t use:

// Too wide  a PDF printer doesn't need scan()
interface AllInOne { void print(); void scan(); void fax(); }

// Better  compose only what you need
interface Printable { void print(); }
interface Scannable { void scan(); }
interface Faxable   { void fax(); }

Marker Interfaces

An interface with no methods. Used to signal a capability to the runtime or frameworks:

public interface Serializable {}   // java.io.Serializable works this way

The JVM and ObjectOutputStream check instanceof Serializable before serialising an object. Annotations (e.g. @JsonSerializable) have largely replaced marker interfaces in modern code, but you still encounter them in the standard library.

Abstract Class vs. Interface

DimensionAbstract classInterface
InstantiationNoNo
Instance fieldsYesNo (static final only)
ConstructorYesNo
Multiple inheritanceNo (single extends)Yes (multiple implements)
Access modifiersAnypublic only (methods)
Default implementationAny method can have a bodydefault keyword required
Best forShared state + partial implementationBehavioural contracts, mix-ins

When in doubt: prefer interfaces. Use an abstract class when you genuinely need shared state or a constructor.

Day 20 Inner Classes

Java has four kinds of nested types, each with a distinct purpose.

Static Nested Class

Declared static inside another class. No implicit link to the outer instance. Behaves like a top-level class but is namespaced inside the outer:

public class Graph {
    private int vertices;

    public static class Edge {   // does not need a Graph instance
        int from, to, weight;

        Edge(int from, int to, int weight) {
            this.from = from;
            this.to = to;
            this.weight = weight;
        }
    }
}

Graph.Edge e = new Graph.Edge(0, 1, 10);

Use static nested classes for helper types that are logically part of the outer class but don’t need access to its instance members. Map.Entry is the canonical standard-library example.

Inner (Non-Static) Class

Has an implicit reference to the enclosing outer instance. Can access all members of the outer, including private ones:

public class OuterClass {
    private int value = 10;

    public class InnerClass {
        public void display() {
            System.out.println("Outer value: " + value);   // direct access
        }
    }
}

OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
inner.display();   // Outer value: 10

The hidden outer reference means inner class instances can’t outlive their outer. Be cautious: inner classes held in long-lived collections can prevent the outer from being garbage-collected.

Local Class

Defined inside a method body. Visible only within that method. Can capture effectively-final local variables:

public void process(int multiplier) {
    class Multiplier {
        int apply(int x) { return x * multiplier; }
    }
    Multiplier m = new Multiplier();
    System.out.println(m.apply(5));
}

Rarely needed; lambdas cover most use-cases more concisely.

Anonymous Class

A one-off class defined and instantiated in a single expression. Common before Java 8 lambdas for single-method interfaces:

Comparator<String> byLength = new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return Integer.compare(a.length(), b.length());
    }
};

List<String> words = Arrays.asList("banana", "kiwi", "fig");
words.sort(byLength);

Post-Java-8, a lambda replaces this: words.sort((a, b) -> Integer.compare(a.length(), b.length())). You’ll still see anonymous classes for multi-method interfaces where a lambda can’t be used.

Day 21 Enums

Defining an Enum

An enum is a fixed set of named constants. Each constant is a singleton instance of the enum type:

public enum Direction {
    NORTH, SOUTH, EAST, WEST
}

Direction d = Direction.NORTH;
System.out.println(d);           // NORTH
System.out.println(d.name());    // NORTH
System.out.println(d.ordinal()); // 0  zero-based position

Adding Fields and Methods

Enum constants can carry data. Each constant calls the constructor:

public enum Planet {
    MERCURY(3.303e+23, 2.4397e6),
    VENUS  (4.869e+24, 6.0518e6),
    EARTH  (5.976e+24, 6.37814e6);

    private final double mass;
    private final double radius;

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }

    static final double G = 6.67300E-11;

    double surfaceGravity() {
        return G * mass / (radius * radius);
    }

    double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }
}

System.out.println(Planet.EARTH.surfaceWeight(75));   // ~735.0 N

Enums can implement interfaces and declare abstract methods that each constant overrides a clean way to attach behaviour to each variant without a switch.

switch with Enums

Enums and switch expressions pair naturally:

String label = switch (direction) {
    case NORTH -> "Up";
    case SOUTH -> "Down";
    case EAST  -> "Right";
    case WEST  -> "Left";
};

The compiler warns if you miss a case (exhaustiveness checking).

EnumSet and EnumMap

Specialised collections backed by bit vectors and arrays respectively. Much faster than HashSet<EnumType> or HashMap<EnumType, V> for enum keys:

EnumSet<Direction> horizontal = EnumSet.of(Direction.EAST, Direction.WEST);
EnumSet<Direction> all        = EnumSet.allOf(Direction.class);
EnumSet<Direction> vertical   = EnumSet.complementOf(horizontal);   // NORTH, SOUTH

EnumMap<Direction, String> labels = new EnumMap<>(Direction.class);
labels.put(Direction.NORTH, "Up");
labels.put(Direction.SOUTH, "Down");

Prefer EnumSet over Set<MyEnum> and EnumMap over Map<MyEnum, V> when you know the key type is an enum.

Day 22 String Deep-Dive

Immutability and the String Pool

String objects are immutable. Every operation that appears to modify a String actually creates a new one. String literals are interned in a pool maintained by the JVM; two literals with the same content share the same object:

String a = "hello";
String b = "hello";
System.out.println(a == b);        // true  same pooled object
System.out.println(a.equals(b));   // true

String c = new String("hello");
System.out.println(a == c);        // false  c is a new heap object outside the pool
System.out.println(a.equals(c));   // true

Never use == to compare String content. Always use equals() or equalsIgnoreCase().

StringBuilder vs. StringBuffer

String concatenation inside a loop creates a new object on every iteration. For heavy string building, use StringBuilder:

// Inefficient  O(n²) allocations
String result = "";
for (int i = 0; i < 1000; i++) {
    result += i;   // new String each iteration
}

// Efficient  single buffer, O(n)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
String result = sb.toString();

StringBuffer is the thread-safe equivalent of StringBuilder. It synchronises every method, so it’s slower. Use StringBuilder unless you genuinely need thread safety on the builder itself (rare usually you synchronise at a higher level).

Common String Methods

String s = "  Hello, Java!  ";

s.trim()                    // "Hello, Java!"  removes leading/trailing whitespace
s.strip()                   // same but Unicode-aware (Java 11+)
s.toLowerCase()             // "  hello, java!  "
s.toUpperCase()             // "  HELLO, JAVA!  "
s.replace("Java", "World")  // "  Hello, World!  "
s.split(", ")               // ["  Hello", "Java!  "]
s.startsWith("  Hello")     // true
s.endsWith("!  ")           // true
s.indexOf("Java")           // 8
s.isEmpty()                 // false
s.isBlank()                 // false  (Java 11+, checks whitespace-only too)
String.valueOf(42)          // "42"  convert any primitive to String
"Hello".repeat(3)           // "HelloHelloHello"  (Java 11+)

Text Blocks (Java 15+)

Multi-line strings without escape sequences:

String json = """
        {
            "name": "Alice",
            "age": 30
        }
        """;

The leading indentation common to all lines is stripped. The closing """ on its own line adds a trailing newline.

Day 23 Arrays

Declaration and Initialisation

int[] nums = new int[5];          // [0, 0, 0, 0, 0]
int[] primes = {2, 3, 5, 7, 11}; // array literal
String[] names = new String[3];   // [null, null, null]

Arrays are objects. The variable holds a reference. The length is fixed at creation and never changes use ArrayList (Day 27) when you need a resizable sequence.

Multi-Dimensional Arrays

Java implements these as arrays of arrays each row can have a different length (a jagged array):

int[][] matrix = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

System.out.println(matrix[1][2]);   // 6  row 1, column 2

// Jagged
int[][] triangle = new int[4][];
for (int i = 0; i < triangle.length; i++) {
    triangle[i] = new int[i + 1];
}
// triangle[0].length == 1, triangle[3].length == 4

Arrays Utility

int[] nums = {5, 2, 8, 1, 9, 3};

Arrays.sort(nums);                         // [1, 2, 3, 5, 8, 9]  in-place
int idx = Arrays.binarySearch(nums, 8);    // 4  requires sorted array
int[] copy = Arrays.copyOf(nums, 4);       // [1, 2, 3, 5]
int[] range = Arrays.copyOfRange(nums, 2, 5); // [3, 5, 8]
Arrays.fill(copy, 0);                      // [0, 0, 0, 0]
boolean eq = Arrays.equals(nums, nums);    // true
System.out.println(Arrays.toString(nums)); // [1, 2, 3, 5, 8, 9]

Arrays.sort uses dual-pivot quicksort for primitives and timsort for objects. Both are O(n log n). binarySearch requires a sorted array calling it on an unsorted array gives undefined results with no error.

For 2D arrays, Arrays.deepToString and Arrays.deepEquals handle nested structure:

int[][] m = {{1, 2}, {3, 4}};
System.out.println(Arrays.deepToString(m));   // [[1, 2], [3, 4]]

Day 24 Wrapper Classes

Primitives vs. Wrappers

Every primitive has a corresponding wrapper class in java.lang:

PrimitiveWrapper
intInteger
longLong
doubleDouble
booleanBoolean
charCharacter
byteByte
shortShort
floatFloat

Collections like ArrayList work with objects, not primitives. Wrappers bridge that gap.

Autoboxing and Unboxing

The compiler automatically converts between primitives and their wrappers where needed:

List<Integer> list = new ArrayList<>();
list.add(42);          // autoboxing: int → Integer
int x = list.get(0);   // unboxing:   Integer → int

Autoboxing has overhead. In tight loops over large collections, this can matter. For performance-critical numeric code, consider primitive arrays instead.

Unboxing a null wrapper throws a NullPointerException:

Integer n = null;
int x = n;   // NullPointerException at runtime

Common Wrapper Methods

// Parsing
int i    = Integer.parseInt("42");
double d = Double.parseDouble("3.14");
boolean b = Boolean.parseBoolean("true");

// Converting to String
String s = Integer.toString(42);       // "42"
String s = String.valueOf(42);         // "42"  works for any primitive

// Constants
Integer.MAX_VALUE   // 2147483647
Integer.MIN_VALUE   // -2147483648
Double.MAX_VALUE
Double.isNaN(0.0 / 0.0)   // true

// Integer operations
Integer.toBinaryString(10)   // "1010"
Integer.toHexString(255)     // "ff"
Integer.bitCount(7)          // 3  number of set bits

// Comparison (safe, no overflow risk unlike subtraction)
Integer.compare(3, 5)   // negative (3 < 5)

Integer Cache

The JVM caches Integer objects for values in the range [-128, 127]. This means == on cached integers works but only in that range:

Integer a = 127;
Integer b = 127;
System.out.println(a == b);   // true  same cached object

Integer c = 128;
Integer d = 128;
System.out.println(c == d);   // false  different objects

This is why equals() is always the right choice for wrapper comparison.

Day 25 java.util.Objects

Objects (note the s) is a utility class of null-safe static helpers introduced in Java 7. It exists because many common operations on objects blow up with a NullPointerException when the object is null.

requireNonNull

Fails fast at the boundary rather than letting null propagate:

public class Service {
    private final Repository repo;

    public Service(Repository repo) {
        this.repo = Objects.requireNonNull(repo, "repo must not be null");
    }
}

Without this, a null repo would cause a NullPointerException deep inside some method call with no hint about where the null came from.

Null-Safe toString and equals

String s = null;
System.out.println(Objects.toString(s, "default"));   // "default"
System.out.println(s.toString());                      // NullPointerException

Objects.equals(null, null)    // true
Objects.equals(null, "hello") // false
Objects.equals("hello", null) // false  no NPE

hash and hashCode

Generates a hash from multiple fields use it when implementing hashCode() for a class:

public class Point {
    int x, y;

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point p)) return false;
        return x == p.x && y == p.y;
    }
}

The equals/hashCode Contract

The contract is what makes objects work correctly in collections:

  1. equals must be reflexive, symmetric, transitive, and consistent.
  2. If a.equals(b) then a.hashCode() == b.hashCode(). The reverse is not required (hash collisions are allowed).
  3. If you override equals, always override hashCode. Violating rule 2 breaks HashMap, HashSet, and any collection that uses hashing.
Map<Point, String> map = new HashMap<>();
Point p = new Point(1, 2);
map.put(p, "origin-ish");

// Without correct hashCode, lookup may fail even though equals returns true
map.get(new Point(1, 2));   // "origin-ish" only if hashCode is consistent with equals

Day 26 Packages and Imports

Organising Code with Packages

A package is a namespace. It maps to a directory structure on the filesystem:

// File: src/com/example/service/UserService.java
package com.example.service;

public class UserService {
    // ...
}

The convention is reverse-domain-name notation (com.company.module). The package declaration must be the first non-comment line in the file.

import

import com.example.service.UserService;   // single class
import com.example.service.*;             // all public types in the package (not subdirectories)

java.lang is imported automatically (String, System, Math, Object, etc.). Everything else requires an explicit import or fully-qualified name.

import static

Imports static members directly, letting you use them without the class prefix:

import static java.lang.Math.PI;
import static java.lang.Math.sqrt;
import static java.util.Arrays.sort;

double circumference = 2 * PI * radius;   // instead of Math.PI
double root = sqrt(16);                   // instead of Math.sqrt
sort(nums);                               // instead of Arrays.sort

Useful for constants and frequently-called utilities. Avoid wildcard static imports (import static X.*) in production code they make it impossible to tell where a name comes from.

Java Module System (JPMS, Java 9+)

The module system adds a layer above packages. A module is a named, self-describing group of packages with explicit dependencies:

// module-info.java at the root of the source tree
module com.example.app {
    requires com.example.service;   // declares dependency
    exports com.example.api;        // makes this package visible to other modules
}

Modules enforce strong encapsulation at the JVM level even reflection is blocked across module boundaries by default. Most projects don’t use the module system unless they’re building libraries or need runtime isolation. You’ll encounter it in the JDK itself (every java.* package is now in a module) but can ignore it for most application code.

Day 27 ArrayList and LinkedList

ArrayList

Backed by a resizable array. Random access by index is O(1). Insertion or deletion in the middle is O(n) because elements must shift:

List<String> names = new ArrayList<>();

names.add("Alice");
names.add("Bob");
names.add("Charlie");

names.get(1);               // "Bob"  O(1)
names.set(0, "Alicia");     // replace at index
names.remove("Bob");        // remove by value  O(n) scan + shift
names.remove(1);            // remove by index  O(n) shift
names.size();               // 2
names.contains("Charlie");  // true  O(n) scan
names.isEmpty();            // false

// Iteration
for (String name : names) {
    System.out.println(name);
}

// With index when you need it
for (int i = 0; i < names.size(); i++) {
    System.out.println(i + ": " + names.get(i));
}

Always declare the variable as List<T>, not ArrayList<T>. Programming to the interface lets you swap implementations without changing callers.

LinkedList

Backed by a doubly-linked list. Insertion and deletion at the head or tail are O(1). Random access by index is O(n) because the list must walk from one end.

LinkedList<Integer> queue = new LinkedList<>();

queue.addFirst(1);    // push to front
queue.addLast(2);     // push to back
queue.peekFirst();    // view front without removing
queue.pollFirst();    // remove and return front
queue.peekLast();
queue.pollLast();

LinkedList implements both List and Deque. It’s a natural fit when you need a queue or stack backed by an O(1) head/tail operations.

When to Use Which

OperationArrayListLinkedList
Random access get(i)O(1)O(n)
Add/remove at endO(1) amort.O(1)
Add/remove at middleO(n)O(n)
Add/remove at headO(n)O(1)
Memory overheadLowHigh (node pointers)

ArrayList is the right default for almost every case. Use LinkedList only when you specifically need O(1) insertion at both ends and don’t need random access.

Day 28 HashMap and HashSet

HashMap

Stores key-value pairs. Keys must be unique. null keys and values are allowed. Lookup, insertion, and deletion are O(1) average (O(n) worst case with many hash collisions):

Map<String, Integer> scores = new HashMap<>();

scores.put("Alice", 95);
scores.put("Bob", 82);
scores.put("Charlie", 91);
scores.put("Alice", 97);   // replaces the previous value for "Alice"

scores.get("Bob");           // 82
scores.getOrDefault("Dave", 0);  // 0  safe default when key is absent
scores.containsKey("Charlie");   // true
scores.containsValue(82);        // true
scores.remove("Bob");

// Iteration
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.println(entry.getKey() + " → " + entry.getValue());
}

// Merge  add to existing count, or start at 1
scores.merge("Alice", 1, Integer::sum);

HashSet

A set of unique elements backed by a HashMap. No duplicates, no guaranteed order. Add, remove, and contains are all O(1) average:

Set<String> seen = new HashSet<>();

seen.add("apple");
seen.add("banana");
seen.add("apple");   // duplicate  ignored silently

seen.contains("apple");    // true
seen.size();               // 2
seen.remove("banana");

Set<String> other = new HashSet<>(Set.of("apple", "cherry"));
seen.retainAll(other);    // intersection  seen is now {"apple"}

equals and hashCode Matter Here

HashMap and HashSet use hashCode() to find the bucket and equals() to identify the key within the bucket. If your key class has incorrect or missing equals/hashCode, lookups silently fail:

// Point without equals/hashCode override
Map<Point, String> map = new HashMap<>();
map.put(new Point(1, 2), "A");
map.get(new Point(1, 2));   // null  different objects, default hashCode differs

After adding equals and hashCode (see Day 25), the lookup works correctly.

Collision Basics

All keys with the same hashCode() % buckets land in the same bucket. Before Java 8, those buckets were linked lists O(n) per bucket in the worst case. From Java 8, buckets with more than 8 entries switch to a red-black tree, giving O(log n) per bucket. A good hash function keeps collisions rare enough that the average stays O(1).

Day 29 OOP Design Principles: SOLID

SOLID is five principles that guide class design toward code that’s easier to extend, test, and maintain.

S Single Responsibility Principle

A class should have one reason to change.

// Violation  Order does three unrelated things
public class Order {
    public void calculateTotal() { ... }
    public void saveToDatabase() { ... }   // persistence concern
    public void sendConfirmationEmail() { ... }  // notification concern
}

// Better  each class owns one concern
public class Order          { public void calculateTotal() { ... } }
public class OrderRepository { public void save(Order o) { ... } }
public class OrderNotifier   { public void sendConfirmation(Order o) { ... } }

O Open/Closed Principle

Open for extension, closed for modification. Add new behaviour without changing existing code.

// Every new shape requires modifying AreaCalculator
public class AreaCalculator {
    public double calculate(Object shape) {
        if (shape instanceof Circle c) return Math.PI * c.radius * c.radius;
        if (shape instanceof Rectangle r) return r.width * r.height;
        // must edit this class for every new shape
    }
}

// Better  each shape knows its own area
public interface Shape { double area(); }
public class Circle    implements Shape { ... }
public class Rectangle implements Shape { ... }

public class AreaCalculator {
    public double calculate(Shape shape) { return shape.area(); }
    // never needs to change for new shapes
}

L Liskov Substitution Principle

A subtype must be substitutable for its supertype without breaking correctness.

// Violation  Square overrides setWidth and setHeight together, breaking Rectangle invariants
public class Square extends Rectangle {
    @Override
    public void setWidth(int w)  { super.setWidth(w);  super.setHeight(w); }
    @Override
    public void setHeight(int h) { super.setWidth(h);  super.setHeight(h); }
}

// Code that works on Rectangle breaks when given a Square
Rectangle r = new Square();
r.setWidth(4);
r.setHeight(5);
assert r.area() == 20;   // fails: area is 25 because Square forces equal sides

The fix is not to inherit. Square and Rectangle should share a Shape interface, not a superclass relationship.

I Interface Segregation Principle

No client should be forced to depend on methods it doesn’t use. Prefer narrow, focused interfaces (see Day 19).

// Too wide  a basic user doesn't need admin methods
interface UserOperations {
    void login();
    void viewProfile();
    void deleteUser(int id);    // admin only
    void viewAllUsers();        // admin only
}

// Better
interface UserOperations  { void login(); void viewProfile(); }
interface AdminOperations { void deleteUser(int id); void viewAllUsers(); }

D Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions.

// Violation  high-level OrderService hard-coded to a specific low-level implementation
public class OrderService {
    private MySQLOrderRepository repo = new MySQLOrderRepository();  // concrete!
    public void process(Order o) { repo.save(o); }
}

// Better  depend on the interface, inject the implementation
public interface OrderRepository { void save(Order o); }

public class OrderService {
    private final OrderRepository repo;

    public OrderService(OrderRepository repo) {   // injected
        this.repo = repo;
    }

    public void process(Order o) { repo.save(o); }
}

Injecting the dependency makes OrderService testable with a mock repository and decoupled from any specific database.

Day 30 Phase 1 Project: Library Management System

Bringing all 29 days together into a coherent system. The design applies encapsulation, inheritance (where genuinely useful), interfaces, and the SOLID principles.

Domain Model

Book     — represents a book in the library
Member   — represents a registered library member
Loan     — records a single borrowing of a Book by a Member
LibraryService — coordinates the core operations

Book

public class Book {
    private final String isbn;
    private final String title;
    private final String author;
    private boolean available;

    public Book(String isbn, String title, String author) {
        this.isbn   = Objects.requireNonNull(isbn,   "isbn");
        this.title  = Objects.requireNonNull(title,  "title");
        this.author = Objects.requireNonNull(author, "author");
        this.available = true;
    }

    public String getIsbn()    { return isbn; }
    public String getTitle()   { return title; }
    public String getAuthor()  { return author; }
    public boolean isAvailable() { return available; }

    void setAvailable(boolean available) { this.available = available; }

    @Override
    public String toString() {
        return String.format("[%s] %s by %s (%s)",
            isbn, title, author, available ? "available" : "on loan");
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Book b)) return false;
        return isbn.equals(b.isbn);
    }

    @Override
    public int hashCode() { return Objects.hash(isbn); }
}

Member

public class Member {
    private static int nextId = 1;

    private final int id;
    private final String name;
    private final List<Loan> activeLoans = new ArrayList<>();

    public Member(String name) {
        this.id   = nextId++;
        this.name = Objects.requireNonNull(name, "name");
    }

    public int getId()     { return id; }
    public String getName() { return name; }

    public List<Loan> getActiveLoans() {
        return Collections.unmodifiableList(activeLoans);
    }

    void addLoan(Loan loan)    { activeLoans.add(loan); }
    void removeLoan(Loan loan) { activeLoans.remove(loan); }

    @Override
    public String toString() {
        return String.format("Member#%d (%s) — %d active loan(s)", id, name, activeLoans.size());
    }
}

Loan

public class Loan {
    private final Book book;
    private final Member member;
    private final LocalDate borrowedOn;
    private LocalDate returnedOn;

    public Loan(Book book, Member member) {
        this.book       = Objects.requireNonNull(book,   "book");
        this.member     = Objects.requireNonNull(member, "member");
        this.borrowedOn = LocalDate.now();
    }

    public Book   getBook()       { return book; }
    public Member getMember()     { return member; }
    public LocalDate getBorrowedOn() { return borrowedOn; }
    public boolean isActive()     { return returnedOn == null; }

    void close() { this.returnedOn = LocalDate.now(); }

    @Override
    public String toString() {
        return String.format("Loan[%s → %s, from %s%s]",
            book.getTitle(), member.getName(), borrowedOn,
            isActive() ? "" : ", returned " + returnedOn);
    }
}

LibraryService

public class LibraryService {
    private final Map<String, Book>   books   = new HashMap<>();
    private final Map<Integer, Member> members = new HashMap<>();
    private final List<Loan>          loans   = new ArrayList<>();

    public void addBook(Book book) {
        books.put(book.getIsbn(), book);
    }

    public void registerMember(Member member) {
        members.put(member.getId(), member);
    }

    public Loan borrow(String isbn, int memberId) {
        Book book = books.get(isbn);
        if (book == null) throw new IllegalArgumentException("Unknown ISBN: " + isbn);
        if (!book.isAvailable()) throw new IllegalStateException("Book is already on loan");

        Member member = members.get(memberId);
        if (member == null) throw new IllegalArgumentException("Unknown member: " + memberId);

        Loan loan = new Loan(book, member);
        book.setAvailable(false);
        member.addLoan(loan);
        loans.add(loan);
        return loan;
    }

    public void returnBook(String isbn, int memberId) {
        Loan loan = loans.stream()
            .filter(l -> l.isActive()
                      && l.getBook().getIsbn().equals(isbn)
                      && l.getMember().getId() == memberId)
            .findFirst()
            .orElseThrow(() -> new IllegalStateException("No active loan found"));

        loan.close();
        loan.getBook().setAvailable(true);
        loan.getMember().removeLoan(loan);
    }

    public List<Book> availableBooks() {
        return books.values().stream()
            .filter(Book::isAvailable)
            .toList();
    }

    public List<Loan> loanHistory() {
        return Collections.unmodifiableList(loans);
    }
}

Putting It Together

public class Main {
    public static void main(String[] args) {
        LibraryService library = new LibraryService();

        Book b1 = new Book("978-0-13-468599-1", "Effective Java",       "Joshua Bloch");
        Book b2 = new Book("978-0-13-110362-7", "The C Programming Language", "Kernighan & Ritchie");
        library.addBook(b1);
        library.addBook(b2);

        Member alice = new Member("Alice");
        Member bob   = new Member("Bob");
        library.registerMember(alice);
        library.registerMember(bob);

        Loan loan1 = library.borrow("978-0-13-468599-1", alice.getId());
        System.out.println(loan1);
        System.out.println(alice);

        library.returnBook("978-0-13-468599-1", alice.getId());
        System.out.println("Available: " + library.availableBooks().size());   // 2

        library.loanHistory().forEach(System.out::println);
    }
}

Design Decisions

  • setAvailable is package-private (no public) only LibraryService should toggle a book’s availability. Clients can read it via isAvailable() but can’t mutate it directly.
  • getActiveLoans returns an unmodifiable view prevents callers from bypassing LibraryService and directly modifying a member’s loan list.
  • ISBN is the identity of Book equals/hashCode are based on isbn alone, making it safe to use Book as a map key or set member.
  • static int nextId in Member the Day 8 class-vs-instance lesson applied: one counter shared across all member objects.
  • LibraryService owns the relationships borrow and return logic lives in one place (SRP), the service depends on abstractions it constructs internally, and each class handles only its own state.