Java

Encapsulation, Access Modifiers, and Packages in Java

Executive Summary

Encapsulation in Java means private state and public behavior: fields become private, and methods expose what the object allows, enforcing its rules in one place. Java offers four access levels: private (this class), package-private (no keyword, same package), protected (package plus subclasses), and public (everyone). Start every field private and widen only with a reason; public is a promise you maintain forever. Prefer behavior methods over reflexive getter and setter pairs. Also, return copies of mutable internals so callers cannot corrupt your state. Packages group related classes and form a visibility boundary, which package-private makes useful. Also, the module system in Part 4 formalizes further. Overall, one habit summarizes all of it: the object enforces its own rules, or nobody does.

Encapsulation in Java: The Contract Behind the Keywords

Encapsulation in Java is not the private keyword; the keyword merely enforces it. The concept is the contract: related state and the rules that govern it live together. So, the only way in is a method that carries those rules. Wrong code first:

// WRONG: anyone can write this field, so the rule lives nowhere
public class Account {
    public long balanceCents;
}

// caller, anywhere in the codebase:
account.balanceCents = -5000;    // compiles, runs, corrupts
// RIGHT: private state, public behavior, rules in one place
public class Account {
    private long balanceCents;

    public void deposit(long amountCents) {
        if (amountCents <= 0) {
            throw new IllegalArgumentException("deposit must be positive: " + amountCents);
        }
        balanceCents += amountCents;
    }

    public long balanceCents() {   // read access, still no write access
        return balanceCents;
    }
}

The delta: with the public field, every line of the codebase is a possible corruption point and no line contains the rules. With encapsulation in Java, one method owns the rule, and a violation of it is impossible rather than merely discouraged.

The Four Access Modifiers

Java has exactly four visibility levels, defined in the JLS access-control chapter and summarized in the official lesson. So, learn them as a ladder: each level grants everything the previous one did, plus more.

Modifier Same class Same package Subclass elsewhere World
private yes no no no
package-private (no keyword) yes yes no no
protected yes yes yes no
public yes yes yes yes
public class VisibilityDemo {
    private int onlyThisClass;        // inside this class only
    int samePackageOnly;             // package-private: the default
    protected int packageAndSubclasses;
    public int everyone;
}

Two details trip people up. First, package-private is the default: omit the modifier and you chose it, silently. Second, private is class-scoped, not package-scoped; even a neighboring class in the same file’s package cannot read it. protected only becomes interesting with inheritance, which is exactly where the next article takes it.

Accessors: Getters, Setters, and Honest APIs

A getter (accessor) reads state; a setter (mutator) writes it. Generated blindly, they re-open every door the private keyword closed, so design them instead of stamping them. The question for every field: does the world need to read this, change this, or neither?

public class Invoice {
    private long subtotalCents;
    private long discountCents;

    public long totalCents() {              // derived read: no setter ever
        return subtotalCents - discountCents;
    }

    public void applyDiscountCents(long amount) {   // behavior, not assignment
        if (amount < 0 || amount > subtotalCents) {
            throw new IllegalArgumentException("bad discount: " + amount);
        }
        this.discountCents = amount;
    }
}

Also, notice what is missing: a setSubtotalCents. Reads and behavior exist; raw assignment does not. When a class is genuinely a data carrier that needs all its fields visible, records make that decision explicit in one line. Also, the records article covers it.

Never Hand Out Mutable Internals

However, private loses its meaning if the field is an array you return by reference. Wrong code first:

// WRONG: the caller now holds your internal array
public String[] tracks() {
    return tracks;               // aliasing: their edits change your state
}

// RIGHT: return a copy
public String[] tracks() {
    return java.util.Arrays.copyOf(tracks, size);
}

The delta: with the alias, every caller is inside your object’s walls without asking. In short, the copy keeps the door closed at the cost of one allocation, a trade you will take almost every time.

Packages: Namespaces and Boundaries

A package is two things at once: a namespace that prevents name collisions and a visibility boundary that makes package-private meaningful. The declaration goes at the top of the file and must match the directory path, as the packages lesson requires.

package com.imraan.library.model;      // file lives in com/imraan/library/model/

import com.imraan.library.util.Isbn;   // bring specific names into scope

public class Book {
    // Isbn usable here without the full prefix
}

Package names are lowercase, usually a reversed domain plus the project and a feature layer: com.imraan.library.model versus com.imraan.library.util. The compiler enforces that Book.java in package com.imraan.library.model sits in the matching folders, which keeps projects honest by construction.

