@FunctionalInterface
interface MyPredicate<T> {
boolean test(T t);
}
public class FunctionalInterfaceExample {
public static void main(String[] args) {
MyPredicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // 출력: true
System.out.println(isEven.test(5)); // 출력: false
}
}
이 예제에서는 MyPredicate 함수형 인터페이스를 사용하여 숫자가 짝수인지 여부를 검사하는 람다 표현식을 작성하였습니다.
커스텀 함수형 인터페이스 활용 예시2
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + '}';
}
}
@FunctionalInterface
interface UserPredicate {
boolean test(User user);
}
public class CustomFunctionalInterfaceExample {
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("Alice", 23),
new User("Bob", 17),
new User("Anna", 25),
new User("Charlie", 19)
);
// 나이가 18세 이상인지 검사
UserPredicate isAdult = user -> user.getAge() >= 18;
// 이름이 'A'로 시작하는지 검사
UserPredicate startsWithA = user -> user.getName().startsWith("A");
// 두 조건을 결합하여 검사
List<User> filteredUsers = users.stream()
.filter(user -> isAdult.test(user) && startsWithA.test(user))
.collect(Collectors.toList());
System.out.println(filteredUsers);
// 출력: [User{name='Alice', age=23}, User{name='Anna', age=25}]
}
}
User 클래스: 사용자의 name과 age를 가지고 있는 간단한 데이터 클래스입니다.
UserPredicate 함수형 인터페이스: User 객체를 받아 특정 조건을 테스트하는 함수형 인터페이스입니다.
isAdult 및 startsWithA: 각각 나이가 18세 이상인지, 이름이 'A'로 시작하는지를 검사하는 두 개의 람다 표현식입니다.
필터링 및 수집: 스트림 API를 사용해 사용자 리스트에서 두 가지 조건을 모두 만족하는 사용자만 필터링한 후 리스트로 수집합니다.
Default
이전 버전의 Java에서는 인터페이스에 메서드를 추가할 경우, 그 인터페이스를 구현하는 모든 클래스에서 해당 메서드를 구현해야 했습니다. 하지만 디폴트 메서드를 사용하면 인터페이스에 메서드를 추가하더라도 구현 클래스에서 해당 메서드를 구현할 필요가 없습니다.
디폴트 메서드의 사용 예시
interface Vehicle {
default void start() {
System.out.println("Vehicle is starting");
}
}
class Car implements Vehicle {
// Car 클래스는 start 메서드를 구현하지 않아도, Vehicle 인터페이스의 기본 구현을 사용합니다.
}
public class DefaultMethodExample {
public static void main(String[] args) {
Car car = new Car();
car.start(); // 출력: Vehicle is starting
}
}
이 예제에서 Vehicle 인터페이스는 start라는 디폴트 메서드를 가지고 있습니다. Car 클래스는 이 인터페이스를 구현하면서 start 메서드를 별도로 구현하지 않았지만, Vehicle 인터페이스의 디폴트 메서드를 사용할 수 있습니다.
디폴트의 단점
다이아몬드 문제(Diamond Problem): 디폴트 메서드를 사용할 때, 다중 상속 구조에서 다이아몬드 문제가 발생할 수 있다. 이는 인터페이스들이 동일한 디폴트 메서드를 제공할 때 발생하며, 이 경우에는 구현 클래스에서 메서드를 오버라이드하여 해결해야 한다.
혼란을 유발할 수 있음: 디폴트 메서드는 인터페이스의 모든 구현체에서 동일하게 동작해야 한다. 만약 기본 구현이 모든 상황에 적합하지 않다면, 이를 오버라이드해야 할 수도 있다. 이 과정에서 오버라이드할 필요가 있는지 혼란스러울 수 있다.
인터페이스의 일관성: 인터페이스는 기본적으로 동작을 정의하는 것이 아니라, 동작을 명시하는 역할을 해야 한다. 디폴트 메서드는 이러한 인터페이스의 일관성을 무너뜨릴 수 있다. 따라서 디폴트 메서드를 추가할 때는 이 메서드가 정말로 모든 구현체에 적합한지 신중하게 고려해야 한다.
다이아몬드 문제
여러 인터페이스를 구현하는 클래스에서 같은 이름의 디폴트 메서드를 상속받을 때 문제가 발생할 수 있습니다. 이를 다이아몬드 문제(Diamond Problem)라고 부릅니다.
다이아몬드 문제 예시
interface InterfaceA {
default void hello() {
System.out.println("Hello from InterfaceA");
}
}
interface InterfaceB {
default void hello() {
System.out.println("Hello from InterfaceB");
}
}
public class DiamondProblemExample implements InterfaceA, InterfaceB {
public void hello() {
InterfaceA.super.hello(); // InterfaceA의 hello 메서드 호출
}
public static void main(String[] args) {
DiamondProblemExample example = new DiamondProblemExample();
example.hello(); // 출력: Hello from InterfaceA
}
}
이 예제에서 DiamondProblemExample 클래스는 InterfaceA와 InterfaceB 모두의 hello 메서드를 상속받습니다. 하지만, 두 인터페이스에서 동일한 메서드 이름을 사용하기 때문에 충돌이 발생합니다. 이를 해결하기 위해 특정 인터페이스의 메서드를 호출하도록 명시해야 합니다.
Completable Future
비동기 프로그래밍은 처음 자바5에 Future가 추가되면서 가능하게 되었고, 이것을 통해 비동기 작업에 대한 결과값을 반환받을 수 있게 됐습니다. Future에는 단점들이 존재했는데, 단점들을 해결하고자 나온게 자바8에서 CompletableFuture입니다.
Future의단점
외부에서 작업을 중단시키는 기능을 지원하지 않음 (제어 불가) 비동기 작업이 무한루프나 오래걸릴경우, 강제로 완료 시킬 수가 없습니다.
완전한 비동기 작업이 아닙니다.Future는 비동기 작업이지만, 결과를 처리하기 위해서는 동기화된 블로킹 코드(’get’)로 돌아가야 합니다.
여러 Future를 조합할 수 없습니다. (예: 회원 정보를 가져오는 작업과 알림을 발송하는 작업을 조합하려면 각 Future의 결과를 순차적으로 처리해야 하며, 이를 효율적으로 조합할 수 있는 메커니즘이 없습니다.)
예외처리가 어렵습니다. 예: 첫번째 작업의 성공 유무에 따른 두번쨰 작업 실행 방향 처리 구현이 어렵습니다.
CompletableFuture의 장점
유연성: 여러 비동기 작업을 유연하게 조합할 수 있습니다.
비동기 예외 처리: 예외 처리를 더욱 간단하게 할 수 있습니다.
명시적인 비동기 API: 비동기 작업의 흐름을 명확하게 표현할 수 있습니다.
병렬 실행 지원: 여러 비동기 작업을 병렬로 실행하여 성능을 향상시킬 수 있습니다.
CompletableFuture의 작업 종류
비동기 작업 실행, 작업 콜백, 작업 조합, 예외처리가 있습니다.
비동기 작업 실행
runAsync(Runnable) - 반환 값이 없는 비동기 작업을 실행합니다.
supplyAsync(Supplier<U>) - 반환 값이 있는 비동기 작업을 실행합니다.
작업 콜백
thenApply(Function<T, U>) - 결과를 받아서 다른 결과로 변환합니다.
thenAccept(Consumer<T>) - 결과를 받아서 소비합니다. 반환 값은 없습니다.
thenRun(Runnable) - 결과를 받지 않고 단순히 실행합니다.
작업 조합
thenCompose(Function<T, CompletionStage<U>>) 이전 작업의 결과를 받아서 새로운 비동기 작업을 실행합니다.
thenCombine(CompletionStage<U>, BiFunction<T, U, V>) - 두 개의 비동기 작업 결과를 조합합니다.
allOf(CompletableFuture<?>...) - 여러 비동기 작업을 모두 완료할 때까지 기다립니다.
anyOf(CompletableFuture<?>...) - 여러 비동기 작업 중 하나라도 완료되면 결과를 반환합니다.
예외 처리
exceptionally(Function<Throwable, T>) - 예외가 발생했을 때 기본값을 반환하거나, 예외를 처리합니다.
handle(BiFunction<T, Throwable, U>) - 정상 결과와 예외를 모두 처리할 수 있습니다.
handleAsync(BiFunction<T, Throwable, U>) - 비동기적으로 정상 결과와 예외를 모두 처리할 수 있습니다.
Optional
Optional은 값이 있을 수도 있고 없을 수도 있는 객체를 감싸는 컨테이너 클래스입니다.
특징
명시적 Null 처리: Optional을 사용하면 null 반환 가능성을 명시적으로 나타낼 수 있습니다.
널 참조 방지: Optional 메서드를 통해 안전한 null 참조를 보장합니다.
간결한 코드: 반복적인 null 체크를 줄이고, 더 간결하고 가독성 높은 코드를 작성할 수 있습니다.
Optional 생성
Optional.of()
Optional.of()는 null이 아닌 값을 감쌉니다. 만약 null 값을 전달하면 NullPointerException이 발생합니다.
String name = "John";
Optional<String> optionalName = Optional.of(name);
Optional.ofNullable()
Optional.ofNullable()는 값이 null일 수도 있고, 아닐 수도 있는 경우에 사용됩니다. null 값이 전달되면 빈 Optional 객체(Optional.empty())를 반환합니다.
String name = null;
Optional<String> optionalName = Optional.ofNullable(name);
isPresent() 메서드는 Optional 객체에 값이 존재하는지 확인합니다. 값이 존재하는 경우 true를 반환하며, get() 메서드를 통해 값을 가져올 수 있습니다. 그러나 get() 메서드는 값이 없을 경우 NoSuchElementException을 발생시키므로, 반드시 isPresent()로 값이 있는지 먼저 확인해야 합니다.
Optional<String> optionalName = Optional.of("John");
if (optionalName.isPresent()) {
System.out.println(optionalName.get()); // 출력: John
}
ifPresent()
ifPresent() 메서드는 값이 존재하는 경우에만 특정 동작을 수행할 수 있습니다. 값이 없을 때는 아무 작업도 하지 않습니다.
optionalName.ifPresent(name -> System.out.println("Name: " + name));
// 출력: Name: John
orElse()
orElse() 메서드는 Optional 객체에 값이 없을 경우 제공된 기본 값을 반환합니다.
String name = optionalName.orElse("Default Name");
System.out.println(name); // 출력: John
Optional<String> emptyOptional = Optional.empty();
String defaultName = emptyOptional.orElse("Default Name");
System.out.println(defaultName); // 출력: Default Name
orElseGet()
orElseGet() 메서드는 Optional에 값이 없을 경우 기본 값을 제공하는 Supplier를 실행합니다. orElse()와의 차이점은 기본 값을 계산하는 비용이 높은 경우, orElseGet()을 사용하여 지연 평가(lazy evaluation)를 할 수 있다는 점입니다.
String name = optionalName.orElseGet(() -> "Default Name");
orElseThrow()
orElseThrow() 메서드는 값이 없을 경우 예외를 발생시킵니다.
String name = optionalName.orElseThrow(() -> new IllegalArgumentException("Name not found"));
Optional + Stream
예시: 사용자 목록에서 특정 사용자 찾기
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class OptionalStreamExample {
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("John", 25),
new User("Jane", 22),
new User("Jack", 28)
);
Optional<User> user = users.stream()
.filter(u -> "Jane".equals(u.getName()))
.findFirst();
user.ifPresent(u -> System.out.println("User found: " + u.getName())); // 출력: User found: Jane
}
}