Classes and Objects
Constructors, initialization blocks, static/final keywords, immutability and records
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;
}
Practical rules for static:
- Use it when the logic does not depend on object state.
- It is a natural fit for utility methods, factories, and shared constants.
- Be careful with mutable shared state because it often creates testing and concurrency problems.
2.5.1 Meaning and usage of `final`
The meaning of final depends on what it modifies: on a variable it means single assignment, on a method it prevents overriding, and on a class it prevents inheritance.
| Where is it used? | What does it mean? | Typical use-case |
|---|---|---|
final variable |
The reference is assigned once | Constants, dependencies, invariants |
final method |
Cannot be overridden | Stable template logic, safer APIs |
final class |
Cannot be extended | Immutable/value objects, utilities |
public final class Money {
private final BigDecimal amount;
public Money(BigDecimal amount) {
this.amount = amount;
}
public final BigDecimal amount() {
return amount;
}
}
An important distinction is that a final reference does not make the referenced object immutable. A final List<String> can still be mutated; only the reference itself cannot be pointed to another list.
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. Deep Dive
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. Interview Questions
What is the practical difference between a class and an object?
A class defines structure and behavior, while an object is the concrete runtime instance carrying actual state in memory.
Why do `equals()` and `hashCode()` matter for ordinary objects?
Because collections such as HashMap and HashSet rely on them for correct lookup and deduplication behavior.
When is a record a better fit than a regular class?
When the type is mainly an immutable data carrier with value-based equality and no complex lifecycle or mutable identity.
Why is defensive copying important?
Because exposing mutable internals lets outside code break invariants without going through the intended API.
9. 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 |
final |
A keyword expressing single assignment, no override, or no inheritance |
| 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 |
10. 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 - â
finalâ single assignment for variables, no overriding for methods, no inheritance for classes - â 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
13 questions