Showing posts with label Java 8. Show all posts

Java 8 introduced default and static methods in interfaces. These features allow us to add new functionality in the interfaces without breaking the existing contract for implementing classes.

How do we define default and static methods?

Default method has default and static method has static keyword in the method signature.

public interface InterfaceA {
  double someMethodA();

  default double someDefaultMethodB() {
    // some default implementation
  }
  
  static void someStaticMethodC() {
    //helper method implementation 
  }

Few important points for default method

  • You can inherit the default method.
  • You can redeclare the default method essentially making it abstract.
  • You can redefine the default method (equivalent to overriding).

Why do we need default and static methods?

Consider an existing Expression interface with existing implementation like ConstantExpression, BinaryExpression, DivisionExpression and so on. Now, you want to add new functionality of returning the signum of the evaluated result and/or want to get the signum after evaluating the expression. This can be done with default and static methods without breaking any functionality as follows.

public interface Expression {
  double evaluate();

  default double signum() {
    return signum(evaluate());
  }

  static double signum(double value) {
    return Math.signum(value);
  }
}

You can find the full code on Github.

Default methods and multiple inheritance ambiguity problem

Java support multiple inheritance of interfaces. Consider you have two interfaces InterfaceA and InterfaceB with same default method and your class implements both the interfaces.

interface InterfaceA {
  void performA();
  default void doSomeWork() {
  
  }
}

interface InterfaceB {
  void performB();

  default void doSomeWork() {
 
  }
}

class ConcreteC implements InterfaceA, InterfaceB {

}

The above code will fail to compile with error: unrelated defaults for doSomeWork() from InterfaceA and InterfaceB.

To overcome this problem, you need to override the default method.

class ConcreteC implements InterfaceA, InterfaceB {
  override
  public void doSomeWork() {

  }
}
If you don't want to provide implementation of overridden default method but want to reuse one. That is also possible with following syntax.
class ConcreteC implements InterfaceA, InterfaceB {
  override
  public void doSomeWork() {
    InterfaceB.super.doSomeWork();
  }
}

I hope you find this post informative and useful. Comments are welcome!!!.

This article is in continuation to my other posts on Functional Interfaces, static and default methods and Lambda expressions.

Method references are the special form of Lambda expression. When your lambda expression are doing nothing other than invoking existing behaviour (method), you can achieve same by referring it by name.

  • :: is used to refer to a method.
  • Method type arguments are infered by JRE at runtime from context it is defined.

Types of method references

  • Static method reference
  • Instance method reference of particular object
  • Instance method reference of an arbitrary object of particular type
  • Constructor reference

Static method reference

When you refer static method of Containing class. e.g. ClassName::someStaticMethodName

class MethodReferenceExample {
  public static int compareByAge(Employee first, Employee second) {
    return Integer.compare(first.age, second.age);
  }
}

Comparator compareByAge = MethodReferenceExample::compareByAge;

Instance method reference of particular object

When you refer to the instance method of particular object e.g. containingObjectReference::someInstanceMethodName

static class MyComparator {
  public int compareByFirstName(User first, User second) {
    return first.getFirstName().compareTo(second.getFirstName());
  }
  
  public int compareByLastName(User first, User second) {
    return first.getLastName().compareTo(second.getLastName());
}

private static void instanceMethodReference() {
  System.err.println("Instance method reference");
  List<User> users = Arrays.asList(new User("Gaurav", "Mazra"),
      new User("Arnav", "Singh"), new User("Daniel", "Verma"));
  MyComparator comparator = new MyComparator();
  System.out.println(users);
  Collections.sort(users, comparator::compareByFirstName);
  System.out.println(users);
}

Instance method reference of an arbitrary object of particular type

When you refer to instance method of some class with ClassName. e.g. ClassName::someInstanceMethod;

Comparator<String> stringIgnoreCase = String::compareToIgnoreCase;
//this is equivalent to
Comparator<String> stringComparator = (first, second) -> first.compareToIgnoreCase(second);

Constructor reference

When you refer to constructor of some class in lambda. e.g. ClassName::new

Function<String, Job> jobCreator = Job::new;
//the above function is equivalent to
Function<String, Job> jobCreator2 = (jobName) -> return new Job(jobName);

You can find the full example on github.

You can also view my other article on Java 8

In this post, we will cover following topics.

