Java Bridge · J2
Packages, imports, classpath — and compiling by hand
- Java Bridge
- 8 min read
- after J1
The idea
import is an abbreviation, not an inclusion
import java.util.List; looks like #include <vector>, and almost nothing you know about include hygiene survives the resemblance. The import opens no file, pastes no text and produces no bytes. It says one thing: for the rest of this file, List means java.util.List.
The worked example proves that with two methods differing only in spelling:
List<String> viaImport() { return List.copyOf(freeSpotIds); }
java.util.List<String> viaFullName() { return java.util.List.copyOf(freeSpotIds); }
Both compile, and both return the same runtime class, which Main checks and prints as one runtime type: true. Delete the import and only the first one breaks. So there are no include guards, no header ordering and no compilation order, because there is no inclusion to order.
Which leaves the question the import was never answering. If it does not find the type, what does? Paths. javac -d out *.java writes one .class per class into out/, mirroring package names as directories. java -cp out Main tells the launcher where those class files are, then names the class holding main. Omit the -cp and the launcher searches the current directory, which holds your sources and not your output:
Error: Could not find or load main class Main
Caused by: java.lang.ClassNotFoundException: Main
A package is a namespace and a directory layout, enforced together. This project uses the default package deliberately. A class there is unreachable from any named package, so there is no import between your own classes to get wrong, and every drill compiles with one flat command.
Coming from C++
From C++ — #include and import are different kinds of thing
Most wrong guesses about the Java build come from reading import as #include. They are not the same mechanism, and almost nothing you know about include hygiene carries over.
The two lines, side by side
C++
#include <vector>
The preprocessor opens <vector>, pastes its entire text into your translation unit, and hands the result to the compiler. Consequences you have absorbed as facts of life:
- The file is genuinely read, costing compile time, once per translation unit.
- Include it twice and you get redefinition errors. Hence
#pragma once. - Order matters. An include depending on a macro defined above it behaves differently further down.
- Forgetting it is a hard error even when the library is installed and linked.
- What you can name is exactly what got pasted.
Java
import java.util.List;
Nothing is opened at this line and nothing is pasted. It is one statement to the compiler: for the rest of this file, the bare name List means java.util.List. Every consequence above evaporates.
C++ #include | Java import |
|---|---|
| Textual paste of a file | A name-to-name alias, scoped to one file |
| Needs an include guard | Importing twice is legal and does nothing |
| Order-sensitive | Order-insensitive; place them anywhere above the type |
| Required to use the type | Optional. You may always write the full name |
| Costs compile time per inclusion | Costs nothing |
| Can bring in macros, templates, anything | Can bring in only a type name |
The one-line proof, from the worked example:
List<String> viaImport() { return List.copyOf(freeSpotIds); }
java.util.List<String> viaFullName() { return java.util.List.copyOf(freeSpotIds); }
Both methods compile. Both return the same runtime class, which Main verifies and prints as one runtime type: true. Delete the import at the top and only the first one breaks. An import is a typing convenience, not a dependency declaration.
The C++ construct it resembles is:
using std::vector; // a scoped name alias, no textual paste
Except using still needs the #include first. Java has no step corresponding to the #include.
What actually finds a type
The paths, and only the paths.
C++
g++ -I include -c SpotBoard.cpp -o SpotBoard.o # -I: where headers are
g++ SpotBoard.o Main.o -o app # a separate link step
Java
javac -d out *.java # -d: where .class output goes
java -cp out Main # -cp: where to find .class files. No link step, ever.
-d outis the output directory, with package subdirectories created as needed. Without-d, class files land beside their sources, which is what you want approximately never.-cp out(identical to-classpath) is the search path for compiled classes. Entries are separated by;on Windows and:elsewhere. That is the one genuinely platform-specific build detail.Mainat the end is a class name, not a filename. The launcher resolves it against the classpath.java -cp out Main.classandjava -cp out out/Main.classboth fail.
There is no object-file stage and no linker. A .class file loads lazily, by name, the first time it is touched at run time. That is why a missing class can surface as a run-time NoClassDefFoundError in a program that compiled without a murmur.
No compilation order, and no need to list every file
C++ — compile each .cpp separately, link the objects. Miss one and the linker tells you, late.
Java — name the sources you care about and javac follows the references:
javac -d out Main.java # SpotBoard.java sits beside it, unnamed
That produces both out/Main.class and out/SpotBoard.class. javac needed SpotBoard and searched its sourcepath, which defaults to the classpath, which defaults to the current directory. It found SpotBoard.java and compiled it too. So javac -d out *.java is a safe habit rather than a requirement, and there is no Makefile-shaped question about ordering.
Packages, and the namespace that is also a directory
C++ — a namespace is pure naming
namespace dojo {
class SpotBoard { };
}
Any file, any directory, any nesting, split across as many files as you like. The filesystem has no opinion.
Java — the package declaration binds the name to a path
package dojo;
public class SpotBoard { }
Two consequences the compiler and launcher both enforce:
-d outputs the result atout/dojo/SpotBoard.class, neverout/SpotBoard.class.- The class's real name is now
dojo.SpotBoard.java -cp out Mainfails withCould not find or load main class Main; you needjava -cp out dojo.Main.
Unlike a namespace, a package cannot be reopened per file the way namespace dojo { } can be scattered. One package line, at the top, covering the whole file. There is also no nesting relationship: dojo.detail is a different package, with no special access to dojo.
Why this project uses the default package
Omit the package line and the class lands in the default, unnamed package. One property makes that a deliberate choice rather than a shortcut:
// Main.java
package dojo;
public class Main {
public static void main(String[] args) {
SpotBoard board = new SpotBoard(); // SpotBoard is in the default package
}
}
Main.java:12: error: cannot find symbol
SpotBoard board = new SpotBoard();
^
symbol: class SpotBoard
location: class Main
A class in the default package is invisible to every named package. No import reaches it, because there is no package name to write after import. A codebase therefore uses the default package throughout or not at all.
For twelve-minute drills, that is the property you want:
- Sources sit flat in one directory. No
src/main/java/com/example/...to create first. javac -d out *.javathenjava -cp out Main, unchanged, every time.- Zero imports between your own classes, so no timed drill loses a minute to an import.
- The hidden test suite drops in beside your files and sees your classes with no ceremony.
Real production Java uses named packages, and you will want them the moment a codebase passes thirty files. Nothing here is an argument for flat layout in real work. It is an argument for flat layout in a stopwatch exercise.
Worked walkthrough
NOTES — two commands, and the six errors they produce
Run it first
From the directory holding the two sources:
.toolchain\jdk-21\bin\javac.exe -d out *.java
.toolchain\jdk-21\bin\java.exe -cp out Main
Real output:
2 free spots
short name and full name agree: true
T0: 2026-08-18T10:00:00Z
one runtime type: true
And out/ afterwards:
out/Main.class
out/SpotBoard.class
Flat, because both classes are in the default package. Add package dojo; to both files and the same command produces out/dojo/Main.class and out/dojo/SpotBoard.class instead. The output tree mirrors the package names, because that is what a package physically is.
Decoding javac -d out *.java
javacreads.java, writes.class. There is no separate link step in Java, at any point.-d outis the output directory.javaccreates it if missing and writes one.classper class into it, building package subdirectories as required. Without-d, class files land beside their sources:lsthen showsMain.class Main.java SpotBoard.class SpotBoard.javain one heap.*.javais your shell expanding a glob beforejavacsees it.javacreceives a filename list.
Naming every file is optional, and that is the part worth knowing. Run javac -d out Main.java with SpotBoard.java sitting beside it, unnamed, and both compile:
out/Main.class
out/SpotBoard.class
javac needed the type SpotBoard and searched its sourcepath, which defaults to the classpath, which defaults to the current directory. It found SpotBoard.java and compiled it too. The invariant: you give javac a name and it goes looking. You never give it a location.
Compare C++, where you name the file for the compiler and the objects for the linker, and nothing searches on your behalf. *.java is still the habit to keep, because it makes the build independent of which file happens to reference which.
Decoding java -cp out Main
-cp out(spelled-classpathif you prefer) is the search path for compiled classes. Several entries are joined with;on Windows and:on macOS and Linux. That separator is the one platform-specific detail in the Java build.Mainis a class name, not a filename. The launcher resolves it against the classpath.java -cp out Main.classandjava -cp out out/Main.classboth fail: the argument takes no extension and no path.- The launcher then reflectively looks up
public static void main(String[])on that class and calls it. Nothing aboutmainis special tojavac. See error 3.
Classes load lazily, by name, on first use. A program can compile and then die partway through with NoClassDefFoundError because something absent from the runtime classpath was finally touched. C++ gives you that experience only with dynamic libraries.
SpotBoard.java, line by line
import java.util.ArrayList;
import java.util.List;
These lines read no files and generate no bytes. Each says: for the rest of this file, that short name means that long name. They are order-insensitive, harmless to duplicate, and optional. Everything below could be written with full names, and the compiled output would be indistinguishable.
Two imports rather than one, because import java.util.X imports exactly X. The wildcard import java.util.*; imports every type in that package and no subpackage; java.util.concurrent.ConcurrentHashMap still needs its own line. Wildcards also make the ambiguity in error 4 possible, which is why most codebases list types explicitly.
private final List<String> freeSpotIds = new ArrayList<>();
Field type is the interface, constructed type is the implementation. That pairing is Java's default habit, and it is the reason List and ArrayList are two separate imports.
new ArrayList<>() uses the diamond operator, inferring <String> from the declared type on the left. It is the direct analogue of C++ CTAD, and omitting the type argument on the right is the normal form. new ArrayList<String>() is not wrong, only dated.
final on a collection field is not const. It stops the reference being reassigned. Any code holding the reference can still call freeSpotIds.add(...). The invariant it maintains is narrow but real: freeSpotIds is never null after construction and never points at a different list. The bug it prevents is the field being swapped out underneath you. The bug it does not prevent is what A7 is about.
List<String> viaImport() {
return List.copyOf(freeSpotIds);
}
java.util.List<String> viaFullName() {
return java.util.List.copyOf(freeSpotIds);
}
These two methods exist to be the proof. Same declared type, written two ways. Same runtime class, which Main checks with == on the Class objects and prints as one runtime type: true. Delete import java.util.List; and only viaImport() breaks.
List.copyOf(...) returns an unmodifiable copy, so neither method leaks the field. That is a design point rather than a J2 point, and it is here because demonstrating imports with a method that hands out internals would teach the wrong reflex.
String describe() {
return freeSpotIds.size() + " free spots";
}
String is never imported, and not because it is a keyword. Every file gets an implicit import java.lang.*;. String, Integer, Object, Exception, Thread and Math are all ordinary classes in java.lang. Nothing there is built into the language the way int is; the shortness of String is a package accident.
Main.java, line by line
SpotBoard board = new SpotBoard();
No import, and no import is possible. SpotBoard is in the default package, same as Main. Types in one package see each other with no ceremony. Error 5 shows what happens when they are in different packages.
System.out.println("T0: " + java.time.Instant.parse("2026-08-18T10:00:00Z"));
A type used with zero imports, spelled in full at the point of use. Nobody writes production code this way. It is here so the belief that an import is required has something concrete to break against. The instant is the one corpus/parking-lot/tests_base pins its clock to.
static boolean sameTypeBothWays(SpotBoard board) {
List<String> viaImport = board.viaImport();
java.util.List<String> viaFullName = board.viaFullName();
return viaImport.equals(viaFullName);
}
.equals, not ==, on purpose: two distinct copies with equal contents. If that distinction is not yet automatic, J6 is the lesson, and it is the most consequential one in the bridge.
The six errors, produced for real
Every message below came from running the code above with one thing changed.
1 · The import removed
Delete import java.util.List; from SpotBoard.java:
SpotBoard.java:11: error: cannot find symbol
private final List<String> freeSpotIds = new ArrayList<>();
^
symbol: class List
location: class SpotBoard
cannot find symbol with symbol: class X is the missing-import shape. Contrast symbol: variable X, which javac prints when the name sat where a variable would also fit. Both appear here: line 19, return List.copyOf(freeSpotIds);, reports symbol: variable List, because List.copyOf(...) is equally consistent with a variable named List.
Three errors follow from one deleted line, one per use site. There is no single declaration to fail at. And viaFullName() is untouched in the output. One method broke, the other did not, from the same missing import.
2 · No -cp, so the class is nowhere
Run java Main from the directory holding the sources and out/:
Error: Could not find or load main class Main
Caused by: java.lang.ClassNotFoundException: Main
The default classpath is the current directory, which holds Main.java but not Main.class. That went to out/. The launcher does not compile, and never looks at .java files.
Read the two lines as one thought. Could not find or load main class is the launcher's framing; ClassNotFoundException underneath is the cause. When you see this, check whether -cp points at the directory holding the .class file.
3 · A class with no main
java -cp out SpotBoard
Error: Main method not found in class SpotBoard, please define the main method as:
public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application
SpotBoard.class was found and loaded. This failure is strictly later than error 2 and means something different. The classpath was right, the class name was right, and the class has no entry point. main is invisible to javac and interesting only to the launcher. So every near-miss signature compiles cleanly and fails right here: void main(), static int main(String[]), public void main(String[]). Ignore the JavaFX line; it prints unconditionally.
4 · An ambiguous import
Add import java.awt.List; beside the existing import java.util.List;:
SpotBoard.java:13: error: reference to List is ambiguous
private final List<String> freeSpotIds = new ArrayList<>();
^
both interface java.util.List in java.util and class java.awt.List in java.awt match
This error is possible only because imports are aliases. With #include you would get two types with distinct qualified names and no conflict until you wrote an unqualified List. Here two imports both claim the short name, so javac refuses to pick.
The fix is not to remove an import. It is to write the full name at the ambiguous use site, which restates the model: short names are a convenience layered over full names that always work. The everyday version of this error is java.util.Date against java.sql.Date.
5 · A packaged class reaching for the default package
Put package dojo; at the top of Main.java only, leaving SpotBoard in the default package:
Main.java:12: error: cannot find symbol
SpotBoard board = new SpotBoard();
^
symbol: class SpotBoard
location: class Main
No import fixes this. A class in the default package has no package name, so there is nothing to write after import. dojo.Main cannot see it under any spelling.
That is why the project uses the default package for everything rather than for some things: the two do not mix. Flat sources, one javac -d out *.java, one java -cp out Main, no imports between your own classes. The hidden test suite lands in the same package and sees your types with no ceremony.
6 · package without -d
Add package dojo; to both files and compile with no -d:
Main.class Main.java SpotBoard.class SpotBoard.java
java -cp . dojo.Main
Error: Could not find or load main class dojo.Main
Caused by: java.lang.ClassNotFoundException: dojo.Main
The class files are flat, but the launcher looks for dojo.Main at ./dojo/Main.class, because a package is a directory path. With -d out the same sources give out/dojo/Main.class and java -cp out dojo.Main runs. Building that tree by hand is the classic first hour with packages, and -d is how you skip it.
Worked source
The 2 files of the worked design
Every file below is the one the app opens, verbatim. This is the part worth reading slowly: the prose above argues for a shape, and these are the lines that have it.
worked/src/SpotBoard.java43 linesworked/src/Main.java35 lines
worked/src/SpotBoard.java43 lines
// SpotBoard.java
//
// The free-spot board for one level of the parking lot. Two imports, and one deliberate
// refusal to import. All three spellings below compile to the same type.
import java.util.ArrayList;
import java.util.List;
public class SpotBoard {
// `List` resolves here only because of the import above. Delete that import and this
// line fails with "cannot find symbol / symbol: class List".
private final List<String> freeSpotIds = new ArrayList<>();
void release(String spotId) {
freeSpotIds.add(spotId);
}
/** The short name, made available by the import at the top of the file. */
List<String> viaImport() {
return List.copyOf(freeSpotIds);
}
/**
* The identical type, spelled out in full. No import is involved, and no import would
* help. This method and viaImport() return the same runtime class, which Main checks.
*/
java.util.List<String> viaFullName() {
return java.util.List.copyOf(freeSpotIds);
}
/**
* String came from java.lang, which every file imports implicitly. That is why String
* has never needed an import, and it is not because String is a keyword. It is an
* ordinary class in an auto-imported package.
*/
String describe() {
return freeSpotIds.size() + " free spots";
}
int size() {
return freeSpotIds.size();
}
}
worked/src/Main.java35 lines
// Main.java
//
// One import. Note what is not imported: SpotBoard. It sits in the same package (the default
// package), so it needs no import, and no syntax exists that could import it.
import java.util.List;
public class Main {
public static void main(String[] args) {
SpotBoard board = new SpotBoard();
board.release("L1-A-014");
board.release("L1-A-015");
System.out.println(board.describe());
System.out.println("short name and full name agree: " + sameTypeBothWays(board));
// java.time.Instant, used with no import, spelled in full at the point of use.
// The corpus tests pin their clock to exactly this instant.
System.out.println("T0: " + java.time.Instant.parse("2026-08-18T10:00:00Z"));
// Class objects prove the two spellings name one type, at run time.
System.out.println("one runtime type: "
+ (board.viaImport().getClass() == board.viaFullName().getClass()));
}
/**
* True when the imported spelling and the fully-qualified spelling produce equal
* results. They always do: it is one method's return type written two ways.
*/
static boolean sameTypeBothWays(SpotBoard board) {
List<String> viaImport = board.viaImport();
java.util.List<String> viaFullName = board.viaFullName();
return viaImport.equals(viaFullName);
}
}
The faded stage is not here, on purpose
In the app, the third stage of a lesson hands you the worked design with a few lines
replaced by // GAP: markers, then compiles your completion and runs a JUnit suite
against it. That needs javac, and a static site has no compiler — so rather than show a
control that cannot work, this page stops at the worked source.
Run the app for the drill: it is the download in the header, and it works offline once unpacked.