---
title: 'Packages and Imports'
source: 'https://academia.sh/en/courses/java-fundamentals/packages-and-imports'
course: 'Java Fundamentals'
language: en
updated: '2026-08-17T18:09:39+00:00'
license: 'CC BY-SA 4.0'
---

# Packages and Imports

An import is not a loading directive, it is a shorthand rule: class files produced when the same program is written with an import versus a fully qualified name are byte-for-byte identical, and a static import adds no step either; a package name, by contrast, changes the file, and two names cannot fit the same shorthand.

The previous lesson measured when a class runs and in what order: the entry point was a single
signature satisfying four conditions at once, class-level initialization ran only once and on
first use, and members not written in the source sat in the class file. But **how** the
virtual machine finds that class was never asked.

The first lesson let a detail slip by: the class file carries the type it will call by name,
and that name was in its **full** form. Yet writing `List` in the source is enough. The gap
between the short name and the full name has to close somewhere — this lesson measures where.
The question is: do the import form and the fully qualified form produce **separate class
files**, or the same one?

## Namespace, Fully Qualified Name, Import

In Java, a class's real name is not the short name we write. Classes are placed into
**packages**, and a class's **fully qualified name** is the combination of the package name
and the short name. A package is a **namespace** that prevents names from clashing: two
`Proxy` classes in two separate packages are separate classes, because their full names are
separate.

An **import** is a declaration that saves us from writing this full name every time. Its name
is misleading: nothing gets "imported," no file gets loaded, no code gets copied. The
declaration only tells the compiler this — when I write `List` in this file, I mean
`java.util.List`.

Some names need no such declaration at all. Classes in the language's core package — `String`,
`Object`, `Math`, and the like — can be written by their short names in every source file; that
package is open to every file by default. No import appearing in a source file, for this
reason, does not mean "no external class is used." A shorthand existing and a declaration
being written are separate things, and this lesson asks what each leaves in the class file.

The JavaScript and Python curricula each established a module system and package structure in
their own courses; the concepts there are not repeated here. What this lesson asks is separate
and measurable: **does name resolution leave a trace in the class file?** In the same way,
scope and shadowing, established in the Programming Fundamentals course, are not repeated
either; what is measured here is at which stage a name arrives at its full form.

## The Measurement Core

What is counted in this lesson is neither an instruction nor a member: what is compared is the
class file's **raw bytes**. The core compiles a source, reads the resulting file as is, and
gives two pieces of helper information — the fully qualified name the file carries, and the
line number recorded for a method.

- **LR11** — The compared sources carry the **same line layout**. A class file records a line
  number; if the layout is not held fixed, the comparison measures the line layout, not the
  name's spelling. This is why the measured sources are written on a single line.
- **LR12** — The comparison is made over the class file's whole bytes; no field is left out.
- **LR13** — A source that fails to compile is written as "not compiled"; the compiler's error
  text is not printed, because what is measured is not the text itself, it is compilation's
  result.
- **LR14** — The file name is produced from the short class name, and the directory from the
  package name; the measurement prints no real path.
- **LR15** — The measurement reads no environment-dependent data; the only things counted are
  byte count, line number, and compilation's result.

```java
// Gauge.java — the class file's raw bytes, the name it carries, and the line number it records
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.LineNumber;
import java.nio.file.*;
import java.util.spi.ToolProvider;

class Gauge {
    static byte[] compile(String source, String className) throws Exception {
        Path d = Files.createTempDirectory("gauge");
        Path f = d.resolve(className.substring(className.lastIndexOf('.') + 1) + ".java");
        Files.writeString(f, source);
        PrintWriter sink = new PrintWriter(Writer.nullWriter());
        if (ToolProvider.findFirst("javac").orElseThrow()
                .run(sink, sink, "-d", d.toString(), f.toString()) != 0)
            throw new IllegalStateException("did not compile: " + className);
        return Files.readAllBytes(d.resolve(className.replace('.', '/') + ".class"));
    }

    static String name(byte[] b) { return ClassFile.of().parse(b).thisClass().asInternalName(); }

    static int lineNumber(byte[] b) {
        for (MethodModel m : ClassFile.of().parse(b).methods())
            if (m.methodName().equalsString("length"))
                for (CodeElement e : m.code().orElseThrow())
                    if (e instanceof LineNumber ln) return ln.line();
        return -1;
    }

    static String attempt(String source, String className) {
        try { compile(source, className); return "compiled"; }
        catch (Exception e) { return "not compiled"; }
    }
}
```

## An Import Leaves No Trace in the Class File

Four forms of the same class are compiled. The first uses an import, the second writes the
fully qualified name, the third adds one more import that is never used, the fourth is
identical to the first but moves the import to the next line. The first three are single-line,
and this is deliberate — the fourth will soon say why it has to be.

