Released March 2014 · Long-term support

Java 8 Features

Java 8 was the biggest change to the language since generics. Lambda expressions, the Stream API, Optional and the new Date and Time API changed how Java code is written. Learn every major feature with clear explanations and runnable examples.

  • Lambda ExpressionsFunctions as values
  • Stream APIDeclarative data processing
  • OptionalSafer than null
  • java.timeModern dates and times
Feature 01

Lambda Expressions

A lambda expression is an anonymous function: a block of code you can pass around like a value. It replaces bulky anonymous classes with a short (parameters) -> expression syntax, which makes sorting, filtering and event handling far more readable.

  • Syntax: (a, b) -> a + b, or x -> { statements; } for a block body
  • Can only implement a functional interface (one abstract method)
  • Can use local variables that are final or effectively final
LambdaDemo.java
import java.util.*;

public class LambdaDemo {
    public static void main(String[] args) {
        List<String> names =
                new ArrayList<>(Arrays.asList("Ravi", "Asha", "Meera", "Jo"));

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

        // Java 8: lambda expression
        names.sort((a, b) -> Integer.compare(a.length(), b.length()));
        System.out.println("Lambda by length: " + names);

        Runnable task = () -> System.out.println("Runnable as a lambda");
        task.run();
    }
}
Output
Anonymous class: [Asha, Jo, Meera, Ravi]
Lambda by length: [Jo, Asha, Ravi, Meera]
Runnable as a lambda
Feature 02

Functional Interfaces

A functional interface has exactly one abstract method, so a lambda can implement it. Java 8 added the @FunctionalInterface annotation and the java.util.function package with 43 general-purpose interfaces such as Predicate, Function, Supplier and Consumer.

  • Predicate<T>: T → boolean (filters and conditions)
  • Function<T,R>: T → R (transformations)
  • Supplier<T> produces values; Consumer<T> accepts them
Read the full tutorial: Functional Interface in Java
FunctionalDemo.java
import java.util.function.*;

public class FunctionalDemo {
    public static void main(String[] args) {
        Predicate<Integer> isEven = n -> n % 2 == 0;
        Function<String, Integer> length = s -> s.length();
        Supplier<String> greeting = () -> "Hello, Java 8";
        Consumer<String> printer = s -> System.out.println("Consumed: " + s);
        BinaryOperator<Integer> add = (a, b) -> a + b;

        System.out.println("isEven(10)      = " + isEven.test(10));
        System.out.println("length(\"Kafka\") = " + length.apply("Kafka"));
        System.out.println("greeting.get()  = " + greeting.get());
        printer.accept("Spring Boot");
        System.out.println("add(4, 5)       = " + add.apply(4, 5));
    }
}
Output
isEven(10)      = true
length("Kafka") = 5
greeting.get()  = Hello, Java 8
Consumed: Spring Boot
add(4, 5)       = 9
Feature 03

Method References

When a lambda only calls an existing method, a method reference such as String::toUpperCase says the same thing more clearly. There are four kinds: static methods, instance methods of a particular object, instance methods of an arbitrary object, and constructors.

  • Static: Integer::parseInt
  • Particular object: System.out::println
  • Arbitrary object: String::toUpperCase
  • Constructor: User::new
MethodRefDemo.java
import java.util.*;
import java.util.function.*;

public class MethodRefDemo {
    static class User {
        private final String name;
        User(String name) { this.name = name; }
        @Override public String toString() { return "User[" + name + "]"; }
    }

    public static void main(String[] args) {
        List<String> words = Arrays.asList("java", "lambda", "stream");

        // instance method of a particular object
        words.forEach(System.out::println);
        // static method
        Function<String, Integer> parse = Integer::parseInt;
        // instance method of an arbitrary String
        Function<String, String> upper = String::toUpperCase;
        // constructor
        Function<String, User> create = User::new;

        System.out.println(parse.apply("42") + 8);
        System.out.println(upper.apply("method references"));
        System.out.println(create.apply("Asha"));
    }
}
Output
java
lambda
stream
50
METHOD REFERENCES
User[Asha]
Feature 04

Stream API

The Stream API lets you describe what to do with data instead of writing loops. A stream pipeline has a source (such as a list), zero or more intermediate operations (filter, map, sorted) and one terminal operation (collect, count, forEach). Streams are lazy and can run in parallel.

  • Intermediate operations are lazy and return a new stream
  • Terminal operations trigger processing and produce a result
  • Collectors builds lists, maps, groups and summaries
  • Primitive streams (IntStream) avoid boxing
StreamDemo.java
import java.util.*;
import java.util.stream.*;

public class StreamDemo {
    static class Employee {
        private final String name, dept;
        private final double salary;
        Employee(String name, String dept, double salary) {
            this.name = name;
            this.dept = dept;
            this.salary = salary;
        }
        String getName() { return name; }
        String getDept() { return dept; }
        double getSalary() { return salary; }
    }

    public static void main(String[] args) {
        List<Employee> staff = Arrays.asList(
                new Employee("Asha", "Backend", 120000),
                new Employee("Ravi", "Frontend", 95000),
                new Employee("Meera", "Backend", 135000),
                new Employee("Kiran", "Data", 110000));

        List<String> highEarners = staff.stream()
                .filter(e -> e.getSalary() > 100000)
                .map(Employee::getName)
                .sorted()
                .collect(Collectors.toList());
        System.out.println("Salary > 1,00,000: " + highEarners);

        Map<String, Long> perDept = staff.stream()
                .collect(Collectors.groupingBy(Employee::getDept,
                        TreeMap::new, Collectors.counting()));
        System.out.println("Count per dept:    " + perDept);

        double avg = staff.stream()
                .mapToDouble(Employee::getSalary)
                .average().orElse(0);
        System.out.printf("Average salary:    %.2f%n", avg);

        int sumOfSquares = IntStream.rangeClosed(1, 5).map(n -> n * n).sum();
        System.out.println("Sum of squares 1..5: " + sumOfSquares);
    }
}
Output
Salary > 1,00,000: [Asha, Kiran, Meera]
Count per dept:    {Backend=2, Data=1, Frontend=1}
Average salary:    115000.00
Sum of squares 1..5: 55
Feature 05

Optional Class

Optional<T> is a container that either holds a value or is empty. Returning it from a method makes the possibility of "no result" explicit and replaces error-prone null checks with methods such as map, orElse and ifPresent.

  • Create with Optional.of, ofNullable or empty
  • Read with orElse, orElseGet, orElseThrow
  • Best used as a method return type, not for fields or parameters
Read the full tutorial: Optional Class in Java
OptionalDemo.java
import java.util.*;

public class OptionalDemo {
    static final Map<Integer, String> USERS = new HashMap<>();
    static { USERS.put(1, "asha"); USERS.put(2, "ravi"); }

    static Optional<String> findUser(int id) {
        return Optional.ofNullable(USERS.get(id));
    }

    public static void main(String[] args) {
        String found = findUser(1).map(String::toUpperCase).orElse("GUEST");
        String missing = findUser(9).map(String::toUpperCase).orElse("GUEST");
        System.out.println(found);
        System.out.println(missing);
        findUser(2).ifPresent(u -> System.out.println("Found user: " + u));
        System.out.println("isPresent(9): " + findUser(9).isPresent());
    }
}
Output
ASHA
GUEST
Found user: ravi
isPresent(9): false
Feature 06

Default and Static Interface Methods

Before Java 8, adding a method to an interface broke every class that implemented it. Default methods provide an implementation inside the interface, so libraries could evolve: this is how forEach, stream and removeIf were added to existing collections. Interfaces can also have static helper methods.

  • default methods are inherited and can be overridden
  • static interface methods are called on the interface name
  • Conflicting defaults must be resolved with Interface.super.method()
Read the full tutorial: Default Methods in Java
DefaultMethodDemo.java
public class DefaultMethodDemo {
    interface Vehicle {
        String name();
        // default method: inherited by every implementation
        default String describe() { return name() + " is a vehicle"; }
        // static method: called on the interface name
        static Vehicle of(String name) { return () -> name; }
    }

    static class Bike implements Vehicle {
        public String name() { return "Bike"; }
        @Override public String describe() { return "Bike has two wheels"; }
    }

    public static void main(String[] args) {
        System.out.println(Vehicle.of("Car").describe()); // inherited
        System.out.println(new Bike().describe());        // overridden
    }
}
Output
Car is a vehicle
Bike has two wheels
Feature 07

Date and Time API (java.time)

The new java.time package replaced the confusing, mutable Date and Calendar classes. Its types are immutable and clearly named: LocalDate, LocalDateTime, ZonedDateTime, Instant, Duration and Period, with the thread-safe DateTimeFormatter for formatting.

  • LocalDate: a date without time; LocalDateTime: date and time
  • ZonedDateTime handles time zones and daylight saving
  • Months are 1–12 and every operation returns a new object
Read the full tutorial: Date Class in Java (and java.time)
DateTimeDemo.java
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Locale;

public class DateTimeDemo {
    public static void main(String[] args) {
        // Java 8 release date
        LocalDate release = LocalDate.of(2014, Month.MARCH, 18);
        LocalDate later = release.plusYears(10).plusDays(5);
        System.out.println("Java 8 released: " + release
                + " (" + release.getDayOfWeek() + ")");
        System.out.println("Plus 10y 5d:     " + later);
        long days = ChronoUnit.DAYS.between(release, later);
        System.out.println("Days between:    " + days);

        LocalDateTime meeting = LocalDateTime.of(2026, 10, 12, 15, 30);
        ZonedDateTime ist = meeting.atZone(ZoneId.of("Asia/Kolkata"));
        System.out.println("IST:             " + ist);
        ZoneId londonZone = ZoneId.of("Europe/London");
        ZonedDateTime london = ist.withZoneSameInstant(londonZone);
        System.out.println("London:          " + london);
        DateTimeFormatter fmt =
            DateTimeFormatter.ofPattern("dd MMM yyyy, hh:mm a", Locale.ENGLISH);
        System.out.println("Formatted:       " + meeting.format(fmt));
        System.out.println("Duration:        " + Duration.ofMinutes(135));
    }
}
Output
Java 8 released: 2014-03-18 (TUESDAY)
Plus 10y 5d:     2024-03-23
Days between:    3658
IST:             2026-10-12T15:30+05:30[Asia/Kolkata]
London:          2026-10-12T11:00+01:00[Europe/London]
Formatted:       12 Oct 2026, 03:30 PM
Duration:        PT2H15M
Feature 08

More Java 8 Additions

Java 8 also brought many smaller improvements: new collection methods, string joining, a standard Base64 encoder, CompletableFuture for asynchronous programming, and more.

  • Iterable.forEach, Collection.removeIf, Map.merge and getOrDefault
  • String.join and StringJoiner
  • java.util.Base64 encoder and decoder
  • CompletableFuture for composing asynchronous tasks
Read the full tutorial: StringJoiner in Java
MoreFeaturesDemo.java
import java.util.*;
import java.util.concurrent.CompletableFuture;

public class MoreFeaturesDemo {
    public static void main(String[] args) {
        // String.join and StringJoiner
        System.out.println(String.join(" | ", "Java", "Spring", "Kafka"));
        StringJoiner sj = new StringJoiner(", ", "[", "]");
        sj.add("lambda").add("stream");
        System.out.println(sj);

        // New collection methods: forEach, removeIf, Map.merge, getOrDefault
        List<Integer> nums = new ArrayList<>(Arrays.asList(5, 12, 7, 20, 3));
        nums.removeIf(n -> n < 6);
        System.out.println("removeIf(<6): " + nums);
        Map<String, Integer> count = new TreeMap<>();
        for (String w : "to be or not to be".split(" ")) {
            count.merge(w, 1, Integer::sum);
        }
        System.out.println("merge counts: " + count);
        System.out.println("getOrDefault: " + count.getOrDefault("java", 0));

        // Base64
        String encoded =
                Base64.getEncoder().encodeToString("JavaByTechie".getBytes());
        String decoded = new String(Base64.getDecoder().decode(encoded));
        System.out.println("Base64: " + encoded + " -> " + decoded);

        // CompletableFuture
        String result = CompletableFuture.supplyAsync(() -> 21)
                .thenApply(n -> n * 2)
                .thenApply(n -> "Answer = " + n)
                .join();
        System.out.println(result);
    }
}
Output
Java | Spring | Kafka
[lambda, stream]
removeIf(<6): [12, 7, 20]
merge counts: {be=2, not=1, or=1, to=2}
getOrDefault: 0
Base64: SmF2YUJ5VGVjaGll -> JavaByTechie
Answer = 42
What Came Next

Beyond Java 8

Java 8 laid the foundation for modern Java. Later releases built on it with these highlights:

VersionKey additions
Java 9Modules, List.of/Map.of, JShell
Java 10var for local variables
Java 11 (LTS)New String methods, the HTTP Client
Java 14Switch expressions
Java 16Records and pattern matching for instanceof
Java 17 (LTS)Sealed classes
Java 21 (LTS)Virtual threads, record patterns, pattern matching for switch