Java 8 introduced functional programming, which helped developers simplify and clean code. Developers new to Java or used to using Java older than Java Development Kit(JDK) version 8 should learn functional programming.
Functional Interfaces and Lambda Expressions
Java 8 introduced functional interfaces, and the FunctionalInterface annotation creates an interface that receives a parameter. The developer can manipulate processes inside the function’s operation.
@FunctionalInterface interface Operation { int apply(int a, int b); } public class FunctionalProgramming { public static void main(String[] args) { Operation addition = (a, b) -> a + b; Operation subtraction = (a, b) -> a - b; System.out.println("Addition: " + addition.apply(5, 3)); System.out.println("Subtraction: " + subtraction.apply(5, 3)); } }
The operation interface has an “apply” method with two parameters.
Addition: 8 Subtraction: 2
The developer can add default methods or static methods inside the operation interface.
@FunctionalInterface interface MathOperation { int operate(int a, int b); default int add(int a, int b) { return a + b; } default int subtract(int a, int b) { return a - b; } // Static method static void greet() { System.out.println("Welcome to Math Operations!"); } } public class FunctionalProgramming { public static void main(String[] args) { // Lambda expression implementing the operate method MathOperation multiplication = (a, b) -> a * b; System.out.println("Multiplication: " + multiplication.operate(5, 3)); // Accessing default methods MathOperation operation = (a, b) -> a + b; System.out.println("Addition: " + operation.add(5, 3)); System.out.println("Subtraction: " + operation.subtract(5, 3)); // Calling static method without an instance MathOperation.greet(); } }
The MathOperation interface has an “operate” method with two parameters: a default “add” and “subtract” method, and a static “greet” method.
Multiplication: 15 Addition: 8 Subtraction: 2 Welcome to Math Operations!
Method References
Allow the developer to call the constructor method without invoking the class. There are four types of method references.
- Static method reference
- Instance method reference
- Constructor reference
- Arbitrary object method reference
Static method reference
import java.util.function.IntBinaryOperator; class MathUtils { // Static method to add two numbers static int add(int a, int b) { return a + b; } } public class MethodReference { public static void main(String[] args) { // Using a static method reference IntBinaryOperator adder = MathUtils::add; int result = adder.applyAsInt(5, 3); System.out.println("Result of addition: " + result); } }
Result of addition: 8
Instance method reference
import java.util.function.BiFunction; class Calculator { // Instance method to multiply numbers int multiply(int a) { return a*a; } } public class MethodReference { public static void main(String[] args) { MethodReference m = new MethodReference(); m.instanceMethod(); } public void instanceMethod(){ // Creating an instance of the Calculator class Calculator calculator = new Calculator(); // Using an instance method reference BiFunction<Calculator, Integer, Integer> multiplier = Calculator::multiply; // Calling the instance method reference with the specified arguments int result = multiplier.apply(calculator, 5); System.out.println("Result of multiplication: " + result); } }
Result of multiplication: 25
Constructor reference
Constructor references provide a concise and readable way to refer to constructors when using functional interfaces, enhancing code clarity and reducing verbosity.
import java.util.function.Function; class Person { private String name; // Constructor public Person(String name) { this.name = name; } // Getter public String getName() { return name; } } public class MethodReference { public static void main(String[] args) { // Using constructor reference Function<String, Person> personCreator = Person::new; // Creating a new Person instance using the constructor reference Person person = personCreator.apply("John Doe"); // Accessing the name using the getter method System.out.println("Person's name: " + person.getName()); } }
The Person class contains the constructor method. The developer can create a new class instance by “Person::new” and call the constructor with “personCreator.apply(“John Doe”) that creates a new person with the name “John Doe”.
Person's name: John Doe
Arbitrary object method reference
Arbitrary object method references provide a convenient way to refer to instance methods on specific objects when using functional interfaces, making the code more readable and expressive.
import java.util.function.Function; class StringUtils { // Instance method to convert a string to uppercase String toUpperCase(String str) { return str.toUpperCase(); } } public class MethodReference { public static void main(String[] args) { // Creating an instance of StringUtils StringUtils stringUtils = new StringUtils(); // Using an arbitrary object method reference Function<String, String> stringConverter = stringUtils::toUpperCase; // Calling the instance method using method reference String result = stringConverter.apply("hello"); System.out.println("Result: " + result); } }
Result: HELLO
Custom function: The developer can use a method reference for any parameter.
@FunctionalInterface interface TriFunction<A, B, C, D, R> { R apply(A a, B b, C c, D d); } class Calculator { // Instance method to perform a calculation with four parameters public int calculate(int a, int b, int c) { return a + b +c; } } public class MethodReference { public static void main(String[] args) { // Creating an instance of Calculator Calculator calculator = new Calculator(); // Using an arbitrary object method reference with four parameters TriFunction<Calculator, Integer, Integer, Integer, Integer> calculatorFunction = Calculator::calculate; // Calling the instance method using method reference int result = calculatorFunction.apply(calculator, 5, 3, 8); System.out.println("Result of calculation: " + result); } }
The developer creates a custom function interface, “TriFunction”, to receive three parameters, as shown in an example.
Streams and Functional Programming
Streams in Java provide a fluent API for processing sequences of elements. They allow you to perform aggregate operations on collections, such as filtering, mapping, and reducing.
Stream: print all words in uppercase
public static void main(String[] args) { List<String> words = Arrays.asList("hello", "world", "java"); // Using streams to convert words to uppercase and print them words.stream() .map(String::toUpperCase) .forEach(System.out::println); //Not functional for(String s : words){ System.out.println(s.toUpperCase()); } }
HELLO WORLD JAVA
Stream: Manipulate values inside the list using Java Streams
public void manipulate(){ // Create a list of integers List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); // Manipulate values inside the list using Java Streams List<Integer> squaredNumbers = numbers.stream() .map(num -> num * num) .collect(Collectors.toList()); // Printing the squared numbers System.out.println("Squared numbers: " + squaredNumbers); //Not functional programming squaredNumbers = new ArrayList<>(); for(int i = 0 ; i < numbers.size() ; i++){ squaredNumbers.add(numbers.get(i) * numbers.get(i)); } System.out.println("Squared numbers: " + squaredNumbers); }
Squared numbers: [1, 4, 9, 16, 25]
Stream: Manipulate values inside the list using Java Streams and method reference
public class FuncStream { public void reference() { // Create a list of integers List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); // Manipulate values inside the list using Java Streams and method reference List<Integer> doubledNumbers = numbers.stream() .map(FuncStream::doubleNumber) .collect(Collectors.toList()); // Printing the doubled numbers System.out.println("Doubled numbers: " + doubledNumbers); } // Method to double a number private static int doubleNumber(int num) { return num * 2; } }
Doubled numbers: [2, 4, 6, 8, 10]
Stream finds the first match
Using findFirst()
For Java stream, find the first match.
findFirst()
Retrieves the first element in the stream that matches the given condition.- Returns an
Optional
element containing it if found, or an emptyOptional
element if no match is found.
import java.util.Arrays; import java.util.List; import java.util.Optional; public class FindFirstExample { public static void main(String[] args) { List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David"); // Find the first name starting with 'C' Optional<String> firstMatch = names.stream() .filter(name -> name.startsWith("C")) .findFirst(); // Print the result firstMatch.ifPresentOrElse( match -> System.out.println("Found: " + match), () -> System.out.println("No match found") ); } }
Found: Charlie
Filter: filter even numbers.
public void filter(){ List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // Using stream and filter to get even numbers List<Integer> evenNumbers = numbers.stream() .filter(num -> num % 2 == 0) .collect(Collectors.toList()); // Printing the even numbers System.out.println("Even numbers: " + evenNumbers); //Not functional evenNumbers = new ArrayList<>(); for(int num : numbers){ if(num % 2 == 0){ evenNumbers.add(num); } } System.out.println("Even numbers: " + evenNumbers); }
Even numbers: [2, 4, 6, 8, 10]
The result is the same, but the code looks more elegant and clean.
Predicate: find the name letter starting with “A”.
public void predicate(){ List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve", "Alan"); // Create a Predicate to filter names starting with 'A' Predicate<String> startsWithA = name -> name.startsWith("A"); // Filter names based on the Predicate List<String> filteredNames = names.stream() .filter(startsWithA) .collect(Collectors.toList()); // Print filtered names System.out.println("Names starting with 'A': " + filteredNames); }
Names starting with 'A': [Alice, Alan]
Predicate: Filter numbers greater than 1000 using Java Streams and Predicate
public void predicateNumbers(){ // Create a list of integers List<Integer> numbers = new ArrayList<>(Arrays.asList(500, 1200, 800, 1500, 2000)); // Define a Predicate to check if a number is greater than 1000 Predicate<Integer> greaterThan1000 = num -> num > 1000; // Filter numbers greater than 1000 using Java Streams and Predicate List<Integer> filteredNumbers = numbers.stream() .filter(greaterThan1000) .collect(Collectors.toList()); // Printing the filtered numbers System.out.println("Numbers greater than 1000: " + filteredNumbers); }
Numbers greater than 1000: [1200, 1500, 2000]
Reduce summary numbers in an ArrayList.
public void reduce(){ List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // Using stream and reduce to find the sum of all numbers int sum = numbers.stream() .reduce(0, (subtotal, num) -> subtotal + num); // Printing the sum System.out.println("Sum of all numbers: " + sum); //Not functional sum = 0; for(int num : numbers){ sum += num; } System.out.println("Sum of all numbers: " + sum); }
Sum of all numbers: 15
Reduce: find the maximum number in an array list.
public void max(){ List<Integer> numbers = Arrays.asList(7, 2, 9, 4, 5); // Using stream and reduce to find the maximum number int max = numbers.stream() .reduce(Integer.MIN_VALUE, Integer::max); // Printing the maximum number System.out.println("Maximum number: " + max); //Not functional max = Integer.MIN_VALUE; for(int num : numbers){ max = Math.max(num, max); } System.out.println("Maximum number: " + max); }
Maximum number: 9
Reduce: concatenate the string.
public static void main(String[] args) { // Create a list of strings List<String> words = new ArrayList<>(Arrays.asList("Hello", "world", "Java", "Streams")); // Concatenate strings using Java Streams and reduce String concatenatedString = words.stream() .reduce("", (str1, str2) -> str1 + str2); // Printing the concatenated string System.out.println("Concatenated string: " + concatenatedString); }
Concatenated string: HelloworldJavaStreams
Optional and Functional Error Handling.
Java’s Optional class provides a way to express the absence of a value instead of using null references.
public void optional(){ divide(4, 2).ifPresent(result -> System.out.println("Result: " + result)); divide(4, 0).ifPresentOrElse( result -> System.out.println("Result: " + result), () -> System.out.println("Division by zero!") ); } public static Optional<Double> divide(int a, int b) { if (b == 0) { return Optional.empty(); } return Optional.of((double) a / b); }
Result: 2.0 Division by zero!
Finally
Functional programming represents a contemporary approach to Java coding. Developers should familiarize themselves with its principles to enhance coding efficiency and maintain code cleanliness.