JV

Java Fundamentals

19 lessons

Progress0%
1. Introduction to Java
1What is Java?
2. Variables and Data Types
1Primitive Types
3. Control Flow
ConditionalsLoops
4. Methods
Defining MethodsMethod Overloading
5. Object-Oriented Programming
Classes and ObjectsInheritanceInterfaces and Abstract Classes
6. Collections
ArrayList and LinkedListHashMap and HashSet
7. Exception Handling
Checked & Unchecked Exceptionstry-with-resources & Custom Exceptions
8. Generics
Generic Classes & MethodsWildcards & Type Erasure
9. Modern Java
Lambdas & Functional InterfacesStream API & Optional
10. File I/O
java.nio.file APIBuffered I/O & try-with-resources
All Tutorials
JavaModern Java
Lesson 16 of 19 min
Chapter 9 · Lesson 1

Lambdas & Functional Interfaces

Java 8 brought functional programming features to the language. Lambdas and functional interfaces are the cornerstone of the Stream API, CompletableFuture, and many modern APIs.

Lambda Syntax

A lambda expression is an anonymous function:

code
(parameters) -> expression
(parameters) -> { statements; }

Examples:

  • () -> 42 — no parameters, returns 42
  • x -> x * 2 — single parameter (parens optional), returns double
  • (a, b) -> a + b — two parameters
  • (String s) -> { System.out.println(s); } — explicit type, block body

Functional Interfaces

A functional interface has exactly one abstract method. It can be annotated with @FunctionalInterface (optional but recommended — the compiler enforces the single-abstract-method rule).

Key interfaces from java.util.function:

InterfaceSignatureDescription
Predicate<T>boolean test(T t)Returns true/false
Function<T,R>R apply(T t)Transforms T to R
Consumer<T>void accept(T t)Consumes T, no return
Supplier<T>T get()Produces T with no input
BiFunction<T,U,R>R apply(T t, U u)Two inputs, one output
UnaryOperator<T>T apply(T t)Function where T == R
BinaryOperator<T>T apply(T t1, T t2)BiFunction where all types == T

Predicate provides default methods and(), or(), negate() for composing predicates.

Method References

A shorter syntax for lambdas that just delegate to an existing method:

KindSyntaxEquivalent lambda
Static methodMath::absx -> Math.abs(x)
Instance method (bound)str::toUpperCase() -> str.toUpperCase()
Instance method (unbound)String::toLowerCases -> s.toLowerCase()
ConstructorArrayList::new() -> new ArrayList<>()

Comparator with Lambda

Lambdas shine for sorting: list.sort((a, b) -> a.length() - b.length()) or the more readable Comparator.comparing(String::length).

Code Examples

Lambda with Comparator for sortingjava
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class LambdaSortDemo {
    record Person(String name, int age) {}

    public static void main(String[] args) {
        List<String> words = Arrays.asList("banana", "apple", "kiwi", "cherry", "fig");

        // Anonymous class (old style)
        words.sort(new Comparator<String>() {
            @Override public int compare(String a, String b) {
                return a.length() - b.length();
            }
        });
        System.out.println("Anon class  : " + words);

        // Lambda — same behavior, much shorter
        words.sort((a, b) -> a.length() - b.length());
        System.out.println("Lambda      : " + words);

        // Comparator.comparing — most readable
        words.sort(Comparator.comparing(String::length));
        System.out.println("Comparing   : " + words);

        // Chained: sort by length, then alphabetically
        words.sort(Comparator.comparing(String::length)
                              .thenComparing(Comparator.naturalOrder()));
        System.out.println("Chained     : " + words);

        // Sort records
        List<Person> people = Arrays.asList(
            new Person("Charlie", 35),
            new Person("Alice",   28),
            new Person("Bob",     28)
        );
        people.sort(Comparator.comparingInt(Person::age)
                               .thenComparing(Person::name));
        people.forEach(p -> System.out.println(p.name() + " " + p.age()));
    }
}