  • What are Streams?
  • What is a pipeline?
  • Key points to remember for Streams.
  • How to create Streams?

What are Streams?

Java 8 introduced new package java.util.stream which contains classes to perform SQL-like operations on elements. Stream is a sequence of elements on which you can perform aggregate operations (reduction, filtering, mapping, average, min, max etc.). It is not a data structure that stores elements like collection but carries values often lazily computed from source through pipeline.

What is a pipeline?

A pipeline is sequence of aggregate (reduction and terminal) operations on the source. It has following components.

  • A source: Collections, Generator Function, array, I/O channel etc.
  • zero or more intermediate operations: filter, map, sequential, sorted, distinct, limit, flatMap, parallel etc. Intermediate operations returns/produces stream.
  • a termination operation: forEach, reduction, noneMatch, allMatch, count, findFirst, findAny, min, max etc.

Key points to remember for Streams

  • No storage.
  • Functional in nature.
  • Laziness-seeking.
  • Possibly unbounded. Operations, for example, limit(n) or findFirst() can permit calculations on infinite streams to finish in finite time.
  • Consumable. The elements can be visited only once. To revisit, you need to create a new stream.

How to create Streams?

1. In Collection, you can create streams by calling stream(), parallelStream().

Collection<Person> persons = StreamSamples.getPersons();
persons.stream().forEach(System.out::println);

// parallel stream
persons.parallelStream().forEach(System.out::println);

2. From Stream interface, calling static factory method of() which takes varargs of T type.

Stream.of("This", "is", "how", "you", "create", "stream", "from", "static", "factory",
      "method").map(s -> s.concat(" ")).forEach(System.out::print);

3. From Arrays class, by calling stream() static method.

Arrays.stream(new String[] { "This", "is", "how", "you", "create", "stream", ".",
      "Above", "function", "use", "this" }).map(s -> s.concat(" "))
      .forEach(System.out::print);

4. From Stream by calling iterate(). It is infinite stream function.

// iterate return infinite stream... beware of infinite streams
Stream.iterate(1, i -> i++).limit(10).forEach(System.out::print);

5. From IntStream by calling range.

int sumOfFirst10PositiveNumbers = IntStream.range(1, 10).reduce(0, Integer::sum);
System.out.println(sumOfFirst10PositiveNumbers);

6. From Random by calling ints(). It is infinite stream function.

// random.ints for random number
new Random().ints().limit(20).forEach(System.out::println);

7. From BufferedReader by calling lines(). Streams of file paths can be obtained by calling createDirectoryStream of Files class and some other classes like JarFile.stream(), BitSet.stream() etc.

try (BufferedReader br = new BufferedReader(new StringReader(myValue))) {
  br.lines().forEach(System.out::print);
  System.out.println();
}
catch (IOException io) {
  System.err.println("Got this:>>>> " + io);
}

I hope the post is informative and helpful in understanding Streams. You can find the full example code on Github.

You can also read on Aggregate opeations on Stream.

This post is in continuation with my earlier posts on Streams. In this post we will discuss about aggregate operations on Streams.

Aggregate operations on Streams

You can perform intermediate and terminal operations on Streams. Intermediate operations result in a new stream and are lazily evaluated and will start when terminal operation is called.

persons.stream().filter(p -> p.getGender() == Gender.MALE).forEach(System.out::println);

In the snippet above, filter() doesn't start filtering immediately but create a new stream. It will only start when terminal operation is called and in above case when forEach().

Intermediate operations

There are many intermediate operations that you can perform on Streams. Some of them are filter(), distinct(), sorted(), limit(), parallel(), sequential, map(), flatMap.

filter() operation

This takes Predicate functional interface as argument and the output stream of this operation will have only those elements which pass the conditional check of Predicate. You can learn a nice explanation on Predicates here.

// all the males
List<Person> allMales = persons.stream().filter(p -> p.getGender() == Gender.MALE).collect(Collectors.toList());
System.out.println(allMales);

map() operation

It is a mapper operation. It expects Function functional interface as argument. Purpose of Function is to transform from one type to other (The other type could be same).

// first names of all the persons
List<String> firstNames = persons.stream().map(Person::getFirstName).collect(Collectors.toList());
System.out.println(firstNames);

distinct()

It returns the unique elements and uses equals() under the hood to remove duplicates.

List<String> uniqueFirstNames = persons.stream().map(Person::getFirstName).distinct().collect(Collectors.toList());

System.out.println(uniqueFirstNames);

sorted()

Sorts the stream elements. It is stateful operation.

List<Person> sortedByAge = persons.stream().sorted(Comparator.comparingInt(Person::getAge)).collect(Collectors.toList());
System.out.println(sortedByAge);

limit() will reduce the number of records. It is helpful to end infinite streams in a finite manner.

Intemediate operations can be divided to two parts stateless and stateful. Most of the streams intermediate operations are stateless e.g. map, filter, limit etc. but some of them are stateful e.g. distinct and sorted because they have to maintain the state of previously visited element.

Terminal/ Reduction operations

There are many terminal operations such as forEach(), reduction(), max(), min(), average(), collect(), findAny, findFirst(), allMatch(), noneMatch().

forEach()

This takes Consumer functional interface as parameter and pass on the element for consumption.

persons.stream().forEach(System.out::println);

max(), min(), average() operations

average() returns OptionalDouble whereas max() and min() return OptionalInt.

//average age of all persons
persons.stream().mapToInt(Person::getAge).average().ifPresent(System.out::println);

// max age from all persons
persons.stream().mapToInt(Person::getAge).max().ifPresent(System.out::println);

// min age from all persons
persons.stream().mapToInt(Person::getAge).min().ifPresent(System.out::println);

noneMatch(), allMatch(), anyMatch()

matches if certain condition satisfies by none, all and/or any elements of stream respectively.

//age of all females in the group is less than 22
persons.stream().filter(p -> p.getGender() == Gender.FEMALE).allMatch(p -> p.getAge() < 22);
//not a single male's age is greater than 30
persons.stream().filter(p -> p.getGender() == Gender.MALE).noneMatch(p -> p.getAge() > 30);

persons.stream().anyMatch(p -> p.getAge() > 45);

Reduction operations

Reduction operations are those which provide single value as result. We have seen in previous snippet some of the reduction operation which do this. E.g. max(), min(), average(), sum() etc. Apart from this, Java 8 provides two more general purpose operations reduce() and collect().

reduce()

int sumOfFirst10 = IntStream.range(1, 10).reduce(0, Integer::sum);
System.out.println(sumOfFirst10);

collect()

It is a mutating reduction. Collectors has many useful collection methods like toList(), groupingBy(),

Collection<Person> persons = StreamSamples.getPersons();
List firstNameOfPersons = persons.stream().map(Person::getFirstName).collect(Collectors.toList());
System.out.println(firstNameOfPersons);

Map<Integer, List<Person>> personByAge = persons.stream().collect(Collectors.groupingBy(Person::getAge));
System.out.println(personByAge);

Double averageAge = persons.stream().collect(Collectors.averagingInt(Person::getAge));
System.out.println(averageAge);

Long totalPersons = persons.stream().collect(Collectors.counting());
System.out.println(totalPersons);

IntSummaryStatistics personsAgeSummary = persons.stream().collect(Collectors.summarizingInt(Person::getAge));

System.out.println(personsAgeSummary);

String allPersonsFirstName = persons.stream().collect(Collectors.mapping(Person::getFirstName, Collectors.joining("#")));
System.out.println(allPersonsFirstName);

The result would look like this.

[Gaurav, Gaurav, Sandeep, Rami, Jiya, Rajesh, Rampal, Nisha, Neha, Ramesh, Parul, Sunil, Prekha, Neeraj]
{32=[Person [firstName=Rami, lastName=Aggarwal, gender=FEMALE, age=32, salary=12000]], 35=[Person [firstName=Rampal, lastName=Yadav, gender=MALE, age=35, salary=12000]], 20=[Person [firstName=Prekha, lastName=Verma, gender=FEMALE, age=20, salary=3600]], 21=[Person [firstName=Neha, lastName=Kapoor, gender=FEMALE, age=21, salary=5500]], 22=[Person [firstName=Jiya, lastName=Khan, gender=FEMALE, age=22, salary=4500], Person [firstName=Ramesh, lastName=Chander, gender=MALE, age=22, salary=2500]], 24=[Person [firstName=Sandeep, lastName=Shukla, gender=MALE, age=24, salary=5000]], 25=[Person [firstName=Parul, lastName=Mehta, gender=FEMALE, age=25, salary=8500], Person [firstName=Neeraj, lastName=Shah, gender=MALE, age=25, salary=33000]], 26=[Person [firstName=Nisha, lastName=Sharma, gender=FEMALE, age=26, salary=10000]], 27=[Person [firstName=Sunil, lastName=Kumar, gender=MALE, age=27, salary=6875]], 28=[Person [firstName=Gaurav, lastName=Mazra, gender=MALE, age=28, salary=10000], Person [firstName=Gaurav, lastName=Mazra, gender=MALE, age=28, salary=10000]], 45=[Person [firstName=Rajesh, lastName=Kumar, gender=MALE, age=45, salary=55000]]}
27.142857142857142
14
IntSummaryStatistics{count=14, sum=380, min=20, average=27.142857, max=45}
Gaurav#Gaurav#Sandeep#Rami#Jiya#Rajesh#Rampal#Nisha#Neha#Ramesh#Parul#Sunil#Prekha#Neeraj

You can't consume same Streams twice

When the terminal operation is completed on stream, it is considered consumed and you can't use it again. You will end up with exception if you try to start new operations on already consumed stream.

Stream<String> stream = lines.stream();
stream.reduce((a, b) -> a.length() > b.length() ? a : b).ifPresent(System.out::println);

// below line will throw the exception
stream.forEach(System.out::println);
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
 at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
 at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
 at com.gauravbytes.java8.stream.StreamExceptionExample.main(StreamExceptionExample.java:18)

Parallelism

Streams provide a convenient way to execute operations in parallel. It uses ForkJoinPool under the hood to run stream operations in parallel. You can use parallelStream() or parallel() on already created stream to perform task parallelly. One thing to note parallelism is not automatically faster than running task in serial unless you have enough data and processor cores.

persons.parallelStream().filter(p -> p.getAge() > 30).collect(Collectors.toList());
Pass java.util.concurrent.ForkJoinPool.common.parallelism property while JVM startup to increase parallelism in fork-join pool.

Concurrent reductions

ConcurrentMap<Integer, List<Person>> personByAgeConcurrent = persons.stream().collect(Collectors.groupingByConcurrent(Person::getAge));
System.out.println(personByAgeConcurrent);
Prevent interference, side-effects and stateful lambda/functions.

Side effects

If the function is doing more than consuming and/ or returning value, like modifying the state is said to have side-effects. A common example of side-effect is forEach(), mutable reduction using collect(). Java handles side-effects in collect() in thread-safe manner.

Interference

You should avoid interference in your lambdas/ functions. It occurs when you modify the underlying collection while running pipeline operations.

Stateful Lambda expressions

A lambda expression is stateful if its result depends on any state which can alter/ change during execution. Avoid using stateful lambdas expressions. You can read more here.

I hope you find this post informative and helpful. You can find the example code for reduction, aggregate operation and stream creation on Github.

In this post, we will cover following topics.

