Java Compiler
The Java compiler, javac, translates Java source code (.java files) into bytecode (.class files) that the JVM can run. Along the way it checks the program thoroughly: syntax, types, method signatures, access rules, definite assignment of variables, and reachability of code. Most mistakes in a Java program are caught here, before the program ever runs.
What the Compiler Does
Source (.java) → Parse → Analyze (types, names, rules) → Generate bytecode → .class files
- Parsing. The source text is broken into tokens (keywords, names, literals, symbols) and arranged into a syntax tree. Missing semicolons, unbalanced braces, and misspelled keywords fail here.
- Analysis. Every name is resolved to a variable, method, or class. Every expression is assigned a type and checked against how it is used. Access modifiers,
finalassignments, checked exceptions, and definite assignment of local variables are verified. - Desugaring. Convenience syntax is rewritten into simpler forms: enhanced
forloops become iterator or index loops, autoboxing becomesInteger.valueOfcalls, string concatenation becomes optimized concatenation calls, and generics are erased to their bounds. - Code generation. One
.classfile is written for every class, interface, enum, and record, including nested and anonymous ones.
The compiler does not produce machine code for your CPU. That happens later, at runtime, inside the JVM's JIT compiler.
Compile-Time Errors Versus Runtime Errors
| Compile-time error | Runtime error (exception) | |
|---|---|---|
| Detected by | javac | The JVM while the program runs |
| When | Before the program can run at all | Only when the faulty code actually executes |
| Examples | Type mismatch, missing semicolon, undefined variable, missing return | Division by zero, null reference, array index out of range |
| Result | No .class file produced | Program stops with a stack trace (unless the exception is caught) |
int count = "five"; // compile-time error: incompatible types
int[] a = new int[3];
a[5] = 1; // compiles; fails at runtime with ArrayIndexOutOfBoundsException
The compiler checks types and rules it can prove from the code. It cannot know the values that variables will hold when the program runs, so errors that depend on values appear at runtime.
Reading Compiler Error Messages
public class Order {
public static void main(String[] args) {
int qty = 3
double price = 49.99;
String total = qty * price;
System.out.println(totl);
}
}
javac Order.java
Order.java:3: error: ';' expected
int qty = 3
^
1 error
Each message has the same structure:
FileName.java:LINE: error: DESCRIPTION
the offending source line
^ caret pointing at the position
Notice that only one error was reported. A syntax error stops the compiler during parsing, before it analyzes types. After fixing the semicolon and recompiling:
Order.java:5: error: incompatible types: double cannot be converted to String
String total = qty * price;
^
Order.java:6: error: cannot find symbol
System.out.println(totl);
^
symbol: variable totl
location: class Order
2 errors
Now analysis runs and reports every type and name problem it finds. Fix errors from the top down: an early error can cause later, misleading ones, and fixing it often removes several messages at once.
Common messages
| Message | Usual cause |
|---|---|
';' expected | Missing semicolon, usually at the end of the line above the caret |
cannot find symbol | Misspelled name, missing import, variable used outside its scope |
incompatible types | Assigning or passing a value of the wrong type |
missing return statement | A non-void method has a path that does not return |
variable x might not have been initialized | A local variable is read before every path assigns it |
unreachable statement | Code after return, break, throw, or an infinite loop |
class X is public, should be declared in a file named X.java | File name does not match the public class |
unreported exception X; must be caught or declared to be thrown | A checked exception is neither caught nor declared with throws |
reached end of file while parsing | A closing brace } is missing |
non-static method cannot be referenced from a static context | Calling an instance method from main without an object |
Warnings
Warnings do not stop compilation but point to likely problems. Enable all of them with -Xlint:
import java.util.ArrayList;
import java.util.List;
public class Legacy {
public static void main(String[] args) {
List names = new ArrayList(); // raw types: no generic parameter
names.add("Ada");
}
}
javac -Xlint:all Legacy.java
Legacy.java:6: warning: [rawtypes] found raw type: List
List names = new ArrayList();
^
missing type arguments for generic class List<E>
...
Legacy.java:7: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
names.add("Ada");
^
Raw types bypass generic type checking. The fix is List<String> names = new ArrayList<>();. Adding -Werror turns warnings into errors, which many projects enable in their build to keep the codebase warning-free.
Essential javac Options
| Option | Effect |
|---|---|
-d <dir> | Write .class files into <dir>, organized by package |
-cp <path> / --class-path <path> | Where to find other compiled classes and JARs the code depends on |
--release <N> | Compile for Java version N: restricts APIs and language features to N and sets the class-file version |
-Xlint:all | Enable all recommended warnings |
-Werror | Treat warnings as errors |
-g | Include full debugging information (local variable names) |
-parameters | Keep method parameter names in the class file, used by some frameworks |
-encoding UTF-8 | Character encoding of the source files |
--source-path <path> | Where to find other source files to compile as needed |
Separating source and output
project/
├── src/
│ └── com/example/shop/
│ ├── App.java
│ └── Cart.java
└── out/
javac -d out src/com/example/shop/*.java
java -cp out com.example.shop.App
-d out writes out/com/example/shop/App.class and Cart.class, creating the package directories automatically. Keeping compiled output out of the source tree means it can be deleted and rebuilt at any time and excluded from version control.
Compiling against a library
javac -cp lib/gson-2.11.0.jar -d out src/com/example/shop/App.java
java -cp out:lib/gson-2.11.0.jar com.example.shop.App # macOS/Linux
java -cp "out;lib/gson-2.11.0.jar" com.example.shop.App # Windows
The classpath separator is : on macOS and Linux and ; on Windows. The library must be on the classpath both when compiling (so javac can check calls into it) and when running (so the JVM can load its classes). Build tools like Maven and Gradle manage this automatically, which is a major reason real projects use them.
What javac Generates
Each class becomes its own file, including nested and anonymous classes:
public class Shop {
class Item {} // inner class
enum Size { S, M, L } // nested enum
Runnable task = new Runnable() { // anonymous class
public void run() {}
};
}
javac Shop.java
ls
Shop.class Shop$Item.class Shop$Size.class Shop$1.class Shop.java
Nested types are named Outer$Inner.class; anonymous classes are numbered Outer$1.class, Outer$2.class, in order of appearance. All of these must be shipped together.
javap -c shows the generated bytecode:
public class Add {
static int add(int a, int b) {
return a + b;
}
}
javap -c Add
static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
The JVM is a stack machine: iload_0 and iload_1 push the two int parameters onto the operand stack, iadd pops both and pushes their sum, and ireturn returns the top value. The i prefix means int; there are separate instructions for long, double, and references, which is how the JVM's verifier can check type safety of the bytecode itself.
The Compiler and the Language Version
Each JDK's compiler supports the language features of its version. Compiling code that uses a newer feature with an older JDK fails:
record Point(int x, int y) {} // records: Java 16+
On a Java 11 JDK:
error: class, interface, or enum expected
The message is confusing because the old compiler does not know the word record at all. When code from a tutorial or project does not compile, checking javac -version is a good first step.
Conversely, --release lets a newer JDK produce code for an older runtime:
javac --release 17 -d out src/**/*.java
The compiler then rejects features and APIs not available in Java 17. Prefer --release over the older -source/-target pair, which set the language level and class-file version but do not check API usage, allowing code that compiles successfully and then fails on the older runtime.
The Compiler Inside Other Tools
Build tools and IDEs do not reimplement Java compilation; they call the JDK's compiler through its API (javax.tools.JavaCompiler). Maven's compiler plugin, Gradle's compileJava task, and the java Source.java single-file launcher all use the same javac engine. Eclipse is the main exception, using its own incremental compiler (ECJ), which is why it can show errors instantly while you type in very large projects. The rules enforced are the same, since both implement the Java Language Specification.