Phase 0 · Foundations

Your computer cannot read Java

By the end of this page you'll know what happens between "text in a file" and "the machine does something" — because you'll have broken it in six places.

⏱ ~50 minutes🧩 Needs: 0.0💻 Code: src/Hello.java

You type some words into a file. You save it. Later, the machine does things — words appear on your screen. Nothing you typed was ever a thing that could happen. It was letters. So what happened in between? Most tutorials answer "compile and run it" and move straight on. That shrug is the source of about half of all beginner confusion in Java.

We're going to take that shrug apart. There are two separate programs involved, and they fail in completely different ways. Learn to tell instantly which one is complaining at you. A whole category of mystery stops happening.

Fair warning: this chapter contains a result that looks like it proves the title wrong. I'm going to show it to you rather than hide it, because that's the interesting bit.

What you'll be able to do at the end

Section 1Why anything needs translating at all

Deep down, a computer's brain — the CPU, or processor, the chip that actually does the work — understands one language. Its own. And that language is astonishingly stupid. Not "hard", stupid. Its instructions are things like copy this number from here to there. Or add these two numbers. Or if this number is zero, jump to a different step. That's about the level of it.

That language is called machine code, and it isn't even letters — it's numbers. Each instruction is a pattern of on/off switches inside the chip. There is no instruction called "print", let alone "print this sentence". Printing a sentence is hundreds of those tiny steps piled up.

Think of it like this

Imagine hiring a builder who only understands about forty words. All of them are about moving single bricks: lift, left, right, down, is-this-brick-red. You cannot say "build me a wall". You can build a wall, but only by saying forty thousand tiny things in exactly the right order. Java is you speaking normally. Machine code is the forty words. Something in the middle has to do the translating.

Now here's the awkward part. There isn't one machine language — there are many. The chip in a modern Mac speaks a different language from the chip in an older Mac. A phone's chip is different again, and so is a web server's. Same idea, different words. Machine code written for one is gibberish to the other.

So any language humans can stand to write has this problem. It has to be turned into the local machine's own language before it can run. The program that does that turning is called a compiler. And how Java solves this problem is genuinely clever. Cleverer than you'd guess. It's why Java took over the world in the 1990s. Hold that thought for two sections.

Section 2Meet the specimen, line by line

In this chapter's src/ folder there's a file called Hello.java. It is the smallest complete Java program that does anything. And we're going to spend the rest of the chapter attacking it.

src/Hello.java
/**
 * The smallest complete Java program — our specimen for dissection.
 *
 * Every word here is load-bearing, and by the end of Phase 1 you'll know
 * what each one means. For now only two things matter:
 *   - main is where the JVM starts executing
 *   - this FILE is not a program; what javac makes from it is
 */
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, machine.");
    }
}

Thirteen lines, and you deserve to know what all of them are. Even the ones whose details wait until Phase 1.

Lines 1–8 are a comment. Anything between /* and */ is a note for humans. The tools skip it entirely — it could say anything at all and the program would behave identically. Comments are free, and this file's comment is doing real work: it tells you what to care about right now.

Line 9, public class Hello {, gives the code a name. In Java every piece of code lives inside a named container called a class. This one is called Hello. The word public means "anything is allowed to use this". The { opens a box. Everything up to the matching } on the last line belongs to Hello.

A rule that bites early

The class name and the file name must match. Class Hello must live in Hello.java — same spelling, same capital letters. This is not a style guideline, it's enforced. Save that same class as Greet.java and javac stops dead with class Hello is public, should be declared in a file named Hello.java. In Lab 6 we break the matching rule from the other end, on the compiled file. The complaint we get is completely different.

Small print, flagged now so you never have to unlearn it. That rule is enforced for classes carrying the word public. And ours carries it. Java does let a class without that word sit in a file of any name. Phase 1 covers why anyone would want that.

Line 10, public static void main(String[] args) {, is the starting line. When something runs your program, it looks for a chunk called exactly main and begins there. Everything before main on that line describes how it can be called. And String[] args is a slot for words you could pass in from the terminal. We don't use that slot yet. Ignore the details, remember the job: main is where the machine starts.

Line 11, System.out.println("Hello, machine.");, is the only line that does anything visible. Read it right to left. The bit in quotes is the text. println means "print this, then move to a new line". System.out is the thing being printed to — your terminal window. The semicolon at the end is a full stop. Java needs one at the end of every instruction like this.

Lines 12 and 13 are the two closing curly brackets, } and }. The first closes main, the second closes the class Hello. Curly brackets always come in pairs, and every unmatched one is an error. That's it — that's the whole program.

