Topic 59 of 119 6. Object-Oriented Programming in Java Interactive Tutorial

Encapsulation in Java: Data Hiding, Getters, and Setters

17 mins read Last updated: 22 Aug 2026 27,486 views

Direct Answer & Core Concept

Encapsulation is the practice of putting data and the methods that operate on it in one place, then hiding the data behind access control so callers cannot put the object into an invalid state. The mechanism is simple: declare fields private, and expose behaviour through public methods that decide what is allowed.
Main.java
Run Online
public class Account {
    private long balanceCents;
    public long getBalanceCents() {
        return balanceCents;
    } public void setBalanceCents(long cents) {
        if (cents < 0L) {
            throw new IllegalArgumentException("Balance cannot be negative");
        } this.balanceCents = cents;
    }
}
The getter and setter are not the point. The point is the if inside the setter: the rule lives in exactly one place, and no caller can bypass it. That is where most explanations stop, and it is not quite enough. A getter that returns a List field hands out a live reference to your private state, so a caller can mutate it without ever touching a setter. Encapsulation that leaks a mutable reference is encapsulation in name only, and fixing it means returning a defensive copy or an unmodifiable view. Two more cases complete the picture. A class with a getter and no setter is read-only, which is the cheapest way to make an object immutable. And some values should have neither: a computed getter derives the result and stores nothing, so there is no field to leak.

Detailed Explanation & Mechanics

1) Two Separate Ideas Share One Name

Encapsulation names a structural choice and an access choice, and beginners tend to conflate them.

IdeaWhat it meansHow
BundlingData and the methods that use it live togetherOne class per concept
HidingInternals cannot be reached from outsideprivate fields

Bundling alone gives no protection. A class can group everything beautifully and still expose every field as public, at which point callers depend on your field names permanently. Hiding is the half that provides the actual guarantee.


Diagram showing public methods forming a boundary around private fields, with validation on the way in and defensive copying on the way out
Data crosses the boundary only through methods that decide whether it may.

2) Private Is the Default Choice

Fields should be private unless there is a stated reason otherwise. The access modifier topic covers the full visibility ladder, but the practical rule is short.

Main.java
Run Online
// BAD: every field name is now part of your public contract
public class Point {
    public int x;
    public int y;
}// BETTER: the representation can change without breaking anyone
public class Point {
    private int x;
    private int y;
    public int getX() {
        return x;
    } public void moveBy(int dx, int dy) {
        this.x = this.x + dx;
        this.y = this.y + dy;
    }
}

In the second version a caller can no longer set x to a negative value, and you are free to store the coordinates differently. moveBy also expresses something a setter pair cannot: the two coordinates change together.

The public keyword on a field is effectively permanent. Changing a public field to private later is a breaking change for every caller, which is why deciding early matters more than fixing late.

3) Setters Are Validation Gates

A setter earns its place by rejecting values the object cannot represent. A setter that assigns blindly is pure overhead.

Main.java
Run Online
public class Account {
    private final String owner;
    private long balanceCents;
    public Account(String owner, long initialCents) {
        this.owner = owner;
        this.balanceCents = initialCents;
    } public long getBalanceCents() {
        return balanceCents;
    } public void setBalanceCents(long cents) {
        if (cents < 0L) {
            throw new IllegalArgumentException("Balance cannot be negative");
        } this.balanceCents = cents;
    } public void deposit(long cents) {
        if (cents <= 0L) {
            throw new IllegalArgumentException("Deposit must be positive");
        } this.balanceCents = this.balanceCents + cents;
    }
}

Note that the setter is not always the best shape. deposit expresses a business rule that a raw assignment cannot, because it knows the amount is arriving rather than being replaced.

When a rule should reject input, throwing is the right move. A silent no-op leaves the caller believing a change happened, and the exception hierarchy topic covers how to choose the right type.

4) No Setter Means Read-Only

The simplest way to make a property read-only is to provide a getter and simply not provide a setter. That is the whole mechanism.

Main.java
Run Online
public class Temperature {
    private final double celsius;
    public Temperature(double celsius) {
        if (celsius < ABSOLUTE_ZERO_CELSIUS) {
            throw new IllegalArgumentException("Below absolute zero");
        } this.celsius = celsius;
    } public double getCelsius() {
        return celsius;
    } // There is deliberately no setCelsius method
}

A final field strengthens this at the compiler level, since it cannot be reassigned even from inside the class. Combined with private fields, no getters, and validation in the constructor, the object is fully immutable. The final keyword topic covers that in detail.