  • What are Lambda expressions?
  • Syntax for Lambda expression.
  • How to define no parameter Lambda expression?
  • How to define single/ multi parameter Lambda expression?
  • How to return value from Lambda expression?
  • Accessing local variables in Lambda expression.
  • Target typing in Lambda expression.

What are Lambda expressions?

Lambda expressions are the first step of Java towards functional programming. Lambda expressions enable us to treat functionality as method arguments, express instances of single-method classes more compactly.

Syntax for Lambda expression

Lambda has three parts:

  • comma separated list of formal parameters enclosed in parenthesis.
  • arrow token ->.
  • and, body of expression (which may or may not return value).

(param) -> { System.out.println(param); }
Lambda expression can only be used where the type they are matched are functional interfaces.

How to define no parameter Lambda expression?

If the lambda expression is matching against no parameter method, it can be written as:

() -> System.out.println("No paramter expression");

How to define single/ multi parameter Lambda expression?

If lambda expression is matching against method which take one or more parameter, it can be written as:

(param) -> System.out.println("Single param expression: " + param);

(paramX, paramY) -> System.out.println("Two param expression: " + paramX + ", " + paramX);

You can also define the type of parameter in Lambda expression.

(Employee e) -> System.out.println(e);

How to return value from Lambda expression?

You can return value from lambda just like a method did.

(param) -> {
  // perform some steps
  return "some value";
};

In case lambda is performing single step and returning value. Then you can write it as:

//either
(int a, int b) -> return Integer.compare(a, b);

// or simply lambda will automatically figure to return this value
(int a, int b) -> Integer.compare(a, b);

Accessing local variables in Lambda expression

Lambda can access the final or effectively final variables of the method in which they are defined. They can also access the instance variables of enclosing class.

Target typing in Lambda expression

You might have seen in earlier code snippets that we have omitted the type of parameter, return value and the type of Lambda. Java compiler determines the target type from the context lambda is defined.

Compiler checks three things:

