Core and Web-based Java

Classes, Constructors, Encapsulation and Object Relationships

PGCP-AC

Classes define types whose objects combine state with operations. Good class design establishes valid construction, protects invariants, exposes intention-revealing behavior and models collaboration through clear ownership and lifetime relationships.

1. Objects and invariants

A class defines fields and behavior for its instances. Encapsulation keeps representation details behind a controlled interface so that operations can preserve invariants. A bank account, for example, should reject an invalid withdrawal through a method rather than expose its balance for arbitrary modification. Private fields help establish this boundary, but meaningful validation is still required.

A constructor initializes a new instance. It has the class name and no return type. Constructors may be overloaded; when no constructor is declared, the compiler supplies a default constructor subject to superclass requirements. Under Java 17 rules, a constructor invocation using this or super must be the first statement. Instance field initialization and constructor execution follow a defined order through the inheritance chain.

2. References and shared data

Java always passes arguments by value. A copied object reference still denotes the same object, so a method can mutate shared state; assigning a new reference to the parameter cannot replace the caller's variable. Static fields belong to the class rather than a particular object, while instance fields belong to individual objects. A static method has no instance this reference.

class Account {
    private int balance;
    Account(int initial) {
        if (initial < 0) throw new IllegalArgumentException();
        balance = initial;
    }
    int balance() { return balance; }
}

Association is a general relationship between objects. Aggregation describes a whole that refers to independently existing parts; composition describes a stronger ownership relationship. These are modeling distinctions rather than separate Java keywords. An employee belonging to a department need not inherit from Department. Choose inheritance for a valid substitutable is-a relationship and fields for has-a relationships.

3. Initialization and association example

Suppose Employee contains a Department reference. Constructing an Employee array creates slots for employee references, not an Employee in every slot. Each intended employee must be created and assigned. Creating an Employee that points to an existing Department does not clone that Department; several employees can share it deliberately.

Instance fields are initialized before the constructor completes and superclass construction precedes the subclass constructor's main body under the Java 17 model. Static initialization belongs to class initialization rather than to every object creation. This distinction explains why an object counter can be static while a particular employee's salary must normally be an instance field. Constructor validation should establish a usable state before the reference is published to other code, especially to other threads.

4. Class members and object state

A class declaration introduces a reference type. Its instance fields describe per-object state, instance methods describe operations on that state and constructors establish new instances.

public final class BankAccount {
    private final String number;
    private long balanceInPaise;

    public BankAccount(String number, long openingBalance) {
        if (number == null || number.isBlank()) {
            throw new IllegalArgumentException("Account number is required");
        }
        if (openingBalance < 0) {
            throw new IllegalArgumentException("Opening balance cannot be negative");
        }
        this.number = number;
        this.balanceInPaise = openingBalance;
    }

    public long balanceInPaise() { return balanceInPaise; }
}

Every account has its own number and balance. The private representation is measured in the smallest currency unit, so floating-point rounding does not enter ordinary balance arithmetic. The constructor prevents an invalid account from being created.

5. Encapsulation and invariants

Encapsulation is control over representation and state transitions, not merely marking fields private and generating getters and setters. An invariant is a condition that must remain true whenever an object is observably usable.

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

A public setBalance would allow callers to bypass these rules. Expose domain operations such as deposit and withdraw. Validation belongs at every public boundary that could violate the invariant; constructor-only validation is insufficient when later mutators exist.

6. Constructors

A constructor has the class name and no return type. Writing void Account() declares a method, not a constructor. Constructors can have access modifiers and can be overloaded by parameter list.

public BankAccount(String number) {
    this(number, 0);
}

If a class declares no constructor, the compiler supplies a default no-argument constructor whose accessibility follows the class. That generated constructor implicitly invokes the superclass no-argument constructor. Once any constructor is declared, the automatic default is not supplied.

A constructor can be private to control creation through factories. Abstract classes can have constructors because subclass construction must initialize the inherited portion.

7. Constructor chaining

this(...) delegates to another constructor in the same class. super(...) selects a superclass constructor. Under Java 17, an explicit constructor invocation must be the first statement and a constructor cannot contain both.

If neither appears, the compiler inserts an implicit super() when legal. If the superclass lacks an accessible no-argument constructor, the subclass constructor must explicitly select an accessible alternative.

Chaining should centralize validation and initialization rather than duplicate it. A recursive chain is rejected because it could never reach actual object initialization.

8. Instance initialization order

For ordinary class construction, reason in this order:

  1. Memory for the object is allocated and fields receive default values.
  2. The selected constructor begins by invoking another constructor.
  3. Superclass construction completes before subclass instance initialization.
  4. Instance field initializers and instance initializer blocks for the current class run in textual order.
  5. The current constructor body runs.
class Sample {
    private int value = initializeValue();
    { value += 2; }
    Sample() { value += 3; }
    private int initializeValue() { return 10; }
}

After field initialization, the block and the constructor body, value is 15. Do not call overridable instance methods from constructors; dynamic dispatch can reach subclass state before the subclass has initialized it.

9. this

Inside an instance context, this refers to the current receiver. It disambiguates fields from parameters and can pass the current object to another operation.

public void rename(String number) {
    // this.number = number; // impossible here because number field is final
}

There is no this in a static method because no receiver object is required to call it. A lambda captures the surrounding this; an anonymous class introduces its own receiver, a distinction relevant in callback code.

Avoid publishing this from a constructor to a global collection, callback or another thread. Other code could observe a partially initialized object.

10. Static members

A static field belongs to the class's runtime state rather than each instance. Use it for genuine class-wide information, not as a shortcut for global mutable state.

public final class Employee {
    private static long nextId = 1;
    private final long id;

    public Employee() {
        id = nextId++;
    }
}

