Java Academy

Master Java Development

Menu

Core Java

Servlets

JDBC

Microservices

Build Tools

APIs

Core Java · Lesson 8

Access Modifiers

Controlling Access with Modifiers

Access modifiers control where code can access a class, method, or field from. Java provides four levels: public (accessible everywhere), protected (accessible in same package and subclasses), package-private (accessible only in same package, the default), and private (accessible only in the same class). Proper use of access modifiers is fundamental to encapsulation and prevents other code from misusing your classes.

Public: Accessible from Anywhere

The public modifier means any code anywhere can access the class, method, or field. Use public for your main API — the interfaces others depend on. A public class must be in its own file with the same name. Every public class, method, and field is part of your code's contract; changing them later breaks other code relying on them.

// File: Calculator.java
public class Calculator {
    // Public method: anyone can call this
    public int add(int a, int b) {
        return a + b;
    }
    
    public int multiply(int a, int b) {
        return a * b;
    }
}

// File: MyApp.java
public class MyApp {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        int result = calc.add(5, 3);
        System.out.println(result);  // 8 - works fine
    }
}

Private: Hidden from Outside Access

The private modifier is the most restrictive — only the class itself can access it. Use private for implementation details you don't want others using directly. Private methods and fields give you freedom to change internal logic without breaking other code. This is the core of encapsulation.

public class BankAccount {
    private double balance;  // Private: others can't access directly
    private String accountNumber;
    
    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }
    
    // Private method: internal helper, not part of public API
    private void validateAmount(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
    }
    
    // Public method: controlled access to balance
    public double getBalance() {
        return balance;
    }
    
    public void deposit(double amount) {
        validateAmount(amount);  // Call private helper
        balance += amount;
    }
    
    public void withdraw(double amount) {
        validateAmount(amount);
        if (amount > balance) {
            throw new IllegalArgumentException("Insufficient funds");
        }
        balance -= amount;
    }
}

Protected: For Subclasses and Same Package

The protected modifier allows access from the same package and from any subclass, even if the subclass is in a different package. Use protected for methods and fields meant for subclasses to override or use. It's less restrictive than private but more controlled than public.

public class Animal {
    protected String name;  // Protected: subclasses can access
    protected int age;
    
    protected void makeSound() {  // Protected: subclasses can override
        System.out.println(name + " makes a sound");
    }
}

public class Dog extends Animal {
    public Dog(String name, int age) {
        this.name = name;  // Can access protected from parent
        this.age = age;
    }
    
    @Override
    protected void makeSound() {  // Override protected method
        System.out.println(name + " barks");
    }
}

Package-Private: Default Access Within a Package

When you don't specify any modifier (the default), a class or member is package-private — accessible only within its package. This is useful for internal classes you don't want exposed publicly. Use package-private for implementation details within a package.

// File: com/example/database/Database.java
package com.example.database;

// No modifier = package-private, can't be used outside this package
class DatabaseConnection {  // package-private class
    void connect() {
        System.out.println("Connecting to database");
    }
}

// File: com/example/database/UserRepository.java
package com.example.database;

public class UserRepository {  // public class
    private DatabaseConnection connection;  // package-private class
    
    void getUser(int id) {  // package-private method
        connection.connect();  // Can access within package
    }
}

// File: com/example/Main.java
package com.example;

public class Main {
    public static void main(String[] args) {
        // UserRepository repo = new UserRepository();  // OK: public class
        // DatabaseConnection conn = new DatabaseConnection();  // ERROR: not accessible
    }
}
ModifierSame ClassSame PackageSubclass (Different Package)Outside Package
private✓✗✗✗
(package-private)✓✓✗✗
protected✓✓✓✗
public✓✓✓✓

Access Modifier Rules and Restrictions

Every class member must have an access level. For classes, you can use only public or package-private (default). For methods and fields, you can use all four. Nested classes can use all four. A public class's public methods cannot return or accept private inner classes as parameters — that would violate access rules. Plan your API carefully: exposing something as public is hard to take back.

public class APIDesign {
    // Good: private implementation class
    private static class InternalProcessor {
        void process() { }
    }
    
    // Good: public interface/method is the contract
    public void doWork() {
        InternalProcessor processor = new InternalProcessor();
        processor.process();
    }
}

// Client code sees only the public method, not the internal implementation
class ClientCode {
    public static void main(String[] args) {
        APIDesign api = new APIDesign();
        api.doWork();  // Only this is accessible
    }
}

Common Mistake: Making everything public for convenience. It seems easier now ("they might need it"), but pollutes your API and locks you into backward compatibility. Future changes risk breaking client code. Be conservative: start private, expose as protected or public only when needed. You can always loosen restrictions later, but tightening them breaks existing code.

Best Practices for Access Modifiers

  • Make fields private by default: expose through getters/setters only if needed
  • Make methods public only if they're part of your intended API
  • Use protected for methods subclasses should override: not for random access
  • Prefer package-private for internal implementation classes: keeps public API small
  • A public class should have few public members: reduces coupling and surface area
  • Follow the principle of least privilege: give minimum access needed
  • Document why access levels were chosen: helps maintainers understand intent

Real-World Example: E-Commerce Library

A payment processing library has a public PaymentProcessor class with a public processPayment() method as its API. Internally, it uses package-private PaymentValidator, EncryptionHandler, and DatabaseConnector classes hidden from users. Fields like apiKey and database are private with no getters — completely hidden. Protected methods in a public abstract class PaymentGateway allow payment providers (customers' subclasses) to implement their own gateways. By carefully controlling visibility, the library's authors can refactor internals, fix bugs, and optimize without breaking client code that depends only on the public API.

Quiz Check

Quick Knowledge Test

Loading quiz questions...