  • Is the target type functional interface?
  • Is list of parameter and its type matched with the single method?
  • Does the return type matched with the single method return type?

Now, Let's jump to an example to verify it.

@FunctionalInterface
interface InterfaceA {
  void doWork();
}

@FunctionalInterface
interface InterfaceB<T> {
  T doWork();
}

class LambdaTypeCheck {
  
  public static void main (String[] args) {
    LambdaTypeCheck typeCheck = new LambdaTypeCheck();
    typeCheck.invoke(() -> "I am done with you");
  }
  
  public <T> T invoke (InterfaceB<T> task) {
    return task.doWork();
  }

  public void invoke (InterfaceA task) {
    task.doWork();
  }
}
When you call typeCheck.invoke(() -> "I am done with you"); then invoke(InterfaceB<T> task) will be called. Because the lambda return value which is matched by InterfaceB<T>.

Java 8 reincarnated SAM interfaces and termed them Functional interfaces. Functional interfaces have single abstract method and are eligible to be represented with Lambda expression. @FunctionalInterface annotation is introduced in Java 8 to mark an interface as functional. It ensures at compile-time that it has only single abstract method, otherwise it will throw compilation error.

Let's define a functional interface.

@FunctionalInterface
public interface Spec<T> {
  boolean isSatisfiedBy(T t);
}

Functional interfaces can have default and static methods in them and still remains functional interface.

@FunctionalInterface
public interface Spec<T> {
  boolean isSatisfiedBy(T t);
 