Without a package declaration, your classes live in the unnamed package, fine for the single files of Part 1 and a dead end afterward. Moreover, package-private members glue the classes of one package together while staying invisible to the rest of the codebase, which makes a package the first real “module-ish” boundary you own. The module system in Part 4 formalizes this further with exports and requires; the JPMS article takes that step.

How Real Systems Do This

In fact, the JDK itself is the showcase. String hides its internal byte array. Instead, you manipulate text through methods, which is why the class could change its internals (from char[] to byte[]) in Java 9 without breaking anyone. Similarly, ArrayList hides its backing array behind size and index methods. Every public class you love is a wrapper around private state. So, every successful upgrade of those internals is encapsulation paying rent.

In my experience, the cheapest security and correctness control in Java is “fields are private, period” in review. The billing bug at the top of this article was possible because a Cart exposed discountCents as a public field. The fix took one line and one review comment, and the class of bug went extinct in that codebase the same afternoon.

Two forward notes. The library practice build in this part asks you to design packages before classes. Then, you will feel the boundary working. Persistence and JSON libraries later read your objects through accessors or fields. So, clean encapsulation now keeps those frameworks from becoming archaeology.

Decision Framework

Decide visibility deliberately, in this order.

  1. Start every field private. Widen only when a concrete consumer needs it.
  2. Helpers shared inside one component, but not part of its API? Package-private, with the classes grouped in a package that matches.
  3. Members subclasses must see, and must see? That is protected, a contract with inheritance; the next article defines it.
  4. Anything public is API: document it, test it, and expect to keep it forever.
  5. Pure data moving between layers? Use a record rather than a class of public accessors.
  6. A constant genuinely for everyone? public static final is the one public field you write.

When NOT to Use This

  • Do not generate getters and setters for every field reflexively. That is private fields wearing a fig leaf: state is still wide open. Meanwhile, behavior migrates to callers where it cannot be found.
  • Do not expose mutable internals. Arrays now, collections in Part 3: hand out copies or read-only views, because an aliased mutable field is a shared field.
  • Also, do not build util packages as dumping grounds. Packages should group by domain or feature; a package named misc with everything public is a design abdication.

Common Mistakes

  • Public mutable fields. Every assignment anywhere is a corruption candidate. In fact, the bug at the top of this article is what that looks like in production.
  • Getters and setters for everything, called encapsulation. If all state is readable and writable through accessors, the rules still live nowhere.
  • Returning the internal array. Callers mutate your state through the alias, and the corruption surfaces far from its cause.
  • Assuming private means package-visible. It does not: package-private is the package level, and private is tighter than it.
  • Forgetting the declaration matches the directory. A package statement that disagrees with the folder path fails compilation with an error that confuses beginners weekly.
  • public static mutable fields. That is global state with a name tag, and Part 5’s threads will find it.

Key Takeaways

  • Encapsulation is the contract, not the keyword: state private, behavior public, rules inside the object.
  • Four levels exist: private, package-private (the silent default), protected, and public; each grants the previous plus more.
  • Start private and widen deliberately; public is a promise you maintain forever.
  • Expose behavior, not assignment; reflexive getters and setters reopen what private closed.
  • Return copies of mutable internals; an aliased array is shared state wearing a disguise.
  • Packages are namespaces and boundaries; package-private makes the boundary useful today, and modules make it formal in Part 4.
  • public static final constants are the only public fields you write.

FAQ

What are the four access modifiers in Java?

private (this class only), package-private (no keyword, same package), protected (package plus subclasses anywhere), and public (everyone). Each level grants everything the previous one did, plus more.

What is the default access modifier in Java?

Package-private: visible to every class in the same package and nothing else. Omitting the modifier chooses it silently, which is why you should choose out loud.

Should every field have getters and setters?

No. Expose what callers need: a read here, a behavior method there. Auto-generated accessors for every field are procedural code in disguise, because rules move to whoever calls the setters.

What is a package in Java?

A namespace plus a visibility boundary. The package declaration must match the directory path, imports bring names into scope, and package-private members make the package a real internal boundary.

Can classes in the same package access private members?

No. private is class-scoped, which is stricter than package-private. Neighboring classes in the same package need at least package-private visibility to cooperate.

Conclusion

Encapsulation turns objects from data bags into small governments: they own their state, publish laws through methods, and refuse to let outsiders legislate by assignment. Access levels are the enforcement tool, packages are the border fence, and records arriving later give pure data an honest shape too.

The next article introduces inheritance and polymorphism, where protected finally means something and where you will learn the trade most teams get wrong: sharing behavior versus sharing state.

Make the fields private and the reasons public. Everything else is negotiation.

Last updated on 7 September 2026.

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *