Access Levels Across Packages

Know exactly what private, package-private, protected and public allow across packages, and use package-private to keep a feature's internals hidden.

Syntaxprivate int a; // this class only int b; // package-private: this package protected int c; // this package + subclasses elsewhere public int d; // everyone (if the package is exported)

Java has four access levels, and three of them are about packages. Writing no modifier at all does not mean private: it means package-private, visible to every class in the same package and to nothing else.

ModifierSame classSame packageSubclass in another packageAny other package
privateyesnonono
(none)yesyesnono
protectedyesyesyes, through its own typeno
publicyesyesyesyes

protected is wider than you think

protected includes package access, so every class in the same package can use a protected member, subclass or not. Across packages, a subclass can use an inherited protected member only through this or a reference of its own type. Calling it on a reference typed as the parent class fails with "has protected access". Most service code never needs protected; it belongs to classes designed to be extended, such as framework base classes.

Top-level types and defaults

A top-level class, record, enum or interface is either public or package-private. A file holds at most one public top-level type and must be named after it. Some members have a different default: interface methods and constants are implicitly public, enum constructors are implicitly private, and the accessor methods a record generates are public.

Package-private as a design tool

The most useful level in a service is the one with no keyword. Put a feature in one package, make the type other features call public, and leave repositories, mappers and helper classes package-private. The compiler then enforces the boundary: a class in com.example.shop.customers that tries to use JdbcOrderRepository fails with "is not public in com.example.shop.orders; cannot be accessed from outside package". Nobody has to remember a convention.

Tests fit this design. A test class in src/test/java under the same package name shares the package at run time, so it can construct package-private classes directly. JUnit 5 test classes and methods can themselves be package-private, and that is the usual style. Spring can create package-private @Component classes and call package-private constructors, so keeping a bean's implementation hidden costs nothing.

Common mistakes

  • Making everything public because the IDE template did. Every public type is a promise other packages may start relying on.
  • Splitting one feature across service and repository packages, which forces the repository to be public.
  • Expecting com.example.shop.orders.web to see package-private members of com.example.shop.orders. It cannot; they are different packages.

Modules add one more layer on top of this table: in a named module, a public type is only visible to other modules if its package is exported.

Example

Example · java
// src/main/java/com/example/shop/payments/PaymentGateway.java
package com.example.shop.payments;

// Public: other features depend on this interface.
public interface PaymentGateway {
    Receipt charge(String customerId, long amountPence);
}

// src/main/java/com/example/shop/payments/Receipt.java
package com.example.shop.payments;

public record Receipt(String reference, long amountPence) {}

// src/main/java/com/example/shop/payments/CardGateway.java
package com.example.shop.payments;

// Package-private: callers cannot name this class, let alone construct it.
final class CardGateway implements PaymentGateway {
    private final String apiKey;   // private: nobody outside this class needs it

    CardGateway(String apiKey) {
        this.apiKey = apiKey;
    }

    @Override
    public Receipt charge(String customerId, long amountPence) {
        return new Receipt("pay_" + customerId, amountPence);
    }
}

// src/main/java/com/example/shop/payments/Payments.java
package com.example.shop.payments;

// The single public way in.
public final class Payments {
    private Payments() {}

    public static PaymentGateway gateway(String apiKey) {
        return new CardGateway(apiKey);
    }
}

When to use it

  • Keep a JDBC repository package-private so other features must go through the public service.
  • Unit test a package-private mapper from a test class in the same package without widening its access.
  • Expose a payment client as a public interface plus a factory while the implementation and its API key stay hidden.

More examples

Reading access levels with reflection

Package-private has no keyword, so reflection reports no modifier at all. Frameworks that check visibility, such as a JSON library deciding which fields to read, see exactly this.

Example · java
import java.lang.reflect.Modifier;
import java.util.List;

public class Main {
    static class Account {
        private long balancePence;
        long version;
        protected String owner;
        public String id;
    }

    public static void main(String[] args) throws Exception {
        for (String name : List.of("balancePence", "version", "owner", "id")) {
            int mods = Account.class.getDeclaredField(name).getModifiers();
            String level = Modifier.toString(mods);
            System.out.println(name + " -> " + (level.isEmpty() ? "package-private" : level));
        }
    }
}

// Output:
// balancePence -> private
// version -> package-private
// owner -> protected
// id -> public

Defaults that are not package-private

Leaving out the modifier means different things in different places. In an interface it means public, in an enum constructor it means private, and record accessors are public whatever you write in the header.

Example · java
import java.lang.reflect.Modifier;

public class Main {
    interface Pricing {
        long priceFor(String sku);       // no modifier, still public
    }

    enum Status {
        ACTIVE, SUSPENDED;
        Status() {}                      // no modifier, always private
    }

    record Money(long pence) {}          // generated accessor is public

    public static void main(String[] args) throws Exception {
        System.out.println(Modifier.toString(
                Pricing.class.getMethod("priceFor", String.class).getModifiers()));
        System.out.println(Modifier.toString(
                Status.class.getDeclaredConstructor(String.class, int.class).getModifiers()));
        System.out.println(Modifier.toString(
                Money.class.getMethod("pence").getModifiers()));
    }
}

// Output:
// public abstract
// private
// public

The protected trap across packages

A subclass in another package can only reach protected members of objects that are known to be its own kind. Anything in package com.example.core, subclass or not, could use other.requestId freely.

Example · java
// com/example/core/BaseHandler.java
package com.example.core;

public abstract class BaseHandler {
    protected String requestId;

    protected void log(String message) {
        System.out.println(requestId + " " + message);
    }
}

// com/example/web/OrderHandler.java
package com.example.web;

import com.example.core.BaseHandler;

public class OrderHandler extends BaseHandler {
    void handle(OrderHandler sibling, BaseHandler other) {
        log("handling");            // fine: inherited member, used through this
        sibling.requestId = "r-1";  // fine: reference of our own type
        other.requestId = "r-2";    // compile error: requestId has protected access in BaseHandler
    }
}

Discussion

  • Be the first to comment on this lesson.