Home
Java from First Principles / Chapter 9 — Access Modifiers and Packages

Access Modifiers and Packages

public, protected, private, and the package-private one nobody knows the name of. What each does, when to use them, and how packages organise the chaos.


Four levels of visibility

Java has exactly four levels of access. You see three keywords (public, protected, private) plus one that has no keyword at all — the **package-private** or "default" access level. Most Java developers can name three of them. The fourth catches everyone occasionally.

| Modifier | Visible to | Common use |
|---|---|---|
| public | Everyone | API surfaces, constants intended for callers |
| protected | Same package + subclasses anywhere | Methods designed for subclass extension |
| (none) — "package-private" | Same package only | Internal helpers and types |
| private | Same class only | All instance fields, internal helper methods |

The omission is the tricky one. When you write:

Java
class User {
    String name;
    void greet() { ... }
}

The class and members have **no modifier**, which means **package-private** — visible within the same package, invisible everywhere else. Many developers assume "no modifier" means public. It doesn't.

This chapter walks through each level, when to use it, and how packages fit in.


private

private restricts access to the same class. Anything marked private can be touched only from code inside the same .java file (actually the same top-level class — inner classes can still see private members of their enclosing class).

Java
public class User {
    private String password;          // only User itself can read/write this

    public boolean checkPassword(String input) {
        return Objects.equals(password, input);   // fine — same class
    }
}

public class Hacker {
    public void steal(User u) {
        String stolen = u.password;   // compile error — password is private
    }
}

**Almost every instance field should be private.** Field access is the most common encapsulation leak. Making fields private and exposing only carefully designed methods is the single most important habit in writing maintainable Java.

private also applies to methods that are internal helpers:

Java
public class OrderProcessor {
    public void process(Order order) {
        validate(order);
        chargePayment(order);
        ship(order);
    }

    private void validate(Order o) { ... }       // internal step
    private void chargePayment(Order o) { ... }
    private void ship(Order o) { ... }
}

These three methods exist only to break the public process() into readable steps. They're not part of the class's public contract. Marking them private means:
- Callers can't accidentally depend on them.
- You can rename, reorder, merge, or split them freely without breaking anyone.
- Tests of OrderProcessor go through the public API, not through private methods.

private is the default you should start from. Loosen access only when you have a specific reason.


Package-private (no modifier)

When you write a member with no access modifier — neither public, protected, nor private — it's package-private. Visible to any code in the same package, invisible to anything outside.

Java
// File: com/example/billing/Invoice.java
package com.example.billing;

class Invoice {                       // package-private class
    String customerId;                // package-private field
    void issue() { ... }              // package-private method
}

// File: com/example/billing/InvoiceService.java
package com.example.billing;          // same package

public class InvoiceService {
    public void process() {
        Invoice inv = new Invoice();  // fine — same package
        inv.issue();                  // fine
    }
}

// File: com/example/orders/OrderService.java
package com.example.orders;           // DIFFERENT package

import com.example.billing.Invoice;   // compile error — Invoice not visible

public class OrderService {
    public void process() {
        Invoice inv = new Invoice();  // not allowed
    }
}

Package-private is **the underused gem of Java access control**. Most developers reach for public or private and forget the middle option exists. But package-private is perfect for:

Real-world example: a payment module might have a package com.example.payment containing PaymentService (public — the API surface), plus PaymentValidator, PaymentSerialiser, TransactionState, etc. (all package-private — internal collaborators). External code uses only PaymentService; the rest can change freely.

**A useful convention for tests:** put your unit tests in the same package as the class under test. That way the tests can poke at package-private internals when needed. The tests typically live in src/test/java/... mirroring the package structure of src/main/java/..., and the build tool merges them at compile time.


protected

protected is package-private plus visibility to subclasses, regardless of which package they live in.

Java
// File: com/example/animals/Animal.java
package com.example.animals;

public class Animal {
    protected String name;            // visible to subclasses anywhere

    protected void log(String msg) {   // visible to subclasses anywhere
        System.out.println("[" + name + "] " + msg);
    }
}

// File: com/example/pets/Dog.java
package com.example.pets;

import com.example.animals.Animal;

public class Dog extends Animal {
    public void bark() {
        log("Woof!");                  // fine — protected, and Dog extends Animal
    }
}

// File: com/example/sneaky/Hacker.java
package com.example.sneaky;

public class Hacker {
    public void steal(Animal a) {
        a.log("breach");               // compile error — Hacker doesn't extend Animal
    }
}

protected is the right access level for:
- **Hooks designed for subclass extension.** A template-method pattern where a base class provides the skeleton and subclasses fill in steps via protected methods.
- **State that subclasses legitimately need to see and possibly modify.**

The honest take: protected is used less often than people think. Most "I need to share this between this class and a subclass" cases are better handled by passing values explicitly through constructors, or by package-private access if the parent and child live in the same package.

A common anti-pattern: making every field protected "in case someone wants to subclass." Don't. That leaks implementation to all subclasses, prevents you from refactoring the field, and is rarely needed.


public

public makes a member visible to all code, anywhere. Use it for the *intentional* public API of a class — the methods and types you want callers to depend on.

Java
public class UserService {
    public User findById(long id) { ... }
    public void save(User user) { ... }
    public void delete(long id) { ... }

