Chapter 15 Functional Programming Flashcards
Working with Built‐in Functional Interfaces
- provided in the
java.util.function
package
IMPLEMENTING SUPPLIER
A Supplier is used when you want to generate or supply values without taking any input. The Supplier interface is defined as follows:
@FunctionalInterface public interface Supplier<T> { T get(); }
- create a
LocalDate
object using the factory methodnow()
. - The
LocalDate::now
method reference is used to create a Supplier to assign to an intermediate variable s1.
Supplier<LocalDate> s1 = LocalDate::now; Supplier<LocalDate> s2 = () -> LocalDate.now(); LocalDate d1 = s1.get(); LocalDate d2 = s2.get(); System.out.println(d1); System.out.println(d2);
- A Supplier is often used when constructing new objects.
- we used a constructor reference to create the object.
- using generics to declare what type of Supplier we are using.
Supplier<StringBuilder> s1 = StringBuilder::new; Supplier<StringBuilder> s2 = () -> new StringBuilder(); System.out.println(s1.get()); System.out.println(s2.get());
Supplier<ArrayList<String>> s3 = ArrayList<String>::new; ArrayList<String> a1 = s3.get(); System.out.println(a1);
What would happen if we tried to print out s3 itself?
System.out.println(s3);
The code prints something like this:
functionalinterface.BuiltIns\$\$Lambda$1/0x0000000800066840@4909b8da
- That’s the result of calling toString() on a lambda.
- test class is named BuiltIns
- in a package that we created named functionalinterface.
- $$, which means that the class doesn’t exist in a class file on the file system. It exists only in memory.
IMPLEMENTING CONSUMER AND BICONSUMER
- You use a Consumer when you want to do something with a parameter but not return anything.
- BiConsumer does the same thing except that it takes two parameters.
- The interfaces are defined as follows:
@FunctionalInterface public interface Consumer<T> { void accept(T t); // omitted default method } @FunctionalInterface public interface BiConsumer<T, U> { void accept(T t, U u); // omitted default method }
Consumer<String> c1 = System.out::println; Consumer<String> c2 = x -> System.out.println(x); c1.accept("Annie"); c2.accept("Annie");
This example prints Annie twice.
var map = new HashMap<String, Integer>(); BiConsumer<String, Integer> b1 = map::put; BiConsumer<String, Integer> b2 = (k, v) -> map.put(k, v); b1.accept("chicken", 7); b2.accept("chick", 1); System.out.println(map);
- BiConsumer is called with two parameters.
- They don’t have to be the same type.
- The output is {chicken=7, chick=1},
- which shows that both BiConsumer implementations did get called.
- When declaring b1, we used an instance method reference on an object since we want to call a method on the local variable map.
- The code to instantiate b1 is a good bit shorter than the code for b2.
var map = new HashMap<String, String>(); BiConsumer<String, String> b1 = map::put; BiConsumer<String, String> b2 = (k, v) -> map.put(k, v); b1.accept("chicken", "Cluck"); b2.accept("chick", "Tweep"); System.out.println(map);
- The output is {chicken=Cluck, chick=Tweep},
- which shows that a BiConsumer can use the same type for both the T and U generic parameters.
IMPLEMENTING PREDICATE AND BIPREDICATE
- You saw Predicate with removeIf() in Chapter 14.
- Predicate is often used when filtering or matching.
- A BiPredicate takes two parameters instead of one.
- The interfaces are defined as follows:
@FunctionalInterface public interface Predicate<T> { boolean test(T t); // omitted default and static methods } @FunctionalInterface public interface BiPredicate<T, U> { boolean test(T t, U u); // omitted default methods }
Predicate<String> p1 = String::isEmpty; Predicate<String> p2 = x -> x.isEmpty(); System.out.println(p1.test("")); // true System.out.println(p2.test("")); // true
This prints true twice.
BiPredicate<String, String> b1 = String::startsWith; BiPredicate<String, String> b2 = (string, prefix) -> string.startsWith(prefix); System.out.println(b1.test("chicken", "chick")); // true System.out.println(b2.test("chicken", "chick")); // true
- The method reference includes both the instance variable and parameter for startsWith().
- This is a good example of how method references save a good bit of typing.
IMPLEMENTING FUNCTION AND BIFUNCTION
- In Chapter 14, we used Function with the merge() method.
- A Function is responsible for turning one parameter into a value of a potentially different type and returning it.
- Similarly, a BiFunction is responsible for turning two parameters into a value and returning it.
- The interfaces are defined as follows:
@FunctionalInterface public interface Function<T, R> { R apply(T t); // omitted default and static methods } @FunctionalInterface public interface BiFunction<T, U, R> { R apply(T t, U u); // omitted default method }
Function<String, Integer> f1 = String::length; Function<String, Integer> f2 = x -> x.length(); System.out.println(f1.apply("cluck")); // 5 System.out.println(f2.apply("cluck")); // 5
- This function turns a String into an Int, which is autoboxed into an Integer
- The types don’t have to be different.
BiFunction<String, String, String> b1 = String::concat; BiFunction<String, String, String> b2 = (string, toAdd) -> string.concat(toAdd); System.out.println(b1.apply("baby ", "chick")); // baby chick System.out.println(b2.apply("baby ", "chick")); // baby chick
- The first two types in the BiFunction are the input types.
- The third is the result type.
- For the method reference, the first parameter is the instance that concat() is called on, and the second is passed to concat().
CREATING YOUR OWN FUNCTIONAL INTERFACES
- Java provides a built‐in interface for functions with one or two parameters.
- You could create a functional interface such as this:
3 paramters:
@FunctionalInterface interface TriFunction<T,U,V,R> { R apply(T t, U u, V v); }
4 parameters:
@FunctionalInterface interface QuadFunction<T,U,V,W,R> { R apply(T t, U u, V v, W w); }
Remember that you can add any functional interfaces you’d like, and Java matches them when you use lambdas or method references.
IMPLEMENTING UNARYOPERATOR AND BINARYOPERATOR
- UnaryOperator and BinaryOperator are a special case of a Function.
- They require all type parameters to be the same type.
- A UnaryOperator transforms its value into one of the same type.
- UnaryOperator extends Function.
- A BinaryOperator merges two values into one of the same type.
- BinaryOperator extends BiFunction.
- The interfaces are defined as follows:
@FunctionalInterface public interface UnaryOperator<T> extends Function<T, T> { } @FunctionalInterface public interface BinaryOperator<T> extends BiFunction<T, T, T> { // omitted static methods }
method signatures look like this:
T apply(T t); // UnaryOperator T apply(T t1, T t2); // BinaryOperator
UnaryOperator<String> u1 = String::toUpperCase; UnaryOperator<String> u2 = x -> x.toUpperCase(); System.out.println(u1.apply("chirp")); // CHIRP System.out.println(u2.apply("chirp")); // CHIRP
This prints CHIRP twice.
We don’t need to specify the return type in the generics because UnaryOperator requires it to be the same as the parameter
BinaryOperator<String> b1 = String::concat; BinaryOperator<String> b2 = (string, toAdd) -> string.concat(toAdd); System.out.println(b1.apply("baby ", "chick")); // baby chick System.out.println(b2.apply("baby ", "chick")); // baby chick
CHECKING FUNCTIONAL INTERFACES
What functional interface would you use in these three situations?
- Returns a String without taking any parameters
- Returns a Boolean and takes a String
- Returns an Integer and takes two Integers
Supplier<String>
-
Function<String, Boolean>
Predicate<String>
. Note that a Predicate returns a boolean primitive and not a Boolean object. -
BinaryOperator<Integer>
or aBiFunction<Integer,Integer,Integer>
BinaryOperator<Integer>
is the better answer of the two since it is more specific.
What functional interface would you use to fill in the blank for these?
6: \_\_\_\_\_\_\_<List> ex1 = x -> "".equals(x.get(0)); 7: \_\_\_\_\_\_\_<Long> ex2 = (Long l) -> System.out.println(l); 8: \_\_\_\_\_\_\_<String, String> ex3 = (s1, s2) -> false;
-
Line 6 Predicate
Since the generic declaration has only one parameter, it is a Predicate. - Line 7 passes one Long parameter to the lambda and doesn’t return anything. This tells us that it is a Consumer.
- Line 8, there are two parameters, so it is a BiPredicate.
These are meant to be tricky:
6: Function<List<String>> ex1 = x -> x.get(0); // DOES NOT COMPILE 7: UnaryOperator<Long> ex2 = (Long l) -> 3.14; // DOES NOT COMIPLE 8: Predicate ex4 = String::isEmpty; // DOES NOT COMPILE
- Line 6 claims to be a Function. A Function needs to specify two generics—the input parameter type and the return value type. The return value type is missing from line 6, causing the code not to compile.
- Line 7 is a UnaryOperator, which returns the same type as it is passed in. The example returns a double rather than a Long, causing the code not to compile.
- Line 8 is missing the generic for Predicate. This makes the parameter that was passed an Object rather than a String. The lambda expects a String because it calls a method that exists on String rather than Object. Therefore, it doesn’t compile.
CONVENIENCE METHODS ON FUNCTIONAL INTERFACES
Several of the common functional interfaces provide a number of helpful default methods.
It’s a bit long to read, and it contains duplication.
What if we decide the letter e should be capitalized in egg?
Predicate<String> egg = s -> s.contains("egg"); Predicate<String> brown = s -> s.contains("brown"); Predicate<String> brownEggs = s -> s.contains("egg") && s.contains("brown"); Predicate<String> otherEggs = s -> s.contains("egg") && ! s.contains("brown");
- reusing the logic in the original Predicate variables to build two new ones.
- It’s shorter and clearer what the relationship is between variables.
- We can also change the spelling of egg in one place, and the other two objects will have new logic because they reference it.
Predicate<String> egg = s -> s.contains("egg"); Predicate<String> brown = s -> s.contains("brown"); Predicate<String> brownEggs = egg.and(brown); Predicate<String> otherEggs = egg.and(brown.negate());
Consumer<String> c1 = x -> System.out.print("1: " + x); Consumer<String> c2 = x -> System.out.print(",2: " + x); Consumer<String> combined = c1.andThen(c2); combined.accept("Annie"); // 1: Annie,2: Annie
- andThen() method, which runs two functional interfaces in sequence.
- Notice how the same parameter gets passed to both c1 and c2.
- This shows that the Consumer instances are run in sequence and are independent of each other.
Function<Integer, Integer> before = x -> x + 1; Function<Integer, Integer> after = x -> x * 2; Function<Integer, Integer> combined = after.compose(before); System.out.println(combined.apply(3)); // 8
- compose() method on Function chains functional interfaces.
- This time the before runs first, turning the 3 into a 4.
- Then the after runs, doubling the 4 to 8.