The happy path

Two commands. Run them in the src folder.

terminal — the two-step
$ javac Hello.java
$ java Hello
Hello, machine.
$ ls
Hello.class     Hello.java

javac Hello.java is stage one: the compiler reads your text and writes out a new file. It printed nothing, which — remember Chapter 0.0 — means it worked. java Hello is stage two: the actual running. And ls shows the evidence: there are now two files where there was one.

Look hard at those two command lines, because there's a detail almost everyone gets wrong for their first week. You compiled Hello.java, with the extension. You ran Hello, without it. That's not sloppiness. javac wants a file, so you give it a file name. java wants a class, so you give it a class name. Different things, deliberately.

That new file, Hello.class, is the translated version. Try opening it in your editor. You'll get a screenful of nonsense — because it isn't text. Here are the real first 230 bytes of it. Every byte that isn't a printable letter or symbol is shown as a dot:

the first 230 bytes of Hello.class (unprintable ones shown as dots)
.......=..................java/lang/Object...<init>...()V........
........java/lang/System...out...Ljava/io/PrintStream;......Hello, machine.
................java/io/PrintStream...println...(Ljava/lang/String;)V
......Hello...Code...

Those line breaks are mine, to fit the page. The real file has no lines in it at all. Your own copy may differ by a byte or two, because different Java builds pack the file slightly differently.

Mostly rubble, with a few recognisable words poking out of it — including your sentence, sitting there in one piece. That's bytecode: a list of very small numbered instructions, plus a table of the names and text your program needs. It's a program written for a machine. Just not for your machine.

You compiled with javac Hello.java — extension and all. Then you ran it with java Hello, no extension. Why does one command want the .java and the other doesn't?

Section 3The imaginary machine

Here's the trick, and it's the best idea in Java.

javac doesn't translate your code into the language of a real chip. It translates it into the language of a machine that doesn't exist. Someone sat down and invented a processor. They decided what instructions it would have, and what its language would look like. Then they never built it. Bytecode is machine code for that invented machine.

Which sounds useless. A program for a chip nobody owns can't run anywhere. Except: you can fake the imaginary machine, in software. You write one real program, for real chips. Its entire job is to pretend to be that invented processor. It carries out those instructions faithfully.

That faking program is the JVM — the Java Virtual Machine. "Virtual" here means exactly what it means in "virtual reality": not real, but convincing enough to work in. And java, the command you typed, is the JVM. When you ran java Hello, you started up an imaginary computer, handed it Hello.class, and it got to work.

Think of it like this

A film script is written once, in English. To show it in Japan you don't rewrite the film — you hire a Japanese dub team. The script never changes. Each country supplies its own translators. Bytecode is the script. Each kind of computer supplies its own JVM. Write once, dub everywhere.

Now the payoff. Your Hello.class will run on a Mac, on a Windows laptop, and on a Linux server in a data centre. Byte for byte identical. No changes, no recompiling. It will even run on a chip that hadn't been designed when you compiled it. Every one of those machines has a JVM, and every JVM speaks the same bytecode.

Compare a compiled C program. That one was translated straight into one real machine's language. It will not run on a different kind of chip at all.

A simplification, flagged honestly

"Every JVM speaks the same bytecode" is true sideways and only half true forwards. Every kind of computer, yes. Every age of JVM, no. Each .class file carries a stamp saying which Java version made it. A JVM will happily run a file stamped older than itself. It flatly refuses one stamped newer. That error is called UnsupportedClassVersionError. So your Java 17 file runs on a Java 21 machine, but not on a Java 8 one. Phase 4 is where we read those stamps ourselves.

Text to running program, one hop at a time

1. You type. Hello.java exists on disk. It is text and nothing else — the same kind of thing as your note.txt from last chapter. No machine can run it. Nobody has translated anything yet.

On disk

Hello.java = text you wrote

Running now

nothing

2. javac Hello.java. The compiler starts up. It reads your file and checks that every word is legal Java. If it is, it writes a brand new file next to it. Your source file — the text file you typed, the one you can read — is never touched or changed.

On disk

Hello.java = text
Hello.class = bytecode (new!)

Running now

javac, briefly

3. javac finishes and leaves. It printed nothing, so nothing was wrong. It is now completely gone from the story. This is the bit worth burning in: from here on, nothing ever looks at Hello.java again. Labs 3 and 4 both depend on that one fact.

