Access Modifiers (public, private, protected)

Reviewed & published by Brayan K

Good objects hide their inner workings and expose only what's safe to touch. Access modifiers are the keywords that draw that boundary — they decide who can read and change each field, method, and class. Master them and you master encapsulation.

Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

Before You Start

You'll want to be comfortable with classes and objects (fields, methods, constructors) and have seen inheritance, since protected only makes sense alongside subclasses.

What You'll Learn

The Big Idea — Doors in a House

💡 Analogy: Think of a class as a house. public members are the front door — anyone may walk through. protected is a door for family (subclasses) and neighbours in the same street (package). Package-private is a gate only your street's neighbours can open. private is your bedroom safe — only you, inside the house, can open it. Encapsulation is choosing the right door for each room so visitors can't wander into the wiring.

The golden rule: make everything as private as you can, and only open it up when you have a concrete reason. A small public surface is easier to use, test, and change without breaking callers.

1️⃣ The Four Access Levels

Java has exactly four levels. Note that the third one — package-private — has no keyword: you get it by writing nothing at all.

ModifierSame classSame packageSubclass (other pkg)Anywhere
public✅✅✅✅
protected✅✅✅❌
(default)✅✅❌❌
private✅❌❌❌
public class Main {
    static class Demo {
        public    String pub  = "anyone can read me";
        protected String prot = "package + subclasses";
        String           pkg  = "package-private (no keyword)";
        private   String priv = "only inside Demo";

        public void show() {
            // Inside the SAME class, every level is reachable:
            System.out.println("public   : " + pub);
            System.out.println("protected: " + prot);
            System.out.println("default  : " + pkg);
            System.out.println("private  : " + priv);
        }
    }

    public static void main(String[] args) {
        Demo d = new Demo();
        d.show();

        // From another class in the SAME package we can reach all but private:
        System.out.println("Read from outside -> public: " + d.pub);
        System.out.println("Read from outside -> default: " + d.pkg);
        // System.out.println(d.priv);  // <- will NOT compile
    }
}

2️⃣ Encapsulation — Why Hide State?

Encapsulation means keeping an object's data private and exposing only safe, controlled methods to work with it. If a bank account's balance were public, any code could set it to -9999 and break the rules. Making it private forces every change to go through deposit and withdraw, which can enforce "no overdrafts" and "no negative deposits".

The example below is the classic case: the field is locked away, and the public methods are the only door in.

public class Main {
    // A class that protects its internal state with 'private'
    static class BankAccount {
        private double balance;          // hidden — only this class touches it

        public BankAccount(double opening) {
            this.balance = opening;
        }

        // public methods are the controlled "door" into the object
        public void deposit(double amount) {
            if (amount > 0) balance += amount;
        }

        public boolean withdraw(double amount) {
            if (amount > 0 && amount <= balance) {
                balance -= amount;
                return true;
            }
            return false;                // reject overdraft
        }

        public double getBalance() {     // read-only access (a "getter")
            return balance;
        }
    }

    public static void main(String[] args) {
        BankAccount acc = new BankAccount(100.0);
        acc.deposit(50.0);
        System.out.println("After deposit: " + acc.getBalance());

        boolean ok = acc.withdraw(200.0);   // too much — rejected
        System.out.println("Withdraw 200 succeeded? " + ok);
        System.out.println("Balance still: " + acc.getBalance());

        // acc.balance = -999;   // <- would NOT compile: balance is private
    }
}

3️⃣ Getters & Setters with Validation

A getter returns a private field; a setter changes it. The reason they exist — instead of a public field — is that a setter can validate before storing. You can also expose a getter with no setter to make a field read-only, or compute a value on the fly that has no backing field at all.

public double getCelsius() { return celsius; }           // read
public void setCelsius(double c) {                       // write, validated
    if (c < -273.15) throw new IllegalArgumentException("too cold");
    this.celsius = c;
}

Naming convention: getters are getX() (or isX() for booleans) and setters are setX(value). Many frameworks (and the records feature) rely on this convention.

public class Main {
    static class Temperature {
        private double celsius;

        public double getCelsius() { return celsius; }

        // A setter can VALIDATE before storing — the whole point of private
        public void setCelsius(double c) {
            if (c < -273.15) {
                throw new IllegalArgumentException("Below absolute zero!");
            }
            this.celsius = c;
        }

        // A derived, read-only property — no field needed
        public double getFahrenheit() {
            return celsius * 9 / 5 + 32;
        }
    }

    public static void main(String[] args) {
        Temperature t = new Temperature();
        t.setCelsius(25);
        System.out.println(t.getCelsius() + "C = " + t.getFahrenheit() + "F");

        try {
            t.setCelsius(-300);          // invalid — guarded by the setter
        } catch (IllegalArgumentException e) {
            System.out.println("Rejected: " + e.getMessage());
        }
        System.out.println("Still safe at " + t.getCelsius() + "C");
    }
}

