Packages give every class a permanent address, and imports are the shortcuts you type to reach that address without repeating it. Once a project passes a handful of files, the package layout stops being tidiness and becomes the only way to know what a class does and where it lives.
Why bother with packages at all?
Because a flat pile of source files stops being navigable. Names collide: two projects will both have a class called Utils, and without a package to qualify it there is no way to tell them apart. Packages solve that by turning every class name into a longer, unique path: com.tutorsbot.bank.util.Money rather than just Money.
The second reason is organisational. When related classes live in a package, you can read a folder and understand what the code does without opening every file. The convention most teams follow is to mirror the domain, not the technical layer, so you get packages like bank, payments and reports instead of models, services and utils.
How does the package statement work?
The package statement is the very first line of a source file, before any imports. It declares which package the file belongs to. The name is a dotted path, and each segment is normally a lower-case word: package com.tutorsbot.bank; means "this file lives under the bank part of the com.tutorsbot namespace".
Two consequences follow. First, a class in the same package needs no import to refer to another class in that package. Second, classes in java.lang are visible everywhere without an import, which is why String, System and Math work with no preamble at all.
Does the folder layout have to match the package?
Yes. The compiler locates a class by turning its package into a directory path, so the folders under your source root must mirror the dotted package names exactly, with the same capitalisation. A mismatch is one of the most common "cannot find symbol" errors beginners hit, and the compiler message often points at the import line rather than the file that is in the wrong place.
bank-demo/
src/
com/tutorsbot/bank/Account.java
com/tutorsbot/bank/util/Money.java
com/tutorsbot/app/Main.javaHere is the smallest interesting piece of that tree. A money value knows how to add itself and how to print itself:
package com.tutorsbot.bank.util;
public record Money(long cents, String currency) {
public Money plus(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("currency mismatch");
}
return new Money(cents + other.cents(), currency);
}
public String format() {
long absolute = Math.abs(cents);
return currency + " " + absolute / 100 + "." + String.format("%02d", absolute % 100);
}
}And the account uses it, pulling it in with a normal import:
package com.tutorsbot.bank;
import com.tutorsbot.bank.util.Money;
public class Account {
private final String id;
private Money balance;
public Account(String id, Money openingBalance) {
this.id = id;
this.balance = openingBalance;
}
public void credit(Money amount) {
balance = balance.plus(amount);
}
public String describe() {
return id + " holds " + balance.format();
}
}Notice what the package declaration does for readability. Inside Account the class name Money is unambiguous, and any file that wants an account writes import com.tutorsbot.bank.Account; to get it.
Which import do you need?
The program that ties it together lives in a different package, so it needs imports for both classes:
package com.tutorsbot.app;
import com.tutorsbot.bank.Account;
import com.tutorsbot.bank.util.Money;
public class Main {
public static void main(String[] args) {
Account account = new Account("ACC-1", new Money(1000, "EUR"));
account.credit(new Money(2550, "EUR"));
System.out.println(account.describe());
}
}Three import styles cover almost everything you will write, and here they are side by side in one file: a single-class import, a wildcard import for a whole package, and a static import for one member. The file also uses a fully qualified name at the point of use, which always works and needs no import at all.
import java.util.ArrayList;
import java.util.List;
import static java.lang.Math.max;
public class Demo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Ada");
names.add("Bo");
System.out.println(names);
System.out.println(max(3, 9));
System.out.println(java.util.Arrays.toString(new int[] {1, 2, 3}));
}
}That program prints the list of names, then 9 from the statically imported max, then the array printed through a fully qualified call to Arrays.toString. Three import styles in one file, all working together.
The single-class import is the clearest and worth defaulting to. The wildcard form saves typing but hides where each name came from, which makes code review slower and makes a renamed class harder to trace. The static import lets you write max(3, 9) instead of Math.max(3, 9), which reads nicely for maths helpers and badly for everything else.
How do you compile and run a multi-file project?
Compile every source file in one command and point the compiler at an output directory with -d. The compiler recreates the package folders inside that directory for you, and you then run the program with the fully qualified class name and -cp pointing at the same directory.
javac -d out src/com/tutorsbot/bank/Account.java src/com/tutorsbot/bank/util/Money.java src/com/tutorsbot/app/Main.java
java -cp out com.tutorsbot.app.MainThe output is a single line:
ACC-1 holds EUR 35.50Two details catch people out. The run command uses the package path with dots, not the directory path with slashes: com.tutorsbot.app.Main, even though the file sits in com/tutorsbot/app. And if you skip -cp out, the runtime cannot find your classes unless they happen to sit in the current directory, because the current directory is not automatically on the classpath.
Once there are a lot of files, list them with a wildcard instead of naming each one: javac -d out src/com/tutorsbot/bank/*.java handles the top-level package, and a shell find command handles everything at once.
Package and import rules that keep projects navigable
- One package per directory, with the directory path matching the package name exactly.
- The
packagestatement comes first, then imports, then the type declaration. - Classes in the same package need no import;
java.langis imported for you. - Prefer explicit single-class imports over wildcards.
- Use static imports for maths and constants, not for everything.
- Compile with
-dand run with-cppointing at the output directory.
Try it yourself: take the three-file bank example and rebuild it yourself from scratch in a folder called shop with packages com.tutorsbot.shop.products for a Product class and com.tutorsbot.shop.app for a Main class. Give Product a name and a price in cents, and a describe() method that prints the name and the price with two decimal places. Compile it with javac -d out listing the two files, run it with java -cp out com.tutorsbot.shop.app.Main, and then move the Main file one folder too high without changing its package statement to see the error message the compiler produces.