Compile Java
Compiling turns .java source files into .class bytecode files using the javac command. This lesson is a practical guide to compiling real projects from the command line: single files, multiple files, packages, external libraries, and larger source trees. The underlying behavior of the compiler is covered in the Java Compiler lesson.
Compiling a Single File
// Greeting.java
public class Greeting {
public static void main(String[] args) {
System.out.println("Compiled and running");
}
}
javac Greeting.java
If the code is correct, javac prints nothing and creates Greeting.class in the same directory. Silence means success; any output is a warning or an error.
ls
# Greeting.class Greeting.java
The command takes a file name with the .java extension. Running it from the directory containing the file is simplest; otherwise give the path (javac src/Greeting.java).
Compiling Multiple Files
When classes depend on each other, compile them together:
// Account.java
public class Account {
private double balance;
public void deposit(double amount) { balance += amount; }
public double getBalance() { return balance; }
}
// Bank.java
public class Bank {
public static void main(String[] args) {
Account acc = new Account();
acc.deposit(500);
System.out.println(acc.getBalance());
}
}
javac Account.java Bank.java
# or
javac *.java
Both class files are created. If you compile only Bank.java, javac automatically looks for Account in the current directory (the default classpath and source path), finds Account.java, and compiles it too. Relying on that is fine for small examples, but listing files or using a source path makes the build predictable.
Separating Output with -d
Mixing .class files with source files clutters the project. -d sends output to a separate directory:
javac -d out *.java
.
├── Account.java
├── Bank.java
└── out/
├── Account.class
└── Bank.class
javac creates out if it does not exist. To run, point the classpath at it:
java -cp out Bank
Compiling Packages
For classes in packages, the source files sit in directories that match the package name:
src/
└── com/example/inventory/
├── App.java
├── model/
│ └── Product.java // package com.example.inventory.model;
└── service/
└── StockService.java // package com.example.inventory.service;
Compile from the project root and let -d create the package directories:
javac -d out \
src/com/example/inventory/App.java \
src/com/example/inventory/model/Product.java \
src/com/example/inventory/service/StockService.java
out/com/example/inventory/
├── App.class
├── model/Product.class
└── service/StockService.class
Then run by fully qualified class name:
java -cp out com.example.inventory.App
Using --source-path
Instead of listing every file, give the root of the source tree and only the entry point. javac finds the other classes it needs by package and name:
javac -d out --source-path src src/com/example/inventory/App.java
javac compiles App.java and every class it references, directly or indirectly. Classes that nothing references are not compiled, so this is ideal for building a program from its main class.
Compiling Every Source File
To compile an entire tree regardless of references, collect the file names first.
macOS / Linux:
find src -name "*.java" > sources.txt
javac -d out @sources.txt
Windows (PowerShell):
(Get-ChildItem -Recurse -Filter *.java src).FullName | Set-Content -Encoding ascii sources.txt
javac -d out "@sources.txt"
Write the file list with Set-Content -Encoding ascii rather than the > redirection operator. In Windows PowerShell 5.1 (the version built into Windows), > writes UTF-16 text, which javac cannot read as an argument file. PowerShell 7 and later write UTF-8 without a byte-order mark, so > works there. If any path contains non-ASCII characters, use PowerShell 7 or keep the project in an ASCII-only path.
The @file syntax is an argument file: javac reads its arguments from the file, separated by spaces or new lines. Argument files also avoid command-line length limits on Windows and can hold options as well as file names. For example, options.txt could contain:
-d out
--release 21
-Xlint:all
-encoding UTF-8
javac @options.txt @sources.txt
Keep argument files to options and file names only. Paths containing spaces must be enclosed in double quotes inside the file.
Compiling with External Libraries
Code that uses a third-party library needs the library's JAR on the classpath at compile time, so javac can check every call against the library's real classes and methods.
// src/com/example/App.java
package com.example;
import com.google.gson.Gson;
public class App {
record User(String name, int age) {}
public static void main(String[] args) {
Gson gson = new Gson();
System.out.println(gson.toJson(new User("Ada", 36)));
}
}
# macOS / Linux
javac -cp "lib/*" -d out src/com/example/App.java
java -cp "out:lib/*" com.example.App
# Windows
javac -cp "lib\*" -d out src\com\example\App.java
java -cp "out;lib\*" com.example.App
Output (with a Gson version that supports records, 2.10 or later):
{"name":"Ada","age":36}
-cp "lib/*"is a classpath wildcard: it adds every JAR inlib. Quote it so the shell passes the*to Java instead of expanding it.- The classpath separator is
:on macOS/Linux and;on Windows. - At run time the classpath must include both your compiled classes (
out) and the libraries.
Without the JAR on the compile classpath, javac reports package com.google.gson does not exist. Downloading JARs by hand and keeping their versions consistent quickly becomes difficult, which is the problem Maven and Gradle solve.
Targeting an Older Java Version
javac --release 17 -d out @sources.txt
--release 17 makes the compiler behave as Java 17's would: newer language features and APIs are rejected, and the class files can run on a Java 17 JVM. Use it whenever the code must run on a JVM older than the JDK you compile with.
Recommended Flags for Everyday Builds
javac -d out --release 21 -Xlint:all -Werror -encoding UTF-8 @sources.txt
| Flag | Why |
|---|---|
-d out | Clean separation of source and output |
--release 21 | Explicit, reproducible target version |
-Xlint:all | Surface warnings about raw types, unchecked casts, deprecation, and more |
-Werror | Fail the build on any warning so they cannot accumulate |
-encoding UTF-8 | Consistent reading of non-ASCII source text on every machine and JDK |
Incremental and Clean Builds
javac itself is not incremental across invocations: it compiles exactly the files it is given (plus any it discovers through the source path). It does not track which files changed since the last build. Two practical consequences:
- After deleting or renaming a class, its old
.classfile stays inoutuntil removed. Stale class files can make a program run with outdated code. - When a class changes in a way that affects others (for example, a constant's value, which is inlined into classes that use it), dependent classes must be recompiled too.
The reliable approach on the command line is a clean build:
rm -rf out # Windows PowerShell: Remove-Item -Recurse -Force out
javac -d out @sources.txt
Build tools track changes and dependencies automatically, recompiling only what is needed while avoiding stale output.
Compiling with Build Tools
Real projects rarely call javac directly. A build tool runs it with the right source paths, classpath, and options:
mvn compile # Maven: compiles src/main/java into target/classes
gradle build # Gradle: compiles into build/classes/java/main
Both read the project's dependencies from a build file, download the JARs, construct the classpath, and invoke the JDK's compiler. Understanding the plain javac workflow is still valuable: it shows exactly what these tools automate, and it is how you diagnose a build that fails in surprising ways.
Practical Example: Script for a Small Project
A minimal build script for a project with src/, lib/, and a main class:
#!/usr/bin/env bash
# build.sh
set -e # stop at the first failing command
MAIN=com.example.inventory.App
rm -rf out
mkdir -p out
find src -name "*.java" > sources.txt
javac --release 21 -Xlint:all -encoding UTF-8 \
-cp "lib/*" -d out @sources.txt
echo "Build OK. Run with:"
echo " java -cp \"out:lib/*\" $MAIN"
chmod +x build.sh
./build.sh
The script performs a clean build every time, compiles all sources with consistent options, and includes libraries from lib. set -e makes the script stop if javac reports an error, so a failed compile is never mistaken for a successful one. This is essentially what a build tool's compile step does, in a few lines.