5) Some Properties Need Neither Field

A getter does not have to return a stored value. When a result is derived, storing it separately invites the two to drift apart.

Main.java
Run Online
public class Rectangle {
    private final double width;
    private final double height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    } public double getWidth() {
        return width;
    } public double getHeight() {
        return height;
    } // DERIVED, so there is no widthTimesHeight field to keep in sync
    public double getArea() {
        return width * height;
    } public boolean isSquare() {
        return width == height;
    }
}

If you cached the area as a field, you would have three places to update instead of two, and a bug the moment one is missed.

6) Returning Mutable State Defeats the Whole Design

This is the most common way encapsulation fails in practice, and it compiles without complaint.

Main.java
Run Online
public class Cart {
    private final List<String> items = new ArrayList<>();
    // BAD: the caller receives your private field, not a copy
    public List<String> getItems() {
        return items;
    }
}
Main.java
Run Online
Cart cart = new Cart();
cart.getItems().add("book");
// cart now contains one item, and no setter was ever involved

The caller reached into private state through a public method that handed out the reference. The field is private in name only.

Main.java
Run Online
// GOOD: an unmodifiable view, cheap and impossible to mutate
public List<String> getItems() {
    return Collections.unmodifiableList(items);
}

If callers genuinely need their own mutable copy, return a real one instead. That trades memory for isolation, which is the correct trade whenever the collection is small.

Field typeSafe getter form
Immutable value such as StringPlain assignment
List, Set, MapUnmodifiable view or a copy
Mutable domain objectA defensive copy
Primitive or immutablePlain assignment
ArrayA clone, never the array itself

7) Validation Belongs at Construction Time

A setter is an opportunity to repair later mistakes, which means invalid state exists between construction and the first call. Validating in the constructor removes that window entirely.

Main.java
Run Online
public class Employee {
    private static final double MINIMUM_SALARY = 0.0;
    private final String name;
    private final double salary;
    public Employee(String name, double salary) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Name is required");
        } if (salary < MINIMUM_SALARY) {
            throw new IllegalArgumentException("Salary cannot be negative");
        } this.name = name;
        this.salary = salary;
    } public String getName() {
        return name;
    } public double getSalary() {
        return salary;
    }
}

Because the fields are final there is no setter, so a valid object cannot later become invalid. This is exactly the shape used by records, which is why they are the right tool for plain data.

Making a field private and adding a public setter for it is not the same as exposing it. The setter can validate, transform, trigger a change event, or refuse outright. A public field can do none of that.

8) When Public Fields Are Acceptable

Encapsulation is a default, not a commandment. A few cases justify a public field.

Main.java
Run Online
public class Constants {
    // A public static final field is a constant, safe to share
    public static final int MAX_RETRIES = 3;
}// CONSTANTS, not object state, since they are per class rather than per instance
public enum Level {
    LOW, HIGH
}

A public static final constant carries the same guarantees as a private field with a getter, because the value cannot change. The constants topic covers this in full.

Short-lived data holders used only inside one package or method can also skip accessors, since there are no external callers to protect. Beyond that, prefer accessors.

9) Encapsulation Across Packages

Hiding is what makes it safe to split a class across files. Once a field is private, the class is free to store it in a different form, split it across two fields, or replace the type, and no caller outside can tell.

Main.java
Run Online
public class Money {
    // INTERNALLY this is stored in cents
    private final long amountInCents;
    public Money(long amountInCents) {
        this.amountInCents = amountInCents;
    } public static Money ofMajorUnits(long units) {
        return new Money(units * CENTS_PER_MAJOR_UNIT);
    } public long getAmountInCents() {
        return amountInCents;
    }
}

Callers work in cents. If the representation later moves to a BigDecimal, only this class changes. That freedom is the return on the effort of hiding the data, and it is the argument for making fields private from the start.

10) Common Misconceptions

ClaimVerdict
Encapsulation means adding getters and settersNo. The access control is the point.
Private fields guarantee privacyNo. A getter can leak a reference.
Getters must return a stored fieldFalse. They may compute a value.
A setter is always neededFalse. Omitting it makes the property read-only.
Returning an unmodifiable list is enoughYes. That is the correct pattern.
Public fields are always wrongNo. Constants and short-lived holders are fine.
Encapsulation requires finalNo. It helps, but hiding is what matters.
Records break encapsulationFalse. They hide fields and expose accessors.