```java
// Main.java — does an import leave a trace in the class file
import java.util.Arrays;

public class Main {
    static String body(String prefix, String type) {
        return prefix + "class Counter { static int length(" + type + " l) { return l.size(); } }\n";
    }

    public static void main(String[] args) throws Exception {
        byte[] imported = Gauge.compile(body("import java.util.List; ", "List"), "Counter");
        byte[] qualified = Gauge.compile(body("", "java.util.List"), "Counter");
        byte[] extraImport = Gauge.compile(
                body("import java.util.List; import java.util.ArrayList; ", "List"), "Counter");
        byte[] importBelow = Gauge.compile("import java.util.List;\n" + body("", "List"), "Counter");
        System.out.printf("%-24s %4d bytes  line %d  name %s%n",
                "import form", imported.length, Gauge.lineNumber(imported), Gauge.name(imported));
        System.out.printf("%-24s %4d bytes  line %d  name %s%n",
                "fully qualified form", qualified.length, Gauge.lineNumber(qualified), Gauge.name(qualified));
        System.out.printf("%-24s %4d bytes  line %d%n",
                "with unused extra", extraImport.length, Gauge.lineNumber(extraImport));
        System.out.printf("%-24s %4d bytes  line %d%n",
                "import on next line", importBelow.length, Gauge.lineNumber(importBelow));
        System.out.println("first two byte-for-byte equal : " + Arrays.equals(imported, qualified));
        System.out.println("first and third equal         : " + Arrays.equals(imported, extraImport));
        System.out.println("first and fourth equal        : " + Arrays.equals(imported, importBelow));
    }
}
```

```
import form               307 bytes  line 1  name Counter
fully qualified form      307 bytes  line 1  name Counter
with unused extra         307 bytes  line 1
import on next line       307 bytes  line 2
first two byte-for-byte equal : true
first and third equal         : true
first and fourth equal        : false
```

The first two files are **byte-for-byte identical**. One of the sources writes `List`, the
other writes `java.util.List`, and the two do not diverge by even a single byte. What this
means is definite: **an import leaves no trace in the class file at all.** The class file
already carries the fully qualified name — the short form is nothing but a rule letting the
compiler find that full name. Name resolution ends at compile time; nothing called an import
ever reaches runtime. The reverse of the same result also holds: writing the fully qualified
form carries no extra cost, because what gets shortened is only the source text.

The third row gives the same result once more. When an entirely unused import is added, the
file stays identical. An unused declaration is not a weight, because a used one is not a
weight either: both cost zero steps.

The fourth row is the measurement's own check. When the import is moved to the next line, the
file stays the **same size** but is no longer byte-for-byte identical, and where the
difference sits is visible: the recorded line number is **2** instead of **1**. So the class
file is not indifferent to everything about the source — it does record line layout. The claim
"leaves no trace" cannot, for this reason, be a loose claim; it holds only once the layout is
held fixed. An equality claim with no counterexample has not really said what it measured.

## A Package Name Changes the File

If an import leaves no trace, does a package declaration leave none either? Both are lines
sitting at the top of the file. The measurement gives a separate answer.

```java
// Pack.java — does a package name change the class file
public class Pack {
    static final String BODY =
            "class Counter { static int length(java.util.List l) { return l.size(); } }\n";

    public static void main(String[] args) throws Exception {
        byte[] unpackaged = Gauge.compile(BODY, "Counter");
        byte[] packaged = Gauge.compile("package store; " + BODY, "store.Counter");
        System.out.println("unpackaged: " + unpackaged.length + " bytes, name " + Gauge.name(unpackaged));
        System.out.println("packaged  : " + packaged.length + " bytes, name " + Gauge.name(packaged));
    }
}
```

```
unpackaged: 307 bytes, name Counter
packaged  : 313 bytes, name store/Counter
```

Once the package declaration is added, the file grows by **six bytes** and the name it carries
becomes `store/Counter` instead of `Counter`. The added prefix is exactly six characters. Since
the class file carries the name as text, growth is exactly that text's growth.

The distinction comes from here: **an import is the convenience of whoever writes the source;
a package is the class's identity.** The first is valid in one file and ends with that file;
the second is part of the class's name and travels with it everywhere. Two separate classes
came out of the same body, because their fully qualified names are separate.

The measurement itself proves a second thing too. While looking for the compiled file, the
core builds the path by turning the dots in the fully qualified name into slashes; since the
`store.Counter` measurement was read successfully, the compiler must have written that file
under a `store` directory. A package is not a naming convention: it is both part of the name
and it decides where the class will be looked for. A declaration sitting as a single word in
the source finds its counterpart as a directory in the file layout.

## A Bounding Measurement: Zero Steps, Not Zero Rules