4. java Hello. The JVM starts. It hunts for a file called Hello.class in the current folder. It loads the bytecode inside, then looks for the chunk named main.

On disk

Hello.java = ignored
Hello.class = being read

Running now

JVM, at main

5. The JVM works through the instructions. One bytecode instruction at a time, each one turned into real instructions for your real chip. It hits the print instruction, and your sentence appears. Then main ends, the JVM shuts down, and you get your prompt back.

terminal
Hello, machine.
A simplification, flagged honestly

I said the JVM works through the instructions one at a time. That's true of part of it. But the JVM also watches which bits of your code run over and over. Those bits it quietly re-translates into real machine code, while your program is running, so they get faster. It's why long-running Java speeds up after a few seconds. We'll do this properly in Phase 4. "An imaginary machine faked in software" is close enough to be useful. And not so wrong you'll have to unlearn it.

Section 4Lab 1: the result that looks like it breaks the title

Everything above says the same thing: source code gets translated first, always, no exceptions. Two files, two steps. So let's try skipping a step and watch it fail.

Predict this one properly. Write your guess down. This is the single most surprising thing in Phase 0. I'd rather you got it wrong on purpose than nodded along.

Predict before you run — Lab 1

You skip javac completely. You have only Hello.java in the folder — no .class file at all. You type java Hello.java, with the .java on the end, handing your plain text file straight to the JVM. Two questions: does it run? And afterwards, does ls show a new Hello.class?

what actually happens (verified, JDK 17)
$ ls
Hello.java
$ java Hello.java
Hello, machine.
$ ls
Hello.java

It works. No compiling, no .class file before, no .class file after. If you predicted an error, you predicted exactly what this chapter's title implies. And you were wrong. So was almost everyone.

So is the title a lie? No, and the second ls is the proof of that. Look at it again. There's no Hello.class. Nothing was left behind.

Here's what really happened. Since Java 11 the java command has been trained to notice when you hand it a filename ending in .java. When it sees that, it does not read your source as bytecode — it can't. Instead it quietly runs the compiler for you. It keeps the resulting bytecode in memory only, runs that, and throws it away when your program ends. Two steps still happened. One of them happened invisibly, in memory only, in the blink between you pressing Enter and the output appearing.

That missing Hello.class is the whole argument. If the JVM could genuinely read Java source, there'd be no reason to compile at all. It compiled — it just didn't tell you and didn't keep the receipt.

Why this exists

It's a convenience for tiny one-file programs, and it's lovely for that. It's useless for real ones. A real program is dozens of files that depend on each other. Re-translating all of them from scratch every single time you run it would be absurd. So for everything past this chapter, we compile once with javac and then run as often as we like.

Before you continue

Lab 1 left you with no .class file. Run javac Hello.java now so you have a real, on-disk Hello.class again — the next five labs are all about that file.

Section 5Two different programs, two species of error

This is the idea the chapter exists for. There are two programs. They run at different times. They complain about different things, in different words. And a huge amount of everyday confusion is not knowing which one is talking to you.

A compile-time error comes from javac, before anything runs. It means: I could not turn this text into a program. Nothing happened, nothing ran, there is no program.

A runtime error comes from java, while your program is running. It means the program was fine as a program. Then it got itself into trouble halfway through doing its job.

Think of it like this

A compile-time error is the recipe having a step written in a language nobody knows. You never even reach the kitchen. A runtime error is different. It's a perfectly clear recipe that says "add the eggs" on a day when you have no eggs. Nothing wrong with the writing. Something wrong with the world when it ran.

Let's break each one on purpose. In your editor, change println to printn — drop the l. Save. Then compile.

One command that differs by machine

The next two labs use ls -lT to see a file's size and its time down to the second. That -T is a Mac thing. On Linux ask for ls -l --full-time instead. That second form is also the one to use in Git Bash or WSL on Windows. Both answer the same question: how big, and when was it last written?

One more thing about that listing. The middle columns name whoever owns the file, and every machine fills them in differently. Mine say you staff. Yours won't. Watch the size and the time — that's all these labs turn on.

Predict before you run — Lab 2

With the typo saved, you run javac Hello.java. You'll get an error, fine — but predict the harder part: what happens to the Hello.class file you already have? Does it get overwritten, deleted, half-written, or left completely alone? Check with ls -lT, which shows sizes and times down to the second.

what actually happens (verified, JDK 17)
$ ls -lT Hello.class
-rw-r--r--  1 you  staff  419  5 Sep 05:56:21 2026 Hello.class
$ javac Hello.java
Hello.java:11: error: cannot find symbol
        System.out.printn("Hello, machine.");
                  ^
  symbol:   method printn(String)
  location: variable out of type PrintStream
