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.
The 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.
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.
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);
}
}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);
}
}
}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.
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.
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.
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.
Mutating 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.
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.
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.
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.
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.
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.
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.
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.
synchronized 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.
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.
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.
Memory 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.
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.
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.
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.
References
- Oracle. Java Platform, Standard Edition Documentation. https://docs.oracle.com/en/java/javase/
- Oracle. Java Language Specification. https://docs.oracle.com/javase/specs/
- Oracle. Java Virtual Machine Specification. https://docs.oracle.com/javase/specs/
- Oracle. Java SE API Documentation. https://docs.oracle.com/en/java/javase/27/docs/api/
- Brian Goetz et al. Java Concurrency in Practice. Addison-Wesley, 2006.
- Joshua Bloch. Effective Java, 3rd ed. Addison-Wesley, 2018.
- OpenJFX. JavaFX Documentation. https://openjfx.io/
- Eclipse Foundation. Jakarta Servlet Specification. https://jakarta.ee/specifications/servlet/
- Eclipse Foundation. Jakarta Pages Specification. https://jakarta.ee/specifications/pages/
- OpenJDK. JEP 444: Virtual Threads. https://openjdk.org/jeps/444
- OpenJDK. Java Microbenchmark Harness (JMH). https://openjdk.org/projects/code-tools/jmh/
- Eclipse Foundation. Standard Widget Toolkit (SWT). https://www.eclipse.org/swt/
- A. Yazıcı, E. Doğdu, M. Özbayoğlu, M. Erten, O. Ergin. Java Bilgisayar Programlamaya Giriş.
- Walter Savitch, Frank M. Carrano. Java: An Introduction to Problem Solving & Programming, 5th ed. Pearson Education.
- P. Deitel, H. Deitel. Java How to Program. Pearson Education.