Frequently Asked Questions

What is the real benefit of encapsulation?


It gives the class control over its own invariants. Because callers cannot reach the fields, every rule about valid state can be written once and enforced in one place, so an object cannot exist in a state the class considers invalid. It also buys freedom to change the representation later without breaking callers.

Are getters and setters required for encapsulation?


No. They are one possible way to expose behaviour, not the definition itself. The defining feature is that callers cannot touch internal state directly. A computed getter needs no field at all, and a read-only property needs a getter with no setter.

How do I make a class read-only?


Make every field private and final, validate in the constructor, provide getters, and write no setters. With no setter and no reassignment path, the object cannot change after construction. A record gives you exactly this shape with far less code.

Why is returning a private List unsafe?


Because the reference is the object, not a copy of it. A caller that receives your private list can add or remove through it, so private state changes with no setter involved and no compile-time signal. Return an unmodifiable view or a defensive copy instead.

Should a getter return a copy or the live collection?


For a small collection, an unmodifiable view is enough and costs nothing extra. Return a real copy only when callers need their own independently mutable version. Never return the field itself, because that hands away the guarantee encapsulation exists to provide.

When is a public field acceptable?


For constants declared public static final, since the value cannot change and the guarantee matches a getter. Also for brief data holders confined to one package, where there are no external callers to protect. For anything with a lifetime beyond the local scope, use private fields.

How does encapsulation relate to the final keyword?


They address different risks. Encapsulation stops outsiders from reaching fields, while final stops any code, including the class itself, from reassigning them after initialisation. Together with constructor validation they produce a fully immutable object, which is the safest shape available.

Do records follow good encapsulation practice?


Yes. A record declares its components private and final, exposes them through accessors, and provides no setters, so instances are immutable by construction. For plain data with no invariants beyond the field values, they are usually better than a hand-written class with getters.

Runnable Code Example: Encapsulation in Java

