Layers of Logic

1.1

Why Java Exists

Start with the portability problem Java was designed to solve, then trace source code to the processor.

Core16 min read4 exercises
01

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.

02

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 architectureOperating system
Examplesx86-64, ARM64Windows, Linux, macOS
ContractInstruction set and registersExecutable format and system services
What can differHow to load, add, branch, and storeHow programs open files, allocate memory, and display output
A native executable must fit both contracts. Matching only the processor is not enough.

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

  1. Write one source programHumans edit the same source files.
  2. Choose a target platformThe compiler and linker produce a native binary for that target.
  3. 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.

03

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 platform

The 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

  1. Write sourceThe programmer writes Hello.java.
  2. Compile to bytecodejavac creates Hello.class for the JVM instruction set.
  3. Distribute the class fileCompatible JVMs can load the same bytecode.
  4. 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.

04

Under the hood

Going deeper

Why 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 system

Application 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.

05

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.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. 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.

  2. 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.

  3. 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.

07

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

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Trace the Journey

Warm up·10 min·10 points

checkedex-1-1-a

Open 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
Journey.java
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
There are two files in this story, not one. One you write. One the compiler makes for you.
Hint 2
Ask yourself which stage the JVM is responsible for. Everything before it happens on your machine. Everything after it happens on the machine that is running the program.
Hint 3almost the answer
The compiling step happens once, wherever you happen to do it. The JVM step happens on every single machine, every single time the program runs. That is the whole trade: you pay once, and they pay every time.
What this is really testing

Whether you can follow one file all the way from something you typed to something a processor ran. If any arrow in that chain is fuzzy, everything built on top of it will be fuzzy too.

B

Find Your Own Platform

Warm up·10 min·10 points

ex-1-1-b

Enough 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

You type
uname -m
uname -s
It prints
arm64
Darwin
Hint 1
On Mac or Linux, open a terminal and try 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
A machine with the same processor as yours but a different operating system is already a different platform. That is the point of the exercise. If you are on ARM + macOS, then ARM + Linux is a different platform and needs a different JVM.
What this is really testing

Whether "platform" names something concrete to you. You cannot reason about portability until you can name your own processor and operating system.

C

Will It Run?

Real work·15 min·20 points

checkedex-1-1-c

Six cases. For each one, say whether the program runs, and why.

  1. You compile hello.cpp on Intel + Windows. You copy the compiled program to another Intel + Windows machine.
  2. You compile hello.cpp on Intel + Windows. You copy the compiled program to Intel + Linux.
  3. You compile hello.cpp on Intel + Windows. You copy the compiled program to ARM + Windows.
  4. You compile Hello.java on Intel + Windows. You copy Hello.class to Intel + Linux, which has a JVM installed.
  5. You compile Hello.java on Intel + Windows. You copy Hello.class to ARM + Linux, which has a JVM installed.
  6. You compile Hello.java on Intel + Windows. You copy Hello.class to 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
WillItRun.java
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
For the C++ cases, ask two questions rather than one. Did the processor change? Did the operating system change? Either one on its own is enough to break a compiled binary.
Hint 2
For the Java cases, ask a different question: is there a JVM on the target machine? Look at how many of javaRuns's parameters your answer actually uses. That is the point of the exercise.
Hint 3almost the answer
Case 2 is the one that catches people. Same processor, different operating system, so the system calls baked into the binary are wrong, and it will not run. Case 5 is the mirror image: both halves changed and the .class file runs anyway, because a Linux ARM JVM exists.
What this is really testing

Whether you understood that a platform has two halves. Most people only check the processor, and that is exactly where this exercise catches them.

D

The 1995 Memo

Real work·20 min·20 points

ex-1-1-d

It 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
The last requirement is the real exercise. Banning the phrase forces you to explain the idea instead of naming it.
Hint 2
A concrete example beats a definition every time. Something like: we ship to four kinds of set-top box, so today every release means four builds and four sets of bugs.
Hint 3almost the answer
Do not skip the honest part. A memo that says a technology has no downside is a memo nobody believes. Name the speed cost, name the fact that every device now needs a JVM installed, and then say why the trade is still worth it.
What this is really testing

Whether you can explain this without repeating the wording you just read. If you can build the argument in your own words, you have understood it.

08

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 platform

Threads you opened in this section

Write once, run anywhere will return in 1.2 - JVM, JRE, JDK, and Your First Program