함수형 인터페이스란, 추상 메서드를 하나만 갖고 있는 인터페이스를 의미한다.
추상 메서드만 하나이면 되는 것이고, 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
메서드는 표기를 생략하도록 하겠다.
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);
}
}
}
public interface Supplier<T> {
T get();
}
Supplier는 인자로 받는 것은 없고, 특정 타입을 return하기만 한다.
Supplier<ArrayList> arrayListSupplier = () -> new ArrayList<>();
// 또는
Supplier<ArrayList> arrayListSupplier = ArrayList::new;
주로 특정 클래스의 인자가 없는 생성자를 호출하여, 인스턴스를 받는 경우에 사용을 한다.
(예시를 들기 위해서 ArrayList
를 쓴 점은 양해해주길 😂)
public interface Consumer<T> {
void accept(T t);
}
이 인터페이스는 뭔가를 인자로 받아서 return은 하지않고 특정 행동만 수행하는 경우에 사용 한다.
Consumer<String> sayHello = (str) -> System.out.println(str);
// 또는
Consumer<String> sayHello = System.out::println;
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());
}
}
}