🧠Core Concepts
- Functional Interface: Exactly one abstract method (SAM).
- @FunctionalInterface: Enforces SAM at compile-time and improves readability.
- Can still have: Multiple
defaultandstaticmethods. - Main benefit: Enables passing behavior as lambda expressions.
❓Why Not Classes?
@FunctionalInterface applies only to interfaces. Classes represent structure/state, while functional interfaces define behavior contracts that lambdas implement dynamically.
📦Example 1: Built-in Interface (Function<T, R>)
Use case: Convert UserEntity (DB model) into UserDTO (API-safe model).
Key method: R apply(T t) — called directly or by Stream.map() internally.
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
class UserEntity {
private final Long id;
private final String firstName;
private final String lastName;
private final String email;
private final String passwordHash;
public UserEntity(Long id, String firstName, String lastName, String email, String passwordHash) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.passwordHash = passwordHash;
}
public Long getId() { return id; }
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public String getEmail() { return email; }
}
class UserDTO {
private final Long id;
private final String fullName;
private final String email;
public UserDTO(Long id, String fullName, String email) {
this.id = id;
this.fullName = fullName;
this.email = email;
}
@Override
public String toString() {
return "UserDTO{id=" + id + ", fullName='" + fullName + "', email='" + email + "'}";
}
}
public class DtoMappingExample {
private static final Function<UserEntity, UserDTO> convertToDTO = entity ->
new UserDTO(
entity.getId(),
entity.getFirstName() + " " + entity.getLastName(),
entity.getEmail().toLowerCase()
);
public static void main(String[] args) {
UserEntity singleDbUser = new UserEntity(101L, "Alice", "Wonderland", "ALICE@example.com", "$2a$12$pPlk8..");
UserDTO singleUserDTO = convertToDTO.apply(singleDbUser);
System.out.println(singleUserDTO);
List<UserEntity> databaseUsers = Arrays.asList(
new UserEntity(1L, "John", "Doe", "John.Doe@example.com", "$2a$12$eImiTx.."),
new UserEntity(2L, "Jane", "Smith", "JANE.SMITH@example.com", "$2a$12$Kj9xY..")
);
List<UserDTO> apiResponse = databaseUsers.stream()
.map(convertToDTO)
.collect(Collectors.toList());
apiResponse.forEach(System.out::println);
}
}
🧩Example 2: Custom Functional Interface (Domain Logic)
Use case: Credential validation with explicit business intent.
@FunctionalInterface
public interface IdentityValidator {
boolean validate(String username, String password);
}
public class AuthenticationRegistry {
public void executeValidation() {
IdentityValidator prodCheck = (user, pass) ->
user.startsWith("admin_") && pass.length() > 12;
boolean result = prodCheck.validate("admin_root", "SecurePassword123!");
System.out.println("Authentication validation passing status: " + result);
}
public static void main(String[] args) {
new AuthenticationRegistry().executeValidation();
}
}
Key takeaway:
IdentityValidator is clearer and more domain-driven than generic BiPredicate<String, String>.
✅Quick Revision Checklist
- SAM = one abstract method only.
- Prefer
@FunctionalInterfacefor compiler checks. - Use built-ins for common transformations/filtering.
- Create custom interfaces when business meaning matters.
- Lambdas make code concise, composable, and test-friendly.