    // Everything else — fields, helper methods — should be private or package-private
}

**The single most common access-modifier mistake: making everything public.** Every public method is a contract you've signed. Once callers depend on it, you can't change it without coordination. Public API is forever (or at least, until a major version bump).

**Practical guidance:**

When in doubt, start with private. Tighten down beats loosening up — narrowing access later breaks callers; widening it doesn't break anything.

One thing many developers don't realise: **a Java source file can have at most one public top-level class**, and that class must be named the same as the file. Other top-level classes in the file must be package-private (no modifier).

Java
// File: User.java
public class User { ... }       // OK — public, matches filename

class UserHelper { ... }        // OK — package-private (no modifier)

public class Address { ... }    // compile error — second public class in same file

Packages — Java's namespacing system

A **package** is a namespace for related classes. The Java standard library has packages like java.util, java.io, java.net. Your own code lives in packages too — typically following the convention of reversed domain name plus your project structure:

Text
com.electrominds.blog.models
com.electrominds.blog.services
com.electrominds.blog.controllers

The package declaration is the first non-comment line in a .java file:

Java
package com.electrominds.blog.models;

public class User { ... }

The file User.java for this class must live in a directory matching the package — com/electrominds/blog/models/User.java relative to the source root. The build tool (Maven, Gradle) and the JVM rely on this convention.

**Why packages exist:**
- **Namespacing.** You can have com.example.User and com.acme.User without conflict.
- **Access control.** Package-private visibility lets you have internal types that don't leak.
- **Organisation.** Grouping related classes makes the codebase navigable.

**Importing.** To use a class from a different package, you import it:

Java
package com.example.app;

import java.util.List;
import java.util.ArrayList;
import com.example.shared.User;

public class Main {
    public static void main(String[] args) {
        List<User> users = new ArrayList<>();
    }
}

java.lang.* is imported automatically — that's why you don't need to import String, Object, Math, System. Everything else requires an explicit import or a fully qualified name.

**Package naming conventions:**
- All lowercase. com.example.foo, not com.Example.Foo.
- Reversed domain name. com.electrominds.* for electrominds.com.
- Then nested by responsibility: com.electrominds.blog.payment.refund.*.

A common project layout in 2026:

Text
src/main/java/com/yourorg/yourapp/
    Application.java                ← entrypoint
    config/                          ← configuration classes
    controllers/                     ← HTTP layer
    services/                        ← business logic
    repositories/                    ← data access
    models/ or domain/               ← entities, records
    util/                            ← shared helpers

This is the Spring Boot convention. Other frameworks use slight variations. The point is to group code by responsibility, not by random file count.


Modules — Java 9's addition

Java 9 (2017) added the **module system** (Project Jigsaw) — a layer above packages for very large codebases. A module is a named, versionable group of packages with explicit "exports" (what's visible to other modules) and "requires" (what it depends on).

A module-info.java file at the root of your module declares:

Java
module com.electrominds.blog {
    requires java.base;
    requires com.fasterxml.jackson.databind;

    exports com.electrominds.blog.api;
    exports com.electrominds.blog.models;
    // com.electrominds.blog.internal.* is NOT exported — invisible to other modules
}

This makes encapsulation stricter than packages alone provide: an exported package's public types are visible outside; a non-exported package's public types are invisible to other modules, even though they're still public within the module.

**The honest truth about modules in 2026:** most Java applications don't use them. They're useful for very large codebases (the JDK itself was modularised), for security-sensitive code that needs strong boundaries, and for library authors who want to enforce visibility. Most application code just uses packages and lives in a classpath like Java pre-9.

If you're starting a new project, you can ignore modules entirely. If you join a project that uses them, the rules are predictable — read its module-info.java files to understand the boundaries.

For most readers, the package level of access control is the relevant one. Modules are a layer above that you'll touch only in specific contexts.


Practical rules of thumb

A short list of habits that pay off.

**Default to the most restrictive access.** Start every field as private. Tighten methods by default — public only when callers genuinely need them.

**Don't expose mutable internal state.**

Java
public class User {
    private final List<String> roles = new ArrayList<>();

    public List<String> getRoles() {
        return roles;   // callers can mutate our internal list!
    }
}

Either return an unmodifiable view, return a copy, or return only the data callers need:

Java
public List<String> getRoles() {
    return List.copyOf(roles);   // immutable snapshot
}

**Avoid public fields.** Even public static final constants are mostly fine — primitive and immutable types are safe. But public static final List<String> is a trap because the LIST is final, not its contents. Use List.of(...) to get a truly immutable list.

**Use package-private boundaries.** Group collaborating classes into packages, mark internals package-private, and only the intended API stays public. This is the strongest encapsulation Java offers without going to modules.

**Tests in the same package can see package-private members.** Useful for testing internals without making them globally visible. Most build tools (Maven, Gradle) merge src/test/java and src/main/java so the test classes can sit alongside their targets.

**Be deliberate about protected.** It's a contract with every future subclass. Only use it for hooks you genuinely want subclasses to override or call.

**Read your import statements.** If a class needs imports from 10 different packages, it might be doing too much. Refactor toward more focused classes with fewer dependencies.


⁂ Back to all modules