1 error
$ ls -lT Hello.class
-rw-r--r--  1 you  staff  419  5 Sep 05:56:21 2026 Hello.class

Same size, same second on the clock. Untouched. javac found a problem, refused to produce anything, and left the old file exactly as it was. It would rather give you nothing than give you something half-translated. Remember that — Labs 3 and 4 are the consequences of it.

Now read the error properly, because you'll read a thousand of these. Hello.java:11 is the file and the line number — line 11 is the println line. Then it quotes the line back at you and puts a ^ under the exact spot it got stuck. Then symbol: method printn(String) means "you asked for something called printn that takes a piece of text, and no such thing exists". Then location: variable out of type PrintStream means "and I looked for it on System.out, which is a PrintStream, and PrintStreams have no printn."

"Cannot find symbol" is the most common compile error in all of Java. Nine times out of ten it's a typo, or a capital letter in the wrong place. It always means the same thing: you used a name I've never heard of.

Leave the typo in. Don't fix it yet. There's something to learn from a broken source file.

Predict before you run — Lab 3

Your Hello.java is broken. javac just refused to compile it. Now run java Hello. Does it refuse too? Crash? Print something? And whatever happens — what does it tell you about which file java actually reads?

what actually happens (verified, JDK 17)
$ java Hello
Hello, machine.

It runs. Perfectly. Happily. With a broken source file sitting right there in the same folder.

Because java never opens Hello.java. It opens Hello.class, and Hello.class is the good one from before — Lab 2 proved javac left it untouched. As far as the JVM is concerned, your source code isn't part of the program and never was. It could have a typo, a poem, or nothing at all in it. Doesn't matter. It isn't the program. The .class file is the program.

Remember the promise in that file's comment: this file is not a program. What javac makes from it is. That stops being a slogan right here. You've now watched it happen.

Section 6Lab 4: the one that bites professionals weekly

Now fix the typo — put the l back in println. While you're in there, change the message from "Hello, machine." to "Hello again.". Save the file. Check it's saved. Look at it with cat Hello.java if you like — the new message is definitely in there.

Then run the program without compiling. This is not a trick question. It is, genuinely, a thing that happens to paid developers on a weekly basis. Usually about twenty minutes before they work out what's going on.

Predict before you run — Lab 4

The source file now says "Hello again.", saved and verified. You skip javac and run java Hello. Which message prints — the new one, the old one, or an error about being out of date?

what actually happens (verified, JDK 17)
$ grep println Hello.java
        System.out.println("Hello again.");
$ java Hello
Hello, machine.
$ ls -lT Hello.java Hello.class
-rw-r--r--  1 you  staff  419  5 Sep 05:56:21 2026 Hello.class
-rw-r--r--  1 you  staff  439  5 Sep 05:56:37 2026 Hello.java

That first command is a spot-check. grep prints only the lines of a file that contain a word you name. So it proves the new message really is on disk.

And you get the old message anyway. Your edit might as well not exist.

And notice there's no warning. No "your class file is out of date". Nothing. The JVM has no opinion about your source file, because it never looked at it. It ran the bytecode it was given. That bytecode still has "Hello, machine." baked into it — you saw the sentence sitting in those raw bytes earlier.

That last command is the tell. Look at the times: the .class is from 05:56:21, the .java from 05:56:37. Sixteen seconds newer. The source has moved on and the program hasn't. Any time behaviour doesn't match the code in front of you, compare those two timestamps.

The habit to build now

When someone says "I fixed the bug but it's still broken", the first question is never about the bug. It's "did you rebuild?" Ask it about your own code too, out loud. Ask before you spend an hour debugging a program you never actually changed. Fix: javac Hello.java, then java Hello — and now you get Hello again.

Section 7Labs 5 and 6: two errors that look the same and aren't

These two are the most valuable thing on this page. Both print the same first line. The second line is different, and the difference still confuses people ten years into a career. Do them one after the other and read slowly.

First, delete the compiled program while keeping the source. Use rm -f Hello.class — rm means remove, and yes, this genuinely deletes a file, which is why I'm saying so. It's safe here: we can always make another one with javac.

Predict before you run — Lab 5

You run rm -f Hello.class, so the folder holds only Hello.java. Then you run java Hello — note, no .java on the end this time, so the Lab 1 shortcut doesn't apply. What exactly does it say? Try to predict the shape of the message, not only "an error".

