Java 8 official logo
Heeeey people!
One more article about the Java 8 features! Oh yeah!
Now we’ll talk about Two-Argument (Bi) Functional Interfaces and Primitive Type Functional Interfaces and understand what the benefits are when working with Primitive Types combined with Functional Interfaces.
If you did not read the second part, I strongly suggest you read it before start here!
Two-Argument (Bi) Functional Interfaces
Ok, now we already saw all the predefined functional interfaces, and I’m sure that you realized something really interesting.
The number of input parameter?
You are simply AMAZING!
Yes, you are right! Function, Predicate, and Supplier can only receive 1 input parameter, and in some cases, we need another one, how can we handle this case?
Simple!
Java 8 provided us with BiFunctional Interfaces from each Predefined Functional Interface (excluding Supplier because the abstract method of Supplier does not receive any parameter).
It means that we have BiFunction, BiPredicate, and BiConsumer that receive 2 input parameters!
Wow, nice! But are they exactly the same as the Single input versions but with 2 input parameters?
Unfortunately no :(
Let’s talk about BiFunction first!
BiFunction
Like the single parameter version, BiFunction has an abstract method called apply but now receiving 2 parameters.
Method signature:
R apply(T t, U u);As you can see, the first type of BiFunction<T, U, R> is T, the middle one is U and the last one is R, where T is the type of the first parameter, U is the type of the second parameter and R is the of the return.
So, what is the difference?
Do you remember the default and static methods of the Function?
Yes, now we only have 1 method called andThen, it means that we don’t have a default method compose and the static method identity.
And the curious fact here is: The default method andThen of a BiFunction interface receives a Function as a parameter, not a BiFunction. So, it is not possible to create a chain of BiFunctions.
Example:
BiFunction<Integer, Integer, String> calcAndToString = (first, second) -> String.valueOf(first + second);
Function<String, Integer> add2 = first -> Integer.valueOf(first) + 2;
calcAndToString.andThen(add2).apply(1, 10);BiPredicate
Different from BiFunction, the BiPredicate<T, U> interface has almost the same methods as the Predicate interface, it is only missing the static method isEquals, and the default methods and andor are receiving a BiPredicate as a parameter, so, here it’s possible to create a BiPredicate chain.
Example:
BiPredicate<String, String> contains = (first, second) -> first.contains(second);
BiPredicate<String, String> startsWith = (first, second) -> first.startsWith(second);
// Will return true because all conditions are true
System.out.println(contains.and(startsWith).test("Julio Falbo", "Julio"));
// Will return true because contains BiPredicate is true
System.out.println(contains.or(startsWith).test("Julio Falbo", "Falbo"));
// Will return false because contains BiPredicate is true and we called the method negate
System.out.println(contains.negate().test("Julio Falbo", "Falbo"));BiConsumer
BiConsumer is the same as Consumer but receiving 2 input parameters. It means that BiConsumer also has an andThen and unlike the BiFunction, it is receiving a BiConsumer as a parameter!
Example:
BiConsumer<String, String> printLove = (first, second) -> System.out.println("I (" + first + ") love " + second);
BiConsumer<String, String> printFamily = (first, second) ->
System.out.println("I also love my wife and my daughter (and " + second + ")");
printLove.andThen(printAnotherPhrase).accept("Julio Falbo", "Java");BinaryOperator
Like a UnaryOperator, BinaryOperator brings the concept of a single type, but now extending a BiFunction.
It means that BinaryOperator is a BiFunction but with both input parameters and returns with the same type!
Example:
BiFunction<Integer, Integer, Integer> addBi = (a, b) -> a + b;
// Result will be 4
System.out.println(addBi.apply(2,2));
BinaryOperator<Integer> addBinary = (a, b) -> a + b;
// Result will be 4
System.out.println(addBinary.apply(2,2));Ok Julio, but do we have something more?
Unlike UnaryOperator, BinaryOperator has 2 extras static methods: minBy and maxBy.
The idea here is to give to us the flexibility to implement our custom logic to compare 2 objects easily.
Note before starting: Comparatoris a Legacy Functional Interface added in Java 1.2 and Java 8 introduced several enhancements to this interface. I’ll not explain those enhancements but if you want to go deep I can suggest this amazing article.
Method: minBy
Implementation:
public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
}This method returns a BinaryOperator which returns the lesser of the two elements according to the specified Comparator.
Example:
BinaryOperator<Integer> minBinaryOperator = BinaryOperator.minBy((a, b) -> (a > b) ? 1 : ((a == b) ? 0 : -1));
System.out.println(minBinaryOperator.apply(2, 4));The code above will print 2 because in the custom logic I’m saying to return
- 1 if the first parameter is bigger than the second parameter
- 0 if they are equals
- -1 if the first parameter is lesser than the second parameter
And I’m calling the method minBy, so if the result of the comparison is lesser than/equal 0 it will return the first parameter (because it already knows which is the lesser value based on the custom compare logic)
Method: maxBy
Implementation:
public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
}This method returns a BinaryOperator which returns the greater of the two elements according to the specified Comparator.
Example:
BinaryOperator<Integer> maxBinaryOperator = BinaryOperator.maxBy((a, b) -> (a > b) ? 1 : ((a == b) ? 0 : -1));
System.out.println(maxBinaryOperator.apply(2, 4));The code above will print 4 because in the custom logic I’m saying to return
- 1 if the first parameter is bigger than the second parameter
- 0 if they are equals
- -1 if the first parameter is lesser than the second parameter
And I’m calling the method maxBy, so if the result of the comparison is bigger than/equal 0 it will return the first parameter (because it already knows which is the bigger value based on the custom compare logic)
Primitive Type Functional Interfaces
Before I start to show the Primitive Type Functional Interfaces I would like to explain some concepts. It will be very important for you to understand why we need them.
First of all, I will explain what the Wrapper Classes are.
Wrapper Classes
Wrapper classes are objects responsible for encapsulating primitive types. Below I’ll show the primitive type and the corresponding wrapper.
- boolean -> Boolean
- byte -> Byte
- short -> Short
- char -> Character
- int -> Integer
- long -> Long
- float -> Float
- double -> Double
Hmm, looks nice, but what is the goal of a wrapper class?
Really important question and the answer is simpler than you imagine!
Generic classes don’t support primitive types. It means that we can not use a primitive type as a Type Parameter.
Example:
// Wrong
Predicate<int> wrongPredicate = i -> i < 0;
// Correct
Predicate<Integer> correctPrecidate = i -> i < 0;Now that we know what is a Wrapper Class I’ll explain the Autoboxing and Autounboxing concepts.
Autoboxing
It is a concept introduced in Java 5 (2004) and consists of the compiler’s capability to automatically convert from a primitive type to a Wrapper Class.
// Using autoboxing
Integer autoboxed = 10;
// Not using autoboxing
Integer withoutAutoboxing = Integer.valueOf(10);Auto-unboxing
It is the capability of a compiler to reverse the Autoboxing, which means, convert from Wrapper Class to a primitive type variable.
Integer object = new Integer(10);
// Auto-unboxing
int autounboxing = object;Now that we already understood these 3 important concepts, let’s start to talk about the PTFI (Primitive Type Functional Interface).
So, to understand the usage of a Primitive Type Functional Interface I would like to quote an amazing author, Joshua Bloch.
In Item 5, of Effective Java, Joshua Bloch says:
Prefer primitives to boxed primitives, and watch out for unintentional autoboxing.
But, why?
And the answer is simple: Memory Impact!
Let’s see the memory impact of primitive types:
boolean -> 1 bit
byte -> 8-bit
short and char-> 16-bit
int and float-> 32-bit
long and double -> 64-bit
Now let’s see the memory impact of wrapper classes:
Boolean, Byte, Short, Character, Integer, Float -> 128 bits
Long and Double -> 192 bits
Note: To measure the Objects size I used a tool called Java Object Layout(or just JOL) and you can see the code in my GitHub repo.
Another thing that is important to know is: Primitive data types live in the stack memory and can be accessed fast (If you don’t know what is a stack memory click here) and Wrapper Classes are objects, they live on the heap and are relatively slow to access.
So, I think you already got the point. Operations for primitive types are faster than those for wrapper classes!
Now I’ll show a simple example, but if you want to know more I suggest you read this amazing article.
Performance Benchmark
Primitive Type Benchmark
And the output is:
ElapsedTime operationWithWrapperClass: 1544
ElapsedTime operationWithPrimitiveTypeFunctionInterface: 1064So, as you could see in the operationWithWrapperClass method we are doing 200000000 unintended conversions (100000000 autoboxing and 100000000 auto-unboxing) since the type parameter of Predicate is Integer.
Now that you understood and saw the benefits of Primitive Type Functional Interfaces I’ll show all the options that we have available in Java 8.
Primitive Functions
We have 15 different types of Primitive Functions present in Java 8.
Primitive Type as input parameter
- IntFunction<R>
R apply(int value);- LongFunction<R>
R apply(long value);R apply(double value);Primitive Type as return
int applyAsInt(T value);- ToIntBiFunction<T, U>
int applyAsInt(T t, U u);long applyAsLong(T value);- ToLongBiFunction<T,U>
long applyAsLong(T t, U u);double applyAsDouble(T value);- ToDoubleBiFunction<T, U>
double applyAsDouble(T t, U u);Primitive Type as input parameter and return
long applyAsLong(int value);double applyAsDouble(int value);int applyAsInt(long value);double applyAsDouble(long value);int applyAsInt(double value);long applyAsLong(double value);Primitive Predicates
boolean test(double value);boolean test(int value);boolean test(long value);Primitive Consumers
void accept(double value);void accept(int value);void accept(long value);void accept(T t, double value);void accept(T t, int value);void accept(T t, long value);Primitive Suppliers
boolean getAsBoolean();double getAsDouble();int getAsInt();long getAsLong();Primitive UnaryOperators
double applyAsDouble(double operand);int applyAsInt(int operand);long applyAsLong(long operand);Primitive BinaryOperators
double applyAsDouble(double left, double right);int applyAsInt(int left, int right);long applyAsLong(long left, long right);If you are reading this line I would like to say something:
Thank you very much and I hope you enjoyed it!
Every code that I mentioned here (and examples that I did not put here) is present in the GitHub!
See you in the fourth part!
Comments
No comments yet. Be the first.