4️⃣ protected — For Subclasses

protected opens a member to subclasses (and to the same package), while still hiding it from unrelated outside code. Use it for the parts of a parent class that a child legitimately needs to read or override — but keep truly internal details private, hidden even from subclasses.

public class Main {
    static class Animal {
        protected String name;           // subclasses may use this directly
        private int secretId = 7;        // hidden even from subclasses

        Animal(String name) { this.name = name; }

        protected String describe() {    // subclasses may override/extend
            return name + " (id hidden)";
        }
    }

    static class Dog extends Animal {
        Dog(String name) { super(name); }

        String bark() {
            // 'name' is protected -> accessible in the subclass:
            return name + " says Woof!";
            // secretId is private -> NOT accessible here
        }
    }

    public static void main(String[] args) {
        Dog d = new Dog("Rex");
        System.out.println(d.bark());
        System.out.println(d.describe());
    }
}

🧩 Reorder Challenge

These lines form a properly encapsulated Score class with a private field and a validating setter. They've been shuffled.

A)     public int getValue() { return value; }
B) class Score {
C)         if (v >= 0) value = v;
D)     private int value;
E)     public void setValue(int v) {
F)     }
G)     }

Correct order: B, D, A, E, C, F, G

class Score {
    private int value;
    public int getValue() { return value; }
    public void setValue(int v) {
        if (v >= 0) value = v;
    }
}

Open the class, declare the private field, then the getter, then the setter whose body validates before storing, and finally close both the method and the class.

🧠 Quick Recall

Predict the result before opening each answer.

class Box { private int n = 5; }

class Main {
    public static void main(String[] args) {
        Box b = new Box();
        System.out.println(b.n);
    }
}

Answer: It does not compile. n is private, so it is invisible to Main even though both are in the same file: error: n has private access in Box. You'd need a public getter to read it.

class Account {
    private double balance = 100;
    public boolean withdraw(double amt) {
        if (amt <= balance) { balance -= amt; return true; }
        return false;
    }
    public double balance() { return balance; }
}
Account a = new Account();
System.out.println(a.withdraw(150) + " " + a.balance());

Answer: false 100.0. The withdrawal of 150 exceeds the balance of 100, so withdraw returns false and leaves balance untouched — exactly the guarantee private + a guarded method gives you.

class Animal { protected String name = "?"; private int id = 1; }
class Cat extends Animal {
    String describe() {
        return name + " " + id;   // <-- this line
    }
}

Answer: The return line fails because id is private — even a subclass can't see it. name is fine because it's protected. Remove id (or make it protected) and it compiles.

🎯 Your Turn #1 — Encapsulate a counter

Make the field private and provide controlled access. The count must only ever go up via increment().

public class Main {
    static class Counter {
        // YOUR TURN: make 'count' private, then add a public increment()
        // method and a public getCount() method. The field must not be
        // changeable from outside except through increment().
        int count = 0;

        // add increment() and getCount() here
    }

    public static void main(String[] args) {
        Counter c = new Counter();
        c.increment();
        c.increment();
        c.increment();
        System.out.println("Count: " + c.getCount());   // Expected: Count: 3
    }
}

🎯 Your Turn #2 — Validating setter

Write a setter that rejects an email without an @, keeping the previous value.

public class Main {
    static class User {
        private String email;

        // YOUR TURN: write setEmail that only stores the value if it
        // contains an '@'. Otherwise leave the old value untouched.
        // Then write getEmail.
        // Expected output:
        //   Email: ok@site.com
        //   Email: ok@site.com   (the bad value "nope" was rejected)
    }

    public static void main(String[] args) {
        User u = new User();
        u.setEmail("ok@site.com");
        System.out.println("Email: " + u.getEmail());
        u.setEmail("nope");
        System.out.println("Email: " + u.getEmail());
    }
}

🧩 Mini-Challenge — Hide a password

Build a Password class that lets callers set and check a password but never read it back.

public class Main {
    // GOAL: a Password class that NEVER exposes the raw password.
    //
    // Expected output:
    //   Set? true
    //   Matches 'hunter2'? true
    //   Matches 'wrong'?   false
    //   Stored length: 7

    static class Password {
        // 1. a private String field for the password
        // 2. public boolean set(String p): store only if p.length() >= 4, return success
        // 3. public boolean matches(String guess): true if guess equals the stored value
        // 4. public int length(): the stored length (do NOT expose the value itself)
    }

    public static void main(String[] args) {
        Password p = new Password();
        System.out.println("Set? " + p.set("hunter2"));
        System.out.println("Matches 'hunter2'? " + p.matches("hunter2"));
        System.out.println("Matches 'wrong'?   " + p.matches("wrong"));
        System.out.println("Stored length: " + p.length());
    }
}

