Java Advanced Course
Master the hardest parts of Java with 25 advanced lessons. Deep-dive into streams, lambdas, concurrency, Optional, reflection, records, serialization, and more — each with code, exact output, and official Oracle documentation links.
Start LearningLambda Expressions
A lambda is a concise way to represent a single-method interface (functional interface). Syntax: (parameters) -> expression. Lambdas enable functional programming in Java and remove the boilerplate of anonymous inner classes.
import java.util.List;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Ali");
names.add("Sara");
names.add("Zain");
names.forEach(name -> System.out.println(name));
}
}
Functional Interfaces
A functional interface has exactly one abstract method. Java provides several built-in ones: Predicate (test), Function (apply), Consumer (accept), and Supplier (get). These are the building blocks of the Stream API.
import java.util.function.Function;
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
Function<Integer, Integer> square = x -> x * x;
Predicate<Integer> isEven = x -> x % 2 == 0;
System.out.println(square.apply(5));
System.out.println(isEven.test(10));
}
}
Method References
A method reference is shorthand for a lambda that calls an existing method. Syntax: Class::method. There are four kinds: static (Class::staticMethod), instance (obj::instanceMethod), arbitrary object (Class::instanceMethod), and constructor (Class::new).
import java.util.List;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("ali");
names.add("sara");
names.replaceAll(String::toUpperCase);
names.forEach(System.out::println);
}
}
Streams Basics
A Stream is a sequence of elements that supports functional-style operations like filter, map, and collect. Streams are lazy and do not modify the source. They process data in a pipeline without explicit loops.
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evensSquared = nums.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(evensSquared);
}
}
Stream map & filter
map transforms each element, and filter selects elements matching a predicate. Both are intermediate operations that return a new stream. They can be chained to build complex transformations.
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> words = List.of("apple", "banana", "cherry", "date");
List<Integer> lengths = words.stream()
.filter(w -> w.length() > 5)
.map(String::length)
.collect(Collectors.toList());
System.out.println(lengths);
}
}
Stream reduce
reduce combines all elements into a single result using an accumulator. For example, summing numbers or finding the max. reduce is a terminal operation that produces a value from a stream.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> nums = List.of(1, 2, 3, 4, 5);
int sum = nums.stream().reduce(0, Integer::sum);
int max = nums.stream().reduce(Integer.MIN_VALUE, Integer::max);
System.out.println(sum);
System.out.println(max);
}
}
Optional
Optional is a container that may or may not hold a value, eliminating null checks and NullPointerException. Methods like orElse, ifPresent, and map handle the empty case safely.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> present = Optional.of("Hello");
Optional<String> empty = Optional.empty();
System.out.println(present.orElse("Fallback"));
System.out.println(empty.orElse("Fallback"));
present.ifPresent(v -> System.out.println(v));
}
}
Threads Basics
A thread is a separate path of execution. You create one by extending Thread or implementing Runnable. start() launches the thread, and the JVM runs it concurrently with the main thread.
class Worker extends Thread {
@Override
public void run() {
System.out.println("Worker running");
}
}
public class Main {
public static void main(String[] args) {
Worker t = new Worker();
t.start();
System.out.println("Main running");
}
}
Runnable Interface
Runnable is a functional interface with a single run() method. Implementing Runnable is preferred over extending Thread because Java supports single inheritance — your class can extend something else while still being runnable.
class Task implements Runnable {
@Override
public void run() {
System.out.println("Task running");
}
}
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new Task());
t.start();
}
}
Synchronization
When multiple threads access shared data, race conditions can corrupt it. The synchronized keyword ensures only one thread enters a critical section at a time, making operations atomic.
class Counter {
private int count = 0;
synchronized void increment() {
count++;
}
int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter c = new Counter();
Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) c.increment(); });
Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) c.increment(); });
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(c.getCount());
}
}
ExecutorService
ExecutorService manages a pool of threads and executes submitted tasks. It abstracts thread creation, improving resource reuse. shutdown() stops accepting new tasks, and awaitTermination waits for completion.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(() -> System.out.println("Task A"));
pool.submit(() -> System.out.println("Task B"));
pool.submit(() -> System.out.println("Task C"));
pool.shutdown();
}
}
Future & Callable
Callable is like Runnable but returns a value. submit() returns a Future that represents the pending result. get() blocks until the result is available, retrieving the return value.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newSingleThreadExecutor();
Callable<Integer> task = () -> {
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i;
return sum;
};
Future<Integer> future = pool.submit(task);
System.out.println(future.get());
pool.shutdown();
}
}
Annotations
Annotations attach metadata to code. Built-in ones include @Override (verifies overriding), @Deprecated (marks outdated), and @SuppressWarnings (silences warnings). Frameworks and tools read annotations for behavior.
class Parent {
void print() {
System.out.println("Parent");
}
}
class Child extends Parent {
@Override
void print() {
System.out.println("Child");
}
}
public class Main {
public static void main(String[] args) {
Child c = new Child();
c.print();
}
}
Reflection
Reflection lets a program inspect classes, methods, and fields at runtime. Class.forName loads a class, and getDeclaredMethods lists its methods. Reflection powers frameworks, serialization, and testing tools.
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
Class<?> cls = Class.forName("java.lang.String");
System.out.println("Class: " + cls.getName());
for (Method m : cls.getDeclaredMethods()) {
if (m.getName().equals("toUpperCase")) {
System.out.println("Method: " + m.getName());
}
}
}
}
Generics Wildcards
Wildcards (?), <? extends T>, and <? super T> make generic code more flexible. ? extends T accepts T or subclasses (reading), ? super T accepts T or superclasses (writing). This is the PECS principle: Producer Extends, Consumer Super.
import java.util.List;
import java.util.ArrayList;
public class Main {
static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) total += n.doubleValue();
return total;
}
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>(List.of(1, 2, 3));
System.out.println(sum(ints));
}
}
try-with-resources Deep Dive
try-with-resources automatically closes resources that implement AutoCloseable. Declaring resources in the try parentheses guarantees close() is called after the block, even on exception. This replaces verbose finally blocks.
import java.io.BufferedReader;
import java.io.FileReader;
public class Main {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
System.out.println(br.readLine());
} catch (Exception e) {
System.out.println("File not found or error");
}
}
}
File I/O & NIO
java.nio.file provides the modern Files and Path APIs. Files.readString reads an entire file, Files.writeString writes one, and Path represents a file location. NIO is simpler and more efficient than the old File API.
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws Exception {
Path path = Path.of("note.txt");
Files.writeString(path, "Hello Java");
String content = Files.readString(path);
System.out.println(content);
Files.delete(path);
}
}
Serialization
Serialization converts an object into bytes for storage or transmission; deserialization reconstructs it. A class must implement Serializable. ObjectOutputStream writes objects, and ObjectInputStream reads them back.
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
class Student implements Serializable {
String name;
int age;
Student(String name, int age) {
this.name = name;
this.age = age;
}
}
public class Main {
public static void main(String[] args) throws Exception {
Student s = new Student("Ali", 25);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("student.ser"))) {
out.writeObject(s);
}
System.out.println("Serialized: " + s.name + " " + s.age);
}
}
Date & Time API (java.time)
The java.time package (Java 8+) provides immutable date-time classes. LocalDate for dates, LocalTime for times, and LocalDateTime for both. These replace the old mutable and confusing Date and Calendar.
import java.time.LocalDate;
public class Main {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
LocalDate birthday = LocalDate.of(2000, 5, 15);
System.out.println(today.getYear());
System.out.println(birthday.getMonth());
}
}
Enum Deep Dive
An enum is a fixed set of constants. Enums can have fields, constructors, and methods. They are type-safe and ideal for representing a fixed domain like days, directions, or statuses.
enum Day {
MONDAY("Weekday"), SATURDAY("Weekend");
private final String type;
Day(String type) {
this.type = type;
}
String type() {
return type;
}
}
public class Main {
public static void main(String[] args) {
System.out.println(Day.MONDAY.type());
System.out.println(Day.SATURDAY.type());
}
}
Records (Java 16+)
A record is a special class for immutable data. It automatically generates the constructor, getters, equals, hashCode, and toString. Records are ideal for simple data carriers.
record Point(int x, int y) {}
public class Main {
public static void main(String[] args) {
Point p = new Point(3, 4);
System.out.println(p.x());
System.out.println(p);
}
}
Default Methods in Interfaces
Default methods give interfaces concrete implementations. They use the default keyword. This allows adding methods to interfaces without breaking existing implementations.
interface Greeting {
default void greet() {
System.out.println("Hello!");
}
}
class Person implements Greeting {
}
public class Main {
public static void main(String[] args) {
Person p = new Person();
p.greet();
}
}
Advanced Recap + Capstone
You now know lambdas, functional interfaces, method references, streams, Optional, threads, synchronization, ExecutorService, Future/Callable, annotations, reflection, generics wildcards, NIO, serialization, the Date-Time API, enums, records, and default methods. Apply them in a capstone that combines streams, records, and concurrency.
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
record Product(String name, double price) {}
public class Main {
public static void main(String[] args) {
List<Product> products = List.of(
new Product("Apple", 1.5),
new Product("Book", 12.0),
new Product("Pen", 2.0)
);
double total = products.stream()
.mapToDouble(Product::price)
.sum();
System.out.printf("Total: %.2f%n", total);
ExecutorService pool = Executors.newSingleThreadExecutor();
pool.submit(() -> System.out.println("Done processing"));
pool.shutdown();
}
}
Java Advanced Projects
Apply streams, concurrency, and advanced features. Click each project to open its full guide.
About this project
Analyze sales records using streams — filter, map, reduce, and collect. Compute total revenue, count high-value sales, and extract product names.
What you'll practice
Step-by-step: How it works
- A
record Saleholds product and amount. - A list of sales is created.
mapToDoubleextracts amounts, andsum()computes total.filterkeeps sales above a threshold.mapextracts product names, andcollectbuilds a list.
import java.util.List;
import java.util.stream.Collectors;
record Sale(String product, double amount) {}
public class SalesAnalyzer {
public static void main(String[] args) {
List sales = List.of(
new Sale("Laptop", 1200),
new Sale("Mouse", 25),
new Sale("Keyboard", 75),
new Sale("Monitor", 300)
);
double total = sales.stream()
.mapToDouble(Sale::amount)
.sum();
List bigSales = sales.stream()
.filter(s -> s.amount() > 100)
.map(Sale::product)
.collect(Collectors.toList());
System.out.printf("Total revenue: $%.2f%n", total);
System.out.println("Big sales: " + bigSales);
}
}
About this project
Execute multiple tasks concurrently using a thread pool and collect their results with Future. Each task computes a different sum.
What you'll practice
Step-by-step: How it works
- A fixed thread pool of 3 threads is created.
- Three Callable tasks are submitted.
- Each task returns a computed sum.
Future.get()retrieves each result.- The pool shuts down after all tasks complete.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ParallelTasks {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(3);
Callable sumTo100 = () -> {
int s = 0;
for (int i = 1; i <= 100; i++) s += i;
return s;
};
Callable sumTo200 = () -> {
int s = 0;
for (int i = 1; i <= 200; i++) s += i;
return s;
};
Callable sumTo300 = () -> {
int s = 0;
for (int i = 1; i <= 300; i++) s += i;
return s;
};
Future f1 = pool.submit(sumTo100);
Future f2 = pool.submit(sumTo200);
Future f3 = pool.submit(sumTo300);
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
pool.shutdown();
}
}
About this project
Read a text file using NIO and analyze it with streams: count lines, count words, and find the longest word.
What you'll practice
Step-by-step: How it works
Files.readStringreads the whole file.lines()streams the lines for counting.split("\\\\s+")splits text into words.max()finds the longest word.- Results are printed.
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Comparator;
public class TextAnalyzer {
public static void main(String[] args) throws Exception {
Path path = Path.of("sample.txt");
Files.writeString(path, "hello world\\njava programming\\n");
String content = Files.readString(path);
long lines = content.lines().count();
String[] words = content.split("\\\\s+");
String longest = Arrays.stream(words)
.max(Comparator.comparingInt(String::length))
.orElse("");
System.out.println("Lines: " + lines);
System.out.println("Words: " + words.length);
System.out.println("Longest word: " + longest);
}
}
Advanced Capstone Project
Combine records, streams, concurrency, and Optional into one complete project.
About this project
Build an employee analytics system using records and streams — compute average salary, find the highest-paid employee, and filter by department using Optional.
What you'll practice
Step-by-step: How it works
- An
Employeerecord stores name, department, and salary. mapToDouble+average()computes average salary.max()finds the highest-paid employee as an Optional.filterselects employees from a department.- Results are printed using Optional's
orElse.
import java.util.List;
import java.util.Optional;
record Employee(String name, String department, double salary) {}
public class EmployeeAnalytics {
public static void main(String[] args) {
List employees = List.of(
new Employee("Ali", "IT", 75000),
new Employee("Sara", "HR", 60000),
new Employee("Zain", "IT", 90000),
new Employee("Mia", "Finance", 80000)
);
double avg = employees.stream()
.mapToDouble(Employee::salary)
.average()
.orElse(0);
Optional highest = employees.stream()
.max((a, b) -> Double.compare(a.salary(), b.salary()));
long itCount = employees.stream()
.filter(e -> e.department().equals("IT"))
.count();
System.out.printf("Average salary: $%.2f%n", avg);
System.out.println("Highest paid: " + highest.map(Employee::name).orElse("None"));
System.out.println("IT employees: " + itCount);
}
}