Comparator.comparing() with a method reference is the most readable sorting idiom. thenComparing() chains secondary sort criteria. Records (Java 16+) provide compact accessor methods like name() and age().

Predicate composition with and/or/negatejava
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class FunctionalInterfacesDemo {

    public static void main(String[] args) {
        // --- Predicate ---
        Predicate<Integer> isEven      = n -> n % 2 == 0;
        Predicate<Integer> isPositive  = n -> n > 0;
        Predicate<Integer> isEvenAndPositive = isEven.and(isPositive);
        Predicate<Integer> isOdd       = isEven.negate();

        System.out.println("isEven(4)            : " + isEven.test(4));
        System.out.println("isPositive(-3)        : " + isPositive.test(-3));
        System.out.println("evenAndPositive(6)   : " + isEvenAndPositive.test(6));
        System.out.println("evenAndPositive(-4)  : " + isEvenAndPositive.test(-4));
        System.out.println("isOdd(5)             : " + isOdd.test(5));

        // --- Function ---
        Function<String, Integer> strLen  = String::length;
        Function<Integer, String> intToStr = Object::toString;
        Function<String, String>  lenStr   = strLen.andThen(intToStr);

        System.out.println("strLen("hello")       : " + strLen.apply("hello"));
        System.out.println("lenStr("world")       : " + lenStr.apply("world"));

        // --- Consumer ---
        Consumer<String> printer     = System.out::println;
        Consumer<String> upperPrinter = s -> System.out.println(s.toUpperCase());
        Consumer<String> both        = printer.andThen(upperPrinter);
        both.accept("java");

        // --- Supplier ---
        Supplier<List<String>> listFactory = () -> new java.util.ArrayList<>(Arrays.asList("x", "y"));
        System.out.println("Supplier   : " + listFactory.get());
    }
}

Predicate's and/or/negate compose boolean conditions without if-chains. Function.andThen() pipelines transformations. Consumer.andThen() lets you apply two side-effects sequentially.

Method reference typesjava
import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

public class MethodRefDemo {

    static int doubleIt(int n) { return n * 2; }

    String greet(String prefix) { return prefix + " " + this; }

    @Override public String toString() { return "MethodRefDemo"; }

    public static void main(String[] args) {
        List<String> words = Arrays.asList("hello", "WORLD", "Java");

        // 1. Static method reference: ClassName::staticMethod
        Function<Integer, Integer> doubler = MethodRefDemo::doubleIt;
        System.out.println("Static ref  : " + doubler.apply(5));

        // 2. Bound instance method: instance::method  (instance is captured)
        String prefix = ">>>";
        UnaryOperator<String> addPrefix = s -> prefix + s;
        words.stream().map(addPrefix).forEach(System.out::println);

        // 3. Unbound instance method: ClassName::instanceMethod
        //    First lambda parameter becomes the receiver
        Function<String, String> toLower = String::toLowerCase;
        words.stream().map(toLower).forEach(System.out::println);

        // 4. Constructor reference: ClassName::new
        Supplier<java.util.ArrayList<String>> listMaker = java.util.ArrayList::new;
        java.util.ArrayList<String> newList = listMaker.get();
        newList.add("constructed");
        System.out.println("Constructor : " + newList);

        // BiFunction with unbound instance method
        BiFunction<String, String, String> concat = String::concat;
        System.out.println("BiFunction  : " + concat.apply("Hello, ", "World!"));
    }
}

The four method reference kinds cover all common delegation patterns. Unbound references treat the first lambda parameter as the receiver object, which is why String::toLowerCase fits Function<String,String>.

Quick Quiz

1. Which functional interface from java.util.function takes no input and returns a value?

2. What is the output of `Predicate<Integer> p = n -> n > 0; System.out.println(p.negate().test(5));`?

3. Which method reference syntax represents an unbound instance method reference?

4. What annotation signals that an interface is intended to be a functional interface?

Was this lesson helpful?

PreviousNext