Java 17+
Main.java
Run Online
// Shows data hiding, validation in setters, read-only properties,
// computed getters, and defensive copying of mutable state.
public class EncapsulationDemo {
    private static final long MINIMUM_CENTS = 0L;
    private static final long INITIAL_CENTS = 50000L;
    private static final int FIRST = 0;
    private static final int MIN_PERCENT = 0;
    private static final int MAX_PERCENT = 100;
    private static final String NO_ITEMS = "none";
    private static final double SQUARED_SIDE = 4.0;
    // A MUTABLE FIELD WITH A VALIDATING SETTER
    public static class Account {
        private final String owner;
        private long balanceCents;
        public Account(String owner, long initialCents) {
            if (owner == null || owner.isBlank()) {
                throw new IllegalArgumentException("Owner is required");
            } if (initialCents < MINIMUM_CENTS) {
                throw new IllegalArgumentException("Balance cannot be negative");
            } this.owner = owner;
            this.balanceCents = initialCents;
        } public String getOwner() {
            return owner;
        } public long getBalanceCents() {
            return balanceCents;
        } // THE GATE, this is the whole reason the field is private
        public void setBalanceCents(long cents) {
            if (cents < MINIMUM_CENTS) {
                throw new IllegalArgumentException("Balance cannot be negative");
            } this.balanceCents = cents;
        } // A BUSINESS RULE, which a plain setter could not express
        public void deposit(long cents) {
            if (cents <= MINIMUM_CENTS) {
                throw new IllegalArgumentException("Deposit must be positive");
            } this.balanceCents = this.balanceCents + cents;
        }
    } // NO SETTER AT ALL, so this property is read-only
    public static class Temperature {
        private final double celsius;
        public Temperature(double celsius) {
            this.celsius = celsius;
        } public double getCelsius() {
            return celsius;
        }
    } // A COMPUTED GETTER, so there is no field to keep in sync
    public static class Rectangle {
        private final double width;
        private final double height;
        public Rectangle(double width, double height) {
            this.width = width;
            this.height = height;
        } public double getWidth() {
            return width;
        } public double getHeight() {
            return height;
        } public double getArea() {
            return width * height;
        } public boolean isSquare() {
            return width == height;
        }
    } // DEFENSIVE COPYING, the fix for a getter that would otherwise leak state
    public static class Playlist {
        private final java.util.List<String> tracks = new java.util.ArrayList<>();
        // Returns an unmodifiable view, so the caller cannot alter our field
        public java.util.List<String> getTracks() {
            return java.util.Collections.unmodifiableList(tracks);
        } public void addTrack(String track) {
            if (track == null || track.isBlank()) {
                throw new IllegalArgumentException("Track name is required");
            } tracks.add(track);
        } public int getTrackCount() {
            return tracks.size();
        } public String getFirstTrack() {
            if (tracks.isEmpty()) {
                return NO_ITEMS;
            } return tracks.get(FIRST);
        }
    } // A CLAMPING SETTER, which normalises instead of rejecting
    public static class Progress {
        private int percent;
        public Progress() {
            this.percent = MIN_PERCENT;
        } public int getPercent() {
            return percent;
        } public void setPercent(int value) {
            this.percent = Math.max(MIN_PERCENT, Math.min(value, MAX_PERCENT));
        }
    } // ALL FIELDS PRIVATE AND FINAL, VALIDATED ONCE, WITH NO SETTERS
    public static class Employee {
        private static final double MINIMUM_SALARY = 0.0;
        private final String name;
        private final double salary;
        public Employee(String name, double salary) {
            if (name == null || name.isBlank()) {
                throw new IllegalArgumentException("Name is required");
            } if (salary < MINIMUM_SALARY) {
                throw new IllegalArgumentException("Salary cannot be negative");
            } this.name = name;
            this.salary = salary;
        } public String getName() {
            return name;
        } public double getSalary() {
            return salary;
        } // Derived from salary, so nothing extra is stored
        public double getAnnualBonus() {
            return salary / SQUARED_SIDE;
        }
    } private static String describe(Object value) {
        if (value == null) {
            return "null";
        } return value.getClass().getSimpleName();
    } public static void main(String[] args) {
        // A VALIDATING SETTER REJECTS WHAT A PUBLIC FIELD WOULD ACCEPT
        Account account = new Account("Alice", INITIAL_CENTS);
        account.setBalanceCents(65000L);
        System.out.println("balance: " + account.getBalanceCents());
        try {
            account.setBalanceCents(-1L);
        } catch (IllegalArgumentException problem) {
            System.out.println("rejected: " + problem.getMessage());
        } // A BUSINESS RULE CANNOT BE EXPRESSED AS A PLAIN ASSIGNMENT
        account.deposit(2000L);
        System.out.println("after deposit: " + account.getBalanceCents());
        // A READ-ONLY PROPERTY, THERE IS NO setCelsius METHOD TO CALL
        Temperature warm = new Temperature(21.5);
        System.out.println("celsius: " + warm.getCelsius());
        // A COMPUTED GETTER WITH NO UNDERLYING FIELD
        Rectangle box = new Rectangle(SQUARED_SIDE, SQUARED_SIDE);
        System.out.println("area: " + box.getArea() + "  square: " + box.isSquare());
        // DEFENSIVE COPYING, THE CALLER SEES THE DATA BUT CANNOT CHANGE IT
        Playlist playlist = new Playlist();
        playlist.addTrack("Blue Monday");
        playlist.addTrack("Bizarre Love Triangle");
        System.out.println("tracks: " + playlist.getTracks());
        System.out.println("count:  " + playlist.getTrackCount());
        System.out.println("first:  " + playlist.getFirstTrack());
        try {
            playlist.getTracks().add("Injected");
        } catch (UnsupportedOperationException problem) {
            System.out.println("playlist stayed unchanged: " + playlist.getTrackCount());
        } // A CLAMPING SETTER NORMALISES INSTEAD OF REJECTING
        Progress progress = new Progress();
        progress.setPercent(140);
        System.out.println("clamped percent: " + progress.getPercent());
        progress.setPercent(-20);
        System.out.println("clamped percent: " + progress.getPercent());
        // AN OBJECT VALIDATED AT CONSTRUCTION CAN NEVER BECOME INVALID
        Employee staff = new Employee("Grace", 90000.0);
        System.out.println(staff.getName() + " earns " + staff.getSalary());
        try {
            new Employee("  ", 100.0);
        } catch (IllegalArgumentException problem) {
            System.out.println("rejected: " + problem.getMessage());
        } System.out.println("live list type: " + describe(playlist.getTracks()));
        /*
            ACTUAL OUTPUT:

                balance: 65000
                rejected: Balance cannot be negative
                after deposit: 67000
                celsius: 21.5
                area: 16.0  square: true
                tracks: [Blue Monday, Bizarre Love Triangle]
                count:  2
                first:  Blue Monday
                playlist stayed unchanged: 2
                clamped percent: 100
                clamped percent: 0
                Grace earns 90000.0
                rejected: Name is required
                live list type: Collections$UnmodifiableRandomAccessList

            THE SEVEN RULES THAT MATTER MOST
            1) PRIVATE IS THE DEFAULT. Making a field public is close to
               permanent, since every caller then depends on its name.
               Changing it to private later breaks all of them.
            2) A SETTER IS A GATE, NOT A DOOR. Its value is the check
               inside it. A setter that only assigns adds ceremony
               without adding safety.
            3) OMITTING THE SETTER IS THE POINT. A getter with no setter
               is a read-only property, which is the cheapest immutable
               shape available.
            4) NOT EVERY PROPERTY NEEDS A FIELD. A computed getter
               derives the answer, which removes any chance of a cached
               value drifting out of sync.
            5) NEVER RETURN A MUTABLE FIELD. An unmodifiable view keeps
               the object intact for almost no cost. A getter that
               hands out your private list makes private a fiction.
            6) VALIDATE AT CONSTRUCTION, NOT ON EVERY SETTER. An object
               that cannot change after construction cannot enter an
               invalid state later.
            7) PUBLIC STATIC FINAL CONSTANTS ARE FINE. The value cannot
               change, so a constant offers the same guarantee as a
               private field with a getter.
        */
    }
}

