1.1
Why Java Exists
Start with the portability problem Java was designed to solve, then trace source code to the processor.
Previously on
This is the first lesson. Its destination is precise: you should be able to explain why one compiled program may fail on another computer.
You will also explain how Java changed that distribution problem. Syntax comes later. This lesson builds the machine model that gives the syntax meaning.
The problem
Imagine that you write a program in a language such as C++. The file you edit is source code.
#include <iostream>
int main() {
std::cout << "Hello\n";
}A processor does not execute C++ source. A compiler translates the source into a native executable. That executable contains machine instructions and information expected by a particular platform.
For this lesson, a platform has two important parts:
| Processor architecture | Operating system | |
|---|---|---|
| Examples | x86-64, ARM64 | Windows, Linux, macOS |
| Contract | Instruction set and registers | Executable format and system services |
| What can differ | How to load, add, branch, and store | How programs open files, allocate memory, and display output |
Suppose you compile the program for Windows on x86-64. Copying that executable to macOS on ARM64 changes both parts of the platform.
The ARM64 processor does not use the same machine instructions as x86-64. macOS also uses different executable and operating-system conventions than Windows.
Even Windows and macOS on the same processor are different platforms. The processor instructions may match, but the operating-system contract does not.
This does not mean C++ source code cannot be portable. The same source can often be compiled for many targets. The distribution problem is about the compiled binary.
Traditional native distribution
- Write one source programHumans edit the same source files.
- Choose a target platformThe compiler and linker produce a native binary for that target.
- Repeat for other targetsEach supported processor and operating system needs a compatible build.
In the early 1990s, software was moving across a growing variety of computers and consumer devices. Rebuilding and testing every application for each target created a real delivery cost.
The idea
Java added a stable middle target between source code and the local machine.
Java source Java bytecode native execution
Hello.java -> Hello.class -> JVM for this platformThe Java compiler translates source into bytecode. Bytecode is an instruction format defined for the Java Virtual Machine, not for x86-64 or ARM64.
A Java Virtual Machine, or JVM, executes that bytecode. The JVM implementation knows how to work with its own processor and operating system.
Java distribution
- Write sourceThe programmer writes Hello.java.
- Compile to bytecodejavac creates Hello.class for the JVM instruction set.
- Distribute the class fileCompatible JVMs can load the same bytecode.
- Execute locallyEach JVM performs the platform-specific work on its machine.
The portable artifact is the bytecode. The JVM itself is not portable.
A Windows x86-64 JVM is a native Windows x86-64 program. A macOS ARM64 JVM is a different native program. Both implement the same JVM contract, so both can understand compatible class files.
That architecture is the basis of write once, run anywhere. The phrase is a design goal, not a guarantee that every program ignores its environment.
A Java program can still depend on platform-specific files, native libraries, fonts, permissions, or devices. Bytecode portability removes one major source of incompatibility. It does not erase all environmental differences.
Under the hood
Going deeperWhy processor architecture changes the binary
A processor implements an instruction set architecture, often shortened to ISA. The ISA defines operations that machine code may request.
It covers details such as instruction encoding, registers, memory access, arithmetic, and branches. x86-64 and ARM64 encode and organise those operations differently.
A sequence of bytes that means “load and add” to one architecture may mean something else to another. It may also be invalid there.
This is why a file containing native instructions has a processor target.
Why the operating system changes the binary
Programs need services outside pure arithmetic. They open files, create threads, request memory, read networks, and write to terminals.
The operating system defines how native programs request those services. It also defines executable formats, loading rules, libraries, and calling conventions.
The C++ line below looks independent of any operating system:
std::cout << "Hello\n";Its implementation eventually reaches platform-specific runtime and operating-system code. The source-level library hides that work, but the final native program must still fit the target system.
What the JVM standardises
The JVM supplies a virtual instruction set and a runtime model. Java class files describe code using that shared model.
The runtime then bridges to the real platform. Depending on the JVM and the code, it may interpret bytecode or compile selected methods into native instructions.
The important boundary is stable:
application bytecode
|
v
JVM implementation for this platform
|
v
processor and operating systemApplication teams target the upper contract. JVM teams implement the lower bridge.
Java had other design goals
Portability was not Java’s only goal. Its designers also removed or restricted several features that made large C++ programs harder to manage.
Java manages object memory with garbage collection. It does not expose pointer arithmetic in ordinary Java code. Class inheritance has one direct superclass, which avoids several multiple-inheritance conflicts.
Those choices do not make programming safe by default. They remove particular categories of error and make other checks possible.
Security also mattered. Early Java was associated with downloaded applets, which ran code from a network inside a browser.
That demanded verification and restricted execution. Browser applets are obsolete now, but bytecode verification and managed execution remain important parts of Java’s architecture.
What it costs
The JVM adds a runtime layer. A Java application needs a compatible runtime, takes time to start, and uses memory for metadata and managed execution.
Garbage collection can also pause application work. Runtime compilation consumes CPU before it can improve frequently executed code.
Java accepted those costs to gain portable bytecode, automatic memory management, runtime checks, and adaptive optimisation. Whether that trade is useful depends on the program.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
What two parts make a computing platform in this lesson, and how can either part make a native binary incompatible?
Show the answer
A platform is the processor architecture plus the operating system. The processor accepts a particular instruction set. The operating system exposes its services through a particular binary interface. Change either contract and an existing native binary may no longer be valid.
What is the portable part of a Java program, and what part remains platform specific?
Show the answer
The compiled class files contain Java bytecode, which is designed for a JVM rather than one processor and operating system. The JVM remains platform specific because it must execute on the local processor and call the local operating system.
Did Java eliminate compilation for every platform?
Show the answer
No. It moved most platform-specific work into JVM implementations. An application developer can distribute the same compatible bytecode, while JVM builders provide a runtime for each supported platform.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises60 pointsabout 55 minutes
Trace the Journey
ex-1-1-aOpen a plain text file, or a notebook, and write out the full journey of a Java program.
Start from the moment you finish typing your code. End at the moment the processor is running real instructions.
Do not look back at the section while you do it. Write what you remember, then check. Getting a stage wrong and then correcting it is worth far more than copying the list correctly.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Journey {
static String[] stagesInOrder()
static String extensionOfWhatYouWrite()
static String extensionOfWhatTheCompilerMakes()
static int filesInTheStory()
static String stageThatHappensOnce()
static String stageThatHappensOnEveryMachine()
static boolean happensOnYourMachine(String stage)
}The five stage names are given so that the exercise is about the order and not about guessing words: source, compiling, bytecode, jvm, machine-code. Capitals and surrounding spaces are ignored everywhere. Extensions include the dot. Write the list from memory first, then check it.
What your program must do
- Write the five stages in order, without looking back at the section
- Name the file extension at each stage that has one
- Mark the stage that happens only once, however many computers run it
- Mark the stage that happens separately on every computer
public class Journey {
// TODO: the five stages, in order, from the file you type to the instructions
// a processor runs. Use exactly these five words, in the right order:
// source, compiling, bytecode, jvm, machine-code
static String[] stagesInOrder() {
return new String[]{};
}
// TODO
static String extensionOfWhatYouWrite() { return "?"; }
// TODO
static String extensionOfWhatTheCompilerMakes() { return "?"; }
// TODO: how many files are in this story?
static int filesInTheStory() { return 1; }
// TODO: which stage happens once, however many computers run the program?
static String stageThatHappensOnce() { return "?"; }
// TODO: which stage happens again on every computer, every single run?
static String stageThatHappensOnEveryMachine() { return "?"; }
// TODO: true if this stage happens on YOUR machine rather than the one
// running the program.
static boolean happensOnYourMachine(String stage) {
return true;
}
public static void main(String[] args) {
int step = 1;
for (String stage : stagesInOrder()) {
System.out.println(step++ + ". " + stage
+ (happensOnYourMachine(stage) ? " (your machine)" : " (theirs)"));
}
}
}
Hint 1
Hint 2
Hint 3almost the answer
Find Your Own Platform
ex-1-1-bEnough theory. Find out what you are actually standing on.
Open a terminal and work out your own platform. Write down both halves. Then find someone else in your class or your group with a different answer, and compare.
This takes five minutes and it makes the rest of the phase concrete. “Platform” should be the name of your own machine, not a word from a slide.
What your program must do
- Find out which processor family your computer uses
- Find out which operating system it runs
- Write your platform as one line, in the form "processor + operating system"
- Name one other platform that would need a different JVM from yours
Sample run
uname -m
uname -s
arm64
Darwin
Hint 1
uname -m and uname -s. On Windows, open Command Prompt and try echo %PROCESSOR_ARCHITECTURE%.Hint 2
arm64 and aarch64 both mean an ARM processor. x86_64 and AMD64 both mean an Intel or AMD processor. Darwin is what macOS calls itself.Hint 3almost the answer
Will It Run?
ex-1-1-cSix cases. For each one, say whether the program runs, and why.
- You compile
hello.cppon Intel + Windows. You copy the compiled program to another Intel + Windows machine. - You compile
hello.cppon Intel + Windows. You copy the compiled program to Intel + Linux. - You compile
hello.cppon Intel + Windows. You copy the compiled program to ARM + Windows. - You compile
Hello.javaon Intel + Windows. You copyHello.classto Intel + Linux, which has a JVM installed. - You compile
Hello.javaon Intel + Windows. You copyHello.classto ARM + Linux, which has a JVM installed. - You compile
Hello.javaon Intel + Windows. You copyHello.classto ARM + Linux, which has no JVM installed.
Write your six answers before you check anything. Then go back through the section and mark the ones you got wrong. Those are the ones worth remembering.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class WillItRun {
static boolean cppRuns(String builtOs, String builtArch, String targetOs, String targetArch)
static boolean javaRuns(String builtOs, String builtArch, String targetOs, String targetArch, boolean jvmInstalled)
static String whatChanged(String builtOs, String builtArch, String targetOs, String targetArch)
static boolean classFileDependsOnThePlatform()
}Write the rule, not the six answers. The tests call these with platforms the exercise never mentions, so a table of six cases will fail. Operating system and processor names are compared without regard to capitals. whatChanged returns one of nothing, os, processor or both.
What your program must do
- Answer the six cases on paper before you write any code
- Write cppRuns as a rule about both halves of the platform
- Write javaRuns, and notice how much of its input it ignores
- Name which half moved for any pair of platforms
public class WillItRun {
// TODO: a compiled C++ binary. When does it run on the target machine?
static boolean cppRuns(String builtOs, String builtArch, String targetOs, String targetArch) {
return true;
}
// TODO: a .class file. Ask a different question from the one above.
static boolean javaRuns(String builtOs, String builtArch,
String targetOs, String targetArch, boolean jvmInstalled) {
return true;
}
// TODO: which half of the platform moved?
// "nothing", "os", "processor" or "both"
static String whatChanged(String builtOs, String builtArch, String targetOs, String targetArch) {
return "?";
}
// TODO
static boolean classFileDependsOnThePlatform() { return true; }
public static void main(String[] args) {
// The six cases from the exercise. Answer them on paper first.
System.out.println("1 " + cppRuns("Windows", "Intel", "Windows", "Intel"));
System.out.println("2 " + cppRuns("Windows", "Intel", "Linux", "Intel"));
System.out.println("3 " + cppRuns("Windows", "Intel", "Windows", "ARM"));
System.out.println("4 " + javaRuns("Windows", "Intel", "Linux", "Intel", true));
System.out.println("5 " + javaRuns("Windows", "Intel", "Linux", "ARM", true));
System.out.println("6 " + javaRuns("Windows", "Intel", "Linux", "ARM", false));
}
}
Hint 1
Hint 2
javaRuns's parameters your answer actually uses. That is the point of the exercise.Hint 3almost the answer
.class file runs anyway, because a Linux ARM JVM exists.The 1995 Memo
ex-1-1-dIt is 1995. You work at a company that makes software for set-top boxes, and every new box means recompiling everything.
Write a short memo to your manager arguing that the company should move to Java.
Your manager is smart but is not a programmer. They will not accept jargon, and they will notice if you avoid the downsides. So explain the actual problem in plain words, explain what Java changes, and be straight about the cost.
Then leave it for a day, read it again, and cut every sentence that is only there to sound impressive.
What your program must do
- Write between 200 and 300 words
- Explain the portability problem using a concrete example, not a definition
- Explain what your company gives up by moving to Java, honestly
- Do not use the words "platform independent" anywhere in the memo
Hint 1
Hint 2
Hint 3almost the answer
After the credits
You now have the reason for the architecture. The next lesson names the runtime and development tools, then uses them on a real Java file.
Keep one diagram in your notes:
source -> javac -> bytecode -> JVM -> current platformThreads you opened in this section
- Write once, run anywhereThe same promise is why you never free memory by hand.Phase XIII. Memory and the Garbage Collector
Write once, run anywhere will return in 1.2 - JVM, JRE, JDK, and Your First Program