Lesson 08 · 9 min read · Java for placements

Encapsulation, access modifiers and packages

The four access levels and what package-private really means, why encapsulation is about invariants, and the four pillars answered with code.

This lesson is the one that turns "the four pillars of OOP" from a recited list into something you can demonstrate.

The four access modifiers

Modifier Same class Same package Subclass, other package Anywhere
private yes no no no
(none) — package-private yes yes no no
protected yes yes yes no
public yes yes yes yes

Two rows deserve attention.

No modifier is not the same as public. The default is package-private: visible to every class in the same package and to nothing outside it. This is a genuinely useful level — it lets classes that collaborate closely see each other's internals without exposing them to the world — and it is the level most students do not know exists.

protected includes package access. A protected member is visible to subclasses and to everything in the same package. It is wider than "subclasses only", which is what most people assume.

Where they can appear:

  • Classes: public or package-private only. A top-level class cannot be private or protected.
  • Members: all four.
  • Interface members: public by default; private allowed for helper methods since Java 9.

Encapsulation

Encapsulation is bundling data with the code that operates on it, and controlling access so the object's rules cannot be broken from outside.

The version most people write:

public class Student {
    private int marks;
    public int getMarks() { return marks; }
    public void setMarks(int marks) { this.marks = marks; }
}

That is not encapsulation. The field is reachable and writable by anyone; the getter and setter are a longer spelling of public int marks. Nothing is protected.

Encapsulation with a purpose:

public class Student {
    private final String roll;
    private final List<Integer> scores = new ArrayList<>();

    public Student(String roll) {
        if (roll == null || roll.isBlank()) {
            throw new IllegalArgumentException("roll is required");
        }
        this.roll = roll;
    }

    public void record(int score) {
        if (score < 0 || score > 100) {
            throw new IllegalArgumentException("out of range: " + score);
        }
        scores.add(score);
    }

    public double average() {
        return scores.isEmpty() ? 0
             : scores.stream().mapToInt(Integer::intValue).average().orElse(0);
    }

    public List<Integer> getScores() {
        return List.copyOf(scores);          // an unmodifiable copy
    }
}

Now there are rules the class enforces and nobody can go around: a roll number is never blank, a score is never out of range, and the internal list cannot be modified from outside. That last line matters — returning scores directly would hand a caller a reference to the object's own state:

// if getScores() returned the field itself:
student.getScores().add(999);      // the invariant is gone

This is the point to make if you are asked what encapsulation is for: it is about protecting invariants, not about writing accessors.

Immutable classes

The strongest form of encapsulation is an object that cannot change after construction. The recipe is asked for directly in interviews:

public final class Offer {                       // 1. final class — no subclass can weaken it
    private final String company;                // 2. all fields private and final
    private final int packageLpa;
    private final List<String> rounds;

    public Offer(String company, int packageLpa, List<String> rounds) {
        this.company = company;
        this.packageLpa = packageLpa;
        this.rounds = List.copyOf(rounds);       // 3. defensive copy IN
    }

    public String getCompany() { return company; }
    public int getPackageLpa() { return packageLpa; }

    public List<String> getRounds() {
        return rounds;                           // 4. already unmodifiable; else copy OUT
    }
}

Four rules: final class, final private fields, no setters, and defensive copies of any mutable field both in and out. Miss the copies and the class is not immutable — a caller keeps a reference to the list they passed in and edits it afterwards.

String, Integer and LocalDate are all built this way, which is why they are safe to share between threads and safe to use as HashMap keys.

Since Java 16, a record gives you most of this automatically — but not the defensive copies, so a record holding a List is still mutable through that list unless you copy in the constructor.

Packages

A package is a namespace and an access boundary:

package com.placedelite.assessments;      // must be the first statement in the file

import java.util.List;                    // one class
import java.util.*;                       // everything in the package (not subpackages)
import static java.lang.Math.max;         // a static member, used unqualified

Conventions worth following because everyone follows them: reverse domain name, all lowercase, and the directory structure must match the package. com.placedelite.assessments.Paper lives in com/placedelite/assessments/Paper.java.

java.lang is imported implicitly, which is why String, System, Integer and Math need no import.

Two classes with the same simple name from different packages need one to be fully qualified:

java.util.Date utilDate = new java.util.Date();
java.sql.Date sqlDate = new java.sql.Date(0);

The four pillars, with code

The list is easy; the demonstrations are what get marks. Keep one example of each ready.

1. Encapsulation — bundle state with behaviour and control access, so invariants hold.

private final List<Integer> scores;      // nobody outside can reach it
public void record(int score) { ... }    // one door, with a lock on it

2. Inheritance — a subclass reuses and specialises a parent.

class Student extends Person { }         // Student is a Person, plus a roll number

3. Polymorphism — one reference type, many behaviours, chosen by the object at run time.

for (Round r : rounds) System.out.println(r.marking());   // each round marks itself

4. Abstraction — expose what something does and hide how.

interface Gradable { int score(); }      // callers depend on the capability, not the class

The distinction interviewers probe is abstraction versus encapsulation, because they sound similar. The cleanest answer:

Abstraction is about the design — deciding what to expose. Encapsulation is about the implementation — enforcing that decision with access modifiers. Abstraction hides complexity; encapsulation hides data.

An interface is abstraction. private is encapsulation.

What to take into an interview

  • Four levels: private, package-private (no keyword), protected, public.
  • No modifier means package-private, not public. protected also grants package access.
  • Encapsulation protects invariants; getters and setters without validation are not encapsulation.
  • Never return a reference to a mutable internal field — copy, or return an unmodifiable view.
  • Immutable class: final class, private final fields, no setters, defensive copies in and out.
  • A package is a namespace and an access boundary; java.lang is imported implicitly.
  • Abstraction is what you expose; encapsulation is how you enforce it.
Lesson 8 of 12 · all 12 are free to read, no account. Course contents