This counter demonstrates shared state but is not thread-safe. Concurrent construction can lose updates. Production identity generation may require synchronization, an atomic type, a database sequence or another authority.

Static methods can directly access static members. They need an explicit object reference to access instance members. Call static methods through the class name to make ownership clear.

11. Method overloading

Overloading declares methods with the same name but different parameter lists. Selection occurs at compile time from the declared argument types and applicable conversions.

void print(int value) { }
void print(long value) { }
void print(String value) { }

Return type alone cannot distinguish overloads. Boxing, varargs and null can create surprising or ambiguous calls. Prefer names that express different semantics when overloads no longer represent one conceptual operation.

Overloading differs from overriding: overloading is compile-time selection among signatures; overriding is runtime selection of an instance method implementation in an inheritance hierarchy.

12. References and aliasing

BankAccount first = new BankAccount("A1", 1_000);
BankAccount alias = first;

first and alias are separate variables holding references to the same object. Mutation through one reference is visible through the other. first == alias compares reference identity. Assigning alias = null changes only that variable.

An object is not automatically copied when its reference is assigned, passed, returned or stored in a field. API documentation must state whether it retains, copies or transfers mutable data.

13. Java is pass-by-value

static void replace(BankAccount account) {
    account = new BankAccount("NEW", 0);
}

static void deposit(BankAccount account) {
    account.deposit(100);
}

Each parameter receives a copy of the argument value. replace assigns a different reference to its local parameter and leaves the caller variable unchanged. deposit uses its copied reference to reach the same object, so the object's state changes. Java does not switch to pass-by-reference for objects.

14. Association, aggregation and composition

An association means objects know about or interact with one another. Aggregation is a whole-part relationship in which the part can exist independently. Composition expresses strong ownership in which the whole controls the part's meaningful lifetime.

public final class Employee {
    private final Department department; // association/aggregation in this model
}

Several employees may refer to one independently existing department. Destroying an Employee does not logically destroy that Department.

A House creating and exclusively owning Room objects can be modeled as composition. Java has no aggregation or composition keyword; constructors, factories, visibility, mutation controls and lifecycle rules express the model.

15. Composition versus inheritance

Inheritance models a substitutable “is-a” relationship. Composition models “has-a” behavior or data. An Employee has a Department and must not extend Department merely for access reuse.

Composition reduces coupling when a class delegates to replaceable collaborators:

public final class PayrollService {
    private final TaxCalculator taxCalculator;
    public PayrollService(TaxCalculator taxCalculator) {
        this.taxCalculator = Objects.requireNonNull(taxCalculator);
    }
}

The collaborator can be supplied and tested independently. Use inheritance only when every subclass instance satisfies the parent contract and polymorphic substitution is meaningful.

16. Arrays of objects

Employee[] employees = new Employee[3];

This constructs one array containing three null references. It does not construct three Employee objects. Each employee must be created and assigned. Iterating and calling a method before filling every slot can produce NullPointerException.

Storing the same Employee reference in two slots creates aliases, not copies. A collection is often preferable when size changes dynamically.

17. Immutability

An immutable object exposes no observable state change after construction. A practical immutable class is commonly final, keeps fields private and final, validates construction, offers no mutators and controls mutable inputs and outputs.

public final class Course {
    private final String title;
    private final List<String> topics;

    public Course(String title, List<String> topics) {
        this.title = Objects.requireNonNull(title);
        this.topics = List.copyOf(topics);
    }
    public List<String> topics() { return topics; }
}

Final fields alone are insufficient if they reference mutable objects that callers can change. List.copyOf creates an unmodifiable snapshot under its element-reference semantics. Elements must also be immutable or defensively copied if their mutation would change the Course's observable state.

18. Defensive copying

Defensive copying prevents callers from retaining a mutable path into internal representation.

public final class Schedule {
    private final int[] slots;
    public Schedule(int[] slots) { this.slots = slots.clone(); }
    public int[] slots() { return slots.clone(); }
}

Clone is sufficient for an int array because components are primitive values. A nested array or collection of mutable elements requires copying at every level the ownership policy promises. Unmodifiable views prevent operations through one reference but may still reflect changes made through another reference to the backing collection.

19. Factory methods

A static factory is a named creation method:

public static BankAccount empty(String number) {
    return new BankAccount(number, 0);
}

Factories can communicate purpose, return cached instances, choose a subtype or validate before construction. Constructors remain appropriate when direct creation is clear. A factory does not automatically guarantee a new object; its contract should state identity and caching behavior where relevant.

20. Complete model example

public final class Order {
    private final String id;
    private final List<LineItem> items;

    public Order(String id, List<LineItem> items) {
        if (id == null || id.isBlank()) throw new IllegalArgumentException("id required");
        if (items == null || items.isEmpty()) throw new IllegalArgumentException("items required");
        this.id = id;
        this.items = List.copyOf(items);
    }

    public long totalInPaise() {
        return items.stream().mapToLong(LineItem::amountInPaise).sum();
    }
}

The constructor establishes identity and nonempty composition, the collection cannot be structurally modified through the returned reference and the total is derived rather than separately stored. Whether LineItem needs copying depends on its own immutability.

21. Practical considerations

  1. A constructor has no return type—not even void.
  2. A default constructor appears only when no constructor is declared.
  3. Constructor chaining must ultimately reach superclass construction.
  4. Static methods have no instance this.
  5. Private fields do not replace invariant-preserving operations.
  6. Java passes object references by value.
  7. Reference assignment aliases an object; it does not copy it.
  8. An object array initially contains null references.
  9. Aggregation and composition are modeling distinctions, not keywords.
  10. Final references can still point to mutable objects.

Continue learning

Related notes

Put this topic into timed practice

Open mock tests when you want full-exam pacing, or keep drilling in practice mode.