Package-private: the forgotten modifier
On This Lesson 11 topics
Lesson Overview
In the access modifiers lesson you met four visibility levels and saw that writing no keyword at all gives a level called package-private. At that point it was a short preview. Now that you know packages, we can use it properly.
Back to the filing cabinet. Some papers are meant for visitors: the front desk hands them out. Other papers are working notes that only the people at that drawer should touch. A public class is a paper at the front desk. A package-private class is a working note kept inside its own drawer, invisible to the rest of the building.
Why this matters: the best way to keep a codebase changeable is to promise as little as possible to the outside. Every public class is a promise that other code may depend on it. Package-private classes let you write as many helpers as you need without making any promise about them. Beginners tend to write public everywhere, which is the second common mistake for this module ("everything public").
This lesson builds on "Imports and name clashes", where you saw the error "is not public ... cannot be accessed from outside package".
Learning Objectives
By the end of this lesson you will be able to:
- State exactly who can use a package-private class, field, method or constructor.
- Declare a package-private helper class and use it from the public class that needs it.
- Read the compiler error produced when other packages try to use it.
- Define the "public surface" of a package and keep it small.
- Decide, for a given class, whether
publicor package-private is the right choice. - Explain why a package-private class cannot be rescued by an import.
- Recall the rule that a file may contain only one public top-level class, named after the file.
Written Explanation
The rule
A declaration with no access modifier has package access. The Java Language Specification says that, absent a modifier, most declarations have package access, meaning they can be used anywhere inside the package that holds them. The Oracle tutorial's access table agrees. For a top-level class with no modifier, "visible only within its own package". For a member with no modifier, the same package, but not subclasses in other packages and not the world.
| Modifier | Same class | Same package | Subclass elsewhere | Anywhere |
|---|---|---|---|---|
private | yes | no | no | no |
| (none): package-private | yes | yes | no | no |
protected | yes | yes | yes | no |
public | yes | yes | yes | yes |
Notice that package-private is the first level that reaches other classes, but only those in the same drawer.
The word "forgotten" in the title has two meanings. Beginners forget it exists, and they also forget to write a modifier and get package-private by accident. Both are worth fixing: use it on purpose, and read a missing modifier as a decision, not an omission.
The public surface
The public surface of a package is the set of classes (and their public members) that code outside the package may use. Everything else is internal. A good package has a small public surface: only what other packages truly need.
Why small is better:
- Freedom to change. You may rename, rewrite or delete a package-private class at will. No code outside the package can depend on it, so nothing outside can break.
- Less to learn. A teammate reading the package sees two public classes, not twelve, and knows where to start.
- Fewer wrong uses. Other code cannot bypass your rules by calling an internal helper directly.
This is the same idea as private fields, applied one level up: classes and packages get a "front desk" just as objects get methods.
Hiding a helper class
Suppose savings accounts compute interest, and the formula lives in its own helper class. Only the accounts need it:
package com.example.bank;
class InterestCalculator { // no modifier: package-private
double monthlyInterest(double balance) { // also package-private
return balance * 0.02 / 12;
}
}package com.example.bank;
public class SavingsAccount {
private double balance;
private final InterestCalculator calculator = new InterestCalculator();
public SavingsAccount(double balance) {
this.balance = balance;
}
public void addMonthlyInterest() {
balance += calculator.monthlyInterest(balance);
}
public double balance() {
return balance;
}
}SavingsAccount is public: it is the front desk. InterestCalculator is a working note. Because both are in com.example.bank, SavingsAccount uses the calculator with no import. Even though SavingsAccount is public, its field of type InterestCalculator is private, so the helper does not leak.
Package-private members
The same rule applies to fields, methods and constructors. Even inside a public class, a member without a modifier is invisible to other packages. Two common uses:
- A method that sibling classes in the package need but the outside world should not call.
- A constructor that only the package may use, while a
public staticfactory method hands out instances. The tutorial advice, "use the most restrictive access level that makes sense", leads here naturally.
The compiler's message differs slightly. For a hidden class:
error: InterestCalculator is not public in com.example.bank; cannot be accessed from outside packageFor a hidden member:
error: rate is not public in Fee; cannot be accessed from outside package
error: Fee() is not public in Fee; cannot be accessed from outside packageBoth messages say "not public ... outside package", which is the compiler's name for "package-private and you are in another package".
A choosing rule for top-level classes
A top-level class can be only public or package-private (the private and protected levels apply to members). Ask:
- Do classes in other packages need to create or call it? If yes,
public. - Is it only a helper for classes in its own package? If so, package-private.
- Unsure? Start package-private. Opening it later (adding
public) cannot break anyone. Hiding a class that others already use can.
One public class per file
A file may contain at most one public top-level class, and the file must carry that class's name. In our test, declaring public class Three inside Two.java produced:
error: class Three is public, should be declared in a file named Three.javaPackage-private classes have no such limit, but keeping one class per file is the habit that keeps projects easy to navigate. In this course, one class per file.
Why an import cannot help
Visibility and imports are separate checks. In Example 3 below, Bad imports InterestCalculator and the compiler still refuses, three times: once on the import line and twice on the usage. The import resolves the name. The visibility check then fails.
The default package, once more
Package-private means "same package". In the default package, every class is in the same package, so nothing is hidden from anything else. Dropping classes in one unnamed package therefore removes the protection this lesson is about. Named packages are what give you drawers to hide things in.
Examples
Example 1: a hidden helper that works (simple)
Using the two classes above, a Main in package com.example.app calls only the public class:
package com.example.app;
import com.example.bank.SavingsAccount;
public class Main {
public static void main(String[] args) {
SavingsAccount s = new SavingsAccount(1200);
s.addMonthlyInterest();
System.out.println(s.balance());
}
}Output (JDK 21):
1202.0Main never mentions InterestCalculator, and cannot. 1200 times 0.02 divided by 12 is 2, so the balance is 1202.0.
Example 2: package-private members (practical)
A Fee class is public but gives its internals only to its own package:
package com.example.bank;
public class Fee {
double rate = 0.01; // package-private field
Fee() { } // package-private constructor
public static Fee standard() { return new Fee(); }
}A class in com.example.app tries to use everything:
Fee f = Fee.standard(); // allowed: public factory method
System.out.println(f.rate); // not allowed
Fee g = new Fee(); // not allowederror: rate is not public in Fee; cannot be accessed from outside package
error: Fee() is not public in Fee; cannot be accessed from outside packageThe public factory is the only way in. The package keeps control over how fees are created. (Constructors and factory methods were covered earlier; the point here is only the visibility.)
Example 3: the compiler stops a leak (realistic)
A teammate in the com.example.app package wants the interest formula and writes:
package com.example.app;
import com.example.bank.InterestCalculator;
public class Bad {
public static void main(String[] args) {
InterestCalculator c = new InterestCalculator();
System.out.println(c.monthlyInterest(100));
}
}error: InterestCalculator is not public in com.example.bank; cannot be accessed from outside package
import com.example.bank.InterestCalculator;
^
error: InterestCalculator is not public in com.example.bank; cannot be accessed from outside package
InterestCalculator c = new InterestCalculator();
^
error: InterestCalculator is not public in com.example.bank; cannot be accessed from outside package
InterestCalculator c = new InterestCalculator();
^
3 errorsThe compiler is doing its job: it enforces the boundary. The wrong fix is to add public to the helper. The right fix is to ask what the teammate really needs. If they need the monthly interest of an account, they should call addMonthlyInterest() on the SavingsAccount, or the bank package can offer a new public method such as projectedInterest(). That way the formula stays free to change.
Backend Insight
Backend systems live for years, and the main cost of an old codebase is code that cannot be changed because something unknown depends on it. Each public class in a package is a possible dependency from every other package. Package-private classes cap that. Many frameworks and tools also depend on you exposing a deliberate surface: Spring Modulith's documentation, for example, treats packages as modules and says that in a simple module the package-private types are hidden from other modules, while the module's public types form its API.
There is one practical limit to know now. The compiler enforces package-private only between different packages. A single huge package gets no protection at all inside itself. So package-private only works well when your packages are small and focused, which is what the next lesson is about.
Common Mistakes
Mistake 1: making everything public "so it compiles"
When another package cannot see a class, the quick fix is to type public. Resist it: first ask whether that code should be using this class. Each public you add widens the surface forever.
Mistake 2: forgetting a modifier by accident
A method meant for the public that has no public works inside its package and fails from outside, with the "not public" error. If you see that message for a class you wrote, check whether you forgot public on something that should be public, or are reaching for something that should be hidden. Both are possible.
Mistake 3: relying on the default package for hiding
In the unnamed package everything is in the same package, so package-private hides nothing. Use named packages.
Mistake 4: putting a second public class in a file
A file may have at most one public top-level class, and its name must match the file. Move other public classes to their own files.
Mistake 5: assuming a public class makes its members public
Class and member visibility are separate. A public class with a package-private method is still hidden from other packages, as Fee showed.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Which classes can use a package-private class declared in package com.example.bank: any class in com.example.bank, any class in com.example.app, or both?
Show solution A1
A1. Any class in com.example.bank only.
A2. What do you write to make a class package-private?
Show solution A2
A2. Write no access modifier before class: class InterestCalculator { }.
A3. State the compiler's error message pattern when another package uses a package-private class.
Show solution A3
A3. X is not public in <package>; cannot be accessed from outside package.
Level B: Application
B1. In package shop.order, class TaxTable is used only by OrderService. Decide modifiers for both classes and for OrderService's field that holds a TaxTable. Justify each.
Show solution B1
B1. TaxTable is package-private (no modifier), because only OrderService in the same package uses it, so there is no reason to promise it to others. OrderService is public if other packages call it (as the feature's front desk). Its TaxTable field is private, which keeps the helper from leaking through the service.
B2. The following file does not compile. What is the error and the smallest sensible fix?
package shop.order;
public class Order { }
public class Discount { }Show solution B2
B2. Two public top-level classes in one file. The compiler says the second must be declared in a file named after it. Fix: move Discount into Discount.java (or remove public from it if it is only a helper in that package).
B3. A class in shop.app calls new OrderValidator(), but OrderValidator (package shop.order) has no modifier. You are tempted to add public. List two questions you should ask first.
Show solution B3
B3. Ask: Does code outside shop.order really need to validate orders, or should the order service do it internally? Would making it public create a dependency we want to keep stable? If outsiders do need it, then expose it deliberately (perhaps through a public method on an existing public class) rather than just opening the helper.
Level C: Challenge
C1. A public class Wallet has a method void audit() with no modifier, and a helper class Ledger (package-private) in the same package. A class Report in the same package and a class Main in another package both try to call wallet.audit(). Which succeeds? Which fails, and with what kind of message?
Show solution C1
C1. Report succeeds, because it is in the same package. Main fails with "audit() is not public in Wallet; cannot be accessed from outside package".
C2. Design a small package shop.pricing with three classes: one that other packages use to get a price, and two helpers. Write the class headings (modifiers and names only) and say which methods are public.
Show solution C2
C2. For example:
public class PriceService { public double priceFor(String sku) { ... } }
class DiscountRules { double discount(String sku) { ... } }
class PriceTable { double base(String sku) { ... } }Only PriceService and its priceFor method are public. The helpers and their methods have no modifier. Other designs are fine if the public surface stays that small.
Mini Challenge
Return to your com.example.shop package. Add a helper class RoundingRules with a method that rounds a price to two decimals, and use it from PriceFormatter (Lesson 2). Make RoundingRules package-private.
Check your work: Main in com.example.app still compiles and runs through PriceFormatter only. Then deliberately import RoundingRules into Main, read the error, and remove the import. Finally, list the public surface of com.example.shop in one sentence.
Summary & Next Lesson
Key takeaways
- No access modifier means package-private: usable only by code in the same package.
- It applies to classes, fields, methods and constructors.
- The public surface of a package is what other packages may use. Keep it as small as needed.
- An import cannot make a package-private class accessible. Fix the design, not the keyword.
- Start restrictive: opening a class later is safe, closing it later is risky.
- At most one public top-level class per file, named after the file.
- Hiding only works in named packages, and works best in small, focused ones.
Next lesson
Now that you can hide classes, the question is how to draw package boundaries. The next lesson, "Package by feature vs by layer", compares two ways of grouping classes and shows why one makes hiding much easier.
Self-check
- Who can use a class that has no access modifier?
- What is a package's public surface?
- Why is it risky to make a class
publicjust to silence a compile error? - Does
importmake a package-private class from another package usable? - Why does package-private protect nothing in the default package?
Show answers
- Only classes in the same package.
- The classes and members that other packages are allowed to use.
- It widens the surface: other code may now depend on a class meant to be internal, which makes it hard to change.
- No. The visibility check still fails.
- Every class there is in the same unnamed package, so every class can see every other.
References
- Oracle, The Java Tutorials: Controlling Access to Members of a Class. Supports the access table, the meaning of no modifier for classes and members, and the advice to use the most restrictive level that makes sense.
- Oracle, The Java Language Specification, SE 21, Chapter 6: Names. Supports the statement that, with no modifier, most declarations have package access. The detailed accessibility rules in section 6.6 were beyond the part of the page we could read, so those details come from the tutorial and our compiler runs.
- Spring, Spring Modulith fundamentals. Supports the Backend Insight statement that a simple module's package-private types are hidden and its public types form the API.
- Compiler messages and outputs were produced with JDK 21. The one-public-class-per-file error was observed directly. We did not cite the section of the JLS that states the rule.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.