☕ Java
Access Modifiers
Access modifiers control the visibility of classes, fields, constructors, and methods. Java provides four access levels — public, protected, package-private (default), and private — each defining a boundary within which the member is visible. Choosing the right access modifier is not a security measure against malicious code; it is a design tool that communicates intent, enforces module boundaries, prevents accidental misuse, and keeps the public API of a class small and stable. This entry covers every access level in depth, their interaction with inheritance, module-level access in Java 9+, and the practical decision process for choosing the right modifier.
The Four Access Levels
Java's four access levels form a strict hierarchy from most restrictive to least restrictive: private, package-private (no keyword), protected, and public. Each level is a superset of the previous — protected includes everything package-private includes, and public includes everything protected includes.
private is the most restrictive level. A private member is visible only within the declaring class itself. Not to subclasses, not to other classes in the same package, only to the exact class that declares it. This is the level that enforces encapsulation most strictly. Private fields and methods are pure implementation details that can be changed or removed without affecting any external code.
Package-private (also called default access) is declared by the absence of any access keyword. A package-private member is visible to any class in the same package but invisible outside the package. It is the right level for implementation helpers that need to be shared among closely related classes in the same package but should not be part of the public API.
protected extends package-private by also granting access to subclasses regardless of which package they live in. A subclass in a different package can access a protected member through the subclass reference, but not through a reference of the superclass type — a subtlety that trips up many developers.
public makes a member visible from anywhere in the entire program — any package, any module (subject to module exports). Every public member is part of the class's API contract. Changing or removing a public method is a breaking change for any code that uses it.
Java
// ── The four access levels in one class ──────────────────────────────
public class AccessDemo {
// private — only THIS class can see or call this
private int secretValue = 42;
private void secretHelper() { System.out.println("internal"); }
// package-private — any class in the SAME PACKAGE can see this
int packageData = 10;
void packageMethod() { System.out.println("package"); }
// protected — same package AND any subclass (any package)
protected String sharedState = "shared";
protected void overridableMethod() { System.out.println("protected"); }
// public — visible EVERYWHERE, forms the public API
public String publicName = "visible everywhere";
public void publicMethod() { System.out.println("public"); }
}
// ── Access from the same class — all levels visible ───────────────────
public class AccessDemo {
private int x = 1;
public void demo() {
System.out.println(x); // private — always accessible
secretHelper(); // private method — accessible
}
}
// ── Access summary table ──────────────────────────────────────────────
//
// Modifier Same class Same package Subclass Any class
// ─────────────────────────────────────────────────────────────────────
// private YES NO NO NO
// (default) YES YES NO NO
// protected YES YES YES * NO
// public YES YES YES YES
//
// * Protected access through subclass reference only — see next section
// ── Applying access modifiers to classes ─────────────────────────────
// Top-level class: only public or package-private allowed
public class PublicClass { } // visible everywhere
class PackageClass { } // visible only in this package
// Inner / nested class: all four modifiers allowed
public class Outer {
public class PublicInner { }
protected class ProtectedInner { }
class PackageInner { }
private class PrivateInner { }
}private — The Foundation of Encapsulation
Private is the default choice for fields and the right default for methods unless there is a specific reason to widen access. Declaring a field private does not mean it cannot be read or written — it means that reading and writing must go through methods the class controls, where invariants can be enforced and behaviour can be changed without breaking external code.
Private methods are implementation helpers. They allow a class to decompose complex logic into smaller named pieces without exposing those pieces to callers. A private method can be renamed, split, merged, or deleted entirely without any concern about breaking other code. Public methods cannot.
A common misconception is that private means inaccessible — it does not. Code within the same class can access private members of other instances of the same class. This is intentional: it enables implementations like copy constructors and comparison methods that need to read the private fields of another instance of the same type.
Java
// ── Private fields with controlled access ────────────────────────────
public class BankAccount {
private final String id;
private double balance;
private int failedLoginAttempts;
private boolean locked;
public BankAccount(String id, double initialBalance) {
this.id = id;
this.balance = initialBalance;
}
// ── Controlled write — validates before modifying ─────────────────
public void deposit(double amount) {
validateAmount(amount); // private helper
balance += amount;
logTransaction("DEPOSIT", amount); // private helper
}
public void withdraw(double amount) {
validateAmount(amount);
if (amount > balance) throw new InsufficientFundsException();
balance -= amount;
logTransaction("WITHDRAWAL", amount);
}
// ── Read-only access to balance ───────────────────────────────────
public double getBalance() { return balance; }
public String getId() { return id; }
public boolean isLocked() { return locked; }
// ── Private methods — internal implementation details ─────────────
private void validateAmount(double amount) {
if (amount <= 0) throw new IllegalArgumentException(
"Amount must be positive, got: " + amount);
if (locked) throw new AccountLockedException("Account is locked");
}
private void logTransaction(String type, double amount) {
// Internal logging — not part of public API
System.out.printf("Account %s: %s %.2f (balance: %.2f)%n",
id, type, amount, balance);
}
// ── Private access across instances of the same class ─────────────
// A class can access ANOTHER INSTANCE'S private members
public boolean hasSameBalance(BankAccount other) {
return this.balance == other.balance; // other.balance is accessible
}
public void transferTo(BankAccount target, double amount) {
this.withdraw(amount);
target.balance += amount; // direct private field access — valid!
// This is legitimate — same class, different instance
}
}protected — Inheritance and Package Access
Protected access is the most nuanced level. It grants access to two distinct groups: any class in the same package (everything package-private grants), and any subclass, regardless of its package. The subclass access has an important restriction that is easy to miss: a subclass in a different package can only access a protected member through its own type, not through a reference to the superclass type.
Protected members are the extension points of a class — the parts designed to be overridden or accessed by subclasses. Template method patterns use protected to define hook methods that subclasses override. Abstract classes use protected to share implementation among subclasses without exposing it publicly. The guideline for protected is similar to public: treat it as part of the API, just an API for subclass authors rather than general callers. Changing a protected member is a breaking change for any subclass.
The alternative to protected for sharing with subclasses is to use private with well-defined public or package-private template methods that call the private implementations. This keeps the internals private while still allowing subclasses to customise behaviour through the public contract.
Java
// ── Protected in a class hierarchy ───────────────────────────────────
public abstract class Shape {
// ── Protected field — subclasses read it directly ─────────────────
protected String colour;
// ── Protected constructor — only subclasses and package use it ────
protected Shape(String colour) {
this.colour = colour;
}
// ── Public API ────────────────────────────────────────────────────
public abstract double area();
public abstract double perimeter();
public String getColour() { return colour; }
// ── Template method pattern — public template calls protected hook ─
public final String describe() {
return String.format(
"%s: area=%.2f perimeter=%.2f colour=%s",
getClass().getSimpleName(),
area(), perimeter(), colour);
}
// ── Protected utility — subclasses use it, external code does not ─
protected static double roundTo2dp(double value) {
return Math.round(value * 100.0) / 100.0;
}
}
// ── Subclass in the SAME package ────────────────────────────────────
class Circle extends Shape {
private final double radius;
Circle(double radius, String colour) {
super(colour); // protected constructor — accessible
this.radius = radius;
}
@Override public double area() {
return roundTo2dp(Math.PI * radius * radius); // protected method
}
@Override public double perimeter() {
return roundTo2dp(2 * Math.PI * radius);
}
public void setColour(String c) {
colour = c; // protected field — directly accessible in subclass
}
}
// ── Protected access restriction — different package ──────────────────
// package com.external;
// import com.shapes.Shape;
//
// class ExternalSubclass extends Shape {
//
// void demo(Shape other) {
// this.colour = "red"; // OK — through own type
// other.colour = "blue"; // COMPILE ERROR — not through supertype ref
// }
// }Package-Private — The Overlooked Level
Package-private access is under-used and under-appreciated. It defines a module boundary at the package level. Classes within the same package form a natural unit of related functionality — they can see each other's package-private members but present only the public API to the outside world. This is the right level for implementation classes, helper types, and internal utilities that exist to support the public-facing classes in the package but should not be independently accessible.
The package as a unit of encapsulation is a powerful design concept. A package can expose a clean public API of one or two classes while hiding a dozen implementation classes at package-private level. Callers in other packages see only the public API, not the internals. The internals can be freely refactored without any risk of breaking external code.
Java 9 modules (the module system, introduced as Project Jigsaw) extend this concept to the module level. A module can export specific packages, making their public members accessible outside the module, while keeping non-exported packages completely inaccessible even if they have public classes. This provides a much stronger encapsulation boundary than packages alone.
Java
// ── Package structure example ─────────────────────────────────────────
//
// com.myapp.payment/
// ├── PaymentService.java public — the API callers use
// ├── PaymentResult.java public — returned by PaymentService
// ├── PaymentGateway.java package-private — internal abstraction
// ├── StripeGateway.java package-private — Stripe implementation
// ├── PayPalGateway.java package-private — PayPal implementation
// └── PaymentValidator.java package-private — internal helper
// ── Public API — callers in other packages see this ───────────────────
public class PaymentService {
private final PaymentGateway gateway; // package-private type
private final PaymentValidator validator; // package-private type
// ── Even though the fields are package-private types,
// the constructor can be public — callers just can't name the types
public PaymentService() {
this.gateway = new StripeGateway(); // package-private class
this.validator = new PaymentValidator();
}
// ── Public API ────────────────────────────────────────────────────
public PaymentResult charge(String customerId, long amountCents) {
validator.validate(amountCents);
return gateway.charge(customerId, amountCents);
}
}
// ── Package-private implementation detail ─────────────────────────────
// Not accessible from com.myapp.orders or any other package
class PaymentGateway {
PaymentResult charge(String customerId, long amountCents) {
throw new UnsupportedOperationException("abstract");
}
}
class StripeGateway extends PaymentGateway {
@Override
PaymentResult charge(String customerId, long amountCents) {
// Stripe-specific logic — completely hidden from outside
return PaymentResult.success("stripe-" + System.nanoTime());
}
}
// ── Java 9 module system — stronger encapsulation ─────────────────────
// module-info.java
// module com.myapp.payment {
// exports com.myapp.payment; // public API visible outside
// // com.myapp.payment.internal NOT exported — fully hidden
// }Choosing Access Modifiers — The Decision Process
The right access modifier is almost never the first one that comes to mind. The instinct of many developers is to make things public until they have a reason not to — this is backwards. The correct instinct is to start as private as possible and widen only when there is a clear and specific reason.
The decision process follows a sequence of questions. Can this member be private? If there is no external code that needs it, make it private. If internal package collaborators need it, make it package-private. If subclasses need it, make it protected. Only make it public when it must be part of the external API.
The cost of widening access is asymmetric. Widening is free — making something public that was package-private is a backwards-compatible change that requires no code updates. Narrowing is expensive — making something public that was private may break every caller. This asymmetry means it is always better to start restrictive and widen later than to start permissive and try to narrow later.
The smallest public API is the best public API. Every public member is a commitment — to maintain it, to document it, to version it, and to support it for the lifetime of the class. Keeping the public API small reduces cognitive load for users, reduces the maintenance burden for implementors, and maximises the freedom to change the implementation in the future.
Java
// ── Decision flowchart in code comments ──────────────────────────────
//
// Does anything outside this CLASS need it?
// No → private
// Yes → Does anything outside this PACKAGE need it?
// No → package-private (no modifier)
// Yes → Does only a SUBCLASS need it?
// Yes → protected
// No → public
// ── Applied example: OrderProcessor ──────────────────────────────────
public class OrderProcessor {
// private — pure internal state, no one else should touch this
private final OrderRepository repository;
private final List<String> auditLog = new ArrayList<>();
// package-private — OrderValidator in the same package needs this
static final int MAX_ITEMS_PER_ORDER = 50;
// public — callers outside the package create OrderProcessor
public OrderProcessor(OrderRepository repository) {
this.repository = repository;
}
// public — external callers invoke this to process an order
public OrderResult process(Order order) {
validate(order); // calls private method
persist(order); // calls private method
auditLog.add(order.getId());
return OrderResult.success(order.getId());
}
// private — validation is an internal detail
// If we change the validation rules, no external code is affected
private void validate(Order order) {
if (order.getItems().size() > MAX_ITEMS_PER_ORDER) {
throw new OrderValidationException(
"Order exceeds " + MAX_ITEMS_PER_ORDER + " items");
}
if (order.getTotal().compareTo(BigDecimal.ZERO) <= 0) {
throw new OrderValidationException("Order total must be positive");
}
}
// private — persistence is an internal detail
private void persist(Order order) {
repository.save(order);
}
// package-private — OrderAuditor in same package reads the log
List<String> getAuditLog() {
return Collections.unmodifiableList(auditLog);
}
}
// ── Common mistakes to avoid ──────────────────────────────────────────
// 1. Public fields — never; always use private fields with accessors
// 2. Reflexive public — "it might need to be public someday"
// → start private; widen when the need actually arises
// 3. Exposing internal types — a public method returning a
// package-private type forces callers to use Object or cast
// 4. Protected fields — usually a mistake; prefer protected methodsAccess Modifiers and Inheritance
Access modifiers interact with method overriding through one critical rule: an overriding method cannot have more restrictive access than the method it overrides. A public method must be overridden with a public method. A protected method can be overridden with either protected or public. A package-private method can be overridden with package-private, protected, or public. This rule exists because a caller using a superclass reference must be able to call the method through any subclass — widening the access of an override is therefore safe.
The reason for this rule is substitutability — the Liskov Substitution Principle. If a caller can call method() on a Shape reference, and they receive a Circle (a subclass) at runtime, they must still be able to call method(). If Circle narrowed the access to private, the caller would see an exception or a compile error, violating the contract established by Shape.
Java
// ── Override rules — can widen, cannot narrow ────────────────────────
public class Animal {
public void breathe() { System.out.println("breathe"); }
protected void sleep() { System.out.println("sleep"); }
void move() { System.out.println("move"); } // pkg-private
private void heartbeat() { System.out.println("beat"); } // not inherited
}
public class Dog extends Animal {
// ── OK: public → public (same level) ─────────────────────────────
@Override
public void breathe() { System.out.println("dog breathes"); }
// ── OK: protected → public (widening is allowed) ─────────────────
@Override
public void sleep() { System.out.println("dog sleeps"); }
// ── OK: package-private → protected (widening) ───────────────────
@Override
protected void move() { System.out.println("dog moves"); }
// ── COMPILE ERROR: public → protected (narrowing not allowed) ────
// @Override
// protected void breathe() { } // ERROR: weaker access privileges
// ── heartbeat() is private in Animal — cannot be overridden ──────
// It can only be hidden (a new private method with same name):
private void heartbeat() {
System.out.println("dog heartbeat — NEW method, not override");
}
}
// ── Why narrowing is forbidden — substitutability ─────────────────────
Animal a = new Dog(); // polymorphism — Dog used as Animal
a.breathe(); // must work — Animal.breathe() is public
// Dog.breathe() must be at least public too
a.sleep(); // must work — Animal.sleep() is protected
// accessible from same package
// ── Constructors are NOT inherited ────────────────────────────────────
// Constructors do not override — access modifier on a constructor
// is independent per class. A subclass can have a public constructor
// even if the superclass constructor is protected.
public class RestrictedParent {
protected RestrictedParent() { } // subclasses and package only
}
public class OpenChild extends RestrictedParent {
public OpenChild() {
super(); // can call protected parent constructor
}
}
OpenChild c = new OpenChild(); // public — anyone can call
// RestrictedParent p = new RestrictedParent(); // ERROR if outside packageRelated Topics in Object-Oriented Programming
OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.