Skip to content

Instantly share code, notes, and snippets.

@ky28059
Last active March 19, 2022 04:12
Show Gist options
  • Select an option

  • Save ky28059/566596eb93ac863332783182ec356e82 to your computer and use it in GitHub Desktop.

Select an option

Save ky28059/566596eb93ac863332783182ec356e82 to your computer and use it in GitHub Desktop.

Lambdas

Lambdas in Java are declared similarly as arrow functions in JavaScript, using -> instead of =>. Unlike other variables, lambdas cannot be declared using var; they must provide an explicit target type.

Lambdas are assignable to functional interfaces as defined in java.util.function and threading interfaces like Runnable and Callable<T>; here, we declare a simple lambda using Runnable which prints a message when run:

Runnable lambda = () -> {
    System.out.println("Hello from a lambda expression!");
};

Like in JS, a lambda can be inlined if the body contains only one line.

Runnable lambda = () -> System.out.println("Hello from a lambda expression!");

Note that like in JS, an inlined lambda will return the value of its expression.

Runnable rand = () -> Math.random(); // valid
Supplier<Double> rand = () -> Math.random(); // also valid; rand returns a double
Supplier<Double> rand = () -> { // invalid; rand returns void
    Math.random();
};

Runnable

The simplest version of a lambda implements the Runnable interface. Runnable is used for threads, but also defines a lambda which accepts no arguments and returns void. A Runnable is called using .run().

public class Main {
    public static void main(String... args) {
        Runnable print = () -> System.out.println("Hello, world!");
        callRunnable(print);
    }

    public static void callRunnable(Runnable func) {
        func.run();
    }
}

Method references are valid as lambdas as long as the types match; here, Main::printHelloWorld is assignable to Runnable as it takes no arguments and returns void.

public class Main {
    public static void main(String... args) {
        callRunnable(Main::printHelloWorld);
    }

    public static void printHelloWorld() {
        System.out.println("Hello, world!");
    }

    public static void callRunnable(Runnable func) {
        func.run();
    }
}

Supplier<T>

To type a lambda that returns a value, use Supplier<T>. Supplier<T> defines a lambda which takes no arguments and returns a value of type T. A Supplier's value can be retrieved using .get().

import java.util.function.Supplier;

public class Main {
    public static void main(String... args) {
        Supplier<Double> rand = () -> Math.random();
        printSupplierValue(rand);
    }

    public static <T> void printSupplierValue(Supplier<T> supplier) {
        System.out.println(supplier.get());
    }
}

Like with all functional interfaces, method references are valid Suppliers as long as they take no arguments, throw no unchecked exceptions, and return the correct type.

import java.util.function.Supplier;

public class Main {
    public static void main(String... args) {
        printSupplierValue(Main::rand);
        printSupplierValue(Math::random);
    }

    public static double rand() {
        return Math.random();
    }

    public static <T> void printSupplierValue(Supplier<T> supplier) {
        System.out.println(supplier.get());
    }
}

Consumer<T>

To type a lambda that takes a single argument and returns void, use Consumer<T>. A Consumer can be called using .accept(T).

import java.util.function.Consumer;

public class Main {
    public static void main(String... args) {
        Consumer<String> printStr = (String str) -> {
            System.out.println("Printed string:");
            System.out.println(str);
        };
        callConsumer(printStr, "Hello, world!");
    }

    public static <T> void callConsumer(Consumer<T> consumer, T value) {
        consumer.accept(value);
    }
}

Method references are valid as well.

import java.util.function.Consumer;

public class Main {
    public static void main(String... args) {
        callConsumer(System.out::println, "Hello, world!");
    }

    public static <T> void callConsumer(Consumer<T> consumer, T value) {
        consumer.accept(value);
    }
}

Function<T, R>

If a lambda both accepts and returns a value, the type Function<T, R> can be used. Function<T, R> specifies a function which takes an argument of type T and returns a value of type R, and can be called using .apply(T).

import java.util.function.Function;

public class Main {
    public static void main(String... args) {
        Function<String, String> ishify = (String str) -> str + "ish";
        printFunctionCall(ishify, "cool");

        Function<Integer, Double> toDouble = (Integer num) -> num * 1.0;
        printFunctionCall(toDouble, 5);
    }

    public static <T, R> void printFunctionCall(Function<T, R> func, T value) {
        System.out.println(func.apply(value));
    }
}
import java.util.function.Function;

public class Main {
    public static void main(String... args) {
        int num = callFunction(Integer::parseInt, "5");
        System.out.println(num + 7);
    }

    public static <T, R> R callFunction(Function<T, R> func, T value) {
        return func.apply(value);
    }
}

Other functional interfaces

For taking in two values, use BiConsumer<T, U> and BiFunction<T, U, R>.

import java.util.function.BiConsumer;
import java.util.function.BiFunction;

public class Main {
    public static void main(String... args) {
        BiConsumer<Integer, Integer> printSum = (Integer x, Integer y) -> System.out.println(x + y);
        callBiConsumer(printSum, 5, 7);

        BiFunction<Integer, Integer, Integer> sum = (Integer x, Integer y) -> x + y;
        int x = callBiFunction(sum, 1, 2);
        int y = callBiFunction(Integer::sum, 3, 4);
        System.out.println(x + y);
    }

    public static <T, U> void callBiConsumer(BiConsumer<T, U> consumer, T a, U b) {
        consumer.accept(a, b);
    }

    public static <T, U, R> R callBiFunction(BiFunction<T, U, R> func, T a, U b) {
        return func.apply(a, b);
    }
}

When a lambda's argument type and return type are the same, the UnaryOperator<T> and BinaryOperator<T> interfaces can be used. UnaryOperator<T> defines a function which takes a single argument of type T and returns a value of type T, while BinaryOperator<T> defines the same but takes two arguments instead. The "ishify" and "sum" examples from earlier, using UnaryOperator and BinaryOperator:

import java.util.function.UnaryOperator;
import java.util.function.BinaryOperator;

public class Main {
    public static void main(String... args) {
        UnaryOperator<String> ishify = (String str) -> str + "ish";
        printUnaryOperatorCall(ishify, "cool");

        printBinaryOperatorCall(Integer::sum, 1, 2);
    }

    public static <T> void printUnaryOperatorCall(UnaryOperator<T> func, T value) {
        System.out.println(func.apply(value));
    }

    public static <T> void printBinaryOperatorCall(BinaryOperator<T> func, T a, T b) {
        System.out.println(func.apply(a, b));
    }
}

When a lambda takes one argument and returns a boolean (like a callback to .filter() or .find()), Predicate<T> can be used. Predicate<T> defines a function which takes one argument of type T and returns boolean, called using .test(T).

import java.util.function.Predicate;

public class Main {
    public static void main(String... args) {
        Predicate<Integer> isOdd = (Integer num) -> num % 2 == 1;
        System.out.println(testPredicate(isOdd, 6));

        System.out.println(testPredicate(Integer.valueOf(5)::equals, 5));
    }

    public static <T> boolean testPredicate(Predicate<T> pred, T value) {
        return pred.test(value);
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment