Classes, methods and blocks

The four things every Java program is made of, why one file can produce three class files, and the block that decides what exists.

7 min read☕ Java Fundamentals

Every Java program is the same four things nested inside each other: a class holds methods, a method holds statements, and statements are grouped into blocks. Once you can name those in a file you have never seen, reading unfamiliar Java stops being decoding and starts being reading.

The smallest complete program, named part by part

Greet.javajava
public class Greet {                                   // a class
    public static void main(String[] args) {           // a method
        String who = "world";                          // a statement
        System.out.println("hello " + who);            // another
    }
}
  • class Greet — a named thing that holds code. In Java nothing lives outside one; there are no free-floating functions.
  • main — a method: a named block of statements you can call, with a list of parameters and a return type.
  • String who = "world"; — a statement. It ends with a semicolon, which is how the compiler knows where it stops. Newlines mean nothing to Java.
  • { ... } — a block. Every pair groups statements and, as the next section shows, decides what is visible.

The file is Greet.java because a public class must be in a file of its own name. That is a rule about public specifically; a non-public class can live anywhere.

One file does not mean one class

That last rule is often mis-learned as one file, one class. Compile a file with a nested class and an anonymous one in it:

Shape.javajava
public class Shape {
    static class Inner { }
    public static void main(String[] a) {
        Runnable r = new Runnable() { public void run() { } };
    }
}
plaintext
$ javac Shape.java
$ ls *.class
Shape$1.class   Shape$Inner.class   Shape.class

Three class files from one source file. The compiler's unit is the class, not the file, and the naming is worth recognising: Outer$Inner for a named nested class, Outer$1 for an anonymous one, numbered in the order they appear.

You will meet those exact names again in a stack trace. A frame reading OrderService$1.run is an anonymous class inside OrderService, and now it is a place you can find rather than a puzzle.

A block decides what exists

Braces are not only punctuation. Anything declared inside a block stops existing when the block ends:

java
{
    int insideTheBlock = 1;
    System.out.println("inside:  " + insideTheBlock);   // fine
}
System.out.println(insideTheBlock);                     // not fine
plaintext
Scope.java:4: error: cannot find symbol
        System.out.println(insideTheBlock);
                           ^
  symbol:   variable insideTheBlock

That is why a variable declared in a for loop's header is gone after the loop, and why declaring one inside an if and using it after does not compile. The rule is the same everywhere: a name lives from its declaration to the closing brace of the block it is in, and nowhere else.

It is also a tool. Declaring a variable in the smallest block that needs it is how you stop it being reused by accident twenty lines later — which the local variables lesson takes further.

Expressions and statements are different things

Worth separating early, because error messages assume you know:

  • An expression produces a value. 2 + 2, name.length(), age > 18.
  • A statement does something. An assignment, a method call used for its effect, an if, a return.

Some expressions can be statements on their own — a method call is the usual case. Most cannot: 2 + 2; is not a legal statement, and the compiler says not a statement, which reads as nonsense until you know it means this produces a value and does nothing with it.

The distinction is why if (x = 5) fails in Java and quietly works in C: x = 5 is an expression producing 5, an int where a boolean is required. Java's type system catches it; that is a deliberate design decision and not an accident.

Keywords and identifiers

Keywords are the roughly fifty words the language reserves — class, public, static, if, return, new, final. You cannot use one as a name; that is the entire rule, and the compiler enforces it immediately.

Three details worth carrying:

  • true, false and null are not keywords, they are literals. The effect is the same: you cannot name a variable null.
  • Some words are keywords only in context. var, record, sealed, yield and permits were added without breaking code that used them as names, so a variable called record still compiles. They are called contextual keywords.
  • goto and const are reserved and unused. They exist in the keyword list so that a program using them as names fails clearly rather than working and then breaking if the words are ever given meaning.

Identifiers — your names — may contain letters, digits, _ and $, and may not start with a digit. The mechanical rule is uninteresting; the convention is not, and the naming lesson covers what a reviewer actually reads. One piece of it belongs here: $ is legal and reserved by convention for generated code, which is why the compiler used it for Shape$Inner. Do not put it in a name you write.

Three modifiers you will read before you write

public, private, static and final you will write on day one. Three others appear in code you read long before you need them, and each means something quite specific:

  • transient marks a field that Java serialization must skip — a cache, a lock, a connection, a password — and that comes back as null or zero after deserialisation. It means nothing outside serialization, and the I/O course's serialization section is where it gets its full story (and its warning).
  • volatile marks a field whose reads and writes go straight to main memory with ordering guarantees, so a value one thread writes is seen by another. It is not a lock and does not make count++ atomic; the concurrency course's memory-model lesson explains what it does promise, and it is the modifier most often written by people who needed AtomicInteger or synchronized instead.
  • native marks a method with no body in Java: the implementation is in a shared library loaded with System.loadLibrary, called through JNI. You will see it in the JDK's own sources (System.arraycopy, Object.hashCode) and in libraries that wrap C code; you will almost never write one, and the modern-Java course's Foreign Function & Memory API is what replaces writing them.

Two more that look like modifiers and are not: strictfp (a no-op since Java 17, when floating point became strict everywhere) and synchronized on a method, which is a lock on this and belongs to the concurrency course.

Comments, and the two kinds worth writing

java
// to the end of the line
/* across
   several lines */
/** a documentation comment, read by javadoc and by your IDE */

The compiler discards all three. They exist for the next person, who is usually you.

The useful distinction is not the syntax, it is what the comment says:

java
// add one to i
i++;                       // noise: it repeats the code and rots when the code changes
 
// Retry three times: the payment provider returns 503 during their
// nightly maintenance window and recovers within ~2 seconds.

The second cannot be recovered from the code at any price. Comment the why, and the surprising: a workaround for somebody else's bug, a constant that came from a measurement, a decision that looks wrong and is not.

The third form, /** ... */, is different in kind because it is published. It becomes the tooltip in every caller's IDE, so it is written for someone who will never open your file: what the method does, what its parameters mean, what it returns, and what it throws.

Progress is saved on this device and to your account when signed in.