When to use Java 8 functional programming, and where does it shine?
Q. What is the difference between imperative and declarative programming paradigms?
A. Imperative (or procedural) programming: is about defining the computation how to do something in terms of statements and state changes, and as a result what you want to happen will happen.
Declarative programming: is about declaratively telling what you would like to happen, and let the library or functions figure out how to do it. SQL, XSLT and regular expressions are declarative languages.
Q. Does functional programming use imperative or declarative approach?
A. Functional programming is a form of declarative programming, where functions are composed of other functions -- g(f(x)) where g and f are functions. An output of one function becomes the input for the composing function. A typical example of functional programming, which you may have used is transforming an XML document using XSLT to other forms. The composable and isolated XSL style sheets are used for transformation.
In imperative programming it makes sense to say x = x + 1, but in maths or functional programming it is not right to say x = x + 1, if x is 2 is it right to say 2 = 2 + 1 or 2 = 3 ?In functional programming f(x) -> x + 1, where f is a function that takes an argument of x.
Q. What does functional programming shine in Java 8?
A.
Scenario 1: Here is an imperative program to extract odd numbers from a given list of numbers and then double each odd number and print each of them.
Imperative program with looping, statements, etc.
package com.java8.examples; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; public class NumberTest { public static void main(String[] args) { Integer[] numbers = {1,2,3,4,5,6}; long start = System.nanoTime(); //odd numbers List<Integer> oddNumbers = new ArrayList<>(); for (Integer e : numbers) { if(e %2 != 0) { oddNumbers.add(e); } } //doubled odd numbers List<Integer> doubledOddNumbers = new ArrayList<>(); for (Integer e : oddNumbers) { doubledOddNumbers.add(e * 2); } //print each for (Integer e : doubledOddNumbers) { System.out.println(e); } System.out.println("Completed in " + (System.nanoTime() - start) + " nano second"); } }Output:
2
6
10
Completed in 371880 nano second
Functional programming using the Java 8 lambda expressions.
Functional programming with good set of libraries can cut down lot of fluff and focus on just transformations. In other words, just tell what you would like to do.
package com.java8.examples; import java.util.Arrays; public class NumberTest { public static void main(String[] args) { Integer[] numbers = {1,2,3,4,5,6}; long start = System.currentTimeMillis(); //static methods are used for clarity Arrays.asList(numbers) .stream() .filter((e) -> (e % 2 != 0)) .map((e) -> (e * 2)) .forEach(System.out::println); System.out.println("Completed in " + (System.currentTimeMillis() - start) + " ms"); } }
Output:
2
6
10
Completed in 46 ms
Shining moment 1: The above code has increased readability and maintainability because each function is designed to accomplish a specific task for given arguments. Lesser moving parts than the imperative code. The above code can be further improved by extracting the cryptic lambda expressions like (e) -> (e * 2) to static methods with meaningful names for re-usability and readability as shown below.
package com.java8.examples; import java.util.concurrent.TimeUnit; import java.util.Arrays; class NumberTest { public static void main(String[] args) { Integer[] numbers = {1,2,3,4,5,6}; long start = System.currentTimeMillis(); //static methods are used for clarity Arrays.asList(numbers) .stream() .filter(NumberTest::isOddNumber) .map(NumberTest::doubleIt) .forEach(System.out::println); System.out.println("Completed in " + (System.currentTimeMillis() - start) + " ms"); } private static boolean isOddNumber(int number){ //simulate time to perform an operation delay(); System.out.println("isOdd " + number); return number % 2 != 0; } private static int doubleIt(int number){ //simulate time to perform an operation delay(); System.out.println("doubleIt: " + number); return number * 2; } private static void delay() { //simulate time to perform an operation try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }
Output:
isOdd 1
doubleIt: 1
2
isOdd 2
isOdd 3
doubleIt: 3
6
isOdd 4
isOdd 5
isOdd : 5
10
isOdd 6
Completed in 9454 ms
Shining moment 2: A time delay was added on purpose to demonstrate that a functional program can be easily made to run in parallel using the "fork and join" feature added in Java 7. To improve performance of the above code that took 9.5 seconds to run, all you have to do is use .parallelStream( ) instead of .stream( ).
Arrays.asList(numbers) .parallelStream() //use fork and join .filter(NumberTest::isOddNumber) .map(NumberTest::doubleIt) .forEach(System.out::println);
The new output due to parallel processing will be:
isOdd 2
isOdd 4
isOdd 1
isOdd 6
isOdd 3
doubleIt: 1
isOdd 5
2
doubleIt: 3
6
doubleIt: 5
10
Completed in 3151 ms
Finished running in 3 seconds with the performance gain. When parallelising tasks, you need to be
- Ensuring that the computation is correct
- Ensuring that parallelism actually brings performance gains
- Using algorithms that are parallelisable
Shining moment 3: code is easier to refactor as shown below. If you want to switch the logic to double it first and then extract the odd numbers, all you have to do is swap the .filter call with .map call.
Arrays.asList(numbers) .parallelStream() .map(NumberTest::doubleIt) .filter(NumberTest::isOddNumber) .forEach(System.out::println);
The output will have no odd numbers at all.
doubleIt: 4
doubleIt: 2
doubleIt: 1
doubleIt: 6
isOdd 4
isOdd 12
isOdd 2
isOdd 8
doubleIt: 5
doubleIt: 3
isOdd 10
isOdd 6
Completed in 4145 ms
Shining moment 4: Easier testing and debugging. Because pure functions can more easily be tested in isolation, you can write test code that calls the pure function with typical values, valid edge cases, and invalid edge cases. In the above example, I was using the Java 8 library that was well tested with confidence. I will only have to provide unit tests for the static functions boolean isOddNumber(int number) and int doubleIt(int number) that provide the logic.
Having said this, OO programming and functional programming can co-exist. In the next post I will discuss how to get accustomed to Function Oriented Programming (FOP) from Object Oriented Programming (OOP).
Labels: functional programming, Java 8