Packages and imports
How package names map to directories and the classpath, what imports really do (nothing at runtime), how to resolve name clashes, why to avoid the default package, and where Java modules fit.
On this page
📦 What are packages and imports?
A package is a namespace for classes, like a folder name that becomes part of every class's full name: Invoice in package com.acme.billing is really com.acme.billing.Invoice. An import is a compile-time shortcut that lets you write Invoice instead of the full name. The JDK alone ships thousands of public classes across a few hundred packages, and packages are what let two of them both be called Date.
Interviewers ask about packages for two reasons. The warm-up version checks that you understand how a class name turns into a file on disk and how the JVM finds it. The deeper version probes whether you've debugged real classpath problems: two versions of the same class, a NoClassDefFoundError that only happens in production, or an import that compiles on one machine and not another.
What you’ll learn
- How a fully qualified name maps to a directory and a
.classfile - How the classpath is searched, and why the first match wins
- Why imports are compile-time only: wildcard imports cost nothing at runtime and skip subpackages
- Static imports, the implicit
java.langimport, and resolvingjava.util.Datevsjava.sql.Date - Why the default package can't be imported, and how JPMS modules add a layer above packages
For this lesson, we'll focus on package naming and directory mapping, the classpath, imports and static imports, name clashes, why the default package is a trap, and a short introduction to Java modules (JPMS) and how they relate to packages.
Below the line (out of scope)
- Access control (
public,protected, package-private): covered in Access modifiers - Class loader internals and delegation: covered in JVM Internals
- Build tools (Maven, Gradle) and JAR packaging in depth
The 30-second answer
A package is a namespace, named by reversed domain (com.acme.billing), that maps one-to-one to a directory path (com/acme/billing/). A class's real name is its fully qualified name. Imports are purely compile-time: the bytecode always uses full names, so wildcard imports cost nothing at runtime and don't import subpackages. java.lang and your own package are imported implicitly. When two packages have the same simple name, like java.util.Date and java.sql.Date, a single-type import wins over a wildcard, otherwise you use the full name. At runtime the class loader turns the name into a path and searches the classpath left to right, first match wins. Java 9 modules sit one level above packages and decide which packages are exported at all.
01Picture it first: name, folder, classpath
Think of a fully qualified class name as a postal address: country, city, street, house. com.acme.billing.Invoice reads from the broadest part to the most specific. Just as the post office maps an address to a physical building, the JVM maps a class name to a physical file by turning dots into slashes.
The package name maps to a directory for both the source and compiled class file, and at runtime the class loader checks each classpath entry from left to right and uses the first match
These pieces work together like this: you write package com.acme.billing;, the source lives in src/com/acme/billing/, javac -d out writes out/com/acme/billing/Invoice.class, and at runtime the class loader looks for com/acme/billing/Invoice.class in each classpath entry in order.
02How it works under the hood
Try it yourself first
Before reading on: you compile the file below with javac -d out Invoice.java. Where does the .class file end up, and what exact command runs it?
// File: src/com/acme/billing/Invoice.java
package com.acme.billing; // must be the first statement (comments are fine above it)
import java.math.BigDecimal; // single-type import
import java.util.*; // on-demand (wildcard) import
import static java.util.Objects.requireNonNull; // static import (Java 5)
public class Invoice {
private final List<BigDecimal> lines = new ArrayList<>();
public void add(BigDecimal amount) {
lines.add(requireNonNull(amount));
}
public static void main(String[] args) {
System.out.println("ok"); // String, System: java.lang, imported implicitly
}
}
javac -d out src/com/acme/billing/Invoice.java
# → out/com/acme/billing/Invoice.class
java -cp out com.acme.billing.Invoice # run by fully qualified name
java -cp out Invoice # Error: Could not find or load main class Invoice
Package naming
The convention is your organisation's domain reversed, all lowercase: com.acme.billing, org.apache.commons.lang3. Reversing puts the most general part first, so packages from different organisations never collide. Hyphens and leading digits aren't legal in identifiers, so my-app.io becomes something like io.my_app.
Two names are off limits: you can't define classes in java.* (the JVM throws SecurityException: Prohibited package name), and javax.* belongs to the platform and its extensions by convention.
Directory mapping
Each dot is a directory level. This mapping is a convention for sources and a requirement for class files: javac will compile a file wherever it sits if you name it explicitly, but -d always writes the class file into the package directory structure, and class loaders always look for it there. Build tools and IDEs enforce the source layout too, so treat it as mandatory.
Packages are not hierarchical in the language. com.acme and com.acme.billing are two completely unrelated packages that happen to share a prefix. A class in com.acme gets no special access to com.acme.billing, and import com.acme.* does not import anything from com.acme.billing.
The classpath
The classpath is an ordered list of directories and JAR files where the class loader looks for classes. You set it with -cp (or --class-path, or the CLASSPATH environment variable). If you set none, it defaults to the current directory ..
# Linux/macOS use ':' as the separator, Windows uses ';'
java -cp "out:lib/pdf.jar:lib/*" com.acme.billing.Main
# └ wildcard: every .jar directly in lib/ (not .class files, not subfolders)
The JDK's own classes (java.*, javax.* and friends) don't come from your classpath. Since Java 9 they come from the runtime image's modules, which are always searched first through the class loader delegation, so you can't accidentally replace java.lang.String.
Imports are a compile-time convenience
import never loads anything. It only tells the compiler how to expand a simple name. Look at any class file with javap -v and you'll see only fully qualified names like java/util/ArrayList. That gives us three useful facts:
- Wildcard imports have zero runtime cost.
import java.util.*;produces exactly the same bytecode as importing each class by name. The only cost is a negligible amount of compile time. - Wildcards import one package, not its subpackages.
import java.util.*;does not give youjava.util.concurrent.ConcurrentHashMap. - You never need an import.
java.util.List<String> names = new java.util.ArrayList<>();compiles fine. Imports just save typing.
Two packages are always imported implicitly: java.lang (that's why String, Math, System and Integer just work) and the class's own package.
Static imports (Java 5) bring static members into scope, so you can write max(a, b) instead of Math.max(a, b):
import static java.lang.Math.max; // one member
import static java.util.stream.Collectors.*; // all static members
int biggest = max(3, 7);
List<String> upper = names.stream().map(String::toUpperCase).collect(toList());
Use static imports for things that read naturally without their class: test assertions (assertEquals), Collectors, constants used everywhere. Avoid them when the owner matters for readability: of(...) alone could be List.of, Set.of, Map.of or Stream.of.
03Real world: name clashes and the default package
Try it yourself first
Before reading on: does this compile? If not, what's the smallest fix?
import java.util.*;
import java.sql.*;
public class Report {
Date created; // ?
}
It doesn't: error: reference to Date is ambiguous, because both on-demand imports offer a Date. Notice the error only appears when you use Date; the two wildcard imports by themselves are fine. You have three fixes, in order of preference:
// 1. A single-type import beats any wildcard
import java.util.*;
import java.sql.*;
import java.util.Date; // Date now means java.util.Date
// 2. If you need both, import one and fully qualify the other
import java.util.Date;
java.sql.Date sqlDate = java.sql.Date.valueOf("2026-10-17");
// 3. Or skip both legacy types and use java.time.LocalDate
The resolution order is: a single-type import or a type declared in the same file wins first, then types in the same package, then on-demand imports (including the implicit java.lang.*). That's also why declaring your own class called String in your package makes every unqualified String in that package refer to yours. Please don't.
The JDK has done this to real code bases. Java 9 added java.lang.Module and Java 16 added java.lang.Record. Projects that had their own Module or Record class in another package, and pulled it in with a wildcard import, suddenly failed to compile with "reference to Record is ambiguous": java.lang.* is an implicit on-demand import, so two wildcards now offered the same name. A single-type import (import com.acme.model.Record;) fixes it, which is the main practical argument for single-type imports. Most style guides, Google's Java style included, forbid wildcards, and an IDE that manages imports removes the typing cost.
Why the default package is a trap
A class with no package statement lives in the unnamed (default) package. It's fine for a one-file experiment, and that's all it's for:
- Nothing in a named package can import it. There's no name to write in an
importstatement (allowed until Java 1.4, a compile error since). Our test above gaveerror: cannot find symbol. - No namespace at all. Two libraries with a default-package
Utilssilently shadow each other on the classpath. - Tools struggle. Frameworks that scan packages, and the module system, can't work with it. A named module can't contain a default-package class at all.
Many candidates think package names only matter for organisation, so they ship small services with classes in the default package. The first time someone needs to call one from a named package, they discover it's impossible without moving the class. Start every file with a package line, even in a prototype.
04Advanced: classpath hell and JPMS modules
Classpath order and shadowing. The class loader stops at the first match. If lib/old.jar and lib/new.jar both contain com.google.common.base.Strings, whichever comes first wins, for every class in that package, and the other version is invisible. Mixing two versions of a library this way causes NoSuchMethodError or NoClassDefFoundError at runtime, often only on the code path that uses a method added in the newer version. Build tools resolve versions to avoid this; mvn dependency:tree and -verbose:class (which prints where each class was loaded from) are how you debug it.
The first lookup of each class walks the classpath entries in order, so a class that lives in the 300th JAR pays for 299 misses first. Each miss is cheap (an open JAR keeps an in-memory table of its entries), but across thousands of classes it shows up in startup time. Once a class is loaded, that loader never looks it up again, so steady-state throughput is unaffected. Class Data Sharing (AppCDS) is the standard tool when startup matters.
Java modules (JPMS, Java 9). A module is a named group of packages with a descriptor, module-info.java, at the root of its source tree:
// src/com.acme.billing/module-info.java
module com.acme.billing {
requires java.sql; // modules I depend on
exports com.acme.billing.api; // only this package is visible to other modules
// com.acme.billing.internal is NOT exported: its public classes are hidden
}
The key relationship: packages organise classes; modules organise packages. Before Java 9, public meant "anyone on the classpath can use this". With modules, a public class is only accessible outside its module if its package is exported. That's how the JDK hides internals like sun.misc and jdk.internal.* (strongly encapsulated by default since Java 16 and 17). Modules also forbid split packages: two named modules may not contain the same package, which removes the shadowing problem described above.
You don't have to adopt modules. Code on the classpath runs in the unnamed module, which can read every module and sees all exported packages. Most applications still use the classpath; libraries and the JDK itself are where modules matter most.
| Concept | What it groups | Declared in | Controls |
|---|---|---|---|
| Package | classes | package line in each file |
namespace, package-private access |
| Module (Java 9+) | packages | module-info.java |
which packages are exported, dependencies |
| Classpath | directories and JARs | -cp |
where classes are found, first match wins |
| Module path | modular JARs | --module-path |
resolved, checked module graph |
05How to say it in the interview
When I ask about packages and imports, I'm listening for three things: that you know imports are compile-time only, that you can explain how a class name becomes a file lookup on the classpath, and that you know where modules fit. If you mention a real classpath conflict and how you'd debug it, that's the senior signal.
In the interview
"A package is a namespace, named by reversed domain, and it maps to a directory path, so com.acme.billing.Invoice lives at com/acme/billing/Invoice.class. Imports are only a compiler shortcut: the bytecode always uses fully qualified names, so wildcard imports have no runtime cost, and they don't include subpackages. java.lang is imported automatically. For clashes like java.util.Date and java.sql.Date, a single-type import wins over a wildcard, or I fully qualify one. At runtime the class loader searches the classpath in order and the first match wins, which is where version conflicts come from. Java 9 modules add a layer above packages: a public class is only visible outside its module if its package is exported."
On the whiteboard
package com.acme.billing → com/acme/billing/Invoice.class
FQN = package + simple name; reversed domain, lowercase; no java.*
import = compile-time only → bytecode uses FQNs
wildcard: no runtime cost, no subpackages
implicit: java.lang.*, own package
static import: members (Math.max → max)
clash: single-type import > same package > on-demand
default package: cannot be imported → always declare a package
classpath: ordered, first match wins → shadowing, NoSuchMethodError
JPMS (9): module-info, requires/exports; public ≠ accessible unless exported