Module 5 · Object-Oriented Fundamentals ⏱ 18 min

Packages, Imports, and Project Layout

By the end of this lesson you will be able to:
  • Declare a package and place source files in matching directories
  • Use single-type imports, import-on-demand, and static imports correctly
  • Predict which names resolve when imports clash or are absent

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"]
A package declaration maps directly to a 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.

Person.java — a package declaration that matches its folder path. Real, compiling Java.
// 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"]
An import maps a simple name to a fully-qualified one; java.lang needs no import.
App.java — single-type imports let you use simple names. Real, compiling Java.
// 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"]
Two imports claiming one simple name make it ambiguous; resolve it with a fully-qualified name.
Main.java — a static import and a fully-qualified name side by side. Real, compiling Java.
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));
    }
}
Exercise

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));
    }
}
Exercise

Which of these can you use in code WITHOUT writing any import statement?

Exercise

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));
    }
}
Exercise

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?

Exercise

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}));
    }
}

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.lang is imported automatically, so String, System, and Math need no import.
  • import static brings 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.

Checkpoint quiz

Where must a package declaration appear in a Java source file?

Which package is imported into every Java file automatically, so its classes need no import?

Go deeper — technical resources