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(): A Predicate that validates if the transaction amount meets the card's criteria.
formulaPoints(): A Function that calculates loyalty points based on the transaction amount.
Validation with Predicate:
The method acceptAmount() ensures the transaction is valid for the card type. For example:
- GOLD accepts transactions up to 1,000.
- BLACK accepts transactions up to 5,000.
- PLATINUM accepts transactions up to 10,000.
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.
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.Reusability:
The Predicate and Function interfaces provide reusable logic for validation and transformation.Readability:
The use of enums for card types ensures the logic is structured and easy to follow.Extensibility:
Adding new rules for card types is straightforward: just implement the abstract methods.
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!
Top comments (0)