  default Spec<T> not() {
    return (t) -> !isSatisfiedBy(t);
  }
 
  default Spec<T> and(Spec<T> other) {
    return (t) -> isSatisfiedBy(t) && other.isSatisfiedBy(t);
  }
 
  default Spec<T> or(Spec<T> other) {
    return (t) -> isSatisfiedBy(t) || other.isSatisfiedBy(t);
  }
}
If an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface's abstract method count since any implementation of the interface will have an implementation from java.lang.Object or elsewhere.

Iterating Collections API

Java 8 introduced new way of iterating Collections API. It is retrofitted to support #forEach method which accepts Consumer in case of Collection and BiConsumer in case of Map.

Consumer

Java 8 added introduced new package java.util.function which also includes Consumer interface. It represents the operation which accepts one argument and returns no result.

Before Java 8, you would have used for loop, extended for loop and/ or Iterator to iterate over Collections .

List<Employee> employees = EmployeeStub.getEmployees();
Iterator<Employee> employeeItr = employees.iterator();
Employee employee;
while (employeeItr.hasNext()) {
  employee = employeeItr.next();
  System.out.println(employee);
}

In Java 8, you can write Consumer and pass the reference to #forEach method for performing operation on every item of Collection.

// fetch employees from Stub
List<Employee> employees = EmployeeStub.getEmployees();
// create a consumer on employee
Consumer<Employee> consolePrinter = System.out::println;
// use List's retrofitted method for iteration on employees and consume it
employees.forEach(consolePrinter);

Or Just one liner as

employees.forEach(System.out::println);

Before Java 8, you would have iterated Map as

Map<Long, Employee> idToEmployeeMap = EmployeeStub.getEmployeeAsMap();
for (Map.Entry<Long, Employee> entry : idToEmployeeMap.entrySet()) {
  System.out.println(entry.getKey() + " : " + entry.getValue());
}

In Java 8, you can write BiConsumer and pass the reference to #forEach method for performing operation on every item of Map.

BiConsumer<Long, Employee> employeeBiConsumer = (id, employee) -> System.out.println(id + " : " + employee);
Map<Long, Employee> idToEmployeeMap = EmployeeStub.getEmployeeAsMap();
idToEmployeeMap.forEach(employeeBiConsumer);

or Just a one liner:

idToEmployeeMap.forEach((id, employee) -> System.out.println(id + " : " + employee));

This is how we can benefit with newly introduced method for iteration. I hope you found this post informative. You can get the full example on Github.

In this post, we will cover following items.

  • What is java.util.function.Predicate?
  • How to filter data with Predicates?
  • Predicate chaining.

Java 8 introduced many new features like Streaming API, Lambdas, Functional interfaces, default methods in interfaces and many more.

Today, we will discuss about Predicate interface added in java.util.function package and its usage in filtering in-memory data.

What is java.util.function.Predicate?

Predicate is like a condition checker, which accepts one argument of type T and return the boolean value.

It's a functional interface with functional method test(Object). Here, Object is typed.

@FunctionalInterface
interface Predicate<T> {
  public boolean test(T t);
}

How we can filter data with Predicates?

Consider we have Collection of employees and we want to filter them based on age, sex, salary and/ or with any other combinations. We can do that with Predicate.

Let's understand this with one short example.

class Employee {
  private long id;
  private String firstName;
  private String lastName;
  private int age;
  private Sex sex;
  private int salary;

  // getters, constructor, hashCode, equals, to String
}

Defining predicates for filtering

Predicate<Employee> male = e -> e.getSex() == Sex.MALE;
Predicate<Employee> female = e -> e.getSex() == Sex.FEMALE;
Predicate<Employee> ageLessThan30 = e -> e.getAge() < 30;
Predicate<Employee> salaryLessThan20 = e -> e.getSalary() < 20000;
Predicate<Employee> salaryGreaterThan25 = e -> e.getSalary() > 25000;

Filtering employees with Predicates

employees.stream().filter(male).collect(Collectors.toList());
employees.stream().filter(female).collect(Collectors.toList());
employees.stream().filter(ageLessThan30).collect(Collectors.toList());
employees.stream().filter(salaryLessThan20).collect(Collectors.toList());

Here, employees reference is of type java.util.List.

Collections framework is retrofitted for Streaming API and have stream() and parallelStream() methods along with few other additions.filter() method is defined in Stream. We are streaming employees collection and filtering them based on the Predicate and then collecting as java.util.List.

Predicate chaining

java.util.function.Predicate have three default method. Two of them and(Predicate<T> other) and or(Predicate<T> other) is used for predicate chaining.

Filtering employees with multiple predicates

Let's say, we want to filter collection of employees which involves multiple conditions like

  • all male salary less than 20k.
  • all female salary greater than 25k.
  • all male salary either less than 20 k or greater than 25k.

Let's understand this with quick example.

Defining predicates

Predicate<Employee> male = e -> e.getSex() == Sex.MALE;
Predicate<Employee> female = e -> e.getSex() == Sex.FEMALE;
Predicate<Employee> ageLessThan30 = e -> e.getAge() < 30;
Predicate<Employee> salaryLessThan20 = e -> e.getSalary() < 20000;
Predicate<Employee> salaryGreaterThan25 = e -> e.getSalary() > 25000;
Predicate<Employee> salaryLessThan20OrGreateThan25 = salaryLessThan20.or(salaryGreaterThan25);

Predicate<Employee> allMaleSalaryLessThan20 = male.and(salaryLessThan20);
Predicate<Employee> allMaleAgeLessThan30 = male.and(ageLessThan30);
Predicate<Employee> allFemaleSalaryGreaterThan25 = female.and(salaryGreaterThan25);

Predicate<Employee> allMaleSalaryLessThan20OrGreateThan25 = male.and(salaryLessThan20OrGreateThan25);

Line 1 => Predicate test for employee male

Line 2 => Predicate test for employee female

Line 3 => Predicate test for employee age less than 30

Line 4 => Pedicate test for employee salary less than 20000

Line 8 => Predicate test for employee male and salary less than 20000

Line 10 => Predicate test for employee female and salary greater than 25000

Line 12 => Predicate test for employee male and salary either less than 20000 or greater than 25000

Filtering employees with predicate chaining

employees.stream().filter(allMaleSalaryLessThan20).collect(Collectors.toList());
employees.stream().filter(allMaleAgeLessThan30).collect(Collectors.toList());
employees.stream().filter(allFemaleSalaryGreaterThan25).collect(Collectors.toList());
employees.stream().filter(allMaleSalaryLessThan20OrGreateThan25).collect(Collectors.toList());

This is how we can use Predicate to filter in-memory data. I hope you find this post informative and helpful. You can get the full example code on Github.