1. java
  2. /oop
  3. /access-modifiers

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:

  1. public - Accessible from anywhere
  2. protected - Accessible within the same package and by subclasses
  3. default (package-private) - Accessible within the same package only
  4. 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.