what actually happens (verified, JDK 17)
$ rm -f Hello.class
$ java Hello
Error: Could not find or load main class Hello
Caused by: java.lang.ClassNotFoundException: Hello

Two lines, and they're saying two different things. The first is the JVM's complaint: I was told to start at Hello and I couldn't. The second, the Caused by: line, is the underlying reason — and that's the one that's actually useful.

Those long names ending in Exception or Error are Java's labels for a thing that went wrong. An exception is one of those labels, and Java has hundreds. Exactly two matter today.

ClassNotFoundException means, in plain words: I looked for a file called Hello.class and there is no such file. Nothing found. Empty-handed. The JVM does not care in the slightest that Hello.java is sitting right there being perfectly good source code. That's not what it eats.

Burn that translation in: ClassNotFound = there is no class file. In real life it usually means one of two things. Either you forgot to compile, or you're standing in the wrong folder. And after Chapter 0.0 you know how to test the second one — pwd.

Now put it back and break it a different way. Recompile with javac Hello.java, then rename the compiled file: mv Hello.class Greet.class. The name mv means move. Renaming is moving something to a new name. Then try to run Greet.

Think carefully before you guess. The file is right there. It contains a real, valid, working program that printed correctly ten seconds ago. All we changed was what it's called.

Predict before you run — Lab 6

The folder now holds Greet.class (which is really the compiled Hello) and Hello.java. You run java Greet. Does it print the message? Or fail — and if it fails, will it be the same error as Lab 5?

what actually happens (verified, JDK 17)
$ javac Hello.java
$ java Hello
Hello again.
$ mv Hello.class Greet.class
$ ls
Greet.class     Hello.java
$ java Greet
Error: Could not find or load main class Greet
Caused by: java.lang.NoClassDefFoundError: Greet (wrong name: Hello)

It fails — and the first line looks exactly like Lab 5's, with a different name in it. If you only ever read the first line, these two problems are indistinguishable, and you will fix the wrong one.

The Caused by: line is a completely different exception. NoClassDefFoundError: Greet (wrong name: Hello) means this: I found a file all right, and I opened it. Inside, it says its name is Hello, not Greet. I asked for Greet. This isn't it.

Here's why. When javac compiles, it writes the class's real name inside the bytecode. It's one of those readable words you spotted in the raw bytes. Renaming the file from the outside doesn't change the name written inside. So the JVM opens a file that claims to be someone else and refuses to guess. Which is exactly right of it. Guessing is how you end up running the wrong program.

The pair, side by side — memorise this

ClassNotFoundException — "there's no class file here at all". Nothing found. Usually: you didn't compile, or you're in the wrong folder.
NoClassDefFoundError (wrong name: …) — "I found a class file, but it disagrees with its own filename". Something found, and it's the wrong something. Usually: a file got renamed, or the class name and file name don't match.

Rename it back with mv Greet.class Hello.class and java Hello works again immediately. Nothing was damaged. The name was the whole problem.

Your turn

Run all six for real, in src/, and write your prediction down before each one. A wrong prediction is a win — it shows you exactly where your picture of the machine is off. A silent guess lets you tell yourself "yeah, I knew that" afterwards.

Section 8Check yourself

Which of these is a compile-time error — a complaint from javac, before anything runs?

Judgment call. A teammate messages: "I get Could not find or load main class Report. I've checked twenty times, Report.class is definitely in the folder." What do you ask them for?

One Hello.class file runs unchanged on your Mac and on a Linux server. A compiled C program can't. Why?

Section 9What broke, and what surprised you

Write this bit down somewhere, in your own words, before you move on. Especially the predictions you got wrong. Those are the valuable ones. A wrong prediction points at exactly one wrong idea in your head — and you now know which one it was.

Some questions worth answering properly:

  1. Which of the six labs produced an error from javac, and which from java? Why does that split matter?
  2. A colleague says "I fixed the bug but it's still broken." What's your first question, and why that one first?
  3. In your own words: what does ClassNotFoundException tell you, and how is it different from NoClassDefFoundError (wrong name: …)?
  4. Lab 1 looked like it proved this chapter's title wrong. What was the one piece of evidence that saved it?
Next up

You can now get from text to a running program, and you know who to blame when it doesn't. Time to make programs that actually hold something. Chapter 0.2 starts from a fact that sounds harmless and isn't. Every value your program stores lives in a box of a fixed size. And a box that size can be made to overflow. The result looks like your computer has forgotten how to count.

Corrections

Nice. Chapter marked complete.