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
- Making everything public — widens coupling and blocks refactoring.
- Mismatch package vs folder — breaks builds on strict CI.
- Circular dependencies between packages — extract shared types to third package.
- Relying on protected for “friend” access — Java has no friend keyword; use package-private co-location.
- Static import abuse —
import static Utils.*obscures code origin.
Practice checkpoint
-
Who can access package-private method in
com.foo.Service?- Answer: Classes in package
com.fooonly.
- Answer: Classes in package
-
Import or FQN for
java.time.LocalDateonce vs many times in file?- Answer: Single class import
import java.time.LocalDate;when used repeatedly.
- Answer: Single class import
-
Can
privatefield be read from subclass?- Answer: No directly; use inherited public/protected getter or constructor in superclass.
-
Why export only
apipackage 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.