🎯 1. 함수형 인터페이스의 정의
- 추상 메소드를 단 하나만 가지고 있는 인터페이스
- default 메소드 & static 메소드를 여러개 가지고 있더라도 추상 메소드가 하나면 함수형 인터페이스이다.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
※ @FunctionalInterface
- 해당 인터페이스가 함수형 인터페이스임을 선언하는 어노테이션
- 해당 어노테이션이 없더라도, 추상 메소드가 하나이면 함수형 인터페이스로 동작한다.
👉 그렇다면, 왜 @FunctionalInterface를 사용할까?
@FunctionalInterface 어노테이션이 있으면, 추상 메소드가 2개 이상이면 컴파일 오류를 발생시킨다.
따라서, 검증 및 유지 보수 측면에서 사용한다.
레거시 코드에서 @FunctionalInterface 어노테이션이 없는 함수형 인터페이스가 있다고 해보자.
다른 개발자는 해당 인터페이스를 보고 함수형 인터페이스라고 생각하지 못할 수 있다.
따라서, 요구 사항이 추가됨에 따라 해당 인터페이스에 추상 메소드를 추가할 수 있다.
이렇게 되면, 예상치 못한 곳에서 컴파일 오류가 발생할 수 있다.
(기존의 추상 메소드를 사용하고 있던 곳)
@FunctionalInterface를 사용하면, 인터페이스 내부에서 컴파일 오류가 발생하기 때문에
다른 개발자가 무엇을 잘못했는지 쉽게 알아차릴 수 있다.
🎯 2. 함수형 인터페이스는 언제, 왜 사용할까?
- 함수형 인터페이스는 ‘동작 파라미터화’를 진행하는 것이 장점이다.
- 즉 여러 동작이 존재할 때, 함수형 인터페이스를 사용하는 것이 장점이다.
하나의 동작만 존재할 때의 예시를 살펴보자.
⚙️ ex) 자동차의 위치가 5 이상인 자동차를 필터링하는 동작
* 동작이 하나일 때
* 함수형 인터페이스 사용
public boolean filterCar(Car car, Predicate<Car> p) {
return p.test(car);
}
public void carMove() {
Car car = new Car(5);
if (filterCar(car, carToPredicate -> carToPredicate.getPosition() >= 5)) {
car.move();
}
}
---
* 함수형 인터페이스 사용 X, 일반 필터링
public void filterCar() {
Car car = new Car(5);
if (car.getPosition() >= 5) {
car.move();
}
}
동작이 하나만 있을 때(함수형 인터페이스를 사용하는 곳이 하나일 때)는
함수형 인터페이스를 사용하는 것보다, 직접 조건을 적어주는 것이 가독성이 좋은 것을 알 수 있다.
함수형 인터페이스의 장점은 '추상화'로 인한 확장성과 재사용성에 있다!
위의 예제에서 다음과 같은 상황이 발생한다면?
1. Car가 아니라, 다른 객체도 필터링해야 하는 상황
2. Car의 여러 조건을 필터링해야 하는 상황
이런 경우에는 함수형 인터페이스가 좋을 것이다.
외부에서 사용하고 싶은 동작에 따라 구현체를 바꿔 끼울 수 있다!
또한, 여러 동작이 있을 경우에 함수형 인터페이스를 사용하는 것이 가독성에도 좋다.
🎯 3. 자바에서 기본으로 제공하는 함수형 인터페이스 사용 예제 (feat. 미션 코드)
✅ 3-1. Predicate<T>
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
- 객체 T를 받아서 boolean 값 반환
- 주로 사용되는 상황 : 조건에 맞는 객체를 필터링할 때 사용
- 다양한 조건식을 표현하는데 유용하게 사용
⚙️ ex) 자동차의 위치, 색상에 따른 필터링
색상 : R, G, B
필터1 : 색상이 G이고 위치가 5 이상인 자동차
필터2 : 색상이 R이고 위치가 3 이상인 자동차
필터3 : 색상이 B이고 위치가 7 이상인 자동차
* 자동차 객체
public class Car {
private final int position;
private final String color;
public Car(int position, String color) {
this.position = position;
this.color = color;
}
public int getPosition() {
return position;
}
public String getColor() {
return color;
}
}
* 필터링 함수형 인터페이스 Predicate
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
* 자동차 위치, 색상에 따른 필터링 진행하는 FilterCar 객체
public class FilterCar {
public List<Car> filter(List<Car> cars, Predicate<Car> filterCondition) {
List<Car> result = new ArrayList<>();
for (Car car : cars) {
if (filterCondition.test(car)) {
result.add(car);
}
}
return result;
}
}
* 외부 Controller
public class CarController {
private final FilterCar filterCar;
private final List<Car> cars;
public CarController(FilterCar filterCar, List<Car> cars) {
this.filterCar = filterCar;
this.cars = cars;
}
// 자동차 위치, 색상에 따른 필터링 후 등수 산정
/*
필터1 : 색상이 G이고 위치가 5 이상인 자동차
필터2 : 색상이 R이고 위치가 3 이상인 자동차
필터3 : 색상이 B이고 위치가 7 이상인 자동차
*/
public void getRank() {
List<Car> filteredCars1 =
filterCar.filter(cars, car -> car.getColor().equals("G") && car.getPosition >= 5);
List<Car> filteredCars2 =
filterCar.filter(cars, car -> car.getColor().equals("R") && car.getPosition >= 3);
List<Car> filteredCars3 =
filterCar.filter(cars, car -> car.getColor().equals("B") && car.getPosition >= 7);
...
}
}
✅ 3-2. Consumer<T>
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
- 객체 T를 받아서 void 반환
- 주로 사용되는 상황 : 객체를 전달받아서 특정 작업을 수행해야 할 때 사용 (객체 소비)
- 입력값을 받아서 출력하는 작업, 입력값을 받아서 파일에 쓰는 작업 등 다양한 작업 수행
⚙️ ex) 체스 미션에서 커맨드에 따른 상태 전이 수행 시 사용
public class CommandManager {
private CommandStatus commandStatus;
private final Map<GameCommand, Consumer<Commands>> executionByGameCommand = Map.of(
GameCommand.RESTART, this::restart,
GameCommand.START, none -> start(),
GameCommand.MOVE, this::move,
GameCommand.END, none -> end(),
GameCommand.STATUS, none -> printGameResult()
);
public void execute(Commands commands) {
executionByGameCommand.get(commands.getCommand()).accept(commands);
}
private void restart(Commands commands) {
long previousGameId = commands.getPreviousGameId();
this.commandStatus = commandStatus.restart(previousGameId);
}
private void start() {
this.commandStatus = commandStatus.start();
}
private void move(Commands commands) {
Position sourcePosition = commands.generateSourcePosition();
Position targetPosition = commands.generateTargetPosition();
this.commandStatus = commandStatus.move(sourcePosition, targetPosition);
}
private void end() {
this.commandStatus = commandStatus.end();
}
private void printGameResult() {
this.commandStatus = commandStatus.printGameResult();
}
}
커맨드 RESTART와 MOVE가 들어오면,
Commands 객체를 사용하여 restart, move 상태 전이를 일으키는 동작을 하도록 설정했다.
나머지 커맨드는 Commands 객체가 필요 없으므로 람다 표현식의 파라미터로 파라미터가 없음을 명시적으로
나타내기 위해 none 이라는 네이밍을 사용하고, 단순 상태 전이만 발생시켰다.
외부에서 execute를 호출하여 accept하도록 했다.
✅ 3-3. Supplier<T>
@FunctionalInterface
public interface Supplier<T> {
T get();
}
- 객체 T를 받아서 객체 T를 반환
- 주로 사용되는 상황 : 조건에 맞는 객체를 반환할 때 사용 (객체 공급)
- 입력값 없이 값을 생성하고 반환하는 작업을 수행, 값을 생성하는 작업
⚙️ ex) 체스 미션에서 GameStatus 바꿀 때 사용
public void run() {
printInitMessage();
GameStatus gameStatus = GameStatus.INIT;
while (gameStatus != GameStatus.END) {
final GameStatus finalGameStatus = gameStatus;
gameStatus = repeat(() -> handleCommand(finalGameStatus, chessGame));
}
}
private <T> T repeat(Supplier<T> supplier) {
try {
return supplier.get();
} catch (IllegalArgumentException e) {
OutputView.printErrorMessage(e);
return repeat(supplier);
}
}
handleCommand() 는 입력받는 커맨드에 따라 GameStatus를 반환하는 메소드이다.
repeat() 에서 supplier.get() 이 호출될 때, handleCommand() 가 실행되고
입력받는 커맨드에 따라 GameStatus 객체가 반환되어 gameStatus 에 저장된다.
✅ 3-4. Function<T, R>
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
- 객체 T를 받아서, 객체 R로 반환
- 주로 사용되는 상황 : 매개변수 객체를 반환 값 객체로 변환하여 반환할 때 사용
- 여러 가지 변환 작업이나 데이터 처리 작업에 활용
⚙️ ex) 체스 미션에서 Position으로 각 기물 생성 시 사용
private static Map<String, Function<Position, Piece>> createPieces = new HashMap<>();
static {
createPieces.put("KING", King::new);
createPieces.put("ROOK", Rook::new);
createPieces.put("BISHOP", Bishop::new);
createPieces.put("PAWN", Pawn::new);
createPieces.put("KNIGHT", Knight::new);
createPieces.put("QUEEN", Queen::new);
}
public Piece generatePiece() {
Position position = new Position(File.of(Integer.parseInt(file)), Rank.of(Integer.parseInt(rank)));
Function<Position, Piece> positionSidePieceFunction = createPieces.get(type);
return positionSidePieceFunction.apply(position);
}
→ Position으로 type(기물 유형)에 맞는 기물 생성하는 함수 Function 구현
✅ 3-5. Comparator<T>
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
- 비교할 객체 T 2개를 받아서, int로 비교 결과를 반환
- int 비교 결과
- 음수 : o1이 o2보다 작다.
- 0 : o1와 o2가 같다.
- 양수 : o1이 o2보다 크다.
- int 비교 결과
- 주로 사용되는 상황 : 객체를 비교할 때 사용
- Comparator는 객체를 비교하는 기준을 명시적으로 정할 수 있다.
- 따라서, 정렬 기준을 변경하고자 할 때 사용할 수 있다.
- Collections.sort() 의 인자로 정렬 기준 Comparator가 들어갈 수 있다.
⚙️ ex) ChatGPT 예시
import java.util.*;
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public String toString() {
return "Person{age=" + age + ", name='" + name + "'}";
}
}
public class Example {
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person(20, "Alice"),
new Person(25, "Bob"),
new Person(30, "Charlie"),
new Person(25, "David")
);
// 나이 순으로 정렬하는 Comparator
Comparator<Person> ageComparator = (p1, p2) -> p1.getAge() - p2.getAge();
// 나이 순으로 정렬된 리스트 출력
Collections.sort(people, ageComparator);
System.out.println("Sorted by age: " + people);
}
}
'우아한테크코스 5기 > 학습 로그' 카테고리의 다른 글
instanceof 사용을 지양하기 - Why? Solution? (1) | 2023.03.19 |
---|---|
VO(Value Object)는 무엇일까? 왜 사용할까? (2) | 2023.03.12 |
도메인에 처음 접근하기 - Out -> In / In -> Out 접근 방식 (0) | 2023.02.27 |
포장 값을 View로 전달하는 방식으로 무엇을 사용할까? (2) | 2023.02.27 |
[Java] ArrayList, LinkedList를 직접 구현해보며 이해한 것 (0) | 2023.02.26 |