Skip to main content
Java 8 (LTS)
  1. Posts/

Java 8 (LTS)

·2327 words·11 mins·
Roman
Author
Roman
Photographer with MSci in Computer Science and a Home Lab obsession
Table of Contents

Lambda
#

An anonymous function that can be stored in a variable or passed as an argument. Syntax: parameters -> body.

x -> x - 1                  // single param, expression body
(x, y) -> x - y             // multiple parameters
(x, y) -> { return x - y; } // block body
// Passed as an argument to forEach
List<Integer> nums = Arrays.asList(42, 300, 90000);
nums.forEach(n -> System.out.println(n));

Functional Interfaces
#

java.util.function


Consumer<T>
#

docs

Accepts a single input and returns nothing. The lambda body becomes the implementation of accept.

Modifier Return Type Method Description
void accept(T t) executes the consumer on the argument
default Consumer<T> andThen(Consumer<? super T> after) Chains two consumers in sequence, first this then after
// accept - executing a single consumer
Consumer<String> print = s -> System.out.println(s);
print.accept("Hello"); // Hello
// passing a consumer to forEach
Consumer<String> printConsumer = t -> System.out.println(t);
Stream<String> cities = Stream.of(
        "London", "New York", "Mexico City");
cities.forEach(printConsumer);
// London, New York, Mexico City
// andThen - chaining consumers
List<String> cities = new ArrayList<>(
        Arrays.asList("London", "New York", "Mexico City")
        );
Consumer<List<String>> upperCaseConsumer = list -> list
        .replaceAll(String::toUpperCase);

Consumer<List<String>> printConsumer = list -> list
        .forEach(System.out::println);

upperCaseConsumer.andThen(printConsumer).accept(cities);
// LONDON, NEW YORK, MEXICO CITY

Supplier<T>
#

docs

Takes no input and produces a value. The lambda body becomes the implementation of get, executed lazily, only when called.

Modifier Return Type Method Description
T get() returns the supplied value
// computation only runs when get() is called
Supplier<String> costly = () -> {
    System.out.println("Computing...");
    return "result";
};

System.out.println("Before get()");
System.out.println(costly.get());
// runs here
System.out.println("After get()");
// Before get()
// Computing...
// result
// After get()
// orElseGet takes a Supplier
// fallback only computed if the Optional is empty
Optional<Double> opt = Optional.empty();
opt.orElseGet(() -> Math.random());
// get() called here, returns random value

Optional<Double> present = Optional.of(3.14);
present.orElseGet(() -> Math.random());
// suppliers get() never called

Predicate<T>
#

docs

Takes one argument and returns a boolean. The lambda body becomes the implementation of test.

Modifier Return Type Method Description
boolean test(T t) returns true/false for the given argument
default Predicate<T> and(Predicate<? super T> other) Both must match
default Predicate<T> or(Predicate<? super T> other) Either must match
default Predicate<T> negate() Inverts the result
static <T> Predicate<T> isEqual(Object targetRef) Matches by equality Objects.equals(Object, Object)
List<String> names = Arrays.asList("Roman", "Scott", "Alex");
Predicate<String> startsWithS = str -> str.startsWith("S");
Predicate<String> longerThan4 = str -> str.length() > 4;

names.stream()
        .filter(startsWithS.and(longerThan4))
        .forEach(System.out::println);
// Scott

Function<T, R>
#

docs

Takes one argument of type T and returns a result of type R. The lambda body becomes the implementation of apply.

Modifier Return Type Method Description
R apply(T t) Transforms the input into the output
default <V> Function<V,R> compose(Function<? super V,? extends T> before) Runs before first, then this
default <V> Function<T,V> andThen(Function<? super R,? extends V> after) Runs this first, then after
static <T> Function<T,T> identity() Returns a function that always returns its input, equivalent to t -> t
// apply - transform each element via stream.map
List<String> names = Arrays.asList("Roman", "Scott", "Alex");
List<Integer> lengths = names.stream()
        .map(String::length) // Function<String, Integer>
        .collect(Collectors.toList());
