Master Java Access Modifiers and Encapsulation
Java Access Modifiers
Access modifiers in Java control the visibility and accessibility of classes, methods, variables, and constructors. They are essential for implementing encapsulation and controlling how different parts of your program interact with each other.
Overview of Access Modifiers
Java provides four access modifiers:
- public - Accessible from anywhere
- protected - Accessible within the same package and by subclasses
- default (package-private) - Accessible within the same package only
- private - Accessible within the same class only
public class AccessModifierDemo {
public String publicField = "Accessible everywhere";
protected String protectedField = "Accessible in package and subclasses";
String defaultField = "Accessible within package";
private String privateField = "Accessible within this class only";
public void publicMethod() {
System.out.println("Public method - accessible everywhere");
}
protected void protectedMethod() {
System.out.println("Protected method - accessible in package and subclasses");
}
void defaultMethod() {
System.out.println("Default method - accessible within package");
}
private void privateMethod() {
System.out.println("Private method - accessible within this class only");
}
}
Public Access Modifier
The public
modifier provides the widest accessibility - members can be accessed from any other class.
// File: PublicExample.java
package com.example.demo;
public class PublicExample {
public String name;
public int age;
public PublicExample(String name, int age) {
this.name = name;
this.age = age;
}
public void displayInfo() {
System.out.println("Name: " + name + ", Age: " + age);
}
public static void staticMethod() {
System.out.println("Public static method");
}
}
// File: AnotherClass.java
package com.example.different;
import com.example.demo.PublicExample;
public class AnotherClass {
public void usePublicClass() {
// Can access public class from different package
PublicExample example = new PublicExample("John", 25);
// Can access public fields
System.out.println("Name: " + example.name);
// Can access public methods
example.displayInfo();
PublicExample.staticMethod();
}
}
Protected Access Modifier
The protected
modifier allows access within the same package and by subclasses in other packages.
// File: Animal.java
package com.example.animals;
public class Animal {
protected String species;
protected int age;
protected Animal(String species, int age) {
this.species = species;
this.age = age;
}
protected void makeSound() {
System.out.println("Animal makes a sound");
}
protected void sleep() {
System.out.println(species + " is sleeping");
}
}
// File: Dog.java - Same package
package com.example.animals;
public class Dog extends Animal {
public Dog(int age) {
super("Canine", age); // Can access protected constructor
}
public void bark() {
makeSound(); // Can access protected method
System.out.println("Woof!");
}
public void displayInfo() {
System.out.println("Species: " + species); // Can access protected field
System.out.println("Age: " + age);
}
}
// File: Cat.java - Different package
package com.example.pets;
import com.example.animals.Animal;
public class Cat extends Animal {
public Cat(int age) {
super("Feline", age); // Can access protected constructor from subclass
}
public void meow() {
makeSound(); // Can access protected method from subclass
System.out.println("Meow!");
}
public void showDetails() {
System.out.println("Species: " + species); // Can access protected field
}
}
// File: SamePackageClass.java - Same package as Animal
package com.example.animals;
public class SamePackageClass {
public void testProtectedAccess() {
Animal animal = new Animal("Generic", 5); // Can access protected constructor
animal.makeSound(); // Can access protected method
System.out.println(animal.species); // Can access protected field
}
}
Default (Package-Private) Access Modifier
When no access modifier is specified, the default access level is package-private.
// File: PackageExample.java
package com.example.demo;
class PackageExample { // Default access - package-private class
String data; // Default access field
int value;
PackageExample(String data, int value) { // Default access constructor
this.data = data;
this.value = value;
}
void displayData() { // Default access method
System.out.println("Data: " + data + ", Value: " + value);
}
static void staticMethod() { // Default access static method
System.out.println("Package-private static method");
}
}
// File: SamePackageUser.java - Same package
package com.example.demo;
public class SamePackageUser {
public void usePackageClass() {
// Can access package-private class and members
PackageExample example = new PackageExample("Test", 42);
// Can access package-private fields
example.data = "Modified";
example.value = 100;
// Can access package-private methods
example.displayData();
PackageExample.staticMethod();
}
}
// File: DifferentPackageUser.java - Different package
package com.example.other;
// import com.example.demo.PackageExample; // Compilation error - cannot access
public class DifferentPackageUser {
public void attemptAccess() {
// PackageExample example = new PackageExample("Test", 42); // Error!
// Cannot access package-private class from different package
}
}
Private Access Modifier
The private
modifier restricts access to within the same class only.
public class BankAccount {
private String accountNumber;
private double balance;
private String accountHolder;
public BankAccount(String accountNumber, String accountHolder, double initialBalance) {
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
this.balance = initialBalance >= 0 ? initialBalance : 0;
}
// Private methods for internal operations
private boolean isValidAmount(double amount) {
return amount > 0;
}
private void logTransaction(String type, double amount) {
System.out.println("Transaction: " + type + " $" + amount +
" for account " + accountNumber);
}
// Public methods provide controlled access
public void deposit(double amount) {
if (isValidAmount(amount)) { // Can access private method
balance += amount; // Can access private field
logTransaction("Deposit", amount);
}
}
public boolean withdraw(double amount) {
if (isValidAmount(amount) && amount <= balance) {
balance -= amount;
logTransaction("Withdrawal", amount);
return true;
}
return false;
}
public double getBalance() {
return balance; // Controlled access to private field
}
public String getAccountHolder() {
return accountHolder;
}
// Private nested class
private static class TransactionLog {
private String timestamp;
private String description;
private TransactionLog(String description) {
this.timestamp = java.time.LocalDateTime.now().toString();
this.description = description;
}
private void print() {
System.out.println("[" + timestamp + "] " + description);
}
}
}
// Usage example
public class BankingDemo {
public static void main(String[] args) {
BankAccount account = new BankAccount("12345", "John Doe", 1000.0);
// Can access public methods
account.deposit(500.0);
System.out.println("Balance: $" + account.getBalance());
// Cannot access private fields or methods
// System.out.println(account.balance); // Compilation error
// account.logTransaction("Test", 100); // Compilation error
// account.isValidAmount(50); // Compilation error
}
}
Access Modifiers with Inheritance
Access modifiers behave differently in inheritance scenarios:
// Parent class
public class Vehicle {
public String brand;
protected String model;
String year; // package-private
private String engineNumber;
public Vehicle(String brand, String model, String year, String engineNumber) {
this.brand = brand;
this.model = model;
this.year = year;
this.engineNumber = engineNumber;
}
public void startEngine() {
System.out.println("Starting engine: " + engineNumber);
}
protected void performMaintenance() {
System.out.println("Performing maintenance on " + model);
}
void displayBasicInfo() {
System.out.println("Vehicle: " + brand + " " + model + " (" + year + ")");
}
private void internalDiagnostic() {
System.out.println("Running internal diagnostic...");
}
}
// Child class in same package
public class Car extends Vehicle {
private int doors;
public Car(String brand, String model, String year, String engineNumber, int doors) {
super(brand, model, year, engineNumber);
this.doors = doors;
}
public void displayCarInfo() {
// Can access public field
System.out.println("Brand: " + brand);
// Can access protected field
System.out.println("Model: " + model);
// Can access package-private field (same package)
System.out.println("Year: " + year);
// Cannot access private field
// System.out.println("Engine: " + engineNumber); // Error!
// Can access public method
startEngine();
// Can access protected method
performMaintenance();
// Can access package-private method (same package)
displayBasicInfo();
// Cannot access private method
// internalDiagnostic(); // Error!
}
}
// Child class in different package
package com.example.transportation;
import com.example.vehicles.Vehicle;
public class Truck extends Vehicle {
private double loadCapacity;
public Truck(String brand, String model, String year, String engineNumber, double loadCapacity) {
super(brand, model, year, engineNumber);
this.loadCapacity = loadCapacity;
}
public void displayTruckInfo() {
// Can access public field
System.out.println("Brand: " + brand);
// Can access protected field (inherited)
System.out.println("Model: " + model);
// Cannot access package-private field (different package)
// System.out.println("Year: " + year); // Error!
// Cannot access private field
// System.out.println("Engine: " + engineNumber); // Error!
// Can access public method
startEngine();
// Can access protected method (inherited)
performMaintenance();
// Cannot access package-private method (different package)
// displayBasicInfo(); // Error!
// Cannot access private method
// internalDiagnostic(); // Error!
}
}
Access Modifiers with Nested Classes
Access modifiers work differently with nested classes:
public class OuterClass {
private String outerPrivate = "Outer private";
protected String outerProtected = "Outer protected";
String outerDefault = "Outer default";
public String outerPublic = "Outer public";
private void outerPrivateMethod() {
System.out.println("Outer private method");
}
// Public nested class
public class PublicInnerClass {
public void accessOuter() {
// Inner class can access all outer class members, including private
System.out.println(outerPrivate);
System.out.println(outerProtected);
System.out.println(outerDefault);
System.out.println(outerPublic);
outerPrivateMethod();
}
}
// Private nested class
private class PrivateInnerClass {
private String innerPrivate = "Inner private";
public void innerMethod() {
System.out.println("Private inner class method");
System.out.println(outerPrivate); // Can access outer private members
}
}
// Protected nested class
protected class ProtectedInnerClass {
protected void protectedInnerMethod() {
System.out.println("Protected inner class method");
}
}
// Package-private nested class
class DefaultInnerClass {
void defaultInnerMethod() {
System.out.println("Default inner class method");
}
}
// Static nested class
public static class StaticNestedClass {
public void staticNestedMethod() {
// Static nested class cannot access non-static outer members directly
// System.out.println(outerPrivate); // Error!
OuterClass outer = new OuterClass();
System.out.println(outer.outerPrivate); // Can access through instance
}
}
public void createInnerInstances() {
PublicInnerClass publicInner = new PublicInnerClass();
PrivateInnerClass privateInner = new PrivateInnerClass();
ProtectedInnerClass protectedInner = new ProtectedInnerClass();
DefaultInnerClass defaultInner = new DefaultInnerClass();
publicInner.accessOuter();
privateInner.innerMethod();
protectedInner.protectedInnerMethod();
defaultInner.defaultInnerMethod();
}
}
// External usage
public class NestedClassUsage {
public void useNestedClasses() {
OuterClass outer = new OuterClass();
// Can access public nested class
OuterClass.PublicInnerClass publicInner = outer.new PublicInnerClass();
publicInner.accessOuter();
// Cannot access private nested class
// OuterClass.PrivateInnerClass privateInner = outer.new PrivateInnerClass(); // Error!
// Can access static nested class
OuterClass.StaticNestedClass staticNested = new OuterClass.StaticNestedClass();
staticNested.staticNestedMethod();
}
}
Best Practices
1. Principle of Least Privilege
public class GoodEncapsulation {
// Private fields - most restrictive by default
private String sensitiveData;
private int internalCounter;
// Protected for inheritance when needed
protected String inheritableProperty;
// Public only for external interface
public String publicProperty;
// Private constructor for utility class
private GoodEncapsulation() {
// Prevent instantiation
}
// Public factory method
public static GoodEncapsulation create(String data) {
GoodEncapsulation instance = new GoodEncapsulation();
instance.sensitiveData = data;
return instance;
}
// Private helper methods
private boolean isValid(String data) {
return data != null && !data.trim().isEmpty();
}
// Protected methods for subclasses
protected void performInternalOperation() {
internalCounter++;
}
// Public methods for external interface
public String getData() {
return sensitiveData;
}
public void setData(String data) {
if (isValid(data)) {
this.sensitiveData = data;
}
}
}
2. Consistent Access Patterns
public class User {
// All fields private
private String username;
private String email;
private int age;
// Public constructor
public User(String username, String email, int age) {
this.username = username;
this.email = email;
this.age = age;
}
// Public getters
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
public int getAge() {
return age;
}
// Public setters with validation
public void setEmail(String email) {
if (isValidEmail(email)) {
this.email = email;
}
}
public void setAge(int age) {
if (age >= 0 && age <= 150) {
this.age = age;
}
}
// Private validation methods
private boolean isValidEmail(String email) {
return email != null && email.contains("@");
}
}
3. Interface Design
// Public interface
public interface PaymentProcessor {
// All interface methods are implicitly public
boolean processPayment(double amount);
String getPaymentMethod();
}
// Implementation with appropriate access levels
public class CreditCardProcessor implements PaymentProcessor {
private String cardNumber;
private String cardHolder;
// Package-private constructor for factory pattern
CreditCardProcessor(String cardNumber, String cardHolder) {
this.cardNumber = cardNumber;
this.cardHolder = cardHolder;
}
// Public interface implementation
@Override
public boolean processPayment(double amount) {
return validateCard() && chargeCard(amount);
}
@Override
public String getPaymentMethod() {
return "Credit Card";
}
// Private implementation details
private boolean validateCard() {
return cardNumber != null && cardNumber.length() == 16;
}
private boolean chargeCard(double amount) {
// Implementation details hidden
return amount > 0 && amount <= getCreditLimit();
}
private double getCreditLimit() {
// Complex logic hidden from clients
return 10000.0;
}
}
Common Pitfalls
1. Overusing Public Access
// Bad: Everything public
public class BadExample {
public String data;
public int count;
public List<String> items;
public void process() {
// Public method doing everything
}
}
// Good: Appropriate access levels
public class GoodExample {
private String data;
private int count;
private List<String> items;
public String getData() {
return data;
}
public void addItem(String item) {
if (items == null) {
items = new ArrayList<>();
}
items.add(item);
count++;
}
public int getItemCount() {
return count;
}
public List<String> getItems() {
return new ArrayList<>(items); // Defensive copy
}
}
2. Protected vs Public Confusion
// Consider carefully when to use protected
public class BaseClass {
// Protected for subclass customization
protected void customizableMethod() {
// Subclasses can override this
}
// Private for internal use only
private void internalMethod() {
// Only this class should use this
}
// Public for external interface
public void publicMethod() {
customizableMethod();
internalMethod();
}
}
Summary
Access modifiers are crucial for:
- Encapsulation: Hiding implementation details
- Security: Controlling access to sensitive data
- Maintainability: Reducing coupling between classes
- API Design: Creating clear public interfaces
Guidelines:
- Start with the most restrictive access level (
private
) - Use
protected
for inheritance hierarchies - Use package-private for package cohesion
- Use
public
only for external API - Apply the principle of least privilege consistently
Proper use of access modifiers leads to more secure, maintainable, and well-designed Java applications.