There is one more form of shorthand. **Static import** lets a class's static member also be
written by its short name: `max` instead of `Math.max`. Does this add zero steps too, and does
the shorthand really cost nothing at all?

```java
// StaticImport.java — static import and name clash
import java.util.Arrays;

public class StaticImport {
    static String body(String prefix, String call) {
        return prefix + "class Greatest { static int biggest(int a, int b) { return " + call + "(a, b); } }\n";
    }

    public static void main(String[] args) throws Exception {
        byte[] qualified = Gauge.compile(body("", "Math.max"), "Greatest");
        byte[] statically = Gauge.compile(body("import static java.lang.Math.max; ", "max"), "Greatest");
        System.out.println("Math.max form       : " + qualified.length + " bytes");
        System.out.println("static import form  : " + statically.length + " bytes");
        System.out.println("byte-for-byte equal : " + Arrays.equals(qualified, statically));
    }
}
```

```
Math.max form       : 288 bytes
static import form  : 288 bytes
byte-for-byte equal : true
```

Static import adds no step either; the two files are byte-for-byte identical. The name of the
class owning the method is already written inside the call itself; the short form only erases
that name from the source text.

Every measurement up to here has given zero. Does shorthand really have no cost at all? The
cost is paid not at runtime, it is paid at compile time, and it is real. The five cases below
write the same two classes — two classes sharing a short name, sitting in two separate
packages — in five separate forms.

```java
// Clash.java — under which condition is a name clash a compile error
public class Clash {
    public static void main(String[] args) {
        String[][] cases = {
            {"two exact names, even unused",
             "import java.net.Proxy; import java.lang.reflect.Proxy; class Clashing { }"},
            {"two wildcard imports, unused",
             "import java.net.*; import java.lang.reflect.*; class Clashing { }"},
            {"two wildcard imports, used",
             "import java.net.*; import java.lang.reflect.*; class Clashing { Proxy p; }"},
            {"exact name + wildcard, used",
             "import java.net.Proxy; import java.lang.reflect.*; class Clashing { Proxy p; }"},
            {"no import, two full names",
             "class Clashing { java.net.Proxy a; java.lang.reflect.Proxy b; }"},
        };
        for (String[] c : cases)
            System.out.printf("%-32s %s%n", c[0], Gauge.attempt(c[1], "Clashing"));
    }
}
```

```
two exact names, even unused     not compiled
two wildcard imports, unused     compiled
two wildcard imports, used       not compiled
exact name + wildcard, used      compiled
no import, two full names        compiled
```

Five rows give the rule of name resolution from end to end. First row: importing two classes
by exact name is a compile error **even if those names are never used** — the shorthand table
is built before the source itself, and one short name cannot point to two full ones. The
second and third rows show the reverse: importing an entire package as a wildcard does not by
itself cause a clash; the error only surfaces once the ambiguous name is **used**. A wildcard
import is not a decision, it is a search list.

The fourth row states the order: an import written by exact name takes **priority** over a
wildcard import, so no ambiguity arises. The fifth row draws the boundary — when both classes
are written by their full names, they can be used side by side without a problem. So using the
two together is not forbidden; **fitting them into the same shorthand** is.

The course's measure is read once more from here. The first lesson showed a case where the
added step **was absent** through constant folding; here there are three more cases where the
added step is absent, and in all three, the side paying the cost is the compiler. A form
costing nothing at runtime does not mean that form has no rules.

## Summary

- A class's real name is its fully qualified name; a package is a namespace, and the class file
  carries this name as text.
- The class files produced by the import form and the fully qualified form are **byte-for-byte
  identical**; an unused import does not change the file either. Name resolution ends at
  compile time.
- A class file records the source's line layout: when an import is moved to the next line, the
  file stays the same size but is no longer identical, because the recorded line number becomes
  2 instead of 1.
- A package declaration **changes** the file: the name it carries becomes `store/Counter`
  instead of `Counter`, and the file grows by exactly as many bytes as the added prefix.
- Static import adds zero steps too; on the other hand, importing two classes sharing a short
  name by exact name is a compile error even if those names are never used. In a wildcard
  import, the error surfaces only once the ambiguous name is used, and an exact-name import
  takes priority.
- Shorthand's runtime cost is zero, its compile-time rule is real: one short name in one file
  can point to only one full name, but two classes can be used side by side by their full
  names.

## Next Step

Throughout this topic, what got measured was always **what the compiler adds**: a step, a
member, a name. In all three, the decision was made at compile time, and only the result
reached runtime — which class was meant was already settled by the time the class file was
written. If name resolution ends at compile time, when is **the value itself** decided? The
next topic starts measuring this from the crossing between a primitive type and a wrapper: an
integer assigned to a wrapper is a single equals sign in the source, and a call producing an
object in the class file.