// [5, 5, 4]
// identity()
// when an API requires a Function but no transformation
List<String> names = Arrays.asList("Roman", "Scott", "Alex");
Map<String, Integer> nameLengths = names.stream()
        .collect(Collectors
                .toMap(Function.identity(), String::length));
// {Roman=5, Scott=5, Alex=4}
// passing a Function as a method parameter
static String format(
        String str, Function<String, String> fn) {
        // Function<Input, Output>
    return fn.apply(str);
}

format("Hello", s -> s + "!"); // "Hello!"
format("Hello", s -> s + "?"); // "Hello?"
// defining your own functional interface
//  SAM can be named anything meaningful
@FunctionalInterface
interface Validator<T> {
    boolean validate(T value);
    // SAM - lambda becomes the implementation of this
}

static void printIfValid(
        String s, Validator<String> validator) {
    if (validator.validate(s)) System.out.println(s);
    // calls the lambda here
}

// lambda s -> s.contains("@") becomes the implementation of validate()
printIfValid("[email protected]", s -> s.contains("@")); 
// [email protected]
printIfValid("notanemail", s -> s.contains("@"));
// (nothing printed)

Streams
#

Stream

Stream flow

A new API for processing collections of data in a declarative way. Consists of a source, followed by zero or more intermediate operations;and a terminal operation. Streams support lazy evaluation, parallel execution, and functional operations such as map, filter, reduce, and collect.

  • Stream is not a data structure and it never modifies the underlying data source.

Stream Creation
#

// Array
private static Employee[] arrayOfEmps = {
        new Employee(1, "Jeff Bezos", 100000.0),
        new Employee(2, "Bill Gates", 200000.0),
        new Employee(3, "Mark Zuckerberg", 300000.0)
};
Stream.of(arrayOfEmps);
// or obtain stream from already existing list
List<Employee> empList = Arrays.asList(arrayOfEmps);
empList.stream();

Java 8 added a new stream() method to the Collection interface

// Create a stream out of individual objects
Stream.of(arrayOfEmps[0], arrayOfEmps[1], arrayOfEmps[2]);
// Or using a Stream.builder()
Stream.Builder<Employee> empStreamBuilder =
        Stream.builder();

empStreamBuilder.accept(arrayOfEmps[0]);
empStreamBuilder.accept(arrayOfEmps[1]);
empStreamBuilder.accept(arrayOfEmps[2]);

Stream<Employee> empStream = empStreamBuilder.build();

Intermediate Operators
#

Return Type Method Description Docs Example
Stream<R> map(Function<T,R>) Transforms each element docs
Stream<T> filter(Predicate<T>) Keeps elements matching the predicate docs
Stream<R> flatMap(Function<T,Stream<R>>) Flattens nested streams into one docs
Stream<T> peek(Consumer<T>) Inspects elements without consuming, debug only docs
Stream<T> distinct() Removes duplicates using equals() docs
Stream<T> limit(long n) Truncates to at most n elements docs
Stream<T> skip(long n) Skips the first n elements docs
Stream<T> sorted() / sorted(Comparator<T>) Sorts elements docs

map
#

List<Integer> lengths = Stream.of("Alice", "Bob", "Charlie")
        .map(String::length)
        .collect(Collectors.toList());
// [5, 3, 7]

filter
#

List<String> result = Stream.of("Alice", "Bob", "Charlie")
        .filter(s -> s.length() > 3)
        .collect(Collectors.toList());
// [Alice, Charlie]

flatMap
#

List<List<String>> namesNested = Arrays.asList(
        Arrays.asList("Jeff", "Bezos"),
        Arrays.asList("Bill", "Gates"),
        Arrays.asList("Mark", "Zuckerberg"));

List<String> flat = namesNested.stream()
        .flatMap(Collection::stream)
        .collect(Collectors.toList());
// [Jeff, Bezos, Bill, Gates, Mark, Zuckerberg]

