JAVA / GETTING STARTED
Source files, class files and the build flow
Predict the exact set of .class files javac writes for a source file, and how packages, directories and run-time class lookup fit together.
What you will learn
- Predict every .class file javac emits, including Outer$Inner and Outer$1
- Map a package name to the exact directory path its class file must occupy
- Tell compile-time type errors apart from run-time class-loading failures
- Spot a stale class file behind a code change that seems to do nothing
Understanding Source files, class files and the build flow
A .java file is a compilation unit: a block of text that javac reads as one piece. Nothing in the compiled result corresponds to that file, though. javac walks the declarations inside it and writes one class file per type, so the BuildFlow.java below produces four files: BuildFlow.class, BuildFlow$Helper.class, BuildFlow$1.class and Support.class. The file name matters for exactly one reason: a public top-level type must sit in a file named after it, so that javac can locate the source of a public type it has only seen mentioned by name.
As soon as packages appear, the directory tree stops being tidiness and becomes part of the lookup algorithm. A class's real identity is its binary name, such as tools.text.Slug; a class loader replaces the dots with slashes and asks each class path root for tools/text/Slug.class. That is why the compiled tree has to mirror the package names, and why a class file cannot just be moved somewhere more convenient: the name would no longer match the place it was found, and the JVM refuses it on that basis.
The flow therefore has two separate checkpoints, and knowing which one you are standing at explains most early errors. javac checks types, resolves names against the sources and class files it can see, and writes bytecode, but it runs nothing. The JVM then loads each class the first moment execution needs it, verifies the bytecode, and resolves further references the same lazy way. That laziness is why a missing or out-of-date class file shows up as a failure part-way through a run instead of at build time.
public class BuildFlow {
static class Helper {
String name() {
return getClass().getName();
}
}
public static void main(String[] args) {
Runnable job = new Runnable() {
@Override
public void run() {
System.out.println("anonymous: " + getClass().getName());
}
};
System.out.println("top level: " + BuildFlow.class.getName());
System.out.println("nested: " + new Helper().name());
job.run();
System.out.println("second top level: " + Support.class.getName());
}
}
class Support {
// not public, so it is allowed to share the file with BuildFlow
}
A source file is only a text container; the unit the JVM actually loads is one class file per type, found by its fully qualified name under a class path root.
Worked examples
A package name is part of the class name
Shows how a binary name is turned into the file path a class loader looks for.
package tools.text;
public class Slug {
public static void main(String[] args) {
Class<?> c = Slug.class;
System.out.println("binary name: " + c.getName());
System.out.println("simple name: " + c.getSimpleName());
System.out.println("package: " + c.getPackageName());
System.out.println("file path: " + c.getName().replace('.', '/') + ".class");
}
}
Example explained
Line 1getName() returns the identity the JVM uses, tools.text.Slug, with the package included.
Line 2getSimpleName() returns only Slug, which is what you write in code once the type is imported or in the same package.
Line 3The replace('.', '/') call reproduces exactly what a class loader does before searching the class path.
Line 4So compiling with -d out must produce out/tools/text/Slug.class, and out is the directory you put on the class path, not out/tools/text.
Classes are found by name while the program runs
Demonstrates that a missing class file is a run-time lookup failure, not a compilation failure.
public class LateLookup {
public static void main(String[] args) {
System.out.println("main started");
try {
Class<?> c = Class.forName("tools.text.Missing");
System.out.println("loaded " + c.getName());
} catch (ClassNotFoundException e) {
System.out.println("no class file for " + e.getMessage());
}
System.out.println("main finished");
}
}
Example explained
Line 1The name is passed as a string, so javac has nothing to check and the file compiles even though the class does not exist.
Line 2Class.forName performs the same name-to-path search the JVM uses for ordinary references, one class at a time.
Line 3ClassNotFoundException carries the requested binary name as its message, which is why the second line names tools.text.Missing.
Line 4The last line still prints, showing that lookup happens on demand rather than as a start-up check of everything the program might need.
Important notes
A non-public top-level class hidden inside another file compiles fine, but javac looks for a source named Support.java when some other file references Support, so that other file can fail to build until Support is compiled or moved.
The 1 in Outer$1 is assigned by the compiler in declaration order and shifts when you reorder code, so never hardcode it; the same $ separator is why Class.forName needs "Outer$Inner" where source code writes Outer.Inner.
Common mistakes
Renaming the file without renaming the public class, or the reverse: javac stops with "class X is public, should be declared in a file named X.java" and writes no class files at all.
Declaring package tools.text; then stepping into tools/text and running java Slug: the file is found but rejected with NoClassDefFoundError: Slug (wrong name: tools/text/Slug), because the class is identified by its name and must be launched as java tools.text.Slug from the class path root.
Editing the .java file and re-running java without recompiling: the JVM loads the previous class file, the change appears to have no effect, and you end up debugging code that is not the code running.
Try it yourself
Change, predict, then run
In a browser editor, write one file containing public class Report, a static nested class Row, one anonymous Runnable, and a second top-level class Footer, and print getClass().getName() for the first three. Before running it, write down the four class file names javac would produce, then check your list against the printed names.
Open the Java workspaceCheck your understanding
Report.java declares public class Report, a static nested class Report.Row, and one anonymous Runnable, and main prints a line before it creates that Runnable. You compile it, delete Report$1.class, then run java Report. What happens?
- javac silently rebuilds the missing class file when java runs.
- The JVM refuses to start, because it verifies every class Report mentions before entering main.
- The first line prints, then the run fails with NoClassDefFoundError when the anonymous class is first needed.
- Nothing changes, because an anonymous class is compiled into the class file of its enclosing class.
Show answer
The JVM resolves a reference the first time execution actually reaches it, so everything printed before the new Runnable(){...} expression appears normally and only then does the load fail. The last option is tempting because the anonymous class is written inside Report.java, but each type in a compilation unit gets its own class file, here Report$1.class.