Functional programming has become an essential part of the Java ecosystem since Java 8, thanks to the introduction of lambdas, streams, and functional interfaces like Predicate and Function. These concepts enable you to write more expressive, modular, and reusable code. From my experience, I see that even experienced programmers, such as seniors, still do not fully master this new approach, thus not exploring its full potential in their code.
In this article, we'll explore how to create a dynamic class that leverages Predicate and Function for data validation and transformation. Additionally, we'll demonstrate how these interfaces can be used with JPA Specifications, collections, and streams for real-world applications.
Recap: What Are Predicate and Function?
Predicate
A Predicate is a functional interface that evaluates a boolean condition on a single argument.
- Signature:
boolean test(T t) - Common Usage: Filters, validations, item removal in collections.
Function
A Function is a functional interface that transforms an input value into an output value.
- Signature:
R apply(T t) - Common Usage: Data transformations, mapping in streams.
Customizing Processes with Predicate and Function
Below, we have a more comprehensive example that demonstrates how to use Predicate and Function with enums to model real-world business logic. This example defines different credit card types (GOLD, BLACK, and PLATINUM) and applies rules for:
- Validating if a transaction is accepted using a
Predicate. - Calculating loyalty points for transactions using a
Function.
The CreditCardType Enum
import java.math.BigDecimal;
import java.util.function.Function;
import java.util.function.Predicate;
public enum CreditCardType {
GOLD {
@Override
Function<BigDecimal, BigDecimal> formulaPoints() {
return amount -> amount.multiply(new BigDecimal("0.10"));
}
@Override
Predicate<BigDecimal> acceptAmount() {
return amount -> amount.compareTo(new BigDecimal("1000")) <= 0;
}
},
BLACK {
@Override
Function<BigDecimal, BigDecimal> formulaPoints() {
return amount -> amount.multiply(new BigDecimal("0.20"));
}
@Override
Predicate<BigDecimal> acceptAmount() {
return amount -> amount.compareTo(new BigDecimal("5000")) <= 0;
}
},
PLATINUM {
@Override
Function<BigDecimal, BigDecimal> formulaPoints() {
return amount -> amount.multiply(new BigDecimal("0.50"));
}
@Override
Predicate<BigDecimal> acceptAmount() {
return amount -> amount.compareTo(new BigDecimal("10000")) <= 0;
}
};
abstract Function<BigDecimal, BigDecimal> formulaPoints();
abstract Predicate<BigDecimal> acceptAmount();
}Usage Example: Validating and Calculating Loyalty Points
public class Main {
public static void main(String[] args) {
calculatePoints(CreditCardType.GOLD, new BigDecimal("1000"));
calculatePoints(CreditCardType.BLACK, new BigDecimal("1000"));
calculatePoints(CreditCardType.PLATINUM, new BigDecimal("1000"));
calculatePoints(CreditCardType.GOLD, new BigDecimal("5000"));
calculatePoints(CreditCardType.BLACK, new BigDecimal("5000"));
calculatePoints(CreditCardType.PLATINUM, new BigDecimal("5000"));
calculatePoints(CreditCardType.GOLD, new BigDecimal("10000"));
calculatePoints(CreditCardType.BLACK, new BigDecimal("10000"));
calculatePoints(CreditCardType.PLATINUM, new BigDecimal("10000"));
}
private static void calculatePoints(CreditCardType creditCardType, BigDecimal amountTransaction) {
System.out.println(creditCardType);
if (creditCardType.acceptAmount().test(amountTransaction)) {
System.out.println("Transaction accepted!");
System.out.println("Bonus points: " + creditCardType.formulaPoints().apply(amountTransaction));
} else {
System.out.println("Transaction rejected!");
}
System.out.println("------------------------------");
}
}Explanation
CreditCardType Enum:
- Each credit card type defines its own rules for:
acceptAmount(): APredicatethat validates if the transaction amount meets the card's criteria.formulaPoints(): AFunctionthat calculates loyalty points based on the transaction amount.
2. Validation with Predicate:
- The method
acceptAmount()ensures the transaction is valid for the card type. For example: GOLDaccepts transactions up to 1,000.BLACKaccepts transactions up to 5,000.PLATINUMaccepts transactions up to 10,000.
3. Transformation with Function:
- The method
formulaPoints()computes the loyalty points for the accepted transactions. For example: GOLD: 10% of the transaction amount.BLACK: 20% of the transaction amount.PLATINUM: 50% of the transaction amount.
4. calculatePoints Method
- This method checks if the transaction is valid using the
Predicate. - If valid, it calculates and displays the loyalty points using the
Function.
Output Example
For the above code, the output would be:
GOLD
Transaction accepted!
Bonus points: 100.0
------------------------------
BLACK
Transaction accepted!
Bonus points: 200.0
------------------------------
PLATINUM
Transaction accepted!
Bonus points: 500.0
------------------------------
GOLD
Transaction rejected!
------------------------------
BLACK
Transaction accepted!
Bonus points: 1000.0
------------------------------
PLATINUM
Transaction accepted!
Bonus points: 2500.0
------------------------------
GOLD
Transaction rejected!
------------------------------
BLACK
Transaction rejected!
------------------------------
PLATINUM
Transaction accepted!
Bonus points: 5000.0
------------------------------Benefits of Using Predicate and Function
- Modularity:
- Each card type encapsulates its own validation and calculation logic.
- Makes it easy to add new card types with unique rules.
2. Reusability:
- The
PredicateandFunctioninterfaces provide reusable logic for validation and transformation.
3. Readability:
- The use of enums for card types ensures the logic is structured and easy to follow.
4. Extensibility:
- Adding new rules for card types is straightforward: just implement the abstract methods.
Real-World Use Cases
1. JPA Specifications
In Spring Data JPA, Predicate plays a crucial role in building dynamic queries using Specifications. The Specification interface uses a Predicate to define query filters.
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.*;
public class CustomerSpecification {
public static Specification<Customer> hasName(String name) {
return (Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder builder) ->
builder.equal(root.get("name"), name);
}
public static Specification<Customer> hasAgeGreaterThan(int age) {
return (Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder builder) ->
builder.greaterThan(root.get("age"), age);
}
}Usage Example:
Specification<Customer> spec = CustomerSpecification.hasName("John")
.and(CustomerSpecification.hasAgeGreaterThan(25));
List<Customer> customers = customerRepository.findAll(spec);This allows you to dynamically filter database queries using Predicate-like behavior.
2. Collections
Java's collection classes extensively use Predicate for filtering and Function for transforming data.
- Filtering with
removeIf:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.removeIf(name -> name.startsWith("B"));
System.out.println(names); // Output: [Alice, Charlie]- Transforming with
replaceAll:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.replaceAll(name -> name.toUpperCase());
System.out.println(names); // Output: [ALICE, BOB, CHARLIE]3. Streams
Streams make heavy use of both Predicate and Function for operations like filtering, mapping, and reducing.
- Filtering with
Predicate:
List<String> names = List.of("Alice", "Bob", "Charlie");
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.toList();
System.out.println(filteredNames); // Output: [Alice]- Mapping with
Function:
List<String> names = List.of("Alice", "Bob", "Charlie");
List<Integer> nameLengths = names.stream()
.map(String::length)
.toList();
System.out.println(nameLengths); // Output: [5, 3, 7]These methods allow you to process collections declaratively, avoiding loops and making your code more readable and expressive.
Conclusion
Predicate and Function are fundamental tools in functional programming in Java, providing expressive ways to validate, filter, and transform data. From real-world use cases like JPA Specifications and stream processing to asynchronous operations with CompletableFuture, these interfaces empower developers to write cleaner, more modular, and reusable code.
Incorporating these patterns into your projects will help you design better applications. Master Predicate and Function, and unlock the full potential of functional programming in Java!
Feel free to leave comments or suggestions, and share with other developers who might benefit from this solution!
Follow me for more exciting content