Common Errors

Pro Tips

📋 Quick Reference

ModifierVisible toUse for
publicEveryoneYour intended API
protectedPackage + subclassesHooks for subclasses
(none)Same package onlyInternal package helpers
privateSame class onlyFields, internal state
private finalSame class, set onceImmutable fields

🎉 Lesson Complete!

You now know all four access levels, why the keyword-less default is package-private, how protected reaches subclasses, and how private fields plus validating getters/setters give you real encapsulation. The habit to keep: make everything as private as possible.

Practice quiz

Which access level applies when you write a field with NO access keyword at all?

  • public
  • protected
  • package-private (default)
  • private

Answer: package-private (default). No keyword means package-private: visible only within the same package, narrower than protected.

From most open to most restrictive, which ordering is correct?

  • private, default, protected, public
  • public, protected, default (package-private), private
  • public, default, protected, private
  • protected, public, private, default

Answer: public, protected, default (package-private), private. public ⊃ protected ⊃ package-private ⊃ private. Each level adds reach to the one below it.

What is the key difference between protected and package-private?

  • They are identical
  • protected also reaches subclasses in other packages; package-private does not
  • package-private is broader than protected
  • protected hides a member from the same package

Answer: protected also reaches subclasses in other packages; package-private does not. protected adds cross-package inheritance access on top of same-package access; package-private stays within one package.

A top-level class (not nested) may be declared as:

  • public or package-private only
  • any of the four levels
  • private or protected only
  • protected only

Answer: public or package-private only. private and protected are not allowed on a top-level class — only public or package-private (no keyword).

Why make a field private and expose a setter instead of a public field?

  • It runs faster
  • So the setter can validate input before storing it
  • Private fields use less memory
  • It is required by the compiler

Answer: So the setter can validate input before storing it. A setter can validate, log, or reject bad values; a public field can be set to anything, breaking invariants.

Inside a subclass, a parent's private field is:

  • Accessible directly
  • Accessible only with a cast
  • NOT accessible — private hides it even from subclasses
  • Accessible only in the same file

Answer: NOT accessible — private hides it even from subclasses. private means same class only. Subclasses cannot see a private member; use protected for members a child needs.

Two classes in the SAME file but different (top-level) classes. Class Main reads a private field of class Box. What happens?

  • It compiles and prints the value
  • It compiles with a warning
  • Compile error: the field has private access in Box
  • It prints null

Answer: Compile error: the field has private access in Box. private is invisible outside its own class even within one file. You need a public getter to read it.

A setter that just assigns the value with no validation gives you, over a public field:

  • Much better performance
  • Essentially nothing extra
  • Automatic thread safety
  • Compile-time constant checking

Answer: Essentially nothing extra. A no-op setter adds ceremony without benefit. Only add a setter when it validates, logs, or notifies.

Which combination best describes a field that can be read but never changed after construction?

  • public field
  • private final field set once in the constructor
  • protected field with a setter
  • package-private field

Answer: private final field set once in the constructor. private final, assigned once in the constructor, is immutable — no setter needed, and it cannot be corrupted.

The golden rule of access modifiers is:

  • Make everything public for convenience
  • Make everything as private as you can, opening up only with a concrete reason
  • Always use protected
  • Prefer package-private for all fields

Answer: Make everything as private as you can, opening up only with a concrete reason. A small public surface is easier to use, test, and change. Widen access only when you truly need to.

Continue this course

Frequently asked questions

What are the four access levels in Java?

From most open to most restrictive: public (any code anywhere), protected (the same package plus subclasses in other packages), package-private (the same package only — this is the default when you write no keyword), and private (only inside the same class). Choosing the right one is how you control your code's public surface.

What is the default access level if I don't write a keyword?

Package-private. A member with no modifier is visible to any class in the same package but invisible everywhere else — not even to subclasses in a different package. There is no 'default' keyword; the absence of a keyword IS the default. It's more restrictive than protected, which surprises many beginners.

Why make fields private and add getters and setters?

This is encapsulation. A private field can only change through methods you control, so you can validate input (reject a negative balance), keep derived values consistent, log changes, or make a field read-only by providing a getter but no setter. If the field were public, any code could set it to anything and break your object's invariants. Private fields also let you change the internal representation later without breaking callers.

What is the difference between protected and package-private?

Package-private (no keyword) is visible only within the same package. protected is visible within the same package AND to subclasses even in other packages. So protected is strictly broader: it adds cross-package inheritance access on top of package access. Use protected for members a subclass legitimately needs; use package-private to share within one package without exposing anything to the outside world.

Can a top-level class be private or protected?

No. A top-level class (one not nested inside another) can only be public or package-private. private and protected are not allowed there because they have no meaning at file scope. Those two modifiers are only valid on members (fields, methods, constructors) and on nested classes.

Related lessons