Modern Java (Java 8 to 21)
Java has evolved significantly over the last decade. If you are still writing anonymous inner classes or checking for nulls with if (x != null), you are living in the past. “Modern Java” is concise, expressive, and functional.
1. Lambda Expressions (Java 8)
Lambdas allow you to treat code as data. You can pass functions as arguments to other functions. This is the foundation of functional programming in Java.
The Old Way vs. The New Way
// Before Java 8: Anonymous Inner Class (Verbose)
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Old way");
}
};
// Java 8+: Lambda Expression (Concise)
Runnable r = () -> System.out.println("New way");
Sorting Example
Lambdas shine when passing behavior, like a comparator for sorting.
List<String> names = Arrays.asList("Bob", "Alice", "Charlie");
// Sort by length
Collections.sort(names, (a, b) -> a.length() - b.length());
Functional Interfaces
A Lambda can be used anywhere a Functional Interface is expected. A Functional Interface is simply an interface with one abstract method.
Predicate<T>: Takes a T, returns boolean. (Used for filtering).
Function<T, R>: Takes a T, returns R. (Used for transformation).
Consumer<T>: Takes a T, returns void. (Used for printing/saving).
Supplier<T>: Takes nothing, returns T. (Used for factories).
2. Stream API (Java 8)
The Stream API allows you to process collections of data in a declarative way. It abstracts away the “how” (loops, iterators) and lets you focus on the “what” (filter, map, reduce).
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
// Goal: Sum the squares of all even numbers
int sum = numbers.stream()
.filter(n -> n % 2 == 0) // Keep evens
.mapToInt(n -> n * n) // Square them
.sum(); // Sum result
System.out.println(sum); // 4 + 16 + 36 = 56
Why use Streams?
- Readability: The code reads like a sentence.
- Parallelism: You can switch to
.parallelStream() to utilize multiple cores effortlessly (use with caution).
- No Side Effects: Encourages writing stateless functions.
3. Optional (Java 8)
NullPointerException is the “billion-dollar mistake”. Optional<T> is a container object which may or may not contain a non-null value. It forces you to think about the case where a value might be missing.
public Optional<String> findUser(int id) {
if (id == 1) return Optional.of("Alice");
return Optional.empty(); // Explicitly return "nothing"
}
// Usage
Optional<String> user = findUser(2);
// Safe handling
user.ifPresent(name -> System.out.println(name));
// Provide a default
String name = user.orElse("Unknown User");
// Throw if missing
String n = user.orElseThrow(() -> new RuntimeException("User not found"));
4. Local Variable Type Inference (Java 10)
Java is known for being verbose. var helps reduce that verbosity without sacrificing type safety. The compiler infers the type from the right-hand side.
// Explicit type
ArrayList<String> list = new ArrayList<String>();
// Inferred type
var list = new ArrayList<String>();
Use var when the type is obvious (e.g., constructors, return values from clearly named methods). Avoid it if it makes the code harder to understand (e.g., var x = getX()).
5. Text Blocks (Java 15)
Writing multi-line strings (like JSON, SQL, or HTML) used to be a nightmare of \n and +. Text Blocks solve this.
// Old way
String json = "{\n" +
" \"name\": \"Alice\",\n" +
" \"age\": 25\n" +
"}";
// Text Block
String json = """
{
"name": "Alice",
"age": 25
}
""";
6. Pattern Matching (Java 16+)
Pattern matching allows you to test an object against a structure and extract data from it in one go.
instanceof
No more casting after checking the type.
Object obj = "Hello";
// Before
if (obj instanceof String) {
String s = (String) obj; // Redundant cast
System.out.println(s.length());
}
// After
if (obj instanceof String s) {
System.out.println(s.length()); // 's' is already a String
}
Switch Pattern Matching (Java 21)
You can switch on types!
String result = switch (obj) {
case Integer i -> "It's an integer: " + i;
case String s -> "It's a string: " + s;
case null -> "It's null";
default -> "Unknown type";
};
Summary
Modern Java is not the verbose, boilerplate-heavy language of the early 2000s.
- Lambdas & Streams: Functional programming style.
- Optional: Null safety.
- Records: Concise data classes.
- Pattern Matching: Expressive control flow.
Embrace these features to write cleaner, safer, and more maintainable code.