Compiling and Executing a Java Program
Every Java project you will ever work with, whether it is built by an IDE, Code Runner, Maven, or Gradle, ultimately does two things with command-line tools. Learn to do them by hand and the abstractions become understandable.
javac the compiler .java source -> .class bytecode
java the launcher loads .class files into the JVM and runs the program
The two steps behave differently, and that difference is the source of most beginner errors. Compilation is a path-aware translation step. At runtime, classes are linked by name only. Everything below follows from that split.
All outputs in this post are real. They were produced with OpenJDK 21, javac 21.0.12.
$ javac -version
javac 21.0.12
$ java -version
openjdk version "21.0.12" 2026-07-21
Getting the code
Every example in this post lives in the repository java-cae. Clone it and run the commands side by side with the source files.
$ git clone https://github.com/vajradevam/java-cae.git
$ cd java-cae
The repository has one directory per section. flat/ contains the first example.
proj/ contains the packaged project used in Parts 2 through 8. access/ contains the
deliberately broken variant of Part 4. oneway/ contains the default-package examples
of Part 9. Commands are run from inside the directory each section names, and the
output shown is exactly what those directories produce.
Part 1. A simple program without packages
Before packages enter the picture, the two commands look trivial. The flat/ directory
contains two files in one folder.
// App.java
class App {
public static void main(String args[]) {
Calculator calc = new Calculator();
System.out.println(calc.add(2, 3));
}
}
// Calculator.java
class Calculator {
public int add(int x, int y) { return x + y; }
public int sub(int x, int y) { return x - y; }
public int mul(int x, int y) { return x * y; }
public int div(int x, int y) { return x / y; }
}
Compile both files and run the one that defines main.
$ ls
App.java Calculator.java
$ javac App.java
(exit 0)
$ java App
5
$ ls
App.class App.java Calculator.class Calculator.java
Three details matter here. Each one changes in the next part.
javac App.javafoundCalculatorwithout any flags. Both classes live in the same default (unnamed) package, so javac needed no lookup configuration.- Without
-d, class files are written next to the sources. The final listing showsApp.classandCalculator.classsitting beside the.javafiles. There is no build directory. java Appran with the bare class name, because the class is not part of any package.
A source file without a package statement belongs to the unnamed package, also called the default package. Everything above relies on that fact. Part 2 adds one package declaration and all three conveniences stop working.
Part 2. Packages and fully qualified names
A package is a named namespace for classes. It does two jobs.
- Logical grouping. A package groups related classes and lets two classes with the same
simple name exist in one program.
functions.Calculatorandfinance.Calculatorare different types even though both classes are namedCalculator. - Physical layout. The package name must mirror the directory structure. A class
declared in package
a.b.Cmust live in directorya/b/relative to the source root, and after compilation it must sit ina/b/C.classrelative to the classpath root.
Defining a package takes one statement at the top of the file.
package functions; // must be the very first statement in the file
Several rules apply.
- The
packagestatement must be the first statement. Only comments and whitespace may precede it, and it applies to the whole file. - One package per file. Two different package declarations cannot appear in a single compilation unit.
- Package names are conventionally written in lowercase and often use a reversed domain
name, such as
com.example.calculator.
The proj/ directory follows these rules. The list below shows its source files
relative to the project root.
./src/calculator/App.java
./src/functions/Calculator.java
App.java declares package calculator;. Calculator.java declares
package functions;.
The full name of a class is the package name followed by the simple name, separated by
a dot. functions.Calculator and calculator.App are the fully qualified names, and
both work anywhere in code without an import.
Part 3. Imports
import statements sit after the package statement and before the class
declaration.
package calculator;
import functions.Calculator; // imports one class
// import functions.*; // imports all classes in functions
public class App {
// ...
}
An import is not needed in three cases.
- The class you use is in your own package. Same-package classes see each other directly.
- The class lives in
java.lang, such asString,System, orMath. That package is always available. - You write the fully qualified name inline.
functions.Calculator calc = new functions.Calculator()works without any import statement.
One rule surprises most beginners. The default package cannot be imported by anyone. A
class in a named package cannot write import App;, because the default package has no
name to import. Part 9 shows what happens when a named package tries to use a
default-package class.
Part 4. Access modifiers
The classes in Part 1 were declared without a modifier.
class Calculator { ... }
A class or member without a modifier is package-private, and that is the default. Within one package this is sufficient. The moment classes live in different packages, it ceases to be sufficient.
The access/ directory contains a variant of the same project in which Calculator
lost its public modifier. Everything else is identical.
// access/src/functions/Calculator.java
package functions;
class Calculator { // no public
public int add(int x, int y) {
return x + y;
}
}
Compiling the entry class (access/src/calculator/App.java) gives the following
result.
$ javac -d out -sourcepath src src/calculator/App.java
src/calculator/App.java:3: error: Calculator is not public in functions; cannot be accessed from outside package
import functions.Calculator;
^
src/calculator/App.java:7: error: Calculator is not public in functions; cannot be accessed from outside package
Calculator calc = new Calculator();
^
src/calculator/App.java:7: error: Calculator is not public in functions; cannot be accessed from outside package
Calculator calc = new Calculator();
^
3 errors
(exit 1)
The file does not compile because Calculator is package-private in functions and
App lives in calculator. An import does not override access control. Visibility is
enforced before anything can be linked.
The same rule applies to members. A public class whose add method were private would
produce an analogous error at the call site. If a class is meant to be used from another
package, the class must be public, and so must every method the caller invokes.
The full visibility ladder is shown below.
| Modifier | Same class | Same package | Subclass in another package | Anywhere |
|---|---|---|---|---|
public |
yes | yes | yes | yes |
protected |
yes | yes | yes | no |
| none (package-private) | yes | yes | no | no |
private |
yes | no | no | no |
private is the most restrictive level. It is visible only inside the enclosing class.
Part 5. Compiling one file is not enough
In Part 1 the commands ran from inside the flat/ directory. Real projects keep
sources below a root directory, as proj/ does. The naive attempt compiles the entry
file the way Part 1 did.
$ javac src/calculator/App.java
src/calculator/App.java:3: error: package functions does not exist
import functions.Calculator;
^
src/calculator/App.java:9: error: cannot find symbol
Calculator calc = new Calculator();
^
symbol: class Calculator
location: class App
src/calculator/App.java:9: error: cannot find symbol
Calculator calc = new Calculator();
^
symbol: class Calculator
location: class App
3 errors
(exit 1)
package functions does not exist, and yet the directory src/functions/ exists, and
the file exists. What happened?
Without -sourcepath, javac uses the classpath as its source search path, and the
classpath defaults to the current directory. The command above runs from proj/. When
javac resolves import functions.Calculator it looks for functions/Calculator.java
relative to ., which resolves to proj/functions/Calculator.java. No such file
exists, because the sources live under src/. javac does not guess where the source
tree root is.
The error is reported in stages. The import triggers a search for the package, which
fails, so the compiler prints package functions does not exist. Every later reference
to the class inside that package fails as well, once per use site. Two references to
Calculator therefore produce two cannot find symbol errors. All three messages have
one cause.
Part 6. The compile command in detail
The command that works makes two things explicit. One option says where dependencies
live. One positional argument says which file to compile. The two occurrences of src
in the command are unrelated parts of the command line.
javac -d out -sourcepath src src/calculator/App.java
option option + value positional argument
-sourcepath srcsets the source search root. When javac meets an import it is not already compiling, it appends the package path to this root and looks for the source there.srcplus/functions/Calculator.javaresolves tosrc/functions/Calculator.java. The file is found, loaded, and compiled as part of the same invocation. This is implicit compilation. The dependency was never named on the command line and it still gets compiled.-
src/calculator/App.javais a positional argument. It names the entry file. The-sourcepathoption compiles nothing by itself, so a source file must always be named explicitly.$ javac -d out -sourcepath src error: no source files (exit 2) -
-d outselects the output directory. javac creates the package directories underout/automatically from thepackagedeclarations.$ javac -d out -sourcepath src src/calculator/App.java (silent success) $ find out -type f out/calculator/App.class out/functions/Calculator.class
An alternative exists that avoids -sourcepath entirely. Name every file on the command
line.
$ javac -d out src/calculator/App.java src/functions/Calculator.java
Like the previous command, this is silent on success. The -sourcepath form exists so
the file list does not have to be maintained by hand. Projects grow, and the list
eventually becomes $(find src -name '*.java').
Part 7. Inside javac
The -verbose flag makes the compiler narrate its phases. The listing below keeps the
lines that concern this project and omits the JDK module files that javac also loads.
The absolute path in the first line reflects the local copy of the repository and will
differ on another machine.
$ javac -verbose -d outv -sourcepath src src/calculator/App.java
[parsing started SimpleFileObject[/home/apathak/mycc/java/java-cae/proj/src/calculator/App.java]]
[parsing completed 9ms]
[search path for source files: src]
[loading src/functions/Calculator.java]
[parsing started DirectoryFileObject[src:functions/Calculator.java]]
[parsing completed 1ms]
[checking calculator.App]
[wrote outv/calculator/App.class]
[checking functions.Calculator]
[wrote outv/functions/Calculator.class]
[total 127ms]
The pipeline has five phases.
- Parse. javac reads source and builds an abstract syntax tree. Syntax errors appear here.
- Resolve. javac meets
import functions.Calculatorand searches for the type. It checks the compilation units already in progress, then the sourcepath for a.javafile, then the classpath for a.classfile. Thecannot find symbolandpackage does not existmessages come from this phase. - Attribute and type-check. The compiler verifies that every expression is legal. Does
add(int, int)exist onCalculator? Do the argument types match? Does each name resolve to exactly one declaration? Access rules are enforced here. Most compile errors are reported in this phase. - Flow analysis. The compiler checks definite assignment, unreachable code, and methods that fail to return on every path.
- Write bytecode. Generics are erased, inner classes become files named
Outer$Inner.class, and the.classfiles are written.
The order in the log matters. App.java is parsed first. Calculator.java is then
loaded, and both files are type-checked together. The unit of compilation is not one
file. It is the closure of dependencies that javac assembles, possibly across packages.
The .class files do not contain native machine code. The javap tool prints bytecode
in a readable form.
$ javap -c -p out/functions/Calculator.class
Compiled from "Calculator.java"
public class functions.Calculator {
public functions.Calculator();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int add(int, int);
Code:
0: iload_1
1: iload_2
2: iadd
3: ireturn
[output elided]
The bytecode for add is a stack-machine sequence. It pushes the two arguments, applies
iadd, and returns the result. The class file refers to other types by name only, with
constant-pool entries such as functions/Calculator and add. The JVM resolves those
names at runtime using the classpath. That is why runtime lookup follows different rules
than compile-time lookup, and why Part 8 spends so much time on names.
Part 8. Running the program
Compilation produces the class files. A separate tool runs them.
$ java -cp out calculator.App
5
The command has two requirements.
- The classpath, set with
-cp out, must point at the package root. The launcher resolvescalculator.Appby appending the package path to the classpath, which producesout/calculator/App.class. The classpath must never point at a package directory itself. - The class name must be fully qualified,
calculator.App. The simple nameAppis not acceptable.
Two failures illustrate the rules. The first command omits the package part of the name.
$ java -cp out App
Error: Could not find or load main class App
Caused by: java.lang.ClassNotFoundException: App
(exit 1)
There is no out/App.class. Only out/calculator/App.class exists, so the JVM cannot
create the class that the name implies.
The second failure comes from changing into the package directory before running.
$ cd out/calculator && java App
Error: Could not find or load main class App
Caused by: java.lang.NoClassDefFoundError: App (wrong name: calculator/App)
(exit 1)
The JVM finds the file this time. out/calculator/App.class exists on the classpath.
But the class inside the file declares itself calculator.App, which does not match the
name App that was requested. The verifier rejects the mismatch. The current directory
plays no part in class resolution. Only the classpath and the fully qualified name
matter.
Part 9. A program in the default package
This section covers a layout that looks like a shortcut. The entry class sits directly
under the source root, without a package statement, while its dependency stays
packaged. The oneway/ directory has that shape.
./src/App.java // no package statement
./src/functions/Calculator.java
./src/functions/UseApp.java
App.java is a member of the default package. It imports a class from a named package.
// oneway/src/App.java
import functions.Calculator;
public class App {
public static void main(String args[]) {
Calculator calc = new Calculator();
System.out.println(calc.add(2, 3));
}
}
Compile and run with the same commands as before, using App as the entry file.
$ javac -d out -sourcepath src src/App.java
$ java -cp out App
5
The layout works. A class in the default package may import from a named package, and
the program runs using the simple name App. This is the structure that appears when a
packaged Calculator.java is combined with an App.java that has no package
statement. Note that compiling the entry file does not pull in UseApp.java; nothing
referenced reaches it, and javac only compiles the closure of what the entry file
depends on.
The restriction is one-directional. A class in a named package cannot see the default
package, because the default package has no name and therefore cannot be imported or
referred to. The third file in the directory demonstrates the failure. UseApp lives
in functions and returns an App.
// oneway/src/functions/UseApp.java
package functions;
public class UseApp {
public App make() {
return new App();
}
}
Compiling UseApp fails.
$ javac -d out -sourcepath src src/functions/UseApp.java
src/functions/UseApp.java:4: error: cannot find symbol
public App make() {
^
symbol: class App
location: class UseApp
src/functions/UseApp.java:5: error: cannot find symbol
return new App();
^
symbol: class App
location: class UseApp
2 errors
(exit 1)
The default package is a one-way street.
default package -> named package works
named package -> default package fails
The layout in this section works today because nothing in functions needs App. The
moment any packaged class references the default-package class, the build breaks. Java 9
modules impose the same restriction, since a module cannot export an unnamed package.
The default package is a scratch space for short examples, not a place to keep
application code.
Why this matters
The compilation and launch steps above run inside every tool that builds Java programs.
- IDEs configure
-sourcepathand the classpath from the project model and then run the same tools invisibly. When an IDE reports an error it is the raw output of this pipeline.package does not existandClassNotFoundExceptionare compiler and launcher messages, not IDE messages. - Maven and Gradle encode the same choices as convention.
src/main/javais the source root,target/classesis the output directory, and dependency jars are added to the classpath. A build error that sayspackage X does not existmeans exactly what it meant in Part 5. The sources or jars ofXare not on the configured lookup paths. - Single-file launchers such as
java App.javawork only for self-contained files. Once a program spans packages, they need the explicit lookup configuration shown in Part 6. - JAR deployment keeps the same model. A jar is a compressed classpath root. The manifest names the main class by its fully qualified name, and the runtime still resolves everything by name.
Two questions cover the whole workflow.
- During compilation, where are the sources of the classes I depend on, and where do
the outputs go? The answers are
-sourcepathand-classpathfor lookup and-dfor output. - During execution, which class starts the program and where is the package root? The
answers are the fully qualified name and
-cp.
Reference
# structure (source root is src/)
src/calculator/App.java -> package calculator;
src/functions/Calculator.java -> package functions;
# compile. -sourcepath tells javac where to find sources of dependencies
javac -d out -sourcepath src src/calculator/App.java
# alternative. list every file, no -sourcepath
javac -d out src/calculator/App.java src/functions/Calculator.java
# run. classpath is the package root (out), never a subfolder.
# the class name is the fully qualified one
java -cp out calculator.App
| Situation | What you get | Cause |
|---|---|---|
javac src/calculator/App.java without -sourcepath |
package functions does not exist |
source lookup defaulted to . and never saw src/ |
No public on a cross-package class |
Calculator is not public in functions; cannot be accessed from outside package |
default access is package-private |
java -cp out App with a packaged main class |
ClassNotFoundException: App |
no out/App.class; the class is calculator.App |
cd out/calculator && java App |
NoClassDefFoundError: App (wrong name: calculator/App) |
the internal name inside the file does not match the requested name |
| named package references the default package | cannot find symbol ... class App |
the unnamed package cannot be imported |
Java separates translation from execution. javac translates whatever is reachable from
the sourcepath, type-checks the whole closure together, and writes bytecode into a
mirror of the package tree. java links by name, and given a classpath root and a fully
qualified class name it finds, verifies, and runs exactly the class that was named. The
IDE button, the build tool, and the launcher all reduce to this.
all posts