Java Programming


A comprehensive Java programming course covering syntax, types, control flow, methods, object-oriented programming, arrays, exceptions, file I/O, generics, collections, concurrency, the JVM, JDBC, and production behavior with executable examples.
Java separates the programming language from the virtual-machine execution model. That separation is central to portability, but portability does not mean that operating-system behavior, native libraries, encodings, hardware limits, or external-service dependencies disappear. Reliable Java systems require the language rules, Java Virtual Machine (JVM), standard libraries, and runtime behavior to be understood together.
Java programming is best understood as one continuum, from problem solving, types, input/output, control flow, methods, classes, arrays, inheritance, exceptions, and file I/O to the modern JVM, collections, generics, concurrency, data access, and production behavior. The goal is therefore not to memorize syntax, but to explain why a Java program behaves as it does from source code to runtime.
Unit 1: Compilation, Bytecode, and the JVM
Compiling a .java source file with javac normally produces .class files containing JVM instructions rather than instructions for one processor family. The JVM loads, verifies, links, and executes class files. Frequently executed code can be compiled to native machine code by a just-in-time compiler. Java therefore does not fit the simplistic label of being only interpreted or only ahead-of-time compiled.
.java source
|
v
javac
|
v
.class / bytecode
|
v
class loading -> verification -> linking -> initialization
|
+--> interpreter
|
+--> JIT compilation -> native machine codeThe JDK contains development tools, the compiler, and the runtime. In modern Java distributions the historical model of installing a separate JRE is no longer the main deployment abstraction; customized runtime images can also be produced with tools such as jlink. Source level, target runtime, library compatibility, and preview-feature use have to be managed as one versioning problem.
As of 15 September 2026, JDK 27 is the latest feature release and JDK 25 is the current LTS line. Production selection should be based on support lifetime, dependency compatibility, security updates, and organizational upgrade policy rather than on “latest” alone.
The principal JVM run-time data areas include per-thread program counters and JVM stacks, the heap that holds objects, class-related metadata and run-time constant pools, and structures required for native execution. Source-level concepts must not be mapped mechanically to physical memory addresses. A JVM may apply escape analysis, scalar replacement, and other optimizations that change how an allocation is realized internally without changing observable program semantics.
Java execution analysis distinguishes source code, bytecode, and runtime behavior. The same method may execute under different compilation states as usage changes, so one short timing run is not a general performance result. Evaluate allocation, virtual calls, and memory access within a representative workload. Preserve correctness while accounting for warm-up, garbage collection, and measurement variability.
1.1. First Program, Compilation, and Execution
The execution entry point of a conventional Java application is the public static void main(String[] args) method. main is not a special JVM instruction; it is a static method with a signature recognized by the application launcher. Java is case-sensitive, so total, Total, and TOTAL are distinct identifiers.
public class HelloJava {
public static void main(String[] args) {
System.out.println("Hello Java");
}
}If the source file is named HelloJava.java, the basic command chain is:
javac HelloJava.java
java HelloJavaimport is not a preprocessor directive in the C/C++ sense. It participates in compile-time name resolution so that types from other packages can be used without fully qualifying every occurrence. Types in java.lang are implicitly available; other packages require a fully qualified name or an import. A wildcard import such as java.util.* does not import subpackages.
Comments may use //, /* ... */, or /** ... */ for API documentation. Source formatting does not alter runtime semantics, but consistent indentation that exposes block structure directly reduces maintenance cost.
Unit 2: Types, Variables, and Expressions
Java uses static type checking. Its primitive types are byte, short, int, long, float, double, char, and boolean. Wrapper classes provide object representations for primitive values. Autoboxing is convenient, but it can introduce allocations, null behavior, and equality mistakes when used without understanding the conversion.
All Java argument passing is by value. An object is not passed “by reference”; the value of the reference is copied into the parameter. Code can mutate the referenced object when the API permits it, but assigning a new object to the parameter does not rebind the caller's variable.
Widening numeric conversions are generally implicit, whereas narrowing conversions require an explicit cast. Integer overflow does not automatically throw an exception. Critical arithmetic may use checked methods such as Math.addExact and Math.multiplyExact. Decimal business values that require exact base-10 semantics often need BigDecimal, with scale and rounding defined as part of the business contract.
String is immutable. == tests reference identity for objects, whereas equals expresses the logical equality contract defined by a class. Repeated dynamic concatenation in a loop is usually modeled more predictably with StringBuilder than by relying on repeated immutable-string construction.
StringBuffer offers a historically synchronized mutable-character API, while StringBuilder provides similar mutable construction without that synchronization overhead. Thread safety should be established at the real ownership boundary rather than selecting StringBuffer mechanically whenever multiple threads exist.
2.1. Variables, Constants, Operators, and Conversions
A variable is declared with a type before it is used. Local variables do not receive automatic default values; definite-assignment rules are checked by the compiler. Object fields, in contrast, are initialized to the default value of their type. final prevents reassignment of a variable but does not automatically make the referenced object immutable.
Integer division, overflow, and numeric promotion rules matter in arithmetic expressions. 5 / 2 evaluates to 2, whereas 5.0 / 2 evaluates to 2.5. % computes the remainder. The difference between prefix and postfix ++/-- becomes visible when the expression value is consumed in the same statement; avoiding complex side-effect expressions usually improves readability.
public class TypesAndConversions {
public static void main(String[] args) {
int count = 7;
double unitPrice = 12.50;
double total = count * unitPrice;
int rounded = (int) Math.round(total);
int checkedSum = Math.addExact(1_500_000_000, 500_000_000);
System.out.println(total);
System.out.println(rounded);
System.out.println(checkedSum);
}
}2.2. Console I/O and Scanner
System.out.print does not append a line terminator, while System.out.println does. Scanner is a convenient teaching and application tool for formatted console input. Token-oriented calls such as nextInt() and line-oriented nextLine() have different consumption rules; when mixed, the remaining line terminator must be accounted for.
import java.util.Scanner;
public class ConsoleInput {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
System.out.print("Name: ");
String name = scanner.nextLine().trim();
System.out.print("First number: ");
double a = Double.parseDouble(scanner.nextLine());
System.out.print("Second number: ");
double b = Double.parseDouble(scanner.nextLine());
System.out.printf("Total for %s = %.2f%n", name, a + b);
}
}
}2.3. String: Content versus Identity
String is a class rather than a primitive type and is immutable. Operations such as length, charAt, substring, indexOf, startsWith, endsWith, replace, trim/strip, toLowerCase, and toUpperCase produce results without mutating the original string. Use equals/equalsIgnoreCase for content equality and compareTo for lexical ordering; == compares reference identity.
public class TextProcessing {
public static void main(String[] args) {
String raw = " Java Programming ";
String clean = raw.strip();
String lower = clean.toLowerCase();
StringBuilder builder = new StringBuilder(32);
builder.append(lower).append(" | length=").append(clean.length());
System.out.println(builder);
System.out.println("java programming".equalsIgnoreCase(clean));
}
}Unit 3: Decision Structures and Loops
Control flow determines which statements an algorithm executes and in what order. if/else handles general predicates, switch fits discrete value sets, and the conditional operator (?:) provides compact value selection. A modern switch expression requires branches to produce a value, which can reduce some classes of accidental fall-through errors.
public class DecisionStructure {
static String grade(int score) {
if (score < 0 || score > 100) {
throw new IllegalArgumentException("Score must be in 0..100");
}
int group = score / 10;
return switch (group) {
case 10, 9 -> "A";
case 8 -> "B";
case 7 -> "C";
case 6 -> "D";
default -> "F";
};
}
public static void main(String[] args) {
System.out.println(grade(86));
}
}while checks its condition before the body, whereas do-while checks after it. do-while is useful when the body must execute at least once. for groups initialization, condition, and update; enhanced for traverses arrays and Iterable values when explicit indexing is unnecessary. break terminates a loop and continue advances to the next iteration.
A sentinel-controlled loop is a classic pattern when the input length is not known in advance:
import java.util.Scanner;
public class SentinelAverage {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
long total = 0;
int count = 0;
while (true) {
int value = scanner.nextInt();
if (value == -999) {
break;
}
total += value;
count++;
}
if (count > 0) {
System.out.println((double) total / count);
}
}
}
}Random-number generation should also consider testability. Random, ThreadLocalRandom, and SecureRandom address different requirements; Math.random() is not a universal choice for every random-number use case.
When designing Java branches or loops, identify valid inputs, boundaries, and preserved invariants. Justify termination through a measure that progresses appropriately at every iteration. Reordering conditions may change results in the presence of side effects; short-circuit evaluation is therefore semantically significant. Unit tests should cover empty collections, single elements, and the largest expected input size.
Unit 4: Methods, Static, Math, and Overloading
A method names a behavior, may receive parameters, and returns a result unless its return type is void. Java always passes arguments by value. For an object argument, the copied value is a reference; the method may mutate the referenced object but cannot rebind the caller's variable.
public class MethodExample {
static int max(int a, int b) {
return a >= b ? a : b;
}
static double average(int... values) {
if (values.length == 0) {
throw new IllegalArgumentException("At least one value is required");
}
long total = 0;
for (int value : values) {
total += value;
}
return (double) total / values.length;
}
public static void main(String[] args) {
System.out.println(max(7, 11));
System.out.println(average(10, 20, 30, 40));
}
}A static member belongs to the class; an instance member requires an object. Helpers such as Math.sqrt, Math.pow, Math.abs, Math.min, and Math.max are static methods. Wrapper classes (Integer, Long, Double, Character, and others) connect primitive values with object APIs. Conversions such as Integer.parseInt("42") are common when converting text input to numeric values.
Overloading reuses a method name with different parameter lists and is resolved at compile time:
public class OverloadExample {
static long area(int side) {
return (long) side * side;
}
static long area(int width, int height) {
return (long) width * height;
}
static double area(double radius) {
return Math.PI * radius * radius;
}
public static void main(String[] args) {
System.out.println(area(5));
System.out.println(area(4, 7));
System.out.println(area(2.5));
}
}Unit 5: Classes and Object-Oriented Programming
A Java class can contain fields, methods, constructors, nested types, and initialization blocks. Abstraction, encapsulation, inheritance, and polymorphism are common object-oriented concepts, but maintainable design is not achieved by maximizing inheritance or the number of classes. The important question is where change and responsibility boundaries belong.
Access is controlled by private, package access, protected, and public. Exposing everything publicly weakens invariants. Immutable objects validate their state at construction time, avoid exposing mutable internal representation, and do not change observable state during their lifetime.
Method overloading is resolved from different parameter lists at compile time. Overriding changes inherited instance behavior and participates in run-time polymorphism. Static methods, private methods, and constructors have different dispatch rules and should not be described as ordinary overriding.
An abstract class can hold state and implemented behavior. An interface defines a contract and, in modern Java, can also contain default, static, and private helper methods. A class extends one class but can implement multiple interfaces. Inheritance is appropriate only when the subtype preserves the behavioral contract of the supertype; composition often produces lower coupling when behavior can be assembled from collaborators.
Records provide a compact model for data-oriented classes, including component accessors and standard equals, hashCode, and toString behavior. Enums are class-like types rather than integer constants. Sealed classes constrain which types may extend or implement a hierarchy.
5.1. Encapsulation, Constructors, and This
Internal state should be managed through invariants protected by the class rather than exposed as raw fields; public/private, constructors, and carefully chosen accessors are mechanisms for enforcing that boundary. Encapsulation does not require a setter for every field; immutable or controlled-mutation designs often provide a stronger contract by exposing only meaningful operations.
public class AccountExample {
static final class BankAccount {
private final String iban;
private long balanceCents;
BankAccount(String iban, long initialBalanceCents) {
if (iban == null || iban.isBlank()) {
throw new IllegalArgumentException("IBAN must not be blank");
}
if (initialBalanceCents < 0) {
throw new IllegalArgumentException("Balance must not be negative");
}
this.iban = iban;
this.balanceCents = initialBalanceCents;
}
void deposit(long amountCents) {
if (amountCents <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
balanceCents = Math.addExact(balanceCents, amountCents);
}
long balanceCents() {
return balanceCents;
}
String iban() {
return iban;
}
}
public static void main(String[] args) {
BankAccount account = new BankAccount("TR0001", 10_000);
account.deposit(2_500);
System.out.println(account.iban() + " " + account.balanceCents());
}
}If a class declares no constructor, the compiler supplies a no-argument default constructor. Once the programmer declares any constructor, that implicit constructor is no longer generated. this(...) invokes another constructor in the same class, while super(...) invokes a superclass constructor; either invocation must be the first statement in the constructor body.
5.2. Inheritance, Abstract Classes, Interfaces, and Polymorphism
Inheritance uses extends; interface implementation uses implements. Overriding preserves the parameter contract, and dynamic dispatch selects the instance method according to the actual runtime type. @Override helps the compiler verify programmer intent.
public class PolymorphismExample {
interface ConsumesEnergy {
double consumptionKWh(double km);
}
static abstract class Vehicle {
private final String brand;
Vehicle(String brand) {
this.brand = brand;
}
String brand() {
return brand;
}
abstract String kind();
}
static final class ElectricCar extends Vehicle implements ConsumesEnergy {
ElectricCar(String brand) {
super(brand);
}
@Override
String kind() {
return "Electric car";
}
@Override
public double consumptionKWh(double km) {
return km * 0.17;
}
}
public static void main(String[] args) {
Vehicle vehicle = new ElectricCar("Example");
System.out.println(vehicle.brand() + " - " + vehicle.kind());
}
}A superclass's private fields are not directly visible to subclasses; access should go through the superclass contract. A final method cannot be overridden and a final class cannot be subclassed. Building inheritance hierarchies merely to reuse code is fragile; composition is preferable when substitutability does not hold.
Unit 6: The Object Contract, Equality, and Identity
Object is the common root of the ordinary class hierarchy. If a class defines logical equality with equals, it must preserve the corresponding hashCode contract: equal objects must produce equal hash codes. The converse is not required because hash collisions are valid.
reference a ---+
+----> object X
reference b ---+
== -> reference identity
.equals(...) -> defined logical equality
.hashCode() -> compatible hash for equal objectsMutating state that participates in equality or hashing while an object is used as a hash-based key can make an entry appear unreachable. Keys should therefore be immutable, or at least stable with respect to equality while stored in the map or set.
toString is useful for diagnostics but is not a persistent serialization format. Passwords, tokens, national identifiers, and other sensitive fields must not leak through generated toString methods, logs, or exception messages.
6.1. Concrete Equals, hashCode, and toString Example
== and equals express different equality semantics. When logical equality is defined, hashCode must be based on the same equality state.
import java.util.Objects;
public final class Fraction {
private final int numerator;
private final int denominator;
public Fraction(int numerator, int denominator) {
if (denominator == 0) {
throw new IllegalArgumentException("Denominator must not be zero");
}
int gcd = gcd(Math.abs(numerator), Math.abs(denominator));
int sign = denominator < 0 ? -1 : 1;
this.numerator = sign * numerator / gcd;
this.denominator = sign * denominator / gcd;
}
private static int gcd(int a, int b) {
while (b != 0) {
int t = a % b;
a = b;
b = t;
}
return a == 0 ? 1 : a;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Fraction other)) return false;
return numerator == other.numerator && denominator == other.denominator;
}
@Override
public int hashCode() {
return Objects.hash(numerator, denominator);
}
@Override
public String toString() {
return numerator + "/" + denominator;
}
}Unit 7: Exceptions and Resource Management
Java exceptions form a hierarchy below Throwable, with Error and Exception as major branches. Checked versus unchecked is primarily a compile-time catch-or-declare distinction. It is not a reliable rule that every checked exception is recoverable or every unchecked exception is necessarily a programmer defect.
Exception boundaries should align with abstraction boundaries. Replacing every low-level error with a generic exception destroys causal information; exposing implementation-specific exceptions through a public API can also break encapsulation. When translating exceptions, retaining the original cause preserves diagnostic context.
try-with-resources provides deterministic closing for AutoCloseable resources such as files, sockets, and JDBC objects, even when an exception occurs. finally remains useful for general cleanup, but resource lifetime is usually clearer with the dedicated construct.
final, finally, and the historical finalize() mechanism are unrelated concepts. Finalization is not a dependable resource-release mechanism and should be treated as obsolete in modern Java. External resources require explicit, deterministic lifecycle management.
Common run-time exceptions should be distinguished by cause rather than memorized as a flat list. NullPointerException denotes use of a null reference where an object is required; ArithmeticException can arise from operations such as integer division by zero; NumberFormatException signals invalid textual numeric conversion; ClassCastException indicates an invalid reference-type cast; and ArrayIndexOutOfBoundsException indicates an array index outside its valid range. InterruptedException is different: it participates in cooperative interruption of blocking operations and should not be swallowed without an explicit policy. RuntimeException is the common unchecked-exception base for many programming and contract violations, not a synonym for every failure at run time.
7.1. Custom Exceptions and Multiple Catch Clauses
An exception may represent not only an unexpected technical failure but also violation of an API contract. Exception types should be meaningful and handled at the abstraction boundary that can decide what to do next.
public class ExceptionExample {
static final class InvalidDivisionException extends Exception {
InvalidDivisionException(String message) {
super(message);
}
}
static int divide(int dividend, int divisor) throws InvalidDivisionException {
if (divisor == 0) {
throw new InvalidDivisionException("Divisor must not be zero");
}
return dividend / divisor;
}
public static void main(String[] args) {
try {
System.out.println(divide(10, 2));
} catch (InvalidDivisionException e) {
System.err.println(e.getMessage());
}
}
}One try block may have multiple catch clauses; more specific subtypes must precede broader supertypes. Multi-catch such as catch (IOException | SQLException e) reduces duplication when unrelated exception types receive the same handling. Nested try-catch is valid, but if it obscures control flow, redesigning method boundaries is usually cleaner.
Unit 8: Generics and Type Safety
Generics move many cast errors from run time to compile time. Java's generic implementation largely uses type erasure, which means List<String> and List<Integer> are not separate reified classes at run time.
Wildcards express variance relationships. ? extends T is useful when an API primarily produces T values, while ? super T is useful when it consumes them. The familiar “producer extends, consumer super” rule is a useful design heuristic, not a replacement for understanding the actual mutation contract.
Raw types should normally be limited to legacy interoperability. Unchecked casts, generic varargs, and reflective operations can undermine type safety and create heap-pollution failures that appear far from the original unsafe operation.
8.1. Arrays and Multidimensional Arrays
An array stores a fixed number of values of one component type and exposes them through indexes. The length field reports the number of elements. Because an array variable holds an object reference, b = a does not copy elements; it binds both variables to the same array. Use Arrays.copyOf, clone, or explicit element copying when an independent copy is required.
import java.util.Arrays;
public class ArrayExample {
static double average(int[] values) {
if (values.length == 0) {
return Double.NaN;
}
long total = 0;
for (int value : values) {
total += value;
}
return (double) total / values.length;
}
public static void main(String[] args) {
int[] scores = {72, 91, 64, 88, 79};
int[] copy = Arrays.copyOf(scores, scores.length);
Arrays.sort(copy);
int[][] matrix = {
{1, 2, 3},
{4, 5, 6}
};
System.out.println(average(scores));
System.out.println(Arrays.toString(copy));
System.out.println(matrix[1][2]);
}
}A type such as int[][] is actually an array of array references; rows are not required to have identical lengths. This permits jagged structures. For large numerical matrices, however, object/array layout, locality, and memory cost should be evaluated explicitly.
8.2. Simple Generic Method Example
import java.util.List;
public class GenericExample {
static <T> T first(List<T> list) {
if (list.isEmpty()) {
throw new IllegalArgumentException("List is empty");
}
return list.get(0);
}
public static void main(String[] args) {
System.out.println(first(List.of("Java", "JVM")));
System.out.println(first(List.of(10, 20, 30)));
}
}Unit 9: Collections
Collection covers list, set, and queue-oriented abstractions; Map represents key-value associations separately. Choosing a collection requires considering algorithmic complexity, memory behavior, ordering requirements, mutation patterns, and concurrency rather than only API familiarity.
ArrayListprovides efficient indexed access and amortized constant-time append, while insertion in the middle can shift elements.LinkedListis a doubly linked list and also aDeque; indexed access is linear.HashSetandHashMapare general-purpose hash structures and do not promise a logical iteration order.LinkedHashMapcan preserve insertion or access order.TreeSetandTreeMapprovide comparator-based ordering.ArrayDequeis generally preferable to the legacyStackclass for stack and deque behavior.PriorityQueueexposes priority at the head; it is not a permanently sorted list representation.
HashMap is not thread-safe. Hashtable is a legacy synchronized class. New concurrent designs should choose ConcurrentHashMap or explicit synchronization according to the operation-level consistency contract. A concurrent map should not be mentally modeled as a table protected by one global lock.
Iterator behavior varies by collection. Many classic collections use fail-fast checks; some concurrent collections provide weakly consistent iteration. ConcurrentModificationException is a bug-detection aid, not a concurrency-safety guarantee.
More specialized collection types encode additional contracts. ListIterator can traverse lists in both directions and supports position-aware updates; LinkedHashSet preserves encounter order while retaining set semantics; EnumSet is a compact set specialized for enum constants; and NavigableMap adds nearest-key and range-navigation operations to sorted maps. IdentityHashMap compares keys by reference identity rather than ordinary equals, so it is suitable only for identity-oriented algorithms. CopyOnWriteArrayList favors read-heavy workloads by copying its backing array on mutation; using it in write-heavy paths can cause substantial allocation and copying cost.
9.1. Seeing Collection Selection in Code
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CollectionExample {
public static void main(String[] args) {
List<String> languages = List.of("Java", "C", "C++", "C#");
Map<String, Integer> lengths = new HashMap<>();
for (String language : languages) {
lengths.put(language, language.length());
}
ArrayDeque<String> queue = new ArrayDeque<>();
queue.addLast("first");
queue.addLast("second");
System.out.println(lengths.get("Java"));
System.out.println(queue.removeFirst());
}
}Unit 10: Lambdas, Functional Interfaces, and Streams
A lambda is targeted to a functional interface with one abstract method. Common examples include Predicate, Function, Consumer, and Supplier, together with primitive specializations. Method references adapt existing methods to compatible functional-interface targets.
The @FunctionalInterface annotation documents and compiler-checks that an interface is intended to satisfy the single-abstract-method contract. It is not required for an interface to be a functional interface, but it protects the intent against accidentally adding a second abstract method.
A stream is a processing pipeline, not a data store. Intermediate operations such as map, filter, flatMap, sorted, and distinct are generally lazy and execute only when a terminal operation demands results.
A parallel stream is not automatically faster because multiple CPU cores exist. Splitting cost, common-pool contention, data size, work per element, ordering, side effects, and shared resources must be measured. Blind parallelization of blocking I/O or lock-heavy code can increase latency instead of reducing it.
10.1. Stream Example
import java.util.List;
public class StreamExample {
public static void main(String[] args) {
List<Integer> values = List.of(3, 8, 11, 14, 21, 30);
int total = values.stream()
.filter(x -> x % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
System.out.println(total);
}
}A simple for loop can perform the same computation. The justification for a stream is not merely fewer characters; it is the ability to read the transformation as a data pipeline. Allocation, boxing, and parallelism costs on hot paths should still be measured.
Unit 11: I/O, NIO, and Serialization
java.io contains classic stream-oriented APIs, while java.nio and java.nio.file provide buffers, channels, charsets, and modern filesystem access. Text conversion should specify a charset explicitly. Relying on the platform default can create silent data corruption when an application moves between environments.
Classic I/O classes still appear in existing code and illustrate the byte/character distinction. FileInputStream and FileOutputStream operate on bytes; FileReader and FileWriter are character-oriented; BufferedReader adds buffered text reading and convenient line-oriented access; PrintStream and PrintWriter provide formatted textual output with different underlying character/byte semantics. Resource ownership and charset behavior should be explicit instead of inferred from a class name.
Path and Files are the main modern filesystem abstractions. Large data should often be streamed or processed in chunks instead of being loaded entirely into heap memory. Where durable publication matters, writing a temporary file and atomically moving it into place can prevent readers from observing partially written output.
Built-in Java object serialization still exists in legacy systems, but deserializing untrusted object graphs is a serious security boundary. New external data contracts should prefer explicit, constrained formats and schemas.
For XML processing, SAX-style parsing streams events and can keep memory use bounded for large documents, whereas DOM builds an in-memory tree that is convenient for random navigation but proportional to document size. The choice is therefore an access-pattern and resource trade-off, not a universal preference.
11.1. Text File I/O: from Classic Streams to the Modern Files API
PrintWriter, BufferedReader, FileReader, and File remain valid text-I/O building blocks. Modern code often uses Path and Files for a clearer filesystem model, and character encoding should not be left to an implicit platform default.
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class FileExample {
public static void main(String[] args) throws Exception {
Path path = Path.of("example.txt");
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
writer.write("Java");
writer.newLine();
writer.write("JVM");
}
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
}File is the historical path/file abstraction; Path/Files exposes richer filesystem and error semantics. The distinction remains fundamental: InputStream/OutputStream for bytes and Reader/Writer for characters. Large files should be streamed, channeled, or processed in bounded chunks rather than loaded entirely into memory.
Unit 12: Annotations, Reflection, and Modules
Annotations carry metadata with source, class-file, or run-time retention. Frameworks use them for mapping, configuration, and validation. An annotation does not execute behavior by itself; a compiler, annotation processor, framework, or reflective component must interpret it.
Reflection can inspect and invoke types, members, and annotations at run time. It is powerful but can weaken static type checking and encapsulation and may complicate performance or native/AOT processing. Explicit registration or build-time metadata generation can be more predictable in systems where dynamic discovery is unnecessary.
The Java Platform Module System uses module-info.java to describe module dependencies and exported packages. Module, package, and JAR are different abstractions.
@Deprecated marks an API whose continued use is discouraged and whose replacement or migration guidance should normally be inspected; it does not mean the API immediately stops functioning. External tools such as Lombok can generate code at compile time; for example, @SuperBuilder is Lombok-specific and is not a Java or Jakarta standard annotation. The origin and generated behavior of such annotations should be explicit in IDE, annotation-processing, and build pipelines.
Unit 13: Concurrency and the Java Memory Model
Creating threads introduces visibility, atomicity, and ordering problems in addition to possible parallel execution. The Java Memory Model defines the conditions under which writes in one thread become visible to another. The happens-before relation is central to reasoning about this behavior.
Thread A Thread B
-------- --------
write x=1
|
+-- synchronization / happens-before --> read x
|
v
visibility guarantee
ordering + visibility + atomicity are distinct propertiessynchronized provides mutual exclusion and memory-visibility effects around monitor acquisition and release. volatile establishes visibility and ordering rules for reads and writes of a variable, but it does not make a compound read-modify-write operation such as count++ atomic. Atomic variables or structures such as LongAdder may be appropriate depending on contention and required semantics.
The Lock APIs add features such as timed acquisition, interruptible acquisition, and multiple conditions. With multiple locks, a consistent acquisition order, short critical sections, and avoiding blocking I/O while holding locks are important deadlock controls.
ExecutorService separates task submission from thread lifecycle. Pool sizing cannot be reduced to a single formula such as “number of CPU cores.” CPU versus I/O behavior, downstream capacity, queue bounds, deadlines, and target latency all matter.
CompletableFuture composes asynchronous operations, but executor selection, exception propagation, cancellation, and timeouts must be explicit. Virtual threads make large numbers of blocking tasks cheaper to represent; they do not create more database connections, remote-service quota, or lock capacity.
Some newer concurrency APIs, including forms of structured concurrency, may be preview features depending on the JDK release. Preview APIs should not be treated as permanently stable without checking the specification of the actual deployment JDK.
13.1. Atomic Counter and virtual-thread Example
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
public class ConcurrencyExample {
public static void main(String[] args) throws Exception {
AtomicInteger counter = new AtomicInteger();
List<Future<?>> tasks = new ArrayList<>();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000; i++) {
tasks.add(executor.submit(counter::incrementAndGet));
}
for (Future<?> task : tasks) {
task.get();
}
}
System.out.println(counter.get());
}
}The atomic counter prevents a data race in this example. A virtual thread reduces task-representation cost; it does not imply that 1,000 concurrent database queries are safe. External resource capacity still requires explicit bounds.
13.2. Separating Virtual Threads from Capacity Control
A virtual thread reduces the cost of dedicating a platform thread to blocking work, but it does not expand the capacity of a shared downstream resource. This distinction matters because the ability to start many tasks is not the same guarantee as the ability to let all of those tasks enter a database, remote service, or storage boundary concurrently.
many virtual threads
|
v
concurrency limit
|
v
scarce downstream resourceA simple permit mechanism such as Semaphore can express this limit. Thread lifetime and resource permission are different concepts: a task may wait on a virtual thread, but it enters the expensive boundary only after acquiring permission. The execution model can remain readable and blocking while resource consumption is tied to an explicit budget.
Capacity control should also be distinguished from the Java Memory Model. volatile, locks, and atomic classes address visibility or atomicity; a semaphore or connection pool controls how many operations may enter a boundary at once. The former is a correctness concern, the latter a resource-management concern, and some designs require both.
Cancellation and interruption are also part of the contract. If a task waiting for a permit or blocked in I/O cannot be terminated predictably after a timeout, overload can produce resource leaks and long queues. Concurrency design therefore needs to answer not only “which thread model?” but also “which resource is bounded, how long may work wait, and how does cancellation propagate?” The broader performance context appears in High-Performance Java Data Systems.
A concurrency limit belongs at the entrance to the scarce resource, not at the point where a particular kind of thread is created. A task acquires a permit before accessing the database and releases it on normal completion or failure; timeouts must not leak permits. A semaphore does not provide overload protection if unlimited tasks continue to accumulate in memory outside the permit boundary. Waiting-task count or queue age needs its own budget and an explicit response when the system is saturated.
Virtual threads are not permission for unbounded concurrency. Creating a virtual thread per task can represent waiting work cheaply, but it does not reduce demand for scarce database connections or remote-service slots. Separate task scheduling from admission to expensive resources. A Semaphore or other admission-control mechanism must be paired with a bounded waiting or rejection policy; a semaphore alone does not prevent unlimited accumulation of blocked tasks.
Diagnose carrier-thread pinning using tools appropriate to the current JVM instead of assuming that every use of synchronized pins virtual threads in every Java release. Reducing thread occupancy may leave completed throughput unchanged. Base the decision on CPU saturation, queue depth, downstream limits, and timeouts rather than thread count in isolation.
A useful permit-accounting invariant is available permits + acquired permits = total permits. Unconditionally calling release() in a finally block for a task that never acquired a permit can accidentally increase capacity. Cancellation is tested while waiting, immediately after acquisition and during resource use. Completion releases only an actually acquired permit. A timeout can bound waiting without proving that the underlying operation has stopped; resource-use completion requires separate verification.
13.3. The Java Memory Model and Concurrent Correctness
Correct multicore programming is more than the absence of a data race. For Java concurrent collections, consistency with a legal sequential history, blocking or lock-free progress properties, monitor or atomic contention, and happens-before visibility under the Java Memory Model are distinct correctness and performance dimensions. Linearizability provides a strong correctness model, while lock-free and wait-free progress classes show why latency cannot be explained merely by counting mutexes.
- Do not reduce happens-before relationships to the synchronized keyword alone.
- Show that atomic classes do not automatically preserve compound invariants.
- Measure contention and progress guarantees when selecting concurrent queues or maps.
Marking a field volatile changes its visibility and ordering guarantees; it does not turn a multi-step update into one atomic operation. count++ can be decomposed into read, addition, and write, allowing two threads to read the same old value and lose an increment. An atomic read-modify-write primitive is suitable for an independent counter, but atomicity of one field does not preserve invariants spanning several fields. Correctness requires a synchronization protocol covering the complete shared-state transition.
13.4. Interruption, Cancellation, and Resource Ownership
In Java concurrency, thread interruption is a cooperative cancellation request rather than a forcible stop instruction. Some blocking operations react by throwing InterruptedException. Silently swallowing that exception conceals cancellation from the calling layer. If a method cannot propagate the exception, it may restore the status with Thread.currentThread().interrupt(), although restoring the flag and propagating the exception represent different control-flow contracts. The appropriate policy belongs in the method's API contract.
Resource ownership is a separate obligation during cancellation. A task holding a database connection, file descriptor, or concurrency permit must release its resources on successful completion, failure, and cancellation. try-with-resources provides lexical ownership for closeable resources; a semaphore permit normally requires a corresponding finally release. Release must occur only after successful acquisition, otherwise the permit count can be corrupted under contention.
Virtual threads can reduce the cost of waiting, but they do not increase available JDBC connections, downstream capacity, or file descriptors. Starting 400 virtual threads against a 40-connection pool does not create 400 concurrent database operations. Admission control must reflect the actual resource boundary and account separately for queue growth and cancellation latency.
13.5. Atomic Publication, Memory Visibility, and ABA
Safe publication under the Java Memory Model determines which field values another thread may observe through a happens-before relationship. A volatile field or suitable synchronization contributes visibility but does not automatically provide a transaction over multiple independent variables. In a CAS loop using AtomicReference, the return of an observed reference to its previous value does not imply that intermediate state changes are irrelevant.
ABA concerns the logical version of a reference-based state transition; AtomicStampedReference can incorporate a version stamp in appropriate cases. Progress guarantees arise from the entire algorithm, not from use of one atomic class. Multicore tests separately examine visibility, linearizability, sustained contention, and garbage-collection load.
Unit 14: JVM Memory, Garbage Collection, and JIT Compilation
Objects that are no longer strongly reachable become candidates for garbage collection; assigning a local variable to null does not mean the object is immediately deleted. Collector choice, heap sizing, allocation rate, live-set size, and latency objectives form one tuning problem.
JVM process
|
+-------------+-------------+
| |
thread-private shared
| |
stack / frames heap
PC / native stack objects
|
garbage collector
class metadata ----------------> metaspace
hot code ----------------------> JIT -> native codeMemory leaks are possible in managed languages. If an object is no longer useful to the application but remains reachable through a strong reference, the collector cannot reclaim it. Unbounded caches, listener registrations, ThreadLocal lifetimes, and long-lived collections are common sources.
JIT compilation introduces warm-up effects. A Java microbenchmark should not simply wrap a few method calls with System.nanoTime(). Dead-code elimination, constant folding, tiered compilation, and garbage collection can invalidate naive measurements. JMH is designed to address many of these hazards.
Older JVM material may refer to PermGen (permanent generation). HotSpot removed PermGen in Java 8 and moved class metadata to Metaspace, which is managed differently and is not a reason to carry historical PermGen tuning advice into current JVM deployments.
Observe allocation, the live set, and GC pauses separately. JVM process memory is not synonymous with heap occupancy. Thread stacks, metaspace, the JIT code cache, direct buffers, and native libraries contribute to resident memory. A process can reach its container memory limit even when heap dashboards appear comfortable. Treating -Xmx, RSS, and cgroup memory usage as interchangeable measurements is therefore incorrect.
When correlating JFR allocation and GC events with application phases, prefer a stable workload after warm-up. A high rate of short-lived allocations is not by itself evidence of a memory leak. A leak hypothesis needs objects retained after work has finished, their reference paths, and an increasing retained live set across comparable steady-state cycles. JIT experiments should likewise distinguish compilation and warm-up effects from a genuine reduction in the amount of work performed.
Unit 15: JDBC and Data Access
JDBC is Java's standard database-access API. Connection, PreparedStatement, and ResultSet are core building blocks. Parameter binding is preferable to concatenating untrusted values into SQL, both for type handling and for reducing SQL-injection exposure.
CallableStatement is the JDBC contract for invoking stored procedures or database routines that expose callable semantics. Its use does not remove transaction, timeout, type-mapping, or resource-lifetime concerns; those remain part of the surrounding JDBC contract.
Transaction boundaries should represent business atomicity. Multi-step writes performed without understanding auto-commit can leave partial results. A connection pool amortizes connection-establishment cost, but its size must respect the database's real concurrent capacity.
ORM and JPA do not remove the need to understand SQL and transactions. N+1 access patterns, broad transactions, inappropriate fetching, unnecessary entity materialization, and unbounded result sets can dominate application cost. The performance side is developed further in High-Performance Java Data Systems.
Hibernate commonly exposes a SessionFactory/Session model, while standard JPA uses EntityManagerFactory/EntityManager. HQL is Hibernate's object-oriented query language; JPQL is the standardized persistence query language. Mapping annotations such as @ManyToOne, @OneToMany, and @GeneratedValue describe relationships or identifier generation, while FetchType.LAZY and FetchType.EAGER express fetching intent rather than a universal performance rule. Accessing an uninitialized lazy association after its persistence context is unavailable can surface as Hibernate's LazyInitializationException; the design response is to define transaction and fetch boundaries deliberately, not to make every association eager.
Older JDBC examples may explicitly load a driver with Class.forName(...) and execute a query through Statement or PreparedStatement.executeQuery(...). Modern JDBC drivers are commonly discovered through the service-provider mechanism, so Class.forName is not a universal startup requirement. executeQuery is intended for operations that return a result set; update and DDL operations follow different execution contracts.
15.1. Parameterized Query with PreparedStatement
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JdbcExample {
static String userName(Connection connection, long id) throws SQLException {
String sql = "select name from users where id = ?";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getString(1) : null;
}
}
}
}Parameter binding is not only about reducing SQL-injection exposure; it also makes type conversion and the query contract explicit. Transaction and timeout management are intentionally outside this small example; real applications must define connection lifetime and business atomicity separately.
Unit 16: Networking, HTTP, and the Enterprise Java Ecosystem
The standard library includes socket and HTTP client APIs. TCP is a byte-stream transport; UDP is datagram-oriented. Application protocols still need framing, timeout, retry, authentication, and resource-limit rules.
Servlets and JSP are historically important Java web technologies and remain present in existing systems. In current Jakarta EE terminology their specification successors are Jakarta Servlet and Jakarta Pages; legacy javax.* and current jakarta.* namespaces are not interchangeable. Modern service applications often use higher-level frameworks such as Spring MVC, Spring Boot, or Jakarta REST. Framework knowledge does not replace language and runtime knowledge: class loading, collections, threads, I/O, and exception behavior become visible during production failures.
At the servlet API level, HttpServlet models HTTP-specific request handling; ServletConfig exposes initialization information for one servlet; and ServletContext represents application-wide servlet-container context and shared resources. JSTL (Jakarta Standard Tag Library, historically JavaServer Pages Standard Tag Library) supplies standard tags for page-layer concerns. These APIs are relevant when maintaining server-rendered Jakarta Pages/JSP systems even when newer applications use different presentation architectures.
On the desktop, AWT and Swing are established toolkits within Java SE. JavaFX is developed separately from the JDK through the OpenJFX project in modern releases and provides scene, layout, and control APIs for graphical applications. The existence of a Swing or JavaFX system is not by itself a reason to rewrite it as a web application; deployment, maintenance, and interaction requirements should drive that decision.
The Spring ecosystem is covered separately in Spring Boot, while browser and protocol concerns are discussed in Web Programming.
In older servlet-based Java web applications, the web.xml deployment descriptor was a central XML mechanism for servlet, filter, listener, and mapping configuration. Annotation-based configuration has reduced many of its uses, but web.xml remains a relevant historical and compatibility mechanism in existing Jakarta Servlet/JSP systems and for selected container settings.
16.1. Standard HttpClient Example
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class HttpExample {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.build();
HttpRequest request = HttpRequest.newBuilder(URI.create("https://example.com"))
.timeout(Duration.ofSeconds(3))
.GET()
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println(response.statusCode());
}
}A real service client should define request timeouts in addition to connection timeouts; retries should only be introduced with explicit knowledge of idempotency and failure classes. The HTTP client example does not replace networking fundamentals: connection pooling, DNS, TLS, proxies, and server capacity all contribute to end-to-end behavior.
Unit 17: Builds, Dependencies, Version Control, and Testing
Maven and Gradle are not merely download tools. They model dependency resolution, build lifecycles, tests, packaging, and plugin execution. Uncontrolled transitive dependencies can cause behavioral differences, reproducibility failures, or security exposure.
JUnit is a core tool for Java unit and integration testing. Mockito and similar libraries can replace selected collaborators with controlled test doubles. Mocking every class is not a sign of good isolation; pure computation code is often clearer and more robust when tested with real objects.
Git provides distributed source version control. Commits, branches, and merges preserve source history and change context. CI/CD makes build, test, and delivery processes repeatable, but automation cannot compensate for an invalid test oracle or a dangerous deployment policy.
In classic JavaFX code, VBox is a vertical layout container, ScrollBar is a scrolling control, and NumberConverter belongs to the converter family used between numeric values and text. These are JavaFX API examples rather than Java language features. Knowing individual widget classes does not replace understanding the layout, threading, and lifecycle model.
Unit 18: Secure and Measurable Java
Secure Java development requires more than catching exceptions. Input validation, authorization, query parameterization, safe deserialization, correct cryptographic APIs, secret handling, and dependency governance are parts of the same trust boundary.
Performance work should begin with the system objective: latency, throughput, CPU, allocation, GC pause, database capacity, or an external-service bottleneck. A collection micro-optimization may be irrelevant next to an inefficient query or a remote call performed while holding a lock. Profiling, Java Flight Recorder, GC information, thread dumps, and system metrics should identify the real constraint before code is changed.
The language, JVM, and frameworks are separate layers. Avoiding category errors between language guarantees, JVM implementation details, and framework conventions is one of the most important disciplines in production Java engineering.
18.1. Value-oriented Modeling with Records, Sealed Types, and Patterns
Java is not limited to mutable class hierarchies. Records provide compact value-oriented models with generated construction, accessors, equality, hashing, and string representation. Sealed types restrict which subtypes may exist.
Combined with pattern matching, these features can model closed domains explicitly. A result known to be one of a finite set of cases can be handled exhaustively.
The goal is not merely shorter code. It is to make the valid state space visible to the compiler. Open-ended domains should remain open rather than being forced into sealed hierarchies.
18.2. Separating Java-language and JVM Behavior
The Java Language Specification and JVM Specification define different contracts. A compile-time language rule and a HotSpot optimization are not the same level of evidence.
Concurrency examples should be grounded in the Java Memory Model. Code that appears correct in several runs does not prove a happens-before relationship; visibility and atomicity must follow the language/runtime contract.
JIT warm-up, GC, and escape analysis can change performance observations. A microbenchmark harness, JFR/GC logs, and representative production workloads provide complementary evidence.
Define what a microbenchmark measures. Two Java methods cannot be compared reliably by source-code length or a tight ad hoc loop. The JIT may eliminate unused computations, fold constant inputs, inline calls, or remove allocations through escape analysis. Even with a purpose-built harness such as JMH, document result consumption, parameter distributions, warm-up, forks, and benchmark environment behavior.
Reapply Amdahl's law before transferring a local speedup to an HTTP request: making a serialization step twice as fast cannot double overall response speed when serialization accounts for only a small fraction of the request. Validate production-like input distributions, GC behavior, and cache locality separately. A faster isolated method and increased system throughput are different outcome measures.
Unit 19: JVM Lifecycle, Resource Semantics, and Modern Java Deep Dives
Reading Java only at the source-code, object, and method level hides important runtime behavior. When and how a class is loaded, which loader defines it, in what order resources close, how failure propagates through an asynchronous pipeline, and how strongly an object is referenced all affect observable program behavior.
19.1. Class Loading and Class Identity
A JVM class lifecycle can be reasoned about as:
loading
↓
linking
├─ verification
├─ preparation
└─ resolution
↓
initializationLoading locates a class definition and creates the JVM representation. Linking verifies bytecode, prepares storage for static state, and resolves symbolic references as required. Initialization executes class initialization logic and static initializers.
Class identity is not merely the fully qualified class name. In practice:
class identity
=
binary name
+
defining class loaderTwo definitions with the same binary name but different defining loaders are not necessarily assignment-compatible. Application servers, plugin architectures, reload mechanisms, and modular runtimes can therefore produce apparently surprising ClassCastException failures.
The purpose of studying class loaders is not to encourage custom loaders everywhere. It is to understand the connection between code discovery and runtime type identity.
19.2. Initialization Time and Static State
Finding class bytecode does not imply that all class initialization code has already executed. Initialization is triggered by specific forms of active use.
Heavy static initialization can affect startup latency, failure behavior, test isolation, and class-initialization locking.
Static global state also survives according to the lifetime of its defining class loader. static is therefore not merely syntax; it is a runtime-lifetime decision.
19.3. try-with-resources, Close Order, and Suppressed Exceptions
try-with-resources is more than generated close() calls. Multiple resources are closed in the reverse order of successful initialization.
try (var first = openFirst();
var second = openSecond()) {
use(first, second);
}Conceptually closes as:
second.close()
first.close()If the main operation throws and resource closing also fails, the close failure should not hide the primary exception. Java can attach the later failures as suppressed exceptions, available through Throwable.getSuppressed().
This matters for files, sockets, JDBC resources, and other external handles.
Garbage collection can reclaim object memory, but it does not replace deterministic release of operating-system resources. Memory lifetime and external-resource lifetime are separate contracts.
19.4. Strong, Soft, Weak, and Phantom References
An ordinary Java reference is a strong reference. As long as a strong reachability path remains, the object is not treated as unreachable.
The java.lang.ref family provides weaker forms:
strong
↓
SoftReference
↓
WeakReference
↓
PhantomReferenceThis is a conceptual strength ordering, not a precise collection schedule.
SoftReference has historically been used for memory-sensitive retention, but it is not a predictable general-purpose cache policy. Explicit capacity, TTL, and eviction are usually more controllable.
WeakReference allows an object to become collectible when no strong reference remains. WeakHashMap is useful for particular key-lifetime relationships, but weak references do not automatically eliminate memory leaks.
PhantomReference is used with ReferenceQueue for post-reachability lifecycle coordination rather than to regain ordinary access to the object. Deterministic cleanup still belongs to explicit lifecycle mechanisms such as AutoCloseable.
19.5. Distinguish Memory Leaks from Collector Choice
Java can leak memory when an object is no longer useful to the business logic but remains strongly reachable:
logically dead object
+
strong reachability path
=
live object from the GC perspectiveUnbounded maps, listeners, long-lived ThreadLocal values, static collections, and poorly bounded caches are common causes.
Changing the collector before identifying the retaining reference chain can treat the symptom rather than the cause. Heap dumps and Java Flight Recorder help identify retention and allocation behavior.
19.6. CompletableFuture: Composition, Not Just Background Work
The key distinction between the two common entry points is:
runAsync(Runnable)
-> no result value
-> CompletableFuture<Void>
supplyAsync(Supplier<T>)
-> produces T
-> CompletableFuture<T>The larger value of CompletableFuture is composition:
thenApply
-> map T to U
thenCompose
-> map T to a new asynchronous stage
-> flatten nested futures
thenCombine
-> combine independent resultsCompletableFuture<User> user =
CompletableFuture.supplyAsync(() -> loadUser(id), executor);
CompletableFuture<Account> account =
user.thenCompose(u -> loadAccountAsync(u.accountId()));Using thenApply for the second step would produce a nested future.
19.7. Failure Flow and Executor Ownership
Asynchronous errors can flow through exceptionally, handle, whenComplete, and completion exceptions. The real design questions are where failure is translated, where it is observed, whether a fallback is semantically safe, how cancellation propagates, and who owns timeouts.
Some asynchronous stages can use shared default executors when no executor is supplied. Library code that silently places blocking or expensive work on a shared resource can create capacity coupling. Critical pipelines should make execution ownership explicit.
Virtual threads and CompletableFuture solve different problems. Virtual threads make blocking tasks cheaper to represent; futures model dependency and completion graphs. The more readable and measurable model depends on the workload.
19.8. Reflection, MethodHandle, And Dynamic Invocation
Reflection supports runtime discovery and generic access. MethodHandle provides a more directly typed callable representation connected to the JVM's dynamic invocation machinery.
Conceptually:
Reflection
-> discovery + general dynamic access
MethodHandle
-> linked dynamic invocation behaviorinvokedynamic is a bytecode-level linkage mechanism used by language runtimes and features such as lambda implementation.
These mechanisms should be evaluated as tools for frameworks, language runtimes, and advanced metaprogramming rather than as default replacements for ordinary static calls.
19.9. The Boundary of built-in Object Serialization
Java object serialization is historically important, but ObjectInputStream and ObjectOutputStream should not automatically define a new external data contract.
Risks include tight coupling to class structure, versioning difficulty, untrusted object-graph creation, accidental exposure of fields, and limited cross-language interoperability.
For external boundaries, explicit schemas and constrained DTOs encoded as formats such as JSON, CBOR, or Protobuf are often easier to audit.
When legacy Java serialization is unavoidable, class filtering, graph limits, authenticated input, and minimal deserialization scope provide additional defense.
19.10. Security Manager Is Historical Architecture
Older Java material may present SecurityManager, policy files, and in-process sandboxing as the current security model.
That information is historically useful, but modern JDKs have permanently disabled the Security Manager. New security architecture should rely on layers such as process/container isolation, operating-system permissions, application authorization, input validation, safe serialization, and explicit network/file access boundaries.
Sometimes the correct answer to an old interview question is not the old API behavior but recognizing that the API is no longer the active security model.
19.11. Java 27: Distinguish GA from Preview
JDK 27 is the current Java SE release as of September 2026, but a feature shipping in the JDK is not automatically a permanent Java SE contract.
Preview features can require explicit compiler/runtime flags, change in later releases, or disappear. Long-lived APIs should use them deliberately.
For every modern feature, ask:
which JDK contains it?
is it final, preview, or incubating?
does my framework officially support that JDK?Learning current Java means separating language specification, JVM semantics, library contracts, and runtime-version support rather than memorizing legacy interview answers.
Unit 20: String Identity, Immutability, and Concurrency Protocols in Java
Many topics from older Java interview material remain useful, but they should be read as language/JVM contracts rather than trivia.
20.1. String Pool, Literals, and intern()
String literals and compile-time constant strings can refer to canonical interned strings.
String a = "trace";
String b = "trace";The important distinction remains:
== -> reference identity
equals -> content/equality contractintern() returns a canonical representation for equal string content. Interning every application string is not a universal optimization; it changes memory and lookup trade-offs.
20.2. Designing Immutable Classes
A final field does not automatically make the referenced object immutable.
final List<String> values;prevents reassignment of the reference, not mutation of the list.
Robust immutability can require defensive copies, non-leaking collection views, construction of valid state, restricted mutation surfaces, and control of subclass behavior.
A record is not deeply immutable when one of its components is itself mutable.
20.3. Comparable And Comparator
Comparable<T> defines a type's natural ordering.
Comparator<T> defines external or alternative orderings.
The relationship between comparison and equals matters for sorted sets and maps because ordering can determine whether two keys are treated as equivalent for placement.
20.4. wait, notify, And notifyAll
These methods are low-level parts of the Object monitor protocol.
Calling them requires ownership of the relevant monitor.
A condition should be checked in a loop:
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait();
}
consume(queue.removeFirst());
}This protects against spurious wakeups and against another thread invalidating the condition before the awakened thread reacquires the monitor.
Higher-level tools such as BlockingQueue, explicit locks/conditions, synchronizers, and structured-concurrency-oriented designs often provide clearer production contracts.
20.5. Native Serialization: transient And serialVersionUID
transient excludes a field from the normal serialized form. serialVersionUID participates in class-version compatibility checking during Java object deserialization.
Native object serialization is historically important but should not automatically become a service boundary or untrusted-input format. Explicit data contracts are usually easier to validate, evolve, and secure.
20.6. Interface Default Methods
A modern Java interface can provide default methods. This helps evolve contracts while preserving implementations.
Conflicting inherited defaults require explicit resolution.
Default methods should support interface evolution rather than become a substitute for arbitrary implementation inheritance.
20.7. Positioning Historical Collection Classes
Vector, Stack, and Hashtable remain part of the platform and may appear in legacy systems.
For new designs, common alternatives include:
Vector
-> ArrayList + explicit concurrency strategy
Stack
-> Deque / ArrayDeque
Hashtable
-> HashMap
-> ConcurrentHashMap when concurrent access is requiredThe point is not that legacy classes cease to work; modern abstractions usually express the intended semantics more clearly.
20.8. Updating Interview Knowledge
For any interview claim, ask whether it is:
- a Java-language rule,
- a JVM/runtime implementation concern,
- a historical API/version behavior.
That distinction prevents old answers from being overgeneralized to modern JDKs.
References
- A. Yazıcı, E. Doğdu, M. Özbayoğlu, M. Erten, O. Ergin. Java Bilgisayar Programlamaya Giriş.
- Benjamin J. Evans; Jason Clark; David Flanagan. Java in a Nutshell: A Desktop Quick Reference, 8th ed. O'Reilly Media, 2023.
- Benjamin J. Evans; Martijn Verburg. The Well-Grounded Java Developer. Manning, 2013.
- Brendan Gregg. Systems Performance: Enterprise and the Cloud. Pearson Education, 2014.
- Brian Goetz et al. Java Concurrency in Practice. Addison-Wesley, 2006.
- Eclipse Foundation. Jakarta Pages Specification. https://jakarta.ee/specifications/pages/
- Eclipse Foundation. Jakarta Servlet Specification. https://jakarta.ee/specifications/servlet/
- Eclipse Foundation. Standard Widget Toolkit (SWT). https://www.eclipse.org/swt/
- Joshua Bloch. Effective Java, 3rd ed. Addison-Wesley, 2018.
- Maurice Herlihy; Nir Shavit. The Art of Multiprocessor Programming. Morgan Kaufmann, 2008.
- OpenJDK. Java Microbenchmark Harness (JMH). https://openjdk.org/projects/code-tools/jmh/
- OpenJDK. JEP 444: Virtual Threads. https://openjdk.org/jeps/444
- OpenJDK. JEP 486: Permanently Disable the Security Manager. https://openjdk.org/jeps/486
- OpenJFX. JavaFX Documentation. https://openjfx.io/
- Oracle. Java Language Specification. https://docs.oracle.com/javase/specs/
- Oracle. Java Platform, Standard Edition 27 & JDK 27 Specifications. https://docs.oracle.com/en/java/javase/27/docs/specs/index.html
- Oracle. Java Platform, Standard Edition Documentation. https://docs.oracle.com/en/java/javase/
- Oracle. Java SE API Documentation. https://docs.oracle.com/en/java/javase/27/docs/api/
- Oracle. Java Virtual Machine Specification. https://docs.oracle.com/javase/specs/
- Oracle. JDK 27 Release Notes. https://www.oracle.com/java/technologies/javase/27all-relnotes.html
- Oracle. String (Java SE 27 & JDK 27). https://docs.oracle.com/en/java/javase/27/docs/api/java.base/java/lang/String.html
- P. Deitel, H. Deitel. Java How to Program. Pearson Education.
- Walter Savitch, F. M. C. Java: An Introduction to Problem Solving & Programming, 5th ed. Pearson Education.