Packages and Access Modifiers

Learning goals

Control visibility with access modifiers, organize code in packages, import types cleanly, and understand module boundaries at a high level for JDK 21 projects.


Access modifiers

Modifier Class Package Subclass World
public
protected
(package-private)
private

Package-private (no keyword) is the default—visible within the same package only.

public class AccountService {
    private final AccountRepository repo;

    AccountSummary summarize(String id) {  // package-private API
        return repo.find(id).toSummary();
    }
}

Expose public only what clients need—keep internals package-private or private.


Packages

package com.example.billing;

import com.example.billing.model.Invoice;
import java.util.List;

Fully qualified name avoids ambiguity: java.util.Date vs java.sql.Date.


Imports

import java.util.ArrayList;
import java.util.List;

import static java.lang.Math.max;  // static import

Rules:

  • No wildcard penalty at runtime—compiler resolves symbols.
  • import com.foo.* can obscure origins—acceptable in tests, less so in production APIs.
  • Same package types need no import.

Directory layout recap

src/com/example/billing/
  AccountService.java     → package com.example.billing;
  model/
    Invoice.java          → package com.example.billing.model;

Package name should match folder path under source root.


protected in practice

Subclasses outside the package can access protected members on themselves, not arbitrarily on other instances of the superclass type (subtle JLS rule):

// package a
public class Base {
    protected int x;
}

// package b
public class Sub extends Base {
    void demo(Base other) {
        x = 1;        // OK (this.x)
        // other.x = 2;  // compile error — not same package, not subclass receiver
    }
}

In practice, prefer protected for hooks in framework-style bases; otherwise use public API.


Java Platform Module System (JPMS)

module-info.java declares dependencies and exports:

module com.example.billing {
    requires java.sql;
    exports com.example.billing.api;
}

Non-exported packages are strongly encapsulated—reflection cannot break in without --add-opens. Most learning projects stay classpath-based; know modules exist for library authors and JDK internals.


Sealed packages and API jars

Published libraries export stable packages; internal packages stay hidden (com.acme.internal). Match your access design to what you ship.


Common mistakes

  1. Making everything public — widens coupling and blocks refactoring.
  2. Mismatch package vs folder — breaks builds on strict CI.
  3. Circular dependencies between packages — extract shared types to third package.
  4. Relying on protected for “friend” access — Java has no friend keyword; use package-private co-location.
  5. Static import abuseimport static Utils.* obscures code origin.

Practice checkpoint

  1. Who can access package-private method in com.foo.Service?

    • Answer: Classes in package com.foo only.
  2. Import or FQN for java.time.LocalDate once vs many times in file?

    • Answer: Single class import import java.time.LocalDate; when used repeatedly.
  3. Can private field be read from subclass?

    • Answer: No directly; use inherited public/protected getter or constructor in superclass.
  4. Why export only api package in a module?

    • Answer: Hide implementation details; enforce boundary for maintainability.

Interview angles

  • “Weakest to strongest access?” — package-private (default), protected, public; private most restrictive for members.
  • “Package vs module?” — Package organizes types; module controls visibility across JAR boundaries at JVM level.
  • “Why package-private?” — Testability and collaboration within module without public API commitment.

Next: /java/learn/oop/equals-hashcode-tostring/