peek
#

Similar to forEach(), but unlike it it’s not terminal. Returns a new stream which can be used further.

Stream.of("Alice", "Bob")
        .peek(s -> System.out.println("before: " + s))
        .map(String::toUpperCase)
        .peek(s -> System.out.println("after: " + s))
        .collect(Collectors.toList());
// before: Alice, after: ALICE, before: Bob, after: BOB

peek should only be used for debugging. Do not use for logic or side effects - it may not be called in all cases.

distinct
#

List<Integer> result = Stream.of(2, 5, 3, 2, 4, 3)
        .distinct()
        .collect(Collectors.toList());
// [2, 5, 3, 4]

limit
#

// infinite: 1, 2, 3, ...
List<Integer> result = Stream.iterate(1, i -> i + 1)
        .skip(3)
        .limit(5)
        .collect(Collectors.toList());
// [4, 5, 6, 7, 8]

skip #

IntStream.range(1, 10)
        .skip(5)
        .forEach(System.out::println);
// 6, 7, 8, 9

sorted
#

// Natural order
Stream.of("banana", "apple", "cherry")
        .sorted()
        .collect(Collectors.toList());
// [apple, banana, cherry]

// Reverse order
Stream.of("banana", "apple", "cherry")
        .sorted(Comparator.reverseOrder())
        .collect(Collectors.toList());
// [cherry, banana, apple]

// Custom comparator
// sort by string length instead of alphabetically
Stream.of("banana", "apple", "kiwi", "cherry")
        .sorted(Comparator.comparingInt(String::length))
        .collect(Collectors.toList());
// [kiwi, apple, banana, cherry]

Terminal Operations
#

Return Type Method Description Docs Example
void forEach(Consumer<T>) Performs action on each element docs
R collect(Collector<T,A,R>) Gathers elements into a collection docs
Object[] toArray() Returns array of elements docs
Optional<T> findFirst() Returns first element as Optional docs
boolean anyMatch(Predicate<T>) True if any element matches docs
boolean allMatch(Predicate<T>) True if all elements match docs
boolean noneMatch(Predicate<T>) True if no element matches docs
long count() Number of elements docs
T reduce(T, BinaryOperator<T>) Aggregates elements into one value docs
int sum() Sum of all elements (IntStream) docs
OptionalInt max() Maximum element (IntStream) docs
OptionalInt min() Minimum element (IntStream) docs
OptionalDouble average() Average of elements (IntStream) docs
IntSummaryStatistics summaryStatistics() All stats: count, sum, min, max, avg (IntStream) docs

forEach
#

Stream.of("Alice", "Bob", "Charlie")
        .forEach(System.out::println);
// Alice, Bob, Charlie

It’s a terminal operation**: after the operation is performed, the stream pipeline is considered consumed and can no longer be used

collect
#

List<String> list = Stream.of("Alice", "Bob")
        .collect(Collectors.toList());
Set<String> set = Stream.of("Alice", "Bob")
        .collect(Collectors.toSet());

toArray
#

String[] arr = Stream.of("Alice", "Bob")
        .toArray(String[]::new);

findFirst
#

Optional<String> first = Stream.of("Alice", "Bob", "Charlie")
        .filter(s -> s.startsWith("B"))
        .findFirst();
// Optional[Bob]

anyMatch allMatch noneMatch
#

List<Integer> intList = Arrays.asList(2, 4, 5, 6, 8);
intList
        .stream()
        .anyMatch(i -> i % 2 == 0);  
        // true  - at least one even
intList
        .stream()
        .allMatch(i -> i % 2 == 0); 
         // false - not all even
intList
        .stream()
        .noneMatch(i -> i % 3 == 0); 
        // false - 6 is divisible by 3

count
#

long count = IntStream.range(1, 10)
        .filter(x -> x <= 2)
        .count(); // 2

sum
#

int total = IntStream.of(1, 2, 3, 4, 5).sum(); // 15

max min
#