Common Java Technical Interview Questions & Answers

Interview Prep
Q1: What is the primary benefit of encapsulation?\nA1: It lets a class guarantee its own invariants, because callers cannot reach the fields directly, so every rule about valid state can be enforced in exactly one place. It also allows the internal representation to change later without breaking any caller, since nothing outside depends on it.\nQ2: Are getters and setters required for encapsulation?\nA2: No. They are merely one way to expose behaviour. The defining feature is that internal state cannot be touched directly. A computed getter needs no field at all, and a read-only property is simply a getter with no matching setter.\nQ3: How do you create a completely read-only class in Java?\nA3: Declare every field private and final, validate the values in the constructor, provide getters, and write no setters. With no reassignment path the object cannot change after construction, which is the cheapest fully immutable shape.\nQ4: Why is returning a private List from a getter unsafe?\nA4: Because the reference is the object rather than a copy of it. A caller can add or remove through that reference, so private state changes with no setter involved and no compile-time warning. Return an unmodifiable view, or a real copy when the caller needs an independently mutable one.\nQ5: When is a public field acceptable rather than a design mistake?\nA5: For public static final constants, since the value can never change and the guarantee matches that of a private field with a getter. Also for short-lived data holders confined to a single package, where there are no external callers to protect. For any longer-lived object state, private fields are the right default.\nQ6: How does encapsulation differ from the final keyword?\nA6: They address different risks. Encapsulation stops outsiders from reaching a field, while final stops any code at all, including the class itself, from reassigning it after initialisation. Combined with constructor validation the two together produce a fully immutable object.\nQ7: Do records practise good encapsulation?\nA7: Yes. A record declares its components private and final, exposes them through accessors, and provides no setters, so instances are immutable by construction. When a class is pure data with no invariants beyond the field values, a record is usually better than hand-written accessors.\nQ8: Should a setter reject bad input or silently ignore it?\nA8: Reject it, by throwing an appropriate unchecked exception. Silently ignoring the call leaves the caller believing a change happened, which is far harder to diagnose than an immediate failure. Use a clamping setter only when silently normalising is the documented intent.

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Declare every field private unless there is a stated reason otherwise.\nTreat changing a public field to private as a breaking change.\nPut a validation check inside every setter, or do not write the setter.\nPrefer named business methods such as deposit over raw setters.\nOmit setters to express a read-only property.\nMake fields final when the object should never change.\nNever return a mutable field directly from a getter.\nReturn an unmodifiable view for small collections, a copy for large ones.\nValidate in the constructor so invalid states cannot exist later.\nUse computed getters instead of caching derived values.\nKeep invariant logic in one place rather than repeated in every method.\nWrite-only accessors such as a write-only log stream are still worth using.\nDo not expose internal collections through direct field access.\nRemember that private without a getter means unreachable, not protected.\nFavour records over hand-written data classes with only getters.\nPrefer this.field over field when a parameter shadows the field name.\nTest that setters reject invalid values rather than assuming they do.
We value your privacy & transparency

OperateTools uses cookies and local storage to ensure basic tool functionality, preserve preferences, and analyze aggregated traffic. We never sell your personal data or index your document files. Learn more in our Privacy Policy.