Runtime vs Compile Time

Understanding When Things Happen in Java

← Back to Index

The Two Phases of Java

Java programs go through two distinct phases: compilation (when source code becomes bytecode) and runtime (when bytecode executes on the JVM). Understanding what happens in each phase is fundamental to Java development.

Key Distinction
  • Compile Time - Source code (.java) → Bytecode (.class)
  • Runtime - JVM executes the bytecode

Compile Time

Compile time is when the Java compiler (javac) processes your source code. The compiler performs syntax checking, type checking, and generates bytecode.

What Happens at Compile Time

// 1. Syntax checking
int x = 5  // Error: missing semicolon

// 2. Type checking
String name = 42;  // Error: incompatible types

// 3. Symbol resolution
System.out.printn("Hi");  // Error: cannot find symbol 'printn'

// 4. Access checking
private int secret = 10;
obj.secret = 20;  // Error: secret has private access

// 5. Constant folding (optimization)
int result = 2 + 3;  // Compiled as: int result = 5;

Compile-Time Constants

public class Constants {
    // Compile-time constant (known at compilation)
    static final int MAX_SIZE = 100;

    // NOT a compile-time constant (computed at runtime)
    static final int RANDOM = new Random().nextInt();

    // Compile-time constant expression
    static final int DOUBLE_MAX = MAX_SIZE * 2;  // Evaluated at compile time
}

// Usage - MAX_SIZE is inlined at compile time
int limit = Constants.MAX_SIZE;  // Compiled as: int limit = 100;

Compile-Time Errors

// These errors prevent compilation:

// Syntax error
public class { }  // Missing class name

// Type mismatch
List<String> names = new ArrayList<Integer>();  // Error!

// Unchecked exception not caught
public void read() {
    new FileReader("file.txt");  // Error: unhandled IOException
}

// Missing return statement
public int getValue() {
    int x = 5;
    // Error: missing return statement
}

Runtime

Runtime is when the JVM loads and executes the compiled bytecode. Many operations that can't be checked at compile time happen here.

What Happens at Runtime

// 1. Object creation
Object obj = new MyClass();  // Memory allocated at runtime

// 2. Method dispatch (polymorphism)
Animal animal = getAnimal();  // Could be Dog, Cat, etc.
animal.speak();  // Actual method determined at runtime

// 3. Array bounds checking
int[] arr = new int[5];
arr[10] = 1;  // ArrayIndexOutOfBoundsException at runtime

// 4. Null checks
String s = null;
s.length();  // NullPointerException at runtime

// 5. Class loading
Class<?> clazz = Class.forName("com.example.MyClass");  // Loaded at runtime

Runtime Exceptions

// These errors occur during execution:

// NullPointerException
String name = null;
name.toUpperCase();  // Crashes at runtime

// ClassCastException
Object obj = "Hello";
Integer num = (Integer) obj;  // Crashes at runtime

// ArithmeticException
int result = 10 / 0;  // Crashes at runtime

// OutOfMemoryError
List<byte[]> list = new ArrayList<>();
while (true) {
    list.add(new byte[1_000_000]);  // Eventually crashes
}

Type Erasure: Compile Time vs Runtime

Generics in Java are a compile-time feature. Due to type erasure, generic type information is removed at runtime.

// At compile time - type checking happens
List<String> strings = new ArrayList<>();
strings.add("hello");
strings.add(42);  // Compile error!

// At runtime - type is erased
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();

// At runtime, both are just "List" (raw type)
System.out.println(strings.getClass() == ints.getClass());  // true!

// This is why you can't do:
if (list instanceof List<String>) { }  // Compile error!
if (list instanceof List<?>) { }      // OK - unbounded wildcard
Type Erasure Implications

Generic type parameters are not available at runtime. You cannot create generic arrays, check instanceof with generic types, or get generic type info via reflection (without extra tricks).

Annotations: Compile vs Runtime

Annotations can be retained for different phases based on their retention policy.

// SOURCE - discarded after compilation
@Retention(RetentionPolicy.SOURCE)
public @interface Todo { }  // Not in .class file

// CLASS - in .class file, not available at runtime (default)
@Retention(RetentionPolicy.CLASS)
public @interface InBytecode { }

// RUNTIME - available via reflection at runtime
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation { }

// Common examples:
@Override      // SOURCE - compiler checks, then discarded
@Deprecated   // RUNTIME - available for reflection
@Entity       // RUNTIME - JPA reads at runtime

Static vs Dynamic Binding

Static Binding (Compile Time)

public class Example {
    // Method overloading - resolved at compile time
    public void print(String s) {
        System.out.println("String: " + s);
    }

    public void print(Integer i) {
        System.out.println("Integer: " + i);
    }

    // Static methods - bound at compile time
    public static void staticMethod() { }

    // Private methods - bound at compile time
    private void privateMethod() { }

    // Final methods - bound at compile time
    public final void finalMethod() { }
}

Dynamic Binding (Runtime)

class Animal {
    public void speak() {
        System.out.println("Animal speaks");
    }
}

class Dog extends Animal {
    @Override
    public void speak() {
        System.out.println("Dog barks");
    }
}

// Dynamic binding - actual method determined at runtime
Animal animal = new Dog();
animal.speak();  // Prints "Dog barks" (determined at runtime)

Reflection: Runtime Introspection

// Reflection provides runtime access to class information

Class<?> clazz = MyClass.class;

// Get methods at runtime
Method[] methods = clazz.getDeclaredMethods();

// Create instance at runtime
Object instance = clazz.getDeclaredConstructor().newInstance();

// Invoke method at runtime
Method method = clazz.getMethod("myMethod");
method.invoke(instance);

// Check annotations at runtime
if (clazz.isAnnotationPresent(MyAnnotation.class)) {
    MyAnnotation ann = clazz.getAnnotation(MyAnnotation.class);
}

Practical Comparison

Aspect Compile Time Runtime
Type Checking Generics, method signatures instanceof, casting
Method Resolution Overloading, static methods Overriding (polymorphism)
Errors Syntax, type mismatches Null pointers, out of bounds
Optimization Constant folding, dead code JIT compilation, inlining
Information Full source code Bytecode, limited generics