IntStream.of(3, 1, 4, 1, 5).max().getAsInt(); // 5
IntStream.of(3, 1, 4, 1, 5).min().getAsInt(); // 1

average
#

IntStream.of(1, 2, 3, 4, 5).average().getAsDouble(); // 3.0

reduce
#

double total = Stream.of(7.3, 1.5, 4.8)
        .reduce(0.0, (a, b) -> a + b); // identity, accumulator
// 13.6

summaryStatistics
#

IntSummaryStatistics summary =
        IntStream.of(7, 2, 19, 88, 73, 4, 10)
                .summaryStatistics();
// {count=7, sum=203, min=2, average=29.000, max=88}

Collectors
#

Collectors

Return Type Method Description Docs Example
Collector toList() Collects to a List docs
Collector toSet() Collects to a Set docs
Collector toCollection(Supplier) Collects to a specific collection type docs
Collector joining(CharSequence) Joins string elements docs
Collector partitioningBy(Predicate<T>) Splits into {true, false} map docs
Collector groupingBy(Function<T,K>) Groups by a classifier docs
Collector mapping(Function<T,U>, Collector) Transforms before collecting docs
Collector reducing(BinaryOperator<T>) Aggregates via a binary operator docs

toSet
#

Set<String> set = Stream.of("Alice", "Bob")
        .collect(Collectors.toSet());

toCollection
#

Vector<String> vector = Stream.of("Alice", "Bob")
        .collect(Collectors.toCollection(Vector::new));

joining
#

String result = Stream.of("Alice", "Bob", "Charlie")
        .collect(Collectors.joining(", "));
// "Alice, Bob, Charlie"

partitioningBy
#

List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
Map<Boolean, List<Integer>> evenOdd = nums.stream()
        .collect(Collectors.partitioningBy(
                n -> n % 2 == 0));
// {false=[1, 3, 5], true=[2, 4, 6]}

groupingBy
#

List<String> words = Arrays.asList(
        "hi", "hello", "hey", "world", "wow");
Map<Character, List<String>> byLetter = words.stream()
        .collect(Collectors.groupingBy(
                w -> w.charAt(0)));
// {h=[hi, hello, hey], w=[world, wow]}

mapping
#

Map<Character, List<Integer>> byLetterLength = words.stream()
        .collect(Collectors.groupingBy(
                w -> w.charAt(0),
                Collectors.mapping(
                        String::length, Collectors.toList())
        ));
// {h=[2, 5, 3], w=[5, 3]}

reducing
#

List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
Optional<Integer> product = nums.stream()
        .collect(Collectors.reducing((a, b) -> a * b));
// Optional[720]  (1*2*3*4*5*6)

Stream Pipeline
#

A pipeline has three parts: source → intermediate ops → terminal op.

  • Intermediate operations are lazy, they return a new stream and don’t execute until a terminal op is called
  • Terminal operations consume the stream, it cannot be reused after
// filter is lazy, count triggers execution
long count = Stream.of("Alice", "Bob", "Charlie")
        .filter(s -> s.startsWith("A"))
        .count(); // 1

Short-circuit operations allow infinite streams to complete in finite time:

Stream.iterate(2, i -> i * 2) // infinite: 2, 4, 8, 16, ...
        .skip(3)
        .limit(5)
        .collect(Collectors.toList());
// [16, 32, 64, 128, 256]

Lazy Evaluation
#

Defining intermediate operations doesn’t execute them, they describe what to do, not when. Execution is deferred until a terminal operation triggers it.

// Pipeline defined - nothing runs yet
Stream<String> pipeline = Stream.of("Alice", "Bob", "Charlie")
        .filter(s -> s.length() > 3)
        .map(String::toUpperCase);

// Execution happens here when the terminal op is called
pipeline.forEach(System.out::println);
// ALICE, CHARLIE

Method References
#

Shorthand for a lambda that just calls an existing method. Syntax: Target::method.

Static Method
#

// Math::abs is shorthand for x -> Math.abs(x)
Function<Double, Double> abs = Math::abs;
abs.apply(-3.14); // 3.14

Instance Method
#

// Method on a specific instance
String prefix = "Hello, ";
Function<String, String> greeter = prefix::concat;
greeter.apply("Alice"); // "Hello, Alice"

// Method on an arbitrary instance
List<String> names = Arrays.asList("charlie", "alice", "bob");
names.stream()
        .map(String::toUpperCase)
        .forEach(System.out::println);
// CHARLIE, ALICE, BOB

Constructor
#

Supplier<ArrayList<String>> listFactory = ArrayList::new;
listFactory.get(); // new ArrayList<>()

Function<String, StringBuilder> sb = StringBuilder::new;
sb.apply("Hello"); // new StringBuilder("Hello")

Default and Static Interface Methods
#

Default Methods
#

Allow interfaces to ship method implementations. Implementing classes inherit them automatically but can override. The main motivation is backwards compatibility. You can add new methods to an existing interface without breaking all classes that already implement it.

interface Greeter {
    String greet(String name);

    default String greetLoud(String name) {
        return greet(name).toUpperCase();
    }
}

class FriendlyGreeter implements Greeter {
    public String greet(String name) {
        return "Hello, " + name + "!";
    }
}

Greeter g = new FriendlyGreeter();
g.greet("Alice");    
// "Hello, Alice!"
g.greetLoud("Alice"); 
// "HELLO, ALICE!" (inherited)

Static Methods
#

Interfaces can define static utility methods scoped to the interface itself. Unlike default methods, they cannot be overridden and are called directly on the interface, not on an instance. Keeps related helpers co-located with the interface rather than in a separate utility class.

interface MathOp {
    int operate(int a, int b);

    static MathOp add() {
        return (a, b) -> a + b;
    }
}

MathOp add = MathOp.add();
add.operate(3, 4); // 7

Optional
#

docs

A container that may or may not hold a value. Avoids null checks and NullPointerException.

// Creating
Optional<String> empty   = Optional.empty();
Optional<String> present = Optional.of("Alice");
Optional<String> maybe   = Optional.ofNullable(null);
// same as empty()

// Checking and getting
present.isPresent();   // true
present.get();         // "Alice"
empty.orElse("default");              // "default"
empty.orElseGet(() -> "computed");    // "computed"
empty.orElseThrow(
        () -> new RuntimeException("No value")); // throws

// Transforming
Optional<Integer> len = present.map(String::length);
// Optional[5]
present.ifPresent(System.out::println); // Alice

// Filtering
present.filter(n -> n.startsWith("A")); // Optional[Alice]
present.filter(n -> n.startsWith("B")); // Optional.empty

Date and Time API
#

A new java.time package replacing the old Date/Calendar. Immutable, thread-safe, and much clearer.

// LocalDate - date only, no time, no timezone
LocalDate today    = LocalDate.now();        // 2026-02-22
LocalDate birthday = LocalDate.of(1990, 3, 15);
Period age = Period.between(birthday, today);
age.getYears(); // 35

// LocalTime - time only
LocalTime now     = LocalTime.now();         // e.g. 14:30:15
LocalTime meeting = LocalTime.of(9, 30);
Duration gap = Duration.between(meeting, now);
gap.toMinutes(); // minutes since 09:30

// LocalDateTime - date + time, no timezone
LocalDateTime dt     = LocalDateTime.now();
LocalDateTime future = dt.plusDays(7).plusHours(3);

// ZonedDateTime - with timezone
ZonedDateTime london = ZonedDateTime
        .now(ZoneId.of("Europe/London"));
ZonedDateTime tokyo = london
        .withZoneSameInstant(ZoneId.of("Asia/Tokyo"));

// Formatting and parsing
DateTimeFormatter fmt =
        DateTimeFormatter.ofPattern("dd/MM/yyyy");
String formatted = today.format(fmt);    // "22/02/2026"
LocalDate parsed = LocalDate.parse("15/03/1990", fmt);

Source
#