[Java] 함수형 인터페이스 이해하기

Kai·2023년 2월 14일
0

Java

목록 보기
10/16

🧐 함수형 인터페이스란?


함수형 인터페이스란, 추상 메서드를 하나만 갖고 있는 인터페이스를 의미한다.
추상 메서드만 하나이면 되는 것이고, default메서드나 static메서드는 여러개가 선언되어 있어도 된다.


💻 사용법


함수형 인터페이스 선언하기

@FunctionalInterface
public interface HelloFunctionalInterface<T> {

    T hello();

}

함수형 인터페이스를 선언하는 법은 아주 간단하다.
위에서 이야기한 조건을 만족하는 인터페이스를 만들어주기만 하면 된다.
즉, 인터페이스를 만들고, 추상 메서드 하나만 만들어주면 되는 것이다.

권장사항

@FunctionalInterface라는 어노테이션을 붙여줄 수 있는데, 이 어노테이션을 안 붙여도 조건만 만족한다면 함수형 인터페이스가 되긴 한다.
하지만, 한 눈에 함수형 인터페이스인 것을 명시하고 싶다고 컴파일 레벨에서 문법오류같은 것을 방지하고 싶다면, 이 어노테이션을 붙여주자.


사용하기

public class Hello {
    public static void main(String[] args) {

        HelloFunctionalInterface<String> sayHello = () -> "Hello!!";
        System.out.println(sayHello.hello());
        
    }
}

함수형 인터페이스에 람다 표현식을 할당해주고, 필요한 상황에 메서드를 호출해주면 된다.

예제로 보니까 뭔가 시시하다. 🤭
함수형 인터페이스는 언제, 왜 필요한 것인지 좀 더 알아보자.


🤔 왜 쓰는 거지?


함수형 인터페이스는 익명 내부 클래스를 사용하지 않기 위해(?) 만들어졌다고 할 수 있다. ㅎㅎ

지난 글에서 예시를 들었던 것 처럼, 특정 배열을 Sorting하기 위해서는 2가지 방법이 있다.
하나는 익명 내부 클래스를 사용하는 것이고 또 하나는 람다 표현식을 사용하는 것이다.

/* 익명 내부 클래스 */
books.sort(new Comparator<Book>() {
	@Override
	public int compare(Book o1, Book o2) {
		return o1.getPage() - o2.getPage();
	}
});

/* 람다 표현식 */
books.sort((a, b) => o1.getPage() - o2.getPage());

이 둘은 동일한 동작을 하는 코드인데, 딱 봐도 익명 내부 클래스를 사용한 코드가 굉장히 그지같은 것을 알 수 있다. 🤭

여기서 람다 표현식으로 작성된 코드를 보면, .sort메서드 안에 함수형 인터페이스의 구현체가 들어가 있는 것을 알 수 있다.

요약하자면, 특정 "역할"만을 수행하는 클래스를 만들기 위해 함수형 인터페이스가 태어났고, 이를 람다 표현식으로 구현할 수 있는 것이다.


🌠 대표적인 함수형 인터페이스


사실 함수형 인터페이스는 직접 만들어 쓰는 것보다 이미 만들어져있는 것을 잘 사용하는 것이 더 중요하다. ㅎㅎ
어지간한 것은 다 만들어져있기 때문이다.

Java에서는 다양한 함수형 인터페이스를 기본적으로 제공하는데, 한번 살펴보자!

참고로, 함수형 인터페이스에 선언된 default메서드와 static메서드는 표기를 생략하도록 하겠다.


1) Function

public interface Function<T, R> {
    R apply(T t);
}

이 인터페이스는 2개의 Generic 타입을 받는다.
T를 인자로 받아서 R을 return한다는 의미를 갖고 있다.

예시

public class Hello {
    public static void main(String[] args) {
        Function<String, String> addStar = (str) -> str + "*";
        List<String> collect = List.of("안녕", "반가워").stream().map(addStar).collect(Collectors.toList());
        for (String s : collect) {
            System.out.println(s);
        }
    }
}


2) Supplier

public interface Supplier<T> {
    T get();
}

Supplier는 인자로 받는 것은 없고, 특정 타입을 return하기만 한다.

예시

Supplier<ArrayList> arrayListSupplier = () -> new ArrayList<>();
// 또는
Supplier<ArrayList> arrayListSupplier = ArrayList::new;

주로 특정 클래스의 인자가 없는 생성자를 호출하여, 인스턴스를 받는 경우에 사용을 한다.
(예시를 들기 위해서 ArrayList를 쓴 점은 양해해주길 😂)


3) Consumer

public interface Consumer<T> {
    void accept(T t);
}

이 인터페이스는 뭔가를 인자로 받아서 return은 하지않고 특정 행동만 수행하는 경우에 사용 한다.

예시

Consumer<String> sayHello = (str) -> System.out.println(str);
// 또는
Consumer<String> sayHello = System.out::println;

4) Predicate

public interface Predicate<T> {
    boolean test(T t);
}

이 인터페이스는 조금 특이한 편인데, 어떤 인자를 받아서 boolean을 return한다.
뭔가를 판별하는데 사용이 된다고 할 수 있다.

대표적으로 .filter() 메서드에 사용이 된다.

예시

public class JavaApplication {

	public static void main(String[] args) {
		List<Book> books = new ArrayList<>();
		
		books.add(new Book(100, "개미"));
		books.add(new Book(10, "돈키호테"));
		books.add(new Book(200, "뇌"));
		books.add(new Book(45, "토비의 스프링"));
		books.add(new Book(13, "코스모스"));
		books.add(new Book(88, "삼국지"));

		Predicate<Book> over100Page = (book) -> book.getPage() > 100; // ⭐
		List<Book> collect = books.stream().filter(over100Page).collect(Collectors.toList());
		for (Book book : collect) {
			System.out.println(book.getInfo());
		}

	}

}

출력 결과


참고


0개의 댓글