Released · improving
Java guide · 6/6
This chapter covers the tools modern Java code leans on every day: lambdas, streams, Optional, and the basics of running work concurrently.
An interface with exactly one abstract method is a functional interface, and a lambda is a compact way to implement one. When an existing method already does the job, pass a method reference such as Integer::parseInt instead. The java.util.function package defines the common shapes:
| Interface | Method | Typical use |
|---|---|---|
Function<T, R> | R apply(T t) | Transform a value |
Predicate<T> | boolean test(T t) | Test a condition |
Supplier<T> | T get() | Produce a value on demand |
Consumer<T> | void accept(T t) | Act on a value |
import java.util.function.*;
Function<String, Integer> length = s -> s.length();
Predicate<String> isEmpty = String::isEmpty;
Supplier<java.util.List<String>> newList = java.util.ArrayList::new;
Consumer<String> printer = System.out::println;
Predicate<String> notEmpty = isEmpty.negate();
Function<String, Integer> lengthPlusOne = length.andThen(n -> n + 1);
printer.accept("len = " + lengthPlusOne.apply("java")); // len = 5Lambdas can capture only local variables that are never reassigned (effectively final). Annotate your own functional interfaces with @FunctionalInterface so the compiler checks them.
A stream is a pipeline: take a source such as a collection, chain intermediate operations like filter, map, and sorted, then finish with a terminal operation like toList, count, or collect. Nothing runs until the terminal operation, and the source is left untouched.
import java.util.*;
import java.util.stream.*;
record Employee(String name, String team, int salary) {}
List<Employee> staff = List.of(
new Employee("Kim", "dev", 5200),
new Employee("Lee", "dev", 6100),
new Employee("Park", "ops", 4800));
List<String> highPaid = staff.stream()
.filter(e -> e.salary() >= 5000)
.map(Employee::name)
.sorted()
.toList(); // [Kim, Lee]
Map<String, Double> avgByTeam = staff.stream()
.collect(Collectors.groupingBy(Employee::team,
Collectors.averagingInt(Employee::salary)));
int total = staff.stream().mapToInt(Employee::salary).sum();Primitive streams like IntStream avoid boxing when you sum or average numbers. A stream can be consumed only once, and a plain for loop is still the clearer choice for simple iteration or early exits.
Optional<T> puts the possibility of "no value" into the type. It is meant mostly for return values, so callers are pushed to handle the empty case. Prefer map, orElse, orElseThrow, and ifPresent over calling get() blindly.
import java.util.Optional;
static Optional<Employee> findByName(List<Employee> list, String name) {
return list.stream()
.filter(e -> e.name().equals(name))
.findFirst();
}
String team = findByName(staff, "Lee")
.map(Employee::team)
.orElse("unknown"); // dev
findByName(staff, "Choi").ifPresentOrElse(
e -> System.out.println(e.team()),
() -> System.out.println("not found"));
Employee lee = findByName(staff, "Lee")
.orElseThrow(() -> new IllegalStateException("Lee missing"));Avoid Optional for fields, parameters, and collection elements; an empty collection already means "nothing here".
Java has had threads since the beginning, but you rarely create them yourself. Submit tasks to an ExecutorService instead: submit returns a Future, and get() blocks until its result is ready.
import java.util.*;
import java.util.concurrent.*;
static int slowSquare(int n) throws InterruptedException {
Thread.sleep(100); // simulate slow work
return n * n;
}
public static void main(String[] args) throws Exception {
try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
List<Future<Integer>> futures = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
int n = i;
futures.add(pool.submit(() -> slowSquare(n)));
}
int sum = 0;
for (Future<Integer> f : futures) sum += f.get();
System.out.println("sum = " + sum); // 55
} // close() waits for running tasks, then shuts down
}Recent JDKs add virtual threads: lightweight threads scheduled by the JVM, cheap enough to use one per task. They suit servers that mostly wait on I/O, and the code stays ordinary blocking code; only the executor changes.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int id = i;
executor.submit(() -> {
Thread.sleep(50); // blocking is cheap here
return id;
});
}
}When threads share mutable data, use thread-safe types such as ConcurrentHashMap and AtomicInteger, or guard access with synchronized. Sharing immutable records avoids most such problems.
filter, map, and collect.Optional marks a possibly missing return value; unwrap it with map or orElse.ExecutorService, and consider virtual threads for I/O-heavy workloads.Official resources for going further:
0 comments
Sign in · Sign in to leave a comment.
Be the first to comment.