Java Filter Map Reduce Example

This example shows how to use filter, map, reduce techniques in Java using what Java 8 and beyond provides us. And I mean stuff from java.util.* package.

Let’s consider an example where we have some phone bills and we want to calculate the total amount for the last 12 months.

I created this Java pojo that models a phone bill. I added only that relevant things like Date and total. Date is a LocalDate object so we can use the date comparator on objects but we set it using a string (for ease of use).

package net.superglobals;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class PhoneBill {
	private LocalDate date;
	private double total;
	private DateTimeFormatter df = DateTimeFormatter.ofPattern("dd.MM.yyyy");
	
	public PhoneBill(String date, double amount) {
		
		this.date = LocalDate.parse(date, df);
		this.total = amount;
	}
	
	public LocalDate getDate() {
		return date;
	}
	
	public void setDate(LocalDate date) {
		this.date = date;
	}
	
	public double getTotal() {
		return total;
	}
	
	public void setTotal(double total) {
		this.total = total;
	}

	@Override
	public String toString() {
		return "PhoneBill [date=" + date.format(df) + ", total=" + total + "]";
	}
}

Let’s create a method that populates a list of some phone bills.

public static List<PhoneBill> getBills() {
    	List<PhoneBill> result = new LinkedList<>();
    	result.add(new PhoneBill("02.05.2017", 301.01));
    	result.add(new PhoneBill("02.06.2017", 123.16));
    	result.add(new PhoneBill("02.07.2017", 142.59));
    	result.add(new PhoneBill("02.08.2017", 241.35));
    	result.add(new PhoneBill("02.09.2017", 124.48));
    	result.add(new PhoneBill("02.10.2017", 214.68));
    	result.add(new PhoneBill("02.11.2017", 111.14));
    	result.add(new PhoneBill("02.12.2017", 102.68));
    	result.add(new PhoneBill("02.01.2018", 102.91));
    	result.add(new PhoneBill("02.02.2018", 215.35));
    	result.add(new PhoneBill("02.03.2018", 150.14));
    	result.add(new PhoneBill("02.04.2018", 198.79));
    	result.add(new PhoneBill("02.05.2018", 123.35));
    	result.add(new PhoneBill("02.06.2018", 555.14));
    	result.add(new PhoneBill("02.07.2018", 123.98));
    	result.add(new PhoneBill("02.08.2018", 301.12));
    	result.add(new PhoneBill("02.09.2018", 300.35));
    	return result;
}

Now to calculate all in one statement, using the magic Java filter, map, reduce functionality:

package net.superglobals;

import java.time.LocalDate;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;

public class Main {
    public static void main(String[] args){
    	List<PhoneBill> bills = getBills();
    	
    	Optional<Double> total = bills.stream()
    			.filter(phoneBill -> phoneBill.getDate().isAfter(LocalDate.of(2017, 9, 2)))
    			.map(phoneBill -> phoneBill.getTotal())
    			.reduce((totalTilNow, nextValue) -> totalTilNow += nextValue);
    	total.ifPresent(result -> System.out.println(result));
    } 
}

We get

2499.6299999999997

As a result. Makes sense if you add the last 12 bills.

But what happened? Explaining the Java filter, map, reduce tehnique

Let’s re-write this in another way to make things clearer. First, I created a function that we are going to use each time we want to print our collections. It consists of a method that takes an object and returns void. It’s up to the programmer to fill in the logic. In our case I decided to just print the object.

I also added the imports we’re going to need.

package net.superglobals;

import java.time.LocalDate;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args){
    	List<PhoneBill> bills = getBills();

    	Consumer<Object> printerFunction = new Consumer<Object>() {
    		@Override
    		public void accept(Object t) {
    			System.out.println(t);
    		}
    	};
    } 
}

Next we’re going to define our filter function. It is a Predicate that needs to be parameterized with the type of object we want to process. It returns a boolean. When true, the object will be added to the result collection, if false will be skipped.

Here we check if the date is after 2/9/2017. All PhoneBill objects’ dates that evaluate true to this condition will be added to the generated (filtered) collection.

Predicate<PhoneBill> filterFunction = new Predicate<PhoneBill>() {
    @Override
    public boolean test(PhoneBill t) {
    	return t.getDate().isAfter(LocalDate.of(2017, 9, 2));
    }
};
    	
List<PhoneBill> filtered  = bills.stream().filter(filterFunction).collect(Collectors.toList());
filtered.forEach(printerFunction);

We get this result.

PhoneBill [date=02.10.2017, total=214.68]
PhoneBill [date=02.11.2017, total=111.14]
PhoneBill [date=02.12.2017, total=102.68]
PhoneBill [date=02.01.2018, total=102.91]
PhoneBill [date=02.02.2018, total=215.35]
PhoneBill [date=02.03.2018, total=150.14]
PhoneBill [date=02.04.2018, total=198.79]
PhoneBill [date=02.05.2018, total=123.35]
PhoneBill [date=02.06.2018, total=555.14]
PhoneBill [date=02.07.2018, total=123.98]
PhoneBill [date=02.08.2018, total=301.12]
PhoneBill [date=02.09.2018, total=300.35]

Now we’re going for the mapper function. We want from this previous result to add the values, we don’t care about the dates. So we create this function that will ‘trim’ the collection and only return the total values.

This function is parameterized with two values <Argument, Return>. So PhoneBill is the argument and we return a Double from it. If we wanted to return a date we would have made <PhoneBill, LocalDate>.

Function<PhoneBill, Double> mapperFunction = new Function<PhoneBill, Double>() {
    @Override
    public Double apply(PhoneBill t) {
    	return t.getTotal();
    }
};
    	
List<Double> totalForEachBill = filtered.stream().map(mapperFunction).collect(Collectors.toList());
totalForEachBill.forEach(printerFunction);

We get this result. As you can see it’s just the values from the above list. Without the dates.

214.68
111.14
102.68
102.91
215.35
150.14
198.79
123.35
555.14
123.98
301.12
300.35

Finally the last part of the filter, map, reducer Java example. The reducer function is a BinaryOperator. It is parameterized to Double because we accumulate (add) double values. Basically the two params in the function are the first value in the list, and the other and so on.

Basically it works like this:

  • First iteration: previousValue: 214.68, nextValue: 111.14 (2nd elem.)
  • Second iteration: previousValue: 325.82 (214.68 + 111.14), nextValue: 102.68 (3rd elem.)
BinaryOperator<Double> reducerFunction = new BinaryOperator<Double>() {
    @Override
    public Double apply(Double previousValue, Double nextValue) {
    	return previousValue += nextValue;
    }
};
    	
Optional<Double> totalAmount = totalForEachBill.stream().reduce(reducerFunction);
totalAmount.ifPresent(printerFunction);

And we finally get the same result.

2499.6299999999997

Obviously in the case of filter, map, reduce and other Java functional techniques instead of creating anonymous classes from interfaces it’s better and more concise to use lambda expressions.

FacebookTwitterLinkedin