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:
publicor package-private only. A top-level class cannot beprivateorprotected. - Members: all four.
- Interface members:
publicby default;privateallowed 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 goneThis 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 unqualifiedConventions 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 it2. Inheritance — a subclass reuses and specialises a parent.
class Student extends Person { } // Student is a Person, plus a roll number3. 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 itself4. Abstraction — expose what something does and hide how.
interface Gradable { int score(); } // callers depend on the capability, not the classThe 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.
protectedalso 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.langis imported implicitly. - Abstraction is what you expose; encapsulation is how you enforce it.