Java 8 Features - Lambda, Streams, Optional, Method References | Java Codeex

1. Overview of Java 8

Java 8, released in March 2014, was one of the most significant releases since Java 5. It fundamentally changed the way Java developers write code by introducing functional programming concepts into the language. Before Java 8, Java was purely object-oriented; after Java 8, it became a multi-paradigm language supporting functional-style programming.

Key Features Introduced in Java 8

  • Lambda Expressions — concise anonymous function syntax
  • Functional Interfaces — single-abstract-method interfaces
  • Stream API — declarative data processing pipeline
  • Optional<T> — null-safe container type
  • Method References — shorthand for lambdas calling existing methods
  • Default and Static Interface Methods — interface evolution without breaking implementations
  • New Date and Time API (java.time) — immutable, thread-safe date/time classes
  • Collectors — powerful reduction operations for streams
  • Nashorn JavaScript Engine — embedded JS engine (later removed)
  • Base64 Encoding/Decoding — built-in java.util.Base64

Impact: Java 8 made Java competitive with functional languages like Scala and Haskell while retaining full backward compatibility.

2. Lambda Expressions

A lambda expression is a concise way to represent an anonymous function — a block of code that can be passed around as a value. Lambdas enable you to treat functionality as a method argument and eliminate verbose anonymous class boilerplate.

Syntax

(parameters) -> expression
(parameters) -> { statements; }
import java.util.*;

public class LambdaDemo {
    public static void main(String[] args) {

        // Example 1: Replacing anonymous Runnable
        // Before Java 8
        Runnable oldWay = new Runnable() {
            @Override
            public void run() {
                System.out.println("Old way: anonymous class");
            }
        };

        // Java 8 lambda
        Runnable newWay = () -> System.out.println("New way: lambda");
        oldWay.run();
        newWay.run();

        // Example 2: Replacing anonymous Comparator
        List<String> names = Arrays.asList("Charlie", "Alice", "Bob");

        // Before Java 8
        Collections.sort(names, new Comparator<String>() {
            @Override
            public int compare(String a, String b) {
                return a.compareTo(b);
            }
        });
        System.out.println("Sorted (old): " + names);

        // Java 8 lambda
        names.sort((a, b) -> b.compareTo(a));  // reverse
        System.out.println("Sorted (lambda): " + names);

        // Example 3: Lambda with block body and multiple statements
        List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
        nums.forEach(n -> {
            int square = n * n;
            System.out.println(n + " squared = " + square);
        });
    }
}
Old way: anonymous class
New way: lambda
Sorted (old): [Alice, Bob, Charlie]
Sorted (lambda): [Charlie, Bob, Alice]
1 squared = 1
2 squared = 4
3 squared = 9
4 squared = 16
5 squared = 25

3. Functional Interfaces

A functional interface is an interface with exactly one abstract method. Lambda expressions are instances of functional interfaces — the compiler infers which abstract method the lambda implements. The @FunctionalInterface annotation is optional but recommended: it causes a compile error if the interface accidentally gets a second abstract method.

import java.util.concurrent.Callable;

// Custom functional interface
@FunctionalInterface
interface Transformer {
    String transform(String input);
    // Adding a second abstract method here would cause a compile error
}

public class FunctionalInterfaceDemo {
    public static void main(String[] args) {

        // Runnable — no args, no return value
        Runnable r = () -> System.out.println("Running!");
        r.run();

        // Comparator — two args, returns int
        java.util.Comparator<String> comp =
            (s1, s2) -> s1.length() - s2.length();
        System.out.println("Compare: " + comp.compare("Hi", "Hello"));

        // Callable — no args, returns value, can throw
        Callable<Integer> callable = () -> 42;
        try {
            System.out.println("Callable result: " + callable.call());
        } catch (Exception e) {
            e.printStackTrace();
        }

        // Custom functional interface
        Transformer upper = s -> s.toUpperCase();
        Transformer shout = s -> s.toUpperCase() + "!!!";
        System.out.println(upper.transform("hello world"));
        System.out.println(shout.transform("hello world"));
    }
}
Running!
Compare: -3
Callable result: 42
HELLO WORLD
HELLO WORLD!!!

4. Predicate<T>

Predicate<T> (in java.util.function) represents a boolean-valued function of one argument. Its single abstract method is boolean test(T t). Predicates can be composed using and(), or(), and negate().

import java.util.*;
import java.util.function.Predicate;

public class PredicateDemo {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        Predicate<Integer> isEven    = n -> n % 2 == 0;
        Predicate<Integer> isGreater5 = n -> n > 5;

        // test
        System.out.println("4 is even? " + isEven.test(4));
        System.out.println("7 is even? " + isEven.test(7));

        // and — both conditions must be true
        Predicate<Integer> evenAndGreater5 = isEven.and(isGreater5);
        System.out.print("Even AND > 5: ");
        numbers.stream()
               .filter(evenAndGreater5)
               .forEach(n -> System.out.print(n + " "));
        System.out.println();

        // or — at least one condition true
        Predicate<Integer> evenOrGreater5 = isEven.or(isGreater5);
        System.out.print("Even OR > 5:  ");
        numbers.stream()
               .filter(evenOrGreater5)
               .forEach(n -> System.out.print(n + " "));
        System.out.println();

        // negate — opposite condition
        System.out.print("NOT even:     ");
        numbers.stream()
               .filter(isEven.negate())
               .forEach(n -> System.out.print(n + " "));
        System.out.println();

        // Filtering a list of strings
        List<String> words = Arrays.asList("apple", "ant", "banana", "avocado", "cherry");
        Predicate<String> startsWithA = s -> s.startsWith("a");
        Predicate<String> longerThan5 = s -> s.length() > 5;
        System.out.print("Starts with 'a' and longer than 5: ");
        words.stream()
             .filter(startsWithA.and(longerThan5))
             .forEach(s -> System.out.print(s + " "));
        System.out.println();
    }
}
4 is even? true
7 is even? false
Even AND > 5: 6 8 10
Even OR > 5: 2 4 6 7 8 9 10