Overview

The much-awaited Java 8 evolution focuses on the shortcode, ease of programming and smart use of multicore processors. The significant features include Lambda expressions, Streams API and Default Methods in interfaces, ...

Features

Let's get started by introducing the new concept in Java and comparing it with the equivalent in C#

Lambda Expressions

Before Lambda, let's get familiarized about the history of passing methods as parameters.

C++ introduced function pointer
C# 3 introduced lambdas and supports delegates, anonymous methods and interface implementations
Java 8 introduced functional interfaces and support anonymous methods implementation

Java supports the passing of behaviours using class implementations. Anonymous allows the class implementations without a name.
Suppose Dogs in dog care are represented by the below Dog class.

public class Dog {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    Sex gender;
    
    public int getAge() {
        // ...
    }
    
    public int getGender() {
        // ...
    }

    public void printDog() {
        // ...
    }
}

Basic Filters

Let's search for a dog less than 1 years

public static void printDogsLessThan(List<Dog> dogs, int age) {
    for (Dog d : dogs) {
        if (d.getAge() <= age) {
            d.printDog();
        }
    }
}

Let's search for a male dog greater than 2 years

public static void printDogsGreaterThan(List<Dog> dogs, int age, Sex gender) {
    for (Dog d : dogs) {
        if (d.getAge() >= age && d.getGender() == gender) {
            d.printDog();
        }
    }
}

Generalized Filters

We will define the search criteria using an interface and then implement the filter critereon as desired.

Definition

Specify the filter criteria using the FilterDog interface.

interface FilterDog {
    boolean filter(Dog d);
}

The following class searches for an old male dog and implements the filter method.

class FilterOldMaleDogService implements FilterDog {
    public boolean filter(Dog d) {
        return d.gender == Dog.Sex.MALE &&
            d.getAge() >= 6;
    }
}

The following method prints the dogs based on our filter criteria.

public static void printDogs(
    List<Dog> dogs, FilterDog filterCriteria) {
    for (Dog d : dogs) {
        if (filterCriteria.filter(d)) {
            d.printDog();
        }
    }
}

Usage

printDogs(
    dogs, new FilterOldMaleDogService());

Anonymous Filters

printDogs(
    dogs, 
    new FilterDog() {
        public boolean filter(Dog p) {
            return d.getGender() == Dog.Sex.MALE
                && d.getAge() >= 6;
        }
    }
);

Lambda Filters

We saw in the previous section, the bulky syntax of anonymous classes

printDogs(
    dogs,
    (Dog d) -> d.getGender() == Dog.Sex.MALE
        && p.getAge() >= 6
);

Predicates

It's a standard functional interface that defines a method test which accepts the object and returns a Boolean variable and is similar to Predicate in C#.
We use FilterDog to pass filter behaviour and print filtered dogs.

interface FilterDog {
    boolean filter(Dog d);
}
public static void printDogs(
    List<Dog> dogs, FilterDog filterCriteria) {
    for (Dog d : dogs) {
        if (filterCriteria.filter(d)) {
            d.printDog();
        }
    }
}

Instead of using FilterDog, let's use the Predicate defined below.

interface Predicate<T> {
    boolean test(T t);
}
interface Predicate<Dog> {
    boolean test(Dog t);
}
public static void printDogs(
    List<Dog> dogs, Predicate<Dog> filterCriteria) {
    for (Dog d : dogs) {
        if (filterCriteria.test(d)) {
            d.printDog();
        }
    }
}

The usage remains the same as previous lambda call.

printDogs(
    dogs,
    (Dog d) -> d.getGender() == Dog.Sex.MALE
        && p.getAge() >= 6
);

Consumer

It's a standard functional interface that defines a method accept which accepts the object and return void and is similar to the action behaviour in C#.
We use FilterDog to pass filter behaviour and print filtered dogs, let's allow the passing of print as action behaviour.

public static void printDogs(
    List<Dog> dogs, Predicate<Dog> filterCriteria) {
    for (Dog d : dogs) {
        if (filterCriteria.test(d)) {
            d.printDog();
        }
    }
}

Let's define processDogs method that works action behaviour using Consumer.

public static void processDogs(
    List<Dog> dogs, 
    Predicate<Dog> filterCriteria,
    Consumer<Dog> process) {
    for (Dog d : dogs) {
        if (filterCriteria.test(d)) {
            process.accept(d);
        }
    }
}

The usage now accepts the print method.

printDogs(
    dogs,
    d -> d.getGender() == Dog.Sex.MALE
        && p.getAge() >= 6,
    d -> d.printDog()
);

Function

It's a standard functional interface that defines a method apply which accepts the object and return another object and is similar to Func behaviour in C#.
We use FilterDog to pass filter behaviour and print filtered dogs, let's allow the passing of print as action behaviour.

public static void processDogs(
    List<Dog> dogs, 
    Predicate<Dog> filterCriteria,
    Consumer<Dog> process) {
    for (Dog d : dogs) {
        if (filterCriteria.test(d)) {
            process.accept(d);
        }
    }
}

Let's define processDogsString method that works Function behaviour using Function which accepts Dog as input and returns string as output.

public static void processDogsString(
    List<Dog> dogs, 
    Predicate<Dog> filterCriteria,
    Function<Dog, String> mapper, 
    Consumer<String> process) {
    for (Dog d : dogs) {
        if (filterCriteria.test(d)) {
            String data = mapper.apply(d);
            process.accept(data);
        }
    }
}

The usage now accepts the print method.

processDogsString(
    dogs,
    d -> d.getGender() == Dog.Sex.MALE
        && p.getAge() >= 6,
    d -> d.getName(),
    name -> System.out.println(name)
);

Generics Conclusion

The following method filters, maps and then act.

public static <X, Y> void processElements(
    Iterable<X> source,
    Predicate<X> tester,
    Function <X, Y> mapper,
    Consumer<Y> block) {
    for (X d : source) {
        if (tester.test(d)) {
            Y data = mapper.apply(d);
            block.accept(data);
        }
    }
}

Comparison

Please find the below table comparing various aspects of Lambdas in C# and Java.

C# Java
creaed using delegate keyword delegate bool FilterDog(Dog dog); created using interfaces interface FilterDog { boolean filter(Dog d);}
initialization takes a reference to a method, let's say we have a FilterMaleDog method of type FilterDog. var myFilter = new FilterDog(FilterMaleDog); initialization involves object creation of class, let's say FilterMaleDog, implementing the interface FilterDog. FilterDog myFilter = new FilterMaleDog();
natural usage using method for invoke myFilter(d) mention both interface and method name myFilter.filter(d)
capture variables defined in the parent scope capture only final or effectively final variables
Note: The interfaces Predicate, Consumer and Function in Java maps functionally to Predicate, Action and Func in C#.

References

https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/

https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html#approach4

http://my-techno-arena.blogspot.com/2014/10/lambda-expressions-comparison-between-c.html

https://www.manning.com/books/java-8-in-action