Java is a statically typed, object-oriented, class-based language that runs on the Java Virtual Machine (JVM). Code compiles to platform-independent bytecode, giving Java its famous "write once, run anywhere" portability. This guide targets Java 21, the current Long-Term Support (LTS) release, and covers modern features like records, sealed classes, switch expressions, and virtual threads.
Class and main Structure
Every Java program lives inside a class. Execution starts at the main method. A source file typically holds one public top-level class matching the file name.
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
Compile with javac Hello.java and run with java Hello. Since Java 11 you can run a single file directly: java Hello.java.
Primitives vs Objects
Java has eight primitive types stored by value, and everything else is an object (a reference). Each primitive has a wrapper class (e.g. int ↔ Integer) used by generics and collections. Autoboxing converts between them automatically.
| Primitive | Size | Wrapper |
|---|---|---|
byte, short, int, long | 8–64 bit integers | Byte, Short, Integer, Long |
float, double | 32/64 bit floats | Float, Double |
char | 16 bit Unicode | Character |
boolean | true/false | Boolean |
Variables and var
Variables are declared with an explicit type or, for local variables, with var (Java 10+) for local type inference. Use final to make a binding immutable.
int count = 42;
final double PI = 3.14159;
var name = "Ada"; // inferred as String
var list = new ArrayList<String>(); // inferred as ArrayList<String>
long big = 1_000_000L; // underscores for readability
Operators
Java supports arithmetic (+ - * / %), comparison (== != < > <= >=), logical (&& || !), and bitwise (& | ^ ~ << >> >>>) operators. Use == to compare references and .equals() to compare object contents.
Gotcha
For Strings and other objects, == tests identity (same object), not value. Always use .equals() to compare values.
Control Flow
Java has the usual if/else, for, enhanced for-each, while, and do/while loops.
for (int i = 0; i < 5; i++) System.out.println(i);
for (String s : List.of("a", "b", "c")) System.out.println(s);
while (count > 0) count--;
Switch Expressions and Patterns
Modern switch expressions (Java 14+) use arrow syntax, return values, and require no break. Java 21 finalized pattern matching for switch, allowing type patterns and guards.
String day = switch (num) {
case 1, 7 -> "Weekend";
case 2, 3, 4, 5, 6 -> "Weekday";
default -> "Unknown";
};
// Pattern matching (Java 21)
String describe(Object obj) {
return switch (obj) {
case Integer i when i > 100 -> "big int";
case Integer i -> "int " + i;
case String s -> "string of length " + s.length();
case null -> "null";
default -> "other";
};
}
Methods
Methods declare a return type, name, and parameters. Overloading allows multiple methods with the same name but different parameter lists. Varargs (...) accept a variable number of arguments.
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; } // overload
int sum(int... nums) { // varargs
int total = 0;
for (int n : nums) total += n;
return total;
}
sum(1, 2, 3, 4); // -> 10
Object-Oriented Programming
Java's core paradigm is OOP. A class bundles state (fields) and behavior (methods). Constructors initialize new instances. Encapsulation hides internal state behind access modifiers and getters/setters.
public class Account {
private double balance; // encapsulated field
public Account(double initial) { // constructor
this.balance = initial;
}
public double getBalance() { return balance; }
public void deposit(double amt) { balance += amt; }
}
Access Modifiers
| Modifier | Visibility |
|---|---|
public | Everywhere |
protected | Same package + subclasses |
| (default) | Same package only |
private | Same class only |
Inheritance, Interfaces, Polymorphism
A class extends one superclass and implements any number of interfaces. Abstract classes cannot be instantiated and may declare abstract methods. Interfaces define contracts and can include default methods. Polymorphism lets a subtype be used wherever the supertype is expected.
abstract class Shape {
abstract double area(); // must be overridden
}
interface Drawable {
void draw();
default void render() { draw(); } // default method
}
class Circle extends Shape implements Drawable {
private final double r;
Circle(double r) { this.r = r; }
@Override double area() { return Math.PI * r * r; }
@Override public void draw() { System.out.println("circle"); }
}
Shape s = new Circle(2.0); // polymorphism
System.out.println(s.area());
Records (Java 16+)
Records are immutable data carriers. The compiler generates the constructor, accessors, equals, hashCode, and toString.
public record Point(int x, int y) {
// compact constructor for validation
public Point {
if (x < 0 || y < 0) throw new IllegalArgumentException();
}
}
var p = new Point(3, 4);
System.out.println(p.x() + ", " + p.y());
Sealed Classes (Java 17+)
Sealed types restrict which classes may extend or implement them, enabling exhaustive pattern matching.
public sealed interface Expr permits Num, Add {}
public record Num(double value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
double eval(Expr e) {
return switch (e) {
case Num n -> n.value();
case Add a -> eval(a.left()) + eval(a.right());
}; // no default needed — compiler knows it's exhaustive
}
Enums
Enums define a fixed set of constants and can carry fields and methods.
enum Planet {
EARTH(5.97e24), MARS(6.42e23);
private final double mass;
Planet(double mass) { this.mass = mass; }
double mass() { return mass; }
}
Planet.EARTH.mass();
Generics
Generics provide compile-time type safety for classes and methods. Type parameters like <T> are placeholders filled in at use.
// Generic class
class Box<T> {
private final T value;
Box(T value) { this.value = value; }
T get() { return value; }
}
Box<String> b = new Box<>("hi");
// Generic method
static <T> T firstOf(List<T> list) { return list.get(0); }
Bounded Wildcards
Wildcards express flexible bounds. Remember PECS: Producer extends, Consumer super.
// reads (produces) from the list -> ? extends
double sumAll(List<? extends Number> nums) {
double total = 0;
for (Number n : nums) total += n.doubleValue();
return total;
}
// writes (consumes) into the list -> ? super
void addInts(List<? super Integer> list) {
list.add(1);
list.add(2);
}
// bounded type parameter
<T extends Comparable<? super T>> T max(List<T> list) {
return list.stream().max(Comparator.naturalOrder()).orElseThrow();
}
Collections Framework
The java.util collections framework provides interfaces (List, Set, Map, Deque) and implementations.
| Type | Common Impl | Use Case |
|---|---|---|
List<E> | ArrayList, LinkedList | Ordered, indexed, duplicates |
Set<E> | HashSet, TreeSet | Unique elements |
Map<K, V> | HashMap, TreeMap | Key-value lookup |
Deque<E> | ArrayDeque | Stack / queue |
List<String> names = new ArrayList<>();
names.add("Ada");
Set<Integer> ids = new HashSet<>(List.of(1, 2, 2, 3)); // {1,2,3}
Map<String, Integer> ages = new HashMap<>();
ages.put("Ada", 36);
ages.getOrDefault("Bob", 0);
ages.computeIfAbsent("Cara", k -> 0);
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); stack.pop();
Streams API
The Streams API enables declarative, functional-style processing of collections with operations like map, filter, reduce, and collect.
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evensDoubled = nums.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.collect(Collectors.toList()); // [4, 8, 12]
int total = nums.stream().reduce(0, Integer::sum); // 21
Map<Boolean, List<Integer>> parts = nums.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
Optional
Optional<T> is a container that may or may not hold a value, replacing null checks with an explicit, composable API.
Optional<String> found = names.stream()
.filter(n -> n.startsWith("A"))
.findFirst();
String result = found
.map(String::toUpperCase)
.orElse("NONE");
found.ifPresent(System.out::println);
Exceptions
Java distinguishes checked exceptions (must be declared or handled) from unchecked exceptions (runtime errors). Use try/catch/finally, and prefer try-with-resources for anything implementing AutoCloseable.
| Aspect | Checked | Unchecked |
|---|---|---|
| Base type | Exception | RuntimeException |
| Must declare/catch | Yes | No |
| Examples | IOException, SQLException | NullPointer, IllegalArgument |
try (var reader = Files.newBufferedReader(path)) {
return reader.readLine();
} catch (IOException e) {
System.err.println("Read failed: " + e.getMessage());
throw new RuntimeException(e);
} finally {
System.out.println("done");
}
Concurrency
Java offers rich concurrency primitives. Prefer high-level abstractions like ExecutorService and CompletableFuture over raw threads.
// Runnable + Thread
Runnable task = () -> System.out.println("running");
new Thread(task).start();
// ExecutorService (thread pool)
try (var pool = Executors.newFixedThreadPool(4)) {
Future<Integer> f = pool.submit(() -> 40 + 2);
System.out.println(f.get());
}
// CompletableFuture (async composition)
CompletableFuture.supplyAsync(() -> fetch())
.thenApply(String::trim)
.thenAccept(System.out::println);
Use synchronized to guard critical sections, volatile for visibility of a single field, and concurrent collections like ConcurrentHashMap for thread-safe shared state.
Virtual Threads (Java 21)
Virtual threads are lightweight threads managed by the JVM, enabling millions of concurrent tasks with blocking-style code. They shine for I/O-bound workloads.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return fetchData();
});
}
} // waits for all tasks
// Or start one directly
Thread.startVirtualThread(() -> System.out.println("light!"));
Why virtual threads
Platform threads map 1:1 to OS threads and are expensive (~1MB each). Virtual threads are cheap, so you can write simple blocking code without a thread-pool bottleneck.
Standard Library Highlights
Useful classes ship with the JDK: String and StringBuilder, Math, java.time (LocalDate, Duration), java.nio.file (Files, Path), java.util.regex, and java.util.stream.
// Text blocks (Java 15+)
String json = """
{
"name": "Ada"
}
""";
LocalDate today = LocalDate.now();
LocalDate next = today.plusDays(7);
String greeting = "Hello, %s! It is %d.".formatted("Ada", 2026);
Tooling
The core toolchain: javac compiles sources, java runs them, and jshell is an interactive REPL. Real projects use build tools like Maven (pom.xml) or Gradle (build.gradle) for dependencies and packaging, and JUnit for testing.
javac Hello.java # compile
java Hello # run
jshell # interactive REPL
mvn clean package # Maven build
gradle build # Gradle build
Practice Exercises
- Define a
record Temperature(double celsius)with a compact constructor that rejects values below -273.15, plus a method returning Fahrenheit. - Write a generic method
<T> List<T> reversed(List<T> in)that returns a new reversed list without mutating the input. - Use the Streams API to group a
List<String>of words into aMap<Integer, List<String>>keyed by word length. - Model a
sealed interface ShapewithCircleandRectanglerecords, then compute area with an exhaustive switch. - Read a text file with try-with-resources and count how many lines contain a given keyword, handling
IOExceptiongracefully. - Submit 1,000 sleep-and-return tasks to a virtual-thread executor and collect all results, timing how long the whole batch takes.