Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
68: Functional Interfaces
I've spent a lot of time reviewing junior PRs, and there is one specific point of confusion that keeps popping up when we hit functional programming in Java: the belief that the @FunctionalInterface annotation is what actually "turns" an interface into a functional one.
The Myth: The Annotation Grants the Power
A lot of developers think that if they don't add @FunctionalInterface to the top of their interface, they can't use a lambda expression to implement it. They treat it like a magic switch. Let's look at this code:
public interface SimpleValidator {
boolean validate(String value);
}
public class Main {
public static void main(String[] args) {
// I didn't use the annotation, but this still works perfectly.
SimpleValidator isNotEmpty = (s) -> s != null && !s.isEmpty();
System.out.println(isNotEmpty.validate("Hello")); // true
}
}
As you can see, the code compiles and runs without a hitch. The annotation isn't a requirement for the functionality; the compiler doesn't actually need it to allow lambdas.
The Reality: The Single Abstract Method (SAM) Rule
The only thing that actually makes an interface "functional" is that it has exactly one abstract method. That's it. I call this the SAM rule (Single Abstract Method). If there is one—and only one—method that doesn't have an implementation, Java can safely map a lambda's parameters and return type to that specific method.
So, why does the @FunctionalInterface annotation even exist? Think of it as a safety guard. If I mark an interface with that annotation and then a teammate (or a future, tired version of myself) tries to add a second abstract method, the compiler will throw an error immediately. It prevents us from accidentally breaking every lambda implementation of that interface across the entire codebase.
It's worth noting that you can have as many default or static methods as you want. Since those have bodies, they don't count against your "one abstract method" quota.
Stop Inventing Your Own Interfaces
Now, here is a piece of professional advice: just because you can write your own functional interfaces doesn't mean you should. In the early days of Java 8, we wrote a lot of our own. Now? It's mostly noise. Java provides a robust set of built-in functional interfaces in the java.util.function package that cover 95% of use cases.
Predicate<T>: Takes one argument, returns aboolean. (Perfect for filtering).Consumer<T>: Takes one argument, returnsvoid. (Great for printing or saving).Function<T, R>: Takes one argument, returns a result of a different type. (The classic mapper).Supplier<T>: Takes nothing, returns a value. (Useful for lazy initialization).
If you find yourself writing public interface MyCustomChecker { boolean check(String s); }, stop. Use Predicate<String> instead. It makes your code instantly more readable to other Java engineers because they already know exactly what a Predicate does without having to go find your interface definition.
📋 Practical Task
Implementing a Dynamic Product Discount Engine
You are building a checkout system for an e-commerce store. Instead of hard-coding discount logic, you need to create a system where different discount strategies can be passed into a processor.
Your Task:
- Create a functional interface named
DiscountStrategywith a single method:double applyDiscount(double price). - Add the
@FunctionalInterfaceannotation to ensure the interface remains functional. - Create a class
DiscountProcessorwith a methoddouble calculateFinalPrice(double originalPrice, DiscountStrategy strategy)that applies the passed strategy to the price. - In your
mainmethod, use theDiscountProcessorto calculate the final price for a product costing $100.00 using three different lambda expressions:- A "10% Off" discount.
- A "Flat $20 Off" discount.
- A "No Discount" (return price as is) strategy.
Ensure your output clearly prints the result of each of the three different strategies.
There are no comments for now.