Definition
Three phases:
- Write: Source code in
.javafiles. - Compile:
javacchecks syntax and types, produces.classbytecode. - Run:
javastarts the JVM, which loads classes, verifies bytecode, interprets, and JIT-compiles hot paths.
Writing a recipe (source), translating it into a universal format (bytecode), and having a kitchen in any country (JVM) cook the meal.
Understanding the Concepts
The flow
# 1. Write source
vi TransferService.java
# 2. Compile: javac checks syntax + types, produces bytecode
javac TransferService.java # produces TransferService.class
# 3. Run: JVM loads, verifies, interprets, JIT-compiles
java TransferService
What javac does
- Lexical analysis (tokenize), parsing (AST), type checking, annotation processing.
- Produces
.classfiles with bytecode, not native code. - Does NOT optimize much – real optimization happens in the JIT at runtime.
What the JVM does at runtime
- Class loading: Reads .class files lazily on first use.
- Bytecode verification: Checks safety (no forged references, valid stack).
- Interpretation: Runs bytecode instruction by instruction.
- JIT compilation: Compiles hot methods to native machine code.
- Garbage collection: Reclaims unreachable objects.
Build tools
# Maven: compile, test, package in one command
mvn clean package
# Gradle
gradle build
Build tools resolve dependencies, compile, run tests, and package into JAR/WAR.
What should a senior engineer know?
- Ahead-of-time (AOT) compilation with GraalVM native-image compiles to native binary at build time.
- javac is not the only compiler. Eclipse uses its own compiler (ecj). Kotlin uses kotlinc. All produce JVM bytecode.
- Incremental compilation (Gradle/IDE) recompiles only changed files.
What are common mistakes?
A common mistake is not understanding the concept fully before using it in production banking applications.
Wrong approach:
package com.bank.service;
public class TransferService {
public void transfer(String fromAccount, String toAccount, double amount) {
// WRONG: using double for money
double balance = getBalance(fromAccount);
balance = balance - amount;
}
private double getBalance(String id) { return 10000.0; }
}
Correct approach:
package com.bank.service;
import java.math.BigDecimal;
public class TransferService {
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
// CORRECT: using BigDecimal for money
BigDecimal balance = getBalance(fromAccount);
balance = balance.subtract(amount);
}
private BigDecimal getBalance(String id) { return new BigDecimal("10000.00"); }
}
Always use BigDecimal for monetary calculations in banking systems to avoid floating-point precision errors.
What are the edge cases?
Edge cases can cause unexpected behavior in banking applications. Here are some to watch for.
package com.bank.edge;
public class EdgeCaseDemo {
public static void main(String[] args) {
// Edge case 1: null handling
String accountId = null;
try {
System.out.println(accountId.length());
} catch (NullPointerException e) {
System.out.println("Null account ID");
}
// Edge case 2: empty vs null
String emptyId = "";
System.out.println(emptyId.isEmpty()); // true
System.out.println(emptyId == null); // false
// Edge case 3: boundary values
java.math.BigDecimal zero = java.math.BigDecimal.ZERO;
java.math.BigDecimal negativeBalance = new java.math.BigDecimal("-0.01");
System.out.println(zero.compareTo(negativeBalance) > 0); // true
}
}
Null account ID
true
false
true
Always test with null values, empty strings, boundary values, and negative amounts in banking applications.
Visual Overview
.java source → javac → .class bytecode → java/JVM
│ │
│ syntax check, type check │ class loading
│ annotation processing │ bytecode verification
│ → bytecode (platform-neutral) │ interpreter → JIT → native code
│ │ garbage collection
▼ ▼
Build time Runtime
Key Takeaway
Java's flow is: write .java, compile with javac to platform-neutral .class bytecode, run on the JVM which loads, verifies, interprets, and JIT-compiles. Build tools like Maven and Gradle automate the entire pipeline.
One sentence to remember: javac produces portable bytecode, the JVM turns it into native code at runtime via interpretation and JIT compilation.
---