Notifications

No notifications

/Phase 2

Classes, Objects & Encapsulation

Your Own Types 🏗️

A class is a blueprint; an object is an instance built from it.

public class Point {
    // === fields (state) ===
    private double x;
    private double y;

// === constructor === public Point(double x, double y) { this.x = x; this.y = y; }

// === instance methods (behaviour) === public double distanceTo(Point other) { double dx = this.x - other.x; double dy = this.y - other.y; return Math.sqrt(dx * dx + dy * dy); }

// === getters / setters (encapsulation) === public double getX() { return x; } public double getY() { return y; } }

// usage Point a = new Point(0, 0); Point b = new Point(3, 4); System.out.println(a.distanceTo(b)); // 5.0

Records (Java 14+) — Immutable Data Carriers

public record Point(double x, double y) {}

Point p = new Point(3, 4); p.x(); // accessor (no get prefix) System.out.println(p); // Point[x=3.0, y=4.0] (auto toString)

A record auto-generates: constructor, accessors, equals, hashCode, toString, and is implicitly final & immutable.

Encapsulation — Private Fields, Public Methods

Hide internal state behind methods so the class controls how it's read/written.

public class BankAccount {
    private double balance;          // hidden
    public void deposit(double a) {
        if (a <= 0) throw new IllegalArgumentException();
        balance += a;
    }
    public double getBalance() { return balance; }
}

On this page

Detailed Theory

Anatomy of a Class

public class Student {
    // === Fields ===
    private String name;
    private int    age;
    private static int totalStudents = 0;     // shared by all instances

// === Constructors === public Student(String name, int age) { this.name = name; this.age = age; totalStudents++; }

public Student(String name) { this(name, 18); // delegate to the other constructor }

// === Instance methods === public String greet() { return "Hi, I'm " + name + " (" + age + ")"; }

// === Static method === public static int howMany() { return totalStudents; }

// === Getters / setters === public String getName() { return name; } public int getAge() { return age; } public void setAge(int age) { if (age < 0) throw new IllegalArgumentException(); this.age = age; }

// === toString — for debugging / logging === @Override public String toString() { return "Student{name='" + name + "', age=" + age + "}"; } }

new & the Object Lifecycle

Student s = new Student("Asha", 19);

1. Allocate memory on the heap. 2. Set fields to their defaults (0, null, false). 3. Run the constructor (with any explicit field initialisers first). 4. Return the reference.

When no references point to the object, the garbage collector reclaims its memory.

this

this is the implicit reference to the current instance. Used:

  • To disambiguate field vs parameter: this.x = x.
  • To call another constructor: this(args) — must be the first statement.
  • To return self for fluent chaining: return this.

Constructors

  • Same name as the class, no return type.
  • A class with no explicit constructor gets a free default no-arg constructor.
  • Once you write any constructor, the default disappears — define a no-arg explicitly if you still want one.
  • Multiple constructors via overloading; chain with this(...).
public Box() { this(0, 0, 0); }
public Box(int s) { this(s, s, s); }
public Box(int w, int h, int d) { /* full ctor */ }

Access Modifiers

ModifierClassPackageSubclassWorld
public
protected
(default)
private

Default rule: fields private, methods public unless you have a reason.

Static — Class-Level

public class MathUtil {
    public static final double PI = 3.14159;
    public static double square(double x) { return x * x; }
}
MathUtil.square(5);          // no instance needed

  • static fields are shared across all instances (and live forever).
  • static methods can't access this or non-static fields.
  • Use for utilities, constants and factory methods.

Encapsulation in Practice

Without encapsulation:

public class Counter {
    public int count;
}
c.count = -50;     // breaks invariant: counts shouldn't be negative

With encapsulation:

public class Counter {
    private int count;
    public void inc() { count++; }
    public int  get() { return count; }
}

The class now controls how its state changes — it can validate, log, fire events.

> Convention: getters return the value, setters validate and assign. Records remove this boilerplate when state should be immutable.

toString, equals, hashCode

Override toString() for readable logging:

@Override
public String toString() {
    return "Point(" + x + ", " + y + ")";
}

For value equality, override BOTH equals and hashCode — they go together. IntelliJ / VS Code generate them for you (Alt+Insert / right-click → Generate). Or use a record.

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Point p)) return false;       // pattern match (Java 16+)
    return Double.compare(p.x, x) == 0 && Double.compare(p.y, y) == 0;
}

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

Records — Skip the Boilerplate (Java 14+)

public record Point(double x, double y) {}
public record User(String name, int age, List<String> tags) {}

A record is:

  • Implicitly final.
  • All fields are private final.
  • Auto-generates: canonical constructor, accessors (x() not getX()), equals, hashCode, toString.
  • Can have additional methods, static factories and a compact constructor for validation:
public record Email(String value) {
    public Email {
        if (value == null || !value.contains("@"))
            throw new IllegalArgumentException();
        value = value.trim().toLowerCase();   // normalise
    }
}

> Use records for immutable data carriers (DTOs, return values, keys). Use full classes when you need mutable state, framework hooks, or inheritance.

Inner & Nested Classes (preview)

public class Outer {
    private int value = 10;

class Inner { int read() { return value; } } // inner — needs Outer instance static class Nested { int compute() { return 42; } } // static — independent }

Plus records, enums, anonymous classes, and (Java 16+) local records inside methods. Deeper coverage is the OOP topic.

Common Mistakes

BugFix
Forgot newUse new ClassName(args) (or factory method)
Static method using instance fieldMake method non-static or pass the instance
this(...) not first in constructorMove it to the first line
Mutable field accessible publiclyMake it private + provide controlled accessors
Records used for mutable stateRecords are immutable by design — use a regular class
Defining equals without hashCodeAlways override both (they're a contract)

Cheat-Sheet

NeedCode
Instancenew ClassName(args)
Field accessobj.field (if visible)
Method callobj.method(args)
Static callClassName.method(args)
Constructor chainthis(args) (first stmt)
Disambiguatethis.field = field
Hide internalsprivate fields, public accessors
Immutable recordrecord Foo(int a, int b) {}
Validate in recordcompact constructor
Override toString@Override public String toString()

You can now design your own types. Phase 2 done. Phase 3 next: inheritance and the OOP toolkit.