On this page:
Overview
11.1 Source files and compiled class files
11.2 Running code and the Tester entry point
11.3 The classpath
11.4 Environment variables and paths
11.5 Inspecting bytecode
11.6 Packaging with jars
11.7 Decompilation
8.13

Java at the Command Line🔗

What IntelliJ hides when it compiles and runs our code

Overview🔗

Up to now, we have relied on IntelliJ and the Tester library to run our programs. IntelliJ takes care of compiling our source files, arranging the classpath, and invoking the Tester’s entry point. In this lecture, we make that process explicit. We will see what happens when Java source code is compiled into bytecode, how the JVM runs that bytecode, and how the Tester library supplies the entry point that we have avoided writing ourselves. We will also look at jars, the classpath, and briefly at how to inspect or even decompile class files.

Everything we do here is what IntelliJ already does for us automatically. The point is to see the hidden steps, so that you recognize them when things go wrong.

11.1 Source files and compiled class files🔗

Java programs start out as .java source files. The javac compiler checks the syntax and type correctness of these files and produces .class files. Each .class file contains bytecode instructions for the Java Virtual Machine. This bytecode is what the JVM actually executes, not the original source. IntelliJ invokes javac whenever you click Run or Build, placing the resulting .class files in its out directory. If the compiler encounters errors, no class file is produced.
A useful way to picture this is a pipeline: .java source files —compiled by javac> .class bytecode files. These bytecode files are then fed to the JVM when you run the program.

11.2 Running code and the Tester entry point🔗

Every standalone Java program requires an entry point. Ordinarily this is a main method inside some class. Our course setup avoids introducing main. Instead, the tester.jar library contains its own tester.Main class, with its own main method. When IntelliJ runs your Examples class, what it actually does is launch the JVM with tester.Main as the entry class, and hands it the name of your Examples class as an argument. The Tester library then uses reflection to load that class and run all of its test... methods.

Conceptually, the process is:

When you change the Run Configuration in IntelliJ, what you are really changing is the equivalent of this command-line invocation. IntelliJ hides it, but it is there.

11.3 The classpath🔗

How does the JVM find your classes and the Tester library? It searches along the classpath. By default the classpath is the current directory. IntelliJ extends it to include both your compiled output and tester.jar. On macOS or Linux, one might write:

java -cp .:tester.jar tester.Main ExamplesFoo

On Windows, the separator is a semicolon:

java -cp .;tester.jar tester.Main ExamplesFoo

If either . or tester.jar is missing from the classpath, the JVM cannot find your Examples class or the Tester library, and execution fails.
This is one of the most common sources of confusing error messages. If you leave out the dot, the JVM cannot find ExamplesFoo.class. If you leave out tester.jar, the JVM cannot find tester.Main. IntelliJ is just setting up this classpath list on your behalf.

11.4 Environment variables and paths🔗

For the command-line tools to be available, the system path must include the JDK. On macOS and Linux, check with echo $PATH. On Windows, check with echo %PATH%. If javac is not found, set the environment variable JAVA_HOME to the installation directory of your JDK, and add \$JAVA_HOME/bin (or %JAVA_HOME%\\bin) to your PATH. On Windows this is configured via Control Panel -> System and Security -> System -> Advanced system settings -> Environment Variables. Setting JAVA_HOME ensures that the correct Java version is used, and avoids subtle mismatches between compiler and runtime.
The difference in separators (: on Unix-like systems, ; on Windows) comes from the way each operating system represents lists of paths. This is exactly parallel to how PATH itself is written.

11.5 Inspecting bytecode🔗

The compiled .class files are binary, but the JDK provides the tool javap to disassemble them. Running javap -c ExamplesFoo reveals the bytecode instructions. These instructions include loading constants, invoking methods, and returning results. Although cryptic, they demonstrate that the source has indeed been compiled into a lower-level form for the JVM.

11.6 Packaging with jars🔗

A jar file is a zipped collection of class files and metadata. The Tester library itself is a jar.

Some jars are used as libraries, like tester.jar. Others are executable jars, with a manifest specifying which class to run. These can be launched with java -jar someapp.jar. For us, jars are primarily libraries, but in the wider Java ecosystem both kinds are common.

IntelliJ automatically builds jars for libraries it depends on. You can also create your own jars with the jar tool, for example jar cf mywork.jar ExamplesFoo.class. Adding a jar to the classpath makes its classes available to the JVM. This is how tester.jar is linked into your runs.
It is helpful to think of a jar as just a zip file with a different extension. You can open it with ordinary zip tools. Inside you will find a directory tree of compiled classes, arranged by package.

11.7 Decompilation🔗

Decompilers attempt to reverse the process, producing readable Java source from bytecode. IntelliJ includes the Fernflower decompiler. If you open a class from tester.jar in the editor, IntelliJ shows you decompiled code. From the command line, Fernflower can be run directly. The following command for example can be used within OSX assuming a default installation.

java -cp "/Applications/IntelliJ IDEA.app/Contents/plugins/java-decompiler/lib/java-decompiler.jar" \

  org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler \

  tester.jar fernflower-out/

This produces a directory of .java files reconstructed from the jar. While not identical to the original source, it is close enough to understand how the library works.
Other decompilers exist as well, but IntelliJ bundles one. This is why you can click on classes in a jar and still see source code, even when the actual source is not present.