When your program was three classes living in one folder, names were simple: there was only one Person, one Account, one Main. Real projects are not like that. A commercial application has hundreds of classes, often written by different teams, and sooner or later two of them will both want to be called Node, Event, or List. Without a way to keep names apart, the compiler would have no way to know which List you meant.
Java solves this with packages. A package is a namespace: a container that groups related classes and gives them a shared, unique prefix. The class String lives in java.lang, the class ArrayList lives in java.util, and the two never collide because their packages differ. Packages also govern access — a member with no modifier is visible to every class in the same package and to nobody outside it. Organising code into packages is how a project grows from a single script into a maintainable codebase.
flowchart TD A["package com.app.model;"] --> B["folder: com/app/model/"] B --> C["file: Person.java"] C --> D["public class Person"] A --> E["declaration must match<br/>the folder path on disk"]
Declaring a package
You put a class into a package by writing a package declaration as the first line of its source file, before any import. The package name uses dots to separate parts, and by long-standing convention it begins with the organisation's domain name in reverse: com.apple.music, org.apache.commons. The dots are not decoration — they map directly to folders on disk. A class declared package com.app.model; must live in a file at com/app/model/Person.java, relative to your project's source root.
Get the folder path wrong and the compiler refuses to compile the file, because the directory and the declaration no longer agree. This strict mapping is what lets packages scale: you can locate any class simply by reading its package, and build tools such as Maven and Gradle can find every source file without you listing them by hand. Reverse-domain naming also guarantees uniqueness, because two organisations never share the same domain.
// File: src/com/app/model/Person.java
package com.app.model;
public class Person {
String name;
Person(String name) {
this.name = name;
}
}
Imports bring names into scope
Writing java.util.ArrayList in full every time would be exhausting, so Java lets you import a class once at the top of the file and then refer to it by its simple name. A single-type import names one class exactly: import java.util.List;. An import-on-demand brings in a whole package with a star: import java.util.*;. The star is convenient, but it hides where a class comes from, so many teams prefer the explicit single-type form for readability.
One package never needs an import: java.lang. It contains String, System, Math, Integer, and the other classes so fundamental that Java imports them into every file automatically. That is why you have written System.out.println and used String from your very first program without a single import line — they were already in scope. Imports only ever bring in types from other packages; they never change what runs, only how you spell it.
flowchart LR C["you write: List"] --> I["import java.util.List;"] I --> F["resolved as<br/>java.util.List"] C --> J["java.lang is auto-imported"] J --> K["String resolves to<br/>java.lang.String with no import"]
// File: src/com/app/App.java
package com.app;
import java.util.List;
import java.util.ArrayList;
public class App {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Ada");
names.add("Lin");
System.out.println(names);
}
}
Static imports and fully-qualified names
The import static form brings in a member of a class rather than the class itself. Writing import static java.lang.Math.PI; lets you say PI instead of Math.PI, and import static java.lang.Math.*; does the same for max, min, abs, and the rest. It is a readability tool, most often used for mathematical constants and helpers — but use it sparingly, because stripping away the Math. prefix can make code harder to trace for someone reading it cold.
When two imports would bring in the same simple name, or when you simply prefer to be explicit, you can skip the import entirely and write the fully-qualified name in full: java.util.List.of(1, 2). A fully-qualified name always resolves, needs no import statement, and removes all ambiguity about which class you mean. It is the escape hatch you reach for whenever imports clash or a name is hard to source.
flowchart TD
U["import java.util.List;"]
A["import java.awt.List;"]
U --> R{"use the name List"}
A --> R
R --> X["ambiguous<br/>compile error"]
R --> Y["fix: spell out<br/>java.awt.List in full"]
import static java.lang.Math.*;
public class Main {
public static void main(String[] args) {
System.out.println(max(3, 7));
System.out.println(java.util.List.of(1, 2, 3));
}
}
A single-type import. Predict the exact three-line output.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> nums = List.of(5, 10, 15);
System.out.println(nums);
System.out.println(nums.size());
System.out.println(nums.get(1));
}
}
List.of builds an immutable list, and its toString places a space after each comma.
The list holds three elements, so size is 3.
Indexes start at 0, so get(1) returns the second element.
Which of these can you use in code WITHOUT writing any import statement?
The package java.lang is implicitly imported into every compilation unit, so String, System, Math, and Integer need no import. Classes in java.util, java.io, and java.time must be imported explicitly before you can refer to them by simple name.
A static import. With Math imported statically, predict the exact three-line output.
import static java.lang.Math.*;
public class Main {
public static void main(String[] args) {
System.out.println(max(3, 7));
System.out.println(min(3, 7));
System.out.println(abs(-5));
}
}
The static import lets you drop the Math. prefix and call max, min, and abs directly.
max(3, 7) returns the larger value.
abs(-5) returns the absolute value, which is positive.
You write both import java.util.List; and import java.awt.List; at the top of a file, then use the name List in your code. What happens?
Two single-type imports that bring in the same simple name make that name ambiguous, and the compiler rejects it rather than guessing. Resolve it by removing one import and using the fully-qualified name, java.awt.List, wherever you mean the AWT type.
Fully-qualified names. No imports are written at all. Predict the exact three-line output.
public class Main {
public static void main(String[] args) {
System.out.println(java.util.List.of("a", "b"));
System.out.println(java.lang.Math.max(2, 9));
System.out.println(java.util.Arrays.toString(new int[]{3, 1, 2}));
}
}
A fully-qualified name always resolves, so no import is needed.
List.of builds a list whose toString shows the elements with comma separators.
Arrays.toString renders an int array as a bracketed, comma-separated list.
Recap
- A package is a namespace that groups related classes; the declaration must match the folder path on disk.
- By convention, package names start with the reverse domain, such as
com.app.model. - A single-type import names one class; import-on-demand (
.*) names a whole package. java.langis imported automatically, soString,System, andMathneed no import.import staticbrings in members such as constants and methods, letting you drop the class prefix.- A fully-qualified name (
java.util.List) always works and removes ambiguity. - Two imports claiming the same simple name make it ambiguous — resolve it with the fully-qualified name.
That completes the object-oriented fundamentals: you can model problems with classes, protect their state, share constants, honour the equality contracts, and organise a growing codebase into packages.