Lecture #8: Java Packages and Visibility Modifiers
Why this matters
Up to this point, every class you’ve written has lived in the same directory, quietly able to see every other class around it. That worked fine for small exercises, but it doesn’t scale: real software is built from many classes, often written by different people or teams, and some of that code is meant to be reused as a library rather than read line by line by whoever imports it.
Java’s answer to “how do we organize hundreds or thousands of classes, and how do we let library authors hide their internal plumbing from the people who use the library?” is the package. A package is simply a named group of related classes and interfaces. Once you have packages, a second question follows immediately: within a class, which fields and methods should be visible to code living in other packages, and which should stay strictly internal? That’s the job of Java’s four visibility modifiers: public, protected, package-private (the default, when you write nothing at all), and private.
This chapter covers both halves of the story: how packages are declared, imported, and mapped onto the filesystem, and how the visibility modifiers interact with packages and inheritance to give you fine-grained control over encapsulation.
Packages at the logical level
At the logical level, a package is just a labeled bucket of classes and interfaces that belong together conceptually. Java’s own standard library is organized this way — java.util holds collection classes, java.io holds input/output classes, java.awt holds windowing/graphics classes, java.net holds networking classes, and so on. Grouping classes into packages gives you a module layer above individual classes and objects: instead of reasoning about a codebase as an undifferentiated pile of .java files, you reason about it as a handful of packages, each with a coherent responsibility.
The diagram below sketches this layering — objects at the bottom, the classes that define them above, and packages as the top-level grouping, with a dashed arrow showing one package depending on (importing from) another.
Layers of a Java system: objects at the bottom, the classes that define them above, and packages as the top-level grouping. The dashed arrow shows elibrary depending on inventory via an import — exactly the relationship built in the worked example below.
Packages at the physical level
The logical grouping isn’t just a diagram convention — it has a direct, mechanical translation onto your filesystem. Each package corresponds to a directory, and the package name mirrors the directory path. A package named com.example.shapes lives in a directory com/example/shapes/, and every .java file that declares package com.example.shapes; must physically sit inside that directory.
For the compiler and the runtime to find these directories, Java consults a search path, conceptually identical to the PATH environment variable your shell uses to locate executables. This path is called CLASSPATH. You can set it as an environment variable, or — more commonly during development — pass it explicitly to the javac and java commands with the -classpath (or short form -cp) option:
javac -cp .:lib/somelibrary.jar Application.java
java -cp .:lib/somelibrary.jar Application
(The : separating entries is for Unix-like systems — macOS and Linux. On Windows, the separator is ; instead: -cp .;lib\somelibrary.jar.)
You don’t need to master CLASSPATH mechanics to understand the language rules in this chapter — that’s a build/tooling concern you’ll work through hands-on in lab exercises — but it’s worth knowing it exists, because “class not found” errors very often trace back to a misconfigured classpath rather than a mistake in your code.
Declaring a package
A source file joins a package via a package statement, which must be the very first non-comment line in the file — before any import statements:
package inventory;
public class Book {
// ...
}
This single line does two things. First, it physically files the class under the inventory bucket, meaning the fully qualified name of this class is inventory.Book (you only need to write out that full name when there’s an ambiguity — for instance if two imported packages both define a class called Book). Second, it determines what other code is allowed to import.
If a file has no package statement at all, its classes fall into what’s called the default package, an unnamed package tied to the current directory. Classes in the default package cannot be imported from anywhere else — they’re only usable by other files that also happen to live in that same directory. This is fine for a quick throwaway script, but any code meant to be reused from elsewhere needs an explicit package name.
Importing packages
To use a class from another package, you bring it into scope with an import statement, again placed near the top of the file (after the package line, if any):
import inventory.Book; // import one specific class
import inventory.*; // import every public class in the package
An import does not physically copy or paste code into your file the way a C preprocessor #include does. It only tells the compiler where to look up the names you use later in the file, so it can resolve them and type-check your code. Nothing about your compiled .class file changes based on which import syntax you used. At run time, the actual bytecode for a class is loaded dynamically by the JVM’s class loader only when that class is first needed — imports are strictly a compile-time bookkeeping device.
Here are two small examples showing the pattern in practice:
// file: inventory/Catalog.java
package inventory;
// Using classes from java.util
import java.util.*;
public class Catalog {
protected Map<String, Book> booksByIsbn = new TreeMap<>();
}
// Importing a single named class instead of a wildcard
import java.util.ArrayList;
public class Playlist {
private ArrayList<String> trackTitles = new ArrayList<>();
}
Using a single-class import (import java.util.ArrayList;) instead of a wildcard (import java.util.*;) is often preferred in real projects: it documents exactly which classes a file depends on, and it avoids surprises if two packages you wildcard-import happen to define a class with the same simple name.
Only public classes cross package boundaries
Here’s the rule that ties packages to encapsulation: only classes declared public can be imported from outside their own package. Any class without the public keyword is, by default, only visible to other classes inside the same package. This lets a package author expose a clean, intentional set of entry points while keeping helper/implementation classes completely hidden from the outside world.
// file: inventory/LoanRecord.java
package inventory;
// no "public" here — this is an internal bookkeeping class,
// only usable by other classes inside the "inventory" package
class LoanRecord {
String borrowerId;
java.time.LocalDate dueDate;
}
// file: inventory/Book.java
package inventory;
public class Book {
private String title;
private boolean onLoan;
public Book(String title) {
this.title = title;
}
public void checkOut() throws BookUnavailableException {
if (onLoan) throw new BookUnavailableException();
onLoan = true;
// LoanRecord is fine to use here — same package
LoanRecord record = new LoanRecord();
}
}
// file: inventory/BookUnavailableException.java
package inventory;
public class BookUnavailableException extends Exception {}
A file outside inventory can do import inventory.Book; and import inventory.BookUnavailableException;, because both are public. It cannot do anything with LoanRecord — that class simply doesn’t exist as far as outside code is concerned, even though it’s sitting right there in the same directory.
Putting it together: two packages and an application
Let’s extend the example with a second package that builds on the first, plus a top-level application class that ties both together:
// file: elibrary/DigitalLibrary.java
package elibrary;
import inventory.*;
public class DigitalLibrary extends Catalog {
// extends a class imported from another package
}
// file: Application.java (no package statement => default package)
import inventory.Book;
import elibrary.DigitalLibrary;
public class Application {
public static void main(String[] args) {
Book b = new Book("Effective Java"); // inventory.Book
DigitalLibrary lib = new DigitalLibrary(); // elibrary.DigitalLibrary
}
}
Notice that Application.java has no package statement, so it lives in the default package — which conveniently maps to the current directory, already on the classpath by default. This is fine for a small standalone program’s entry point, but remember: since default-package classes can’t be imported, you’d never structure a reusable library’s main classes this way.
The visibility modifiers
Within a public class, each field, constructor, and method carries its own visibility, independent of the class’s own visibility — declaring a class public says nothing about how encapsulated its members are. Java gives you four levels, ordered here from most open to most closed:
| Modifier | Same class | Same package | Subclass in another package | Unrelated class in another package |
|---|---|---|---|---|
public | yes | yes | yes | yes |
protected | yes | yes | yes | no |
| (none — package-private) | yes | yes | no | no |
private | yes | no | no | no |
A useful mental model: within a single package, Java assumes classes are “friends” — by default, everything is visible to everything else in the same package, no keyword required. Once you cross a package boundary, only public members are visible to ordinary client code, and protected members open up a little further — visible to subclasses, wherever they live, because a subclass has a legitimate need to build on its parent’s implementation. private is the strictest: visible only inside the exact class that declares it, not even to subclasses.
Example: public vs private
package complex;
public class Complex {
private double re, im; // hidden implementation detail
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
public double re() { return re; } // public accessor
public Complex add(Complex other) {
// fine: 'other.re' and 'other.im' are private, but this code
// IS the Complex class, so it can see its own private fields
// even on a different instance
return new Complex(re + other.re, im + other.im);
}
}
// same package, or a different one via "import complex.*;"
public class ComplexDemo {
public static void main(String[] args) {
Complex c = new Complex(10.0, 20.0); // constructor is public: OK
double r = c.re(); // public method: OK
// double bad = c.re; // compile error: 're' is private
}
}
A subtlety worth noticing: add reads other.re directly, not through a getter, even though re is private. That’s allowed — private restricts access to the class, not to the individual object. Any code inside Complex can touch the private fields of any Complex instance, not just this.
Example: protected across packages
package inventory;
public class Catalog {
protected java.util.Map<String, Book> booksByIsbn = new java.util.TreeMap<>();
}
package elibrary;
import inventory.*;
public class DigitalLibrary extends Catalog {
public void listAll() {
// booksByIsbn is protected: a subclass, even in another
// package, may access it directly
for (Book b : booksByIsbn.values()) {
System.out.println(b);
}
}
}
One nuance worth knowing about, even if it rarely bites in practice: this cross-package protected access only works through a reference typed as the subclass itself (or something further down the hierarchy) — never through a plain Catalog-typed reference, even from inside DigitalLibrary. So this.booksByIsbn and booksByIsbn (accessed as an inherited member, as above) are fine, but a method that received a Catalog c parameter from somewhere else could not legally write c.booksByIsbn unless c’s declared type were DigitalLibrary or narrower. The rule exists so that a subclass in another package can’t use inheritance as a backdoor to peek at some unrelated object’s protected state — only at state reachable through its own inheritance chain.
import inventory.*;
public class Application {
public static void main(String[] args) {
Catalog c = new Catalog();
// c.booksByIsbn.clear(); // compile error: not a subclass,
// protected is invisible here
}
}
Application is a client of Catalog, not a subclass of it, so booksByIsbn stays out of reach — exactly the protection you want: outside code can only manipulate the catalog through its public methods, never by reaching in and mutating internal state directly.
Common pitfalls
Forgetting public on a class you meant to export. If you write a class without public and expect another package to import it, you’ll get a compile error at the import site, not at the declaration site — which can be confusing the first time you hit it.
Assuming private hides a field from sibling instances. As shown above, private is scoped to the class, not the object. This is by design and is exactly what lets methods like equals or add compare/combine internal state between two instances of the same class.
Trying to narrow visibility when overriding. You cannot override a method with a more restrictive modifier in a subclass — for example, overriding a public method as protected is a compile error. This is required for polymorphism to hold: any code that could call the method through the parent type must still be able to call it through the subclass. You may only keep the same visibility or widen it.
Confusing “no package statement” with “package ..” A file with no package line isn’t in some catch-all shared space — it’s in the default package tied to that specific directory. Two default-package files in different directories are not in the same package and cannot see each other’s package-private members.
Mixing up final with the visibility modifiers. final is a separate, orthogonal keyword: on a class it prevents subclassing (final class String), on a method it prevents overriding, and on a variable it makes the variable a constant after initialization. A private method is implicitly non-overridable in practice (subclasses can’t even see it to override it), but private and final are not the same concept — you can combine final with public or protected to say “visible/inheritable, but not redefinable.”
Recap
- A package groups related classes/interfaces logically, and maps directly onto a directory physically; the JVM and compiler locate packages via
CLASSPATHor the-cp/-classpathoption. package p;at the top of a file assigns its classes to packagep;import p.Name;orimport p.*;brings classes from another package into scope for compile-time name resolution — no code is physically copied in, and classes are loaded dynamically at run time.- A file with no
packagestatement lives in the unnamed default package tied to the current directory, and its classes cannot be imported elsewhere. - Only classes declared
publiccan be imported from another package; non-public classes are implementation details, visible only within their own package. - The four member-visibility levels, from most to least open, are
public,protected, package-private (default, no keyword), andprivate. Within one package, everything is mutually visible regardless of modifier; across packages, onlypublic(and, for subclasses,protected) is reachable. - A class’s own
public/non-publicstatus is independent of the visibility of its members. - Overriding a method can only keep or widen its visibility, never narrow it.