Classes and Objects
Constructors, initialization blocks, immutability and records
Classes and Objects
The two cornerstones of object-oriented programming: the class as a blueprint and the object as its living instance.
1. Definition
What is it?
A class is a template (blueprint) that defines the data (fields) and behavior (methods) for a given type of object. An object is a concrete, living instance of that template, allocated on the heap.
Class vs object at a glance:
- Class â the blueprint: defines fields, methods, and constructors.
- Object â the instance: stores concrete values such as
name = "Alice"andage = 30, and lives on the heap.
Why does it exist?
- Abstraction: models real-world concepts in code
- Encapsulation: bundles data with the operations that act on it
- Reusability: write a class once â create unlimited instances from it
- Type safety: the compiler verifies that an object truly matches its declared type
Where does it fit?
This is the foundation of the OOP paradigm â every other concept (inheritance, polymorphism, design patterns) builds on classes and objects. In Java, nearly everything is a class (except primitives).
2. Core Concepts
2.1 Class Anatomy
A Java class can contain the following elements:
public class Employee {
// 1. Fields (state)
private String name;
private int age;
private static int employeeCount = 0; // class-level
// 2. Static initializer block
static {
System.out.println("Class loaded");
}
// 3. Instance initializer block
{
employeeCount++;
}
// 4. Constructor
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
// 5. Methods
public String getName() { return name; }
// 6. Static method
public static int getEmployeeCount() { return employeeCount; }
// 7. Inner class
public class Badge {
private String badgeId;
}
// 8. Enum
public enum Department { ENGINEERING, HR, FINANCE }
}
2.2 Access Modifiers
| Modifier | Within Class | Within Package | Subclass | Everywhere |
|---|---|---|---|---|
private |
â | â | â | â |
| (default) | â | â | â | â |
protected |
â | â | â | â |
public |
â | â | â | â |
Rule of thumb: fields should be
private, getter/setter methods should bepublicâ this is the basis of encapsulation.
2.3 Object Creation
// 1. new keyword + constructor (most common)
Employee emp = new Employee("Alice", 30);
// 2. Reflection
Employee emp2 = Employee.class.getDeclaredConstructor(String.class, int.class)
.newInstance("Bob", 25);
// 3. Clone (if Cloneable is implemented)
Employee emp3 = (Employee) emp.clone();
// 4. Deserialization
// ObjectInputStream â readObject()
// 5. Factory method (recommended pattern)
Employee emp4 = Employee.of("Charlie", 28);
Object lifecycle:
- Allocation â
newâ memory on the heap - Initialization â instance initializer block â constructor
- Usage â reachable via reference
- Unreachable â no references point to it
- Garbage Collection â GC reclaims the memory
2.4 The `this` Keyword
this refers to the current object instance:
public class Account {
private double balance;
public Account(double balance) {
this.balance = balance; // field vs parameter disambiguation
}
public Account deposit(double amount) {
this.balance += amount;
return this; // fluent API / method chaining
}
public Account() {
this(0.0); // constructor chaining
}
}
// Usage: fluent API
Account acc = new Account(100).deposit(50).deposit(25);
2.5 Static vs Instance
| Property | static |
Instance |
|---|---|---|
| Belongs to | The class | The object |
| Memory | Single copy in Method Area | One copy per object on the heap |
| Access | ClassName.method() |
object.method() |
this accessible? |
â | â |
| Typical use-case | Utility methods, counters, factories | State-dependent behavior |
// Static: utility method â no state, no side effects
public static double celsiusToFahrenheit(double celsius) {
return celsius * 9.0 / 5.0 + 32;
}
// Instance: depends on object state
public double getBalance() {
return this.balance;
}
2.6 Records (Java 16+)
A record is an immutable data carrier â Java's answer to the boilerplate problem:
// Traditional class: ~40 lines (fields, constructor, getters, equals, hashCode, toString)
// Record: 1 line
public record Point(int x, int y) { }
The compiler automatically generates:
private finalfields- All-args constructor (canonical constructor)
- Accessor methods (
x(),y()â notgetX()) equals(),hashCode(),toString()
// Custom validation with compact constructor
public record Email(String value) {
public Email { // compact constructor â no parameter list
if (!value.contains("@")) {
throw new IllegalArgumentException("Invalid email: " + value);
}
value = value.toLowerCase(); // normalization
}
}
Limitations:
- Cannot be extended (implicitly
final) - Cannot have mutable fields (implicitly
final) - Cannot be abstract
- Can implement interfaces
2.7 Sealed Classes (Java 17+)
A sealed class restricts which classes can inherit from it:
public sealed class Shape permits Circle, Rectangle, Triangle {
// shared logic
}
public final class Circle extends Shape { // final: cannot be extended further
private final double radius;
public Circle(double radius) { this.radius = radius; }
}
public non-sealed class Rectangle extends Shape { // non-sealed: open for extension
protected double width, height;
}
public sealed class Triangle extends Shape permits EquilateralTriangle {
// sealed: can further restrict subclasses
}
Why is it useful?
- Enables exhaustive checks with pattern matching (
switch) - Closed type hierarchy â safer domain modeling
- Algebraic data types (ADT) in Java
2.8 Inner and Nested Classes
| Type | Declaration | static? |
Can access outer state? |
|---|---|---|---|
| Static nested class | static class Inner {} |
Yes | Only static members |
| Inner class | class Inner {} |
No | Yes, everything |
| Local class | Inside a method | No | Yes + effectively final locals |
| Anonymous class | new Interface() { } |
No | Yes + effectively final locals |
// Static nested class â common pattern (Builder)
public class Pizza {
private final String dough;
private final String topping;
public static class Builder {
private String dough;
private String topping;
public Builder dough(String d) { this.dough = d; return this; }
public Builder topping(String t) { this.topping = t; return this; }
public Pizza build() { return new Pizza(this); }
}
private Pizza(Builder b) {
this.dough = b.dough;
this.topping = b.topping;
}
}
2.9 Enums
An enum is a special class with a fixed set of predefined instances:
public enum Status {
ACTIVE("Active"),
INACTIVE("Inactive"),
SUSPENDED("Suspended");
private final String displayName;
Status(String displayName) { // constructor is implicitly private
this.displayName = displayName;
}
public String getDisplayName() { return displayName; }
}
// Usage
Status s = Status.ACTIVE;
String name = s.getDisplayName(); // "Active"
Enums are implicitly
final, extendjava.lang.Enum, and can have abstract methods (if every constant implements them).
3. Practical Usage
When to Use What?
| Situation | Choice | Why? |
|---|---|---|
Stateful, mutable entity (e.g., User, Order) |
Class | Mutable state needed |
Simple data carrier, immutable (e.g., DTO, Event) |
Record | Eliminates boilerplate, guarantees immutability |
Fixed, closed set of values (e.g., Status, Direction) |
Enum | Type-safe, singleton-like |
| Closed type hierarchy, pattern matching | Sealed class | Exhaustive switch, domain safety |
| Utility methods, no state | Static methods (utility class) | No instance needed |
Decision Tree
Quick decision guide:
- If you need mutable state, use a class.
- Otherwise, if you have a finite and known value set, use an enum.
- Otherwise, if you need a simple immutable data carrier, use a record.
- Otherwise, if you need a closed hierarchy, use a sealed class.
- If none of those apply, use a regular class or interface.
4. Code Examples
4.1 Basic â Simple Class with Constructor
public class BankAccount {
private final String iban;
private double balance;
public BankAccount(String iban, double initialBalance) {
if (iban == null || iban.isBlank()) {
throw new IllegalArgumentException("IBAN cannot be blank");
}
this.iban = iban;
this.balance = initialBalance;
}
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
this.balance += amount;
}
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
if (amount > balance) throw new IllegalStateException("Insufficient funds");
this.balance -= amount;
}
// Getters â no setter for iban (immutable field)
public String getIban() { return iban; }
public double getBalance() { return balance; }
}
4.2 Advanced â Records, Sealed Classes, Builder Pattern
// 1. Record as Value Object
public record Money(BigDecimal amount, Currency currency) {
public Money {
Objects.requireNonNull(amount, "amount must not be null");
Objects.requireNonNull(currency, "currency must not be null");
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), this.currency);
}
}
// 2. Sealed class hierarchy
public sealed interface PaymentMethod permits CreditCard, BankTransfer, Wallet {
Money maxLimit();
}
public record CreditCard(String number, Money maxLimit) implements PaymentMethod { }
public record BankTransfer(String iban, Money maxLimit) implements PaymentMethod { }
public record Wallet(String walletId, Money maxLimit) implements PaymentMethod { }
// 3. Pattern matching switch (Java 21+)
public String describe(PaymentMethod pm) {
return switch (pm) {
case CreditCard cc -> "Card ending in " + cc.number().substring(cc.number().length() - 4);
case BankTransfer bt -> "Bank transfer to " + bt.iban();
case Wallet w -> "Wallet " + w.walletId();
};
}
// 4. Builder pattern (static nested class)
public class HttpRequest {
private final String url;
private final String method;
private final Map<String, String> headers;
private final String body;
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = Map.copyOf(builder.headers); // defensive copy
this.body = builder.body;
}
public static class Builder {
private final String url; // required
private String method = "GET"; // optional, default
private final Map<String, String> headers = new HashMap<>();
private String body;
public Builder(String url) { this.url = url; }
public Builder method(String m) { this.method = m; return this; }
public Builder header(String k, String v) { headers.put(k, v); return this; }
public Builder body(String b) { this.body = b; return this; }
public HttpRequest build() { return new HttpRequest(this); }
}
}
// Usage
HttpRequest req = new HttpRequest.Builder("https://api.example.com")
.method("POST")
.header("Content-Type", "application/json")
.body("{\"key\":\"value\"}")
.build();
5. Trade-offs
| Aspect | Option A | Option B | When to choose? |
|---|---|---|---|
| Mutability | Mutable class | Immutable class / Record | Immutable when no state change is needed (thread-safe, simpler reasoning) |
| Class vs Record | Traditional class | Record | Record for pure data carriers, class for behavior-rich entities |
| Inheritance vs Composition | extends |
Field injection / delegation | Favor composition â inheritance creates tight coupling |
| Inner class vs top-level | Inner class | Separate file | Inner when only the outer class uses it (e.g., Builder); top-level when independently meaningful |
| Enum vs Sealed class | Enum | Sealed class | Enum for fixed instances, sealed when each subtype has different structure |
6. Common Mistakes
â 1. Missing or Inconsistent `equals()` and `hashCode()`
// BAD: only equals() overridden, hashCode() missing
public class UserId {
private final String value;
@Override
public boolean equals(Object o) { /* ... */ }
// hashCode() missing â object may be lost in HashMap!
}
// GOOD: both overridden consistently
@Override
public int hashCode() {
return Objects.hash(value);
}
â 2. Mutable Fields in Records
// BAD: List passed by reference â can be modified externally!
public record Team(String name, List<String> members) { }
Team t = new Team("Dev", new ArrayList<>(List.of("Alice")));
t.members().add("Hacker"); // đ± mutation!
// GOOD: defensive copy in compact constructor
public record Team(String name, List<String> members) {
public Team {
members = List.copyOf(members); // unmodifiable copy
}
}
â 3. Misusing `static` Fields as Shared State
// BAD: mutable static field â thread-safety problem
public class Counter {
public static int count = 0; // data race!
}
// GOOD: AtomicInteger or proper synchronization
public class Counter {
private static final AtomicInteger count = new AtomicInteger(0);
}
â 4. Exposing Internal State
// BAD: returning a mutable list
public List<String> getItems() { return items; }
// GOOD: return an unmodifiable view or defensive copy
public List<String> getItems() { return Collections.unmodifiableList(items); }
7. Senior-level Insights
7.1 Object Identity vs Equality
String a = new String("hello");
String b = new String("hello");
a == b // false â different references (identity)
a.equals(b) // true â same value (equality)
==: reference comparison (same object on the heap?).equals(): value comparison (do the contents match?)
Interview tip: any class used as a
HashMapkey orHashSetelement must have a consistentequals()+hashCode()implementation.
7.2 Defensive Copying
Internal mutable references in immutable objects must be copied on both input and output:
public class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
// Defensive copy on input
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.after(this.end)) {
throw new IllegalArgumentException("start after end");
}
}
public Date getStart() {
return new Date(start.getTime()); // defensive copy on output
}
}
Best practice: use immutable types (
LocalDateinstead ofDate) and you won't need defensive copies.
7.3 Value Objects (DDD)
Domain-Driven Design (DDD) distinguishes between:
| Concept | Identity | Mutable? | Example |
|---|---|---|---|
| Entity | Has one (id) | Yes | User, Order |
| Value Object | None â identified by value | No | Money, Address, Email |
| Aggregate | Root entity + child entities | Mixed | Order + OrderLines |
// Value Object with record â natural fit
public record Address(String city, String street, String zip) { }
// Two Addresses are equal if their content matches â no separate id needed
Address a1 = new Address("Budapest", "Main St 1", "1011");
Address a2 = new Address("Budapest", "Main St 1", "1011");
a1.equals(a2); // true â record generates equals automatically
7.4 The `Class` Object and Reflection
Every Java class has an associated java.lang.Class<?> object:
Class<?> clazz = Employee.class; // compile-time
Class<?> clazz2 = emp.getClass(); // runtime
Class<?> clazz3 = Class.forName("com.example.Employee"); // dynamic
// Reflection: query fields, methods, annotations
Field[] fields = clazz.getDeclaredFields();
Caution: reflection is slow and breaks encapsulation â rarely used directly in production code (but frameworks use it heavily).
8. Glossary
| Term | Meaning |
|---|---|
| Class | A template (blueprint) that defines data structure and behavior |
| Object | A concrete, heap-allocated instance of a class |
| Instance | = Object; one realization of a class |
| Constructor | A special method responsible for initializing an object |
| Field | A data member (member variable) of a class |
| Method | A function that defines the behavior of a class |
| Access modifier | Visibility control: private, default, protected, public |
this |
A reference pointing to the current object instance |
static |
A class-level member â belongs to the class, not to an individual object |
| Record | An immutable data carrier class with automatic equals/hashCode/toString (Java 16+) |
| Sealed class | A class that defines a closed inheritance hierarchy (Java 17+) |
| Inner class | A non-static class defined inside another class |
| Enum | A special class with a fixed set of instances |
| Value Object | DDD concept â an object identified by its value, not by an identity field |
9. Cheatsheet
- â A class is a blueprint; an object is a concrete instance of it on the heap
- â
Use
privatefields +publicgetters/setters (encapsulation) - â
Always override both
equals()ANDhashCode()if objects go into Collections - â Use records for simple, immutable data carriers (Java 16+)
- â Use sealed classes to model closed type hierarchies (Java 17+)
- â Enum: fixed, finite set of values â type-safe, singleton-like
- â
thisâ current object reference; use for fluent APIs and constructor chaining - â
staticâ class-level member; for utility methods, factories, constants - â Favor composition over inheritance
- â Defensive copying: copy mutable references on both input and output
- â Value Objects (DDD): records are a natural fit for identity-less concepts
- â Builder pattern: use a static nested class when there are many optional parameters
- â ïž
==compares references,.equals()compares values â don't confuse them! - â ïž Mutable collections in records must be protected with
List.copyOf()/Map.copyOf()
đź Games
8 questions