Advanced Java Concepts
Explore sophisticated features including generics, multithreading, JVM internals, and design patterns
Advanced Java Concepts
Master advanced Java programming with free flashcards covering generics, reflection, concurrency, streams, and design patterns. This lesson explores the powerful features that distinguish expert Java developers from beginners, including type-safe collections, runtime introspection, parallel processing, and functional programming paradigms essential for building robust enterprise applications.
Welcome to Advanced Java π»
Welcome to the world of advanced Java! If you've mastered the basics of Javaβclasses, objects, inheritance, and simple collectionsβyou're ready to level up. Advanced Java concepts unlock the language's true power, enabling you to write elegant, efficient, and maintainable code that scales to enterprise-level applications.
This lesson covers five critical advanced topics:
- Generics - Type-safe collections and methods
- Reflection API - Runtime class inspection and manipulation
- Concurrency - Multithreading and parallel processing
- Stream API - Functional-style data processing
- Design Patterns - Proven architectural solutions
π€ Did you know? Java's generics were introduced in Java 5 (2004), but the designers had to maintain backward compatibility with existing non-generic code. This led to a fascinating compromise called "type erasure" that we'll explore!
Core Concepts in Depth
1. Generics: Type Safety Without Casting π
Generics allow you to write flexible, reusable code that works with different types while maintaining compile-time type safety. Before generics, collections could hold any Object, requiring dangerous casting.
The Problem Without Generics:
// Old way (pre-Java 5)
ArrayList list = new ArrayList();
list.add("Hello");
list.add(42); // Compiles fine!
String s = (String) list.get(1); // Runtime ClassCastException!
The Solution With Generics:
// Modern way
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
list.add(42); // Compile error! Type safety!
Generic Classes
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
// Usage
Box<Integer> intBox = new Box<>();
intBox.set(123);
Integer value = intBox.get(); // No casting needed!
Generic Methods
You can create generic methods independent of the class:
public class Utils {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
}
}
// Usage
Integer[] intArray = {1, 2, 3};
String[] strArray = {"A", "B", "C"};
Utils.printArray(intArray); // Works!
Utils.printArray(strArray); // Also works!
Bounded Type Parameters
Restrict generic types to specific bounds:
// Upper bound: T must be Number or its subclass
public class Calculator<T extends Number> {
public double add(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
}
Calculator<Integer> intCalc = new Calculator<>(); // OK
Calculator<String> strCalc = new Calculator<>(); // Compile error!
Wildcards
| Wildcard Type | Syntax | Meaning | Use Case |
|---|---|---|---|
| Unbounded | List<?> | List of unknown type | Read-only operations |
| Upper bounded | List<? extends Number> | List of Number or subtypes | Reading from structure |
| Lower bounded | List<? super Integer> | List of Integer or supertypes | Writing to structure |
π§ Mnemonic: PECS - "Producer Extends, Consumer Super"
- Use
extendswhen you're reading (producing values from the structure) - Use
superwhen you're writing (consuming values into the structure)
π‘ Tip: Type erasure means generic type information is removed at runtime. List<String> and List<Integer> become just List in bytecode. This is why you can't do new T[] or instanceof T.
2. Reflection API: Runtime Introspection π
Reflection allows your code to examine and modify classes, methods, fields, and constructors at runtimeβeven private ones!
Why Use Reflection?
- Frameworks (Spring, Hibernate) use it for dependency injection
- Testing tools (JUnit) use it to discover test methods
- Serialization libraries (Jackson, Gson) use it to map objects to JSON
- IDE tools use it for code completion and refactoring
Getting Class Objects
// Three ways to get Class object
Class<?> c1 = String.class; // From class literal
Class<?> c2 = "Hello".getClass(); // From object
Class<?> c3 = Class.forName("java.lang.String"); // From string name
Inspecting Class Members
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void sayHello() {
System.out.println("Hello, I'm " + name);
}
}
// Reflection in action
Class<?> personClass = Person.class;
// Get all fields (including private)
Field[] fields = personClass.getDeclaredFields();
for (Field f : fields) {
System.out.println(f.getName() + ": " + f.getType());
}
// Get all methods
Method[] methods = personClass.getDeclaredMethods();
for (Method m : methods) {
System.out.println(m.getName());
}
Modifying Private Fields
Person person = new Person("John", 25);
Field nameField = Person.class.getDeclaredField("name");
nameField.setAccessible(true); // Bypass private access!
nameField.set(person, "Jane");
System.out.println(nameField.get(person)); // "Jane"
Invoking Methods Dynamically
Person person = new Person("Alice", 30);
Method sayHello = Person.class.getDeclaredMethod("sayHello");
sayHello.invoke(person); // Calls person.sayHello()
β οΈ Common Mistake: Forgetting to call setAccessible(true) when accessing private members. Without it, you'll get IllegalAccessException.
π‘ Performance Tip: Reflection is significantly slower than direct method calls. Cache reflected Method and Field objects if you need to use them repeatedly.
3. Concurrency: Parallel Processing β‘
Java's concurrency utilities enable you to write programs that do multiple things simultaneously, leveraging multi-core processors.
Threads: The Traditional Way
// Method 1: Extend Thread
class MyThread extends Thread {
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
}
}
MyThread t = new MyThread();
t.start(); // Don't call run() directly!
// Method 2: Implement Runnable (preferred)
Runnable task = () -> {
System.out.println("Task running: " + Thread.currentThread().getName());
};
Thread t2 = new Thread(task);
t2.start();
ExecutorService: Modern Thread Management
import java.util.concurrent.*;
// Create thread pool with 4 threads
ExecutorService executor = Executors.newFixedThreadPool(4);
// Submit tasks
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " on " + Thread.currentThread().getName());
Thread.sleep(1000);
});
}
executor.shutdown(); // Stop accepting new tasks
executor.awaitTermination(1, TimeUnit.MINUTES); // Wait for completion
| Executor Type | Description | Use Case |
|---|---|---|
newFixedThreadPool(n) | Fixed number of threads | CPU-intensive tasks |
newCachedThreadPool() | Creates threads as needed | Many short-lived tasks |
newSingleThreadExecutor() | One thread, sequential execution | Order matters |
newScheduledThreadPool(n) | Schedule tasks with delays | Periodic tasks |
Synchronization: Avoiding Race Conditions
public class Counter {
private int count = 0;
// Without synchronized: race condition!
public void increment() {
count++; // Not atomic! (read, add, write)
}
// With synchronized: thread-safe
public synchronized void safeIncrement() {
count++;
}
// Alternative: synchronized block
public void blockIncrement() {
synchronized(this) {
count++;
}
}
}
Modern Concurrency: CompletableFuture
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Runs in separate thread
Thread.sleep(1000);
return "Result from background task";
});
// Chain operations
future.thenApply(result -> result.toUpperCase())
.thenAccept(result -> System.out.println(result))
.exceptionally(ex -> {
System.out.println("Error: " + ex.getMessage());
return null;
});
// Block and get result
String result = future.get(); // Waits for completion
π§ Mnemonic: DAVE for thread lifecycle:
- Declared (new Thread())
- Alive (start())
- Visible (running)
- Ended (dead)
β οΈ Critical Mistake: Calling thread.run() instead of thread.start(). The former runs the code in the current thread (no parallelism!), while the latter creates a new thread.
4. Stream API: Functional Data Processing π
Introduced in Java 8, Streams provide a declarative way to process collections using functional programming principles.
Creating Streams
// From collections
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream1 = list.stream();
// From arrays
String[] array = {"x", "y", "z"};
Stream<String> stream2 = Arrays.stream(array);
// Static methods
Stream<Integer> stream3 = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> stream4 = Stream.iterate(0, n -> n + 2).limit(10);
Intermediate Operations (Lazy)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Stream<Integer> stream = numbers.stream()
.filter(n -> n % 2 == 0) // Keep only even numbers
.map(n -> n * n) // Square each number
.distinct() // Remove duplicates
.sorted() // Sort ascending
.limit(5); // Take first 5
// Nothing happens yet! Stream is lazy.
Terminal Operations (Trigger Execution)
// Collect to list
List<Integer> result = numbers.stream()
.filter(n -> n > 5)
.collect(Collectors.toList());
// Count
long count = numbers.stream()
.filter(n -> n % 2 == 0)
.count();
// Sum
int sum = numbers.stream()
.mapToInt(Integer::intValue)
.sum();
// Find any
Optional<Integer> any = numbers.stream()
.filter(n -> n > 7)
.findAny();
// For each
numbers.stream()
.forEach(System.out::println);
Common Stream Patterns
| Operation | Purpose | Example |
|---|---|---|
filter | Select elements | filter(x -> x > 10) |
map | Transform elements | map(String::toUpperCase) |
flatMap | Flatten nested streams | flatMap(List::stream) |
reduce | Combine to single value | reduce(0, Integer::sum) |
collect | Convert to collection | collect(Collectors.toList()) |
Parallel Streams
// Sequential
long count1 = numbers.stream()
.filter(n -> n % 2 == 0)
.count();
// Parallel (uses multiple threads automatically)
long count2 = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.count();
π‘ When to use parallel streams:
- Large datasets (thousands+ elements)
- CPU-intensive operations
- Stateless operations (no shared mutable state)
β οΈ Don't use parallel streams if:
- Dataset is small (overhead > benefit)
- Operations are fast
- Order matters and you need sequential processing
π Real-world analogy: Think of streams like an assembly line. Intermediate operations are workers who pass items along, but nothing happens until the final terminal operation (the packaging station) starts the whole line moving.
5. Design Patterns: Proven Solutions ποΈ
Design patterns are reusable solutions to common software design problems. Here are three essential patterns every Java developer should know:
Singleton Pattern
Ensures only one instance of a class exists:
public class DatabaseConnection {
private static DatabaseConnection instance;
private Connection conn;
// Private constructor prevents instantiation
private DatabaseConnection() {
// Initialize connection
conn = DriverManager.getConnection("jdbc:...");
}
// Thread-safe singleton
public static synchronized DatabaseConnection getInstance() {
if (instance == null) {
instance = new DatabaseConnection();
}
return instance;
}
public Connection getConnection() {
return conn;
}
}
// Usage
DatabaseConnection db1 = DatabaseConnection.getInstance();
DatabaseConnection db2 = DatabaseConnection.getInstance();
// db1 == db2 (same instance!)
Factory Pattern
Creates objects without exposing creation logic:
interface Vehicle {
void drive();
}
class Car implements Vehicle {
public void drive() {
System.out.println("Driving car");
}
}
class Bike implements Vehicle {
public void drive() {
System.out.println("Riding bike");
}
}
class VehicleFactory {
public static Vehicle createVehicle(String type) {
switch(type.toLowerCase()) {
case "car": return new Car();
case "bike": return new Bike();
default: throw new IllegalArgumentException("Unknown vehicle type");
}
}
}
// Usage
Vehicle v1 = VehicleFactory.createVehicle("car");
v1.drive(); // "Driving car"
Observer Pattern
One-to-many dependency where observers are notified of state changes:
import java.util.*;
interface Observer {
void update(String message);
}
class NewsAgency {
private List<Observer> observers = new ArrayList<>();
private String news;
public void addObserver(Observer observer) {
observers.add(observer);
}
public void setNews(String news) {
this.news = news;
notifyObservers();
}
private void notifyObservers() {
for (Observer observer : observers) {
observer.update(news);
}
}
}
class NewsChannel implements Observer {
private String name;
public NewsChannel(String name) {
this.name = name;
}
public void update(String news) {
System.out.println(name + " received: " + news);
}
}
// Usage
NewsAgency agency = new NewsAgency();
NewsChannel cnn = new NewsChannel("CNN");
NewsChannel bbc = new NewsChannel("BBC");
agency.addObserver(cnn);
agency.addObserver(bbc);
agency.setNews("Breaking news!"); // Both channels notified
Design Pattern Selection Guide βββββββββββββββββββββββββββββββββββββββββββββββ β Need single instance? β β β β β ββYESββ SINGLETON β β β β Need to create objects flexibly? β β β β β ββYESββ FACTORY β β β β Need to notify multiple objects? β β β β β ββYESββ OBSERVER β β β β Need to add behavior dynamically? β β β β β ββYESββ DECORATOR β βββββββββββββββββββββββββββββββββββββββββββββββ
Practical Examples with Explanations
Example 1: Building a Type-Safe Generic Repository
import java.util.*;
public class Repository<T> {
private Map<Long, T> storage = new HashMap<>();
private Long currentId = 1L;
public Long save(T entity) {
Long id = currentId++;
storage.put(id, entity);
return id;
}
public Optional<T> findById(Long id) {
return Optional.ofNullable(storage.get(id));
}
public List<T> findAll() {
return new ArrayList<>(storage.values());
}
public void delete(Long id) {
storage.remove(id);
}
}
// Usage with different types
class User {
String name;
User(String name) { this.name = name; }
}
class Product {
String sku;
double price;
Product(String sku, double price) {
this.sku = sku;
this.price = price;
}
}
// Type-safe repositories
Repository<User> userRepo = new Repository<>();
Repository<Product> productRepo = new Repository<>();
Long userId = userRepo.save(new User("Alice"));
Long productId = productRepo.save(new Product("SKU123", 29.99));
Optional<User> user = userRepo.findById(userId);
Optional<Product> product = productRepo.findById(productId);
Why this works: Generics enable us to write a single Repository class that works with any type while maintaining type safety. No casting required, and the compiler catches type errors.
Example 2: Reflection-Based Dependency Injection
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Inject {}
class Container {
private Map<Class<?>, Object> instances = new HashMap<>();
public <T> void register(Class<T> type, T instance) {
instances.put(type, instance);
}
public <T> T resolve(Class<T> type) throws Exception {
// Create new instance
T instance = type.getDeclaredConstructor().newInstance();
// Inject dependencies
for (Field field : type.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class)) {
field.setAccessible(true);
Object dependency = instances.get(field.getType());
if (dependency != null) {
field.set(instance, dependency);
}
}
}
return instance;
}
}
// Services
class Logger {
public void log(String msg) {
System.out.println("LOG: " + msg);
}
}
class UserService {
@Inject
private Logger logger;
public void createUser(String name) {
logger.log("Creating user: " + name);
}
}
// Usage
Container container = new Container();
container.register(Logger.class, new Logger());
UserService service = container.resolve(UserService.class);
service.createUser("Bob"); // Logger is automatically injected!
Key insight: Reflection allows frameworks like Spring to examine your classes and automatically wire dependencies at runtime, eliminating boilerplate configuration code.
Example 3: Concurrent Data Processing Pipeline
import java.util.concurrent.*;
import java.util.stream.*;
import java.util.*;
public class DataPipeline {
private ExecutorService executor = Executors.newFixedThreadPool(4);
public void processLargeDataset(List<String> urls) throws Exception {
// Create tasks for each URL
List<CompletableFuture<String>> futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> {
// Simulate downloading data
try {
Thread.sleep(1000);
return "Data from " + url;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, executor))
.collect(Collectors.toList());
// Wait for all to complete
CompletableFuture<Void> allDone = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
// Process results
allDone.thenRun(() -> {
List<String> results = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
System.out.println("Processed " + results.size() + " items");
results.forEach(System.out::println);
}).get();
executor.shutdown();
}
public static void main(String[] args) throws Exception {
List<String> urls = Arrays.asList(
"http://api1.com", "http://api2.com",
"http://api3.com", "http://api4.com"
);
new DataPipeline().processLargeDataset(urls);
}
}
Performance benefit: This downloads data from 4 URLs concurrently instead of sequentially, reducing total time from 4 seconds to ~1 second.
Example 4: Stream API for Complex Data Transformation
import java.util.*;
import java.util.stream.*;
class Employee {
String name;
String department;
double salary;
Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
}
public class StreamExample {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("Alice", "Engineering", 85000),
new Employee("Bob", "Engineering", 75000),
new Employee("Charlie", "Sales", 65000),
new Employee("Diana", "Sales", 70000),
new Employee("Eve", "HR", 60000)
);
// Find average salary by department
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
avgSalaryByDept.forEach((dept, avg) ->
System.out.println(dept + ": $" + avg)
);
// Get top 3 earners
List<String> topEarners = employees.stream()
.sorted(Comparator.comparing(Employee::getSalary).reversed())
.limit(3)
.map(Employee::getName)
.collect(Collectors.toList());
System.out.println("Top earners: " + topEarners);
// Total salary for Engineering department
double totalEngSalary = employees.stream()
.filter(e -> e.getDepartment().equals("Engineering"))
.mapToDouble(Employee::getSalary)
.sum();
System.out.println("Total Engineering salary: $" + totalEngSalary);
}
}
Output:
Engineering: $80000.0
Sales: $67500.0
HR: $60000.0
Top earners: [Alice, Bob, Diana]
Total Engineering salary: $160000.0
Elegance factor: Notice how we expressed complex business logic (grouping, filtering, sorting, aggregating) in just a few readable lines without explicit loops or conditionals.
Common Mistakes to Avoid β οΈ
1. Misusing Raw Types with Generics
// β WRONG: Raw type loses type safety
List list = new ArrayList();
list.add("String");
list.add(123);
Integer num = (Integer) list.get(0); // ClassCastException!
// β
RIGHT: Use proper generic type
List<String> list = new ArrayList<>();
list.add("String");
// list.add(123); // Compile error - caught early!
2. Modifying Collections While Iterating
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
// β WRONG: ConcurrentModificationException
for (Integer n : numbers) {
if (n % 2 == 0) {
numbers.remove(n);
}
}
// β
RIGHT: Use iterator.remove() or streams
numbers.removeIf(n -> n % 2 == 0);
// Or with iterator
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
if (it.next() % 2 == 0) {
it.remove();
}
}
3. Forgetting to Start Threads
Runnable task = () -> System.out.println("Task running");
Thread thread = new Thread(task);
// β WRONG: Calls run() in current thread
thread.run();
// β
RIGHT: Creates new thread
thread.start();
4. Stream Reuse
Stream<String> stream = Stream.of("a", "b", "c");
// β WRONG: Stream already operated upon
stream.forEach(System.out::println);
stream.count(); // IllegalStateException!
// β
RIGHT: Create new stream for each operation
List<String> list = Arrays.asList("a", "b", "c");
list.stream().forEach(System.out::println);
list.stream().count();
5. Ignoring Thread Safety
public class Counter {
private int count = 0;
// β WRONG: Not thread-safe
public void increment() {
count++; // Read-modify-write (3 operations)
}
// β
RIGHT: Use atomic or synchronization
private AtomicInteger atomicCount = new AtomicInteger(0);
public void safeIncrement() {
atomicCount.incrementAndGet();
}
}
6. Overusing Reflection
// β WRONG: Using reflection for every call
for (int i = 0; i < 10000; i++) {
Method m = obj.getClass().getMethod("getValue");
m.invoke(obj);
}
// β
RIGHT: Cache reflection objects
Method m = obj.getClass().getMethod("getValue");
for (int i = 0; i < 10000; i++) {
m.invoke(obj);
}
// β
BEST: Use direct call when possible
for (int i = 0; i < 10000; i++) {
obj.getValue();
}
Key Takeaways π―
β
Generics provide compile-time type safety and eliminate casting. Use bounded types (extends) for flexibility and wildcards (?) for reading/writing scenarios.
β Reflection enables runtime introspection and modification but is slower than direct access. Cache reflected objects and use sparingly.
β
Concurrency leverages multi-core processors. Use ExecutorService for thread pools and CompletableFuture for asynchronous pipelines.
β Streams enable declarative, functional-style data processing. Remember: intermediate operations are lazy, terminal operations trigger execution.
β Design Patterns solve recurring problems. Learn Singleton (single instance), Factory (object creation), and Observer (event notification) patterns.
β
Thread Safety matters in concurrent code. Use synchronized, atomic classes, or immutable objects to avoid race conditions.
β Performance considerations: Reflection is slow, parallel streams have overhead, and streams aren't always faster than loops.
π§ Try this: Take an existing project and refactor one class to use generics, convert one loop to streams, and add one design pattern. You'll immediately see the benefits!
π Further Study
- Oracle Java Tutorials - Generics: https://docs.oracle.com/javase/tutorial/java/generics/index.html
- Java Concurrency in Practice (book summary): https://www.baeldung.com/java-concurrency
- Stream API Guide: https://www.baeldung.com/java-8-streams
π Quick Reference Card
| Generics | class Box<T> - Type parameter for flexibility |
| Bounded Types | <T extends Number> - Restrict to subclasses |
| Wildcards | List<?>, <? extends T>, <? super T> |
| Reflection | Class.forName(), getDeclaredFields(), setAccessible(true) |
| Thread Creation | Thread t = new Thread(runnable); t.start(); |
| ExecutorService | Executors.newFixedThreadPool(n) |
| Synchronization | synchronized keyword or synchronized(lock) {} |
| CompletableFuture | CompletableFuture.supplyAsync(() -> ...) |
| Stream Creation | list.stream(), Stream.of(...) |
| Intermediate Ops | filter(), map(), sorted() - lazy |
| Terminal Ops | collect(), forEach(), count() - trigger execution |
| Singleton Pattern | Private constructor + static getInstance() method |
| Factory Pattern | Static method returns interface type based on input |
| Observer Pattern | Subject notifies observers of state changes |