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.
Returning an Optional
- An Optional is created using a factory.
- You can either request an empty Optional or pass a value for the Optional to wrap.
CREATING AN OPTIONAL
10: public static Optional<Double> average(int… scores) { 11: if (scores.length == 0) return Optional.empty(); 12: int sum = 0; 13: for (int score: scores) sum += score; 14: return Optional.of((double) sum / scores.length); 15: }
- Line 11 returns an empty Optional when we can’t calculate an average.
Optional.empty()
- Lines 12 and 13 add up the scores.
- There is a functional programming way of doing this math, but we will get to that later in the chapter. In fact, the entire method could be written in one line, but that wouldn’t teach you how Optional works!
- Line 14 creates an Optional to wrap the average.
Optional.of((double) sum / scores.length)
When creating an Optional, it is common to want to use empty() when the value is null. You can do this with an if statement or ternary operator.
Optional o = (value == null) ? Optional.empty() : Optional.of(value);
Java provides a factory method to do the same thing.
Optional o = Optional.ofNullable(value);
20: Optional<Double> opt = average(90, 100); 21: if (opt.isPresent()) 22: System.out.println(opt.get()); // 95.0
- Line 21 checks whether the Optional actually contains a value.
- Line 22 prints it out.
if we didn’t do the check and the Optional was empty
26: Optional<Double> opt = average(); 27: System.out.println(opt.get()); // NoSuchElementException
We’d get an exception since there is no value inside the Optional.
java.util.NoSuchElementException: No value present
DEALING WITH AN EMPTY OPTIONAL
30: Optional<Double> opt = average(); 31: System.out.println(opt.orElse(Double.NaN)); 32: System.out.println(opt.orElseGet(() -> Math.random()));
This prints something like the following:
NaN 0.49775932295380165
- Line 31 shows that you can return a specific value or variable. In our case, we print the “not a number” value.
- Line 32 shows using a Supplier to generate a value at runtime to return instead. I’m glad our professors didn’t give us a random average, though!
30: Optional<Double> opt = average(); 31: System.out.println(opt.orElseThrow());
This prints something like the following:
Exception in thread "main" java.util.NoSuchElementException: No value present at java.base/java.util.Optional.orElseThrow(Optional.java:382)
- Without specifying a Supplier for the exception, Java will throw a NoSuchElementException.
- This method was added in Java 10. Remember that the stack trace looks weird because the lambdas are generated rather than named classes.
30: Optional<Double> opt = average(); 31: System.out.println(opt.orElseThrow( 32: () -> new IllegalStateException()));
This prints something like the following:
Exception in thread "main" java.lang.IllegalStateException at optionals.Methods.lambda$orElse$1(Methods.java:30) at java.base/java.util.Optional.orElseThrow(Optional.java:408)
- Line 32 shows using a Supplier to create an exception that should be thrown. Notice that we do not write throw new IllegalStateException().
- The orElseThrow() method takes care of actually throwing the exception when we run it.
System.out.println(opt.orElseGet( () -> new IllegalStateException())); // DOES NOT COMPILE
- The opt variable is an
Optional<Double>
. - This means the Supplier must return a Double.
- Since this supplier returns an exception, the type does not match.
Optional<Double> opt = average(90, 100); System.out.println(opt.orElse(Double.NaN)); System.out.println(opt.orElseGet(() -> Math.random())); System.out.println(opt.orElseThrow());
It prints out 95.0 three times. Since the value does exist, there is no need to
use the “or else” logic.