본문 바로가기
programming_kr/java

JAVA 8 함수형 인터페이스

by JSsunday 2022. 11. 12.
728x90

함수형 인터페이스(Functional interface)는 1개의 추상 메서드를 갖고 있는 인터페이스입니다.

public interface FunctionalInterface {
    public abstract void printText(String text);
}

 

자바의 람다식은 함수형 인터페이스로 접근이 되기 때문에 함수형 인터페이스를 사용합니다.


람다함수 : 람다 함수는 프로그래밍 언어에서 사용되는 개념으로 익명 함수(Anonymous functions)를 지칭하는 용어입니다. 현재 사용되고 있는 람다의 근간은 수학과 기초 컴퓨터과학 분야에서의 람다 대수이다. 람다 대수는 간단히 말하자면 수학에서 사용하는 함수를 보다 단순하게 표현하는 방법입니.


아래 코드에서 변수 func는 람다식으로 생성한 객체를 가지고 있습니다. printText()에 인자로 String을 전달하면 람다식에 정의된 것처럼 출력됩니다.

 

public class LambdaTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		FunctionalInterface func = text -> System.out.println(text);
		func.printText("play"); // play
	}

}

interface FunctionalInterface {
    public void printText(String text);
}

 

개발하면서 익명 클래스로 객체를 만든 적이 있을텐데, 익명 클래스를 사용하여 리팩토링한 코드입니다. 함수형 인터페이스와 람다식으로 익명 클래스를 간단하게 표현했습니다.

 

FunctionalInterface func = new FunctionalInterface() {
    @Override
    public void printText(String text) {
        System.out.println(text);
    }
};
func.printText("play");

 

함수형 인터페이스를 사용하는 것은 람다식으로 만든 객체에 접근하기 위해서입니다. 위 예제처럼 람다식을 사용할 때 마다 함수형 인터페이스를 매번 정의하기 불편하기 때문에 자바에서 라이브러리로 제공하는 것이 있습니다.

 

기본 함수형 인터페이스

자바에서 지공하는 기본 함수형 인터페이스는 다음과 같습니다.

  • Runnable
  • Supplier
  • Consumer
  • Function<T, R>
  • Predicate

이외에는 java.util.function 패키지에 정의되어 있습니다.

 

Runnable

Runnable은 인자를 받지 않고 리턴값도 없는 인터페이스입니다.

 

public interface Runnable {
	public abstract void run();
}

 

아래와 같이 사용할 수 있습니다.

 

Runnable runnable = () -> System.out.println("go go!");
runnable.run();

 

Runnablerun()을 호출해야 합니다. 함수형 인터페이스마다 run()과 같은 실행 메서드 이름이 다릅니다. 인터페이스 종류마다 만들어진 목적이 다르고, 그 목적에 맞는 이름을 실행 메서드 이름으로 정했기 때문입니다.

 

Supplier

Supplier<T>는 인자를 받지 않고 T 타입의 객체를 리턴합니다.

 

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

 

아래 코드처럼 사용할 수 있습니다. get() 메서드를 호출해야 합니다.

 

Supplier<String> getString = () -> "Function interface Supplier";
String result = getString.get();
System.out.println(result); // Function interface Supplier

 

Consumer

Consumer<T>는 T 타입의 객체를 인자로 받고 리턴 값은 없습니다.

 

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

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

 

아래와 같이 사용할 수 있습니다.

 

HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put("first", "Java");
hashMap.put("second", "hello");
Consumer<HashMap<String, String>> printMap = map -> {
    for(Entry<String, String> entry : map.entrySet()) {
        System.out.println(entry.getKey() + " : " + entry.getValue());
    }
};
printMap.accept(hashMap); // first : Java, second : hello

 

andThen() 메서드를 사용하면 두 개 이상의 Consumer를 연속적으로 사용할 수 있습니다.

 

HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put("first", "Java");
hashMap.put("second", "hello");
Consumer<HashMap<String, String>> printMap = map -> {
    for(Entry<String, String> entry : map.entrySet()) {
        System.out.println(entry.getKey() + " : " + entry.getValue());
    }
};
Consumer<HashMap<String, String>> printMap2 = printMap;
printMap.andThen(printMap2).accept(hashMap);

 

Function

Function<T, R>는 T타입의 인자를 받고, R타입의 객체를 리턴합니다.

 

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

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

 

다음과 같이 사용할 수 있습니다.

 

Function<String, String> func = (val) -> "Hello " + val;
String result = func.apply("Java");
System.out.println(result); // Hello Java

 

compose()는 두 개의 Function을 조합해서 새로운 Function 객체를 만들어주는 메서드입니다. andThen() 메서드와 실행순서가 반대니 주의하시길 바랍니다. compose()에 인자로 전달되는 Function이 먼저 수행되고 그 이후에 호출하는 객체의 Function이 수행됩니다.

아래와 같이 compose를 사용하여 새로운 Function을 만들 수 있습니다. func3()의 apply를 호출하면 func 메서드가 먼저 호출되고 func2 메서드가 호출됩니다.

 

Function<String, String> func = (val) -> "Hello " + val;
Function<String, String> func2 = (val) -> val +  " Bye";

Function<String, String> func3 = func.compose(func2);
String result = func3.apply("Java");
System.out.println(result); // Hello Java Bye

 

Predicate

Predicate<T>는 T타입 인자를 받고 결과로 boolean을 리턴합니다.

 

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

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

 

다음과 같이 사용할 수 있습니다. test() 메서드를 사용합니다.

 

Predicate<Integer> isBiggerThanTen = num -> num > 5;
System.out.println(isBiggerThanTen.test(10)); // true

 

and()or() 은 다른 Predicate와 함께 사용됩니다. and() 는 두 개의 Predicate가 true일 때, true를 리턴하며 or() 은 두 개중에 하나만 true이면 true를 리턴합니다.

 

Predicate<Integer> isBiggerThanTen = num -> num > 5;
Predicate<Integer> isBiggerThanFive = num -> num > 10;

System.out.println(isBiggerThanTen.and(isBiggerThanFive).test(100)); // true
System.out.println(isBiggerThanTen.and(isBiggerThanFive).test(9)); // false
System.out.println(isBiggerThanTen.or(isBiggerThanFive).test(7)); // true

 

isEqual() 은 static 메서드로, 인자로 전달되는 객체와 같은지 체크하는 Predicate 객체를 만들어줍니다. 아래와 같이 사용할 수 있습니다.

 

Predicate<String> isEquals = Predicate.isEqual("Java");
System.out.println(isEquals.test("Java")); // true

 

Java에서 기본적으로 제공하는 함수형 인터페이스에 대해서 알아보았습니다. 인터페이스 마다 인자와 리턴 값이 다르기 때문에 사용하는 목적에 따라 메서드를 확인하시고 사용하기 바랍니다.

 

메서드 참조(Method Reference)

 

메소드 참조란 함수형 인터페이스를 람다식이 아닌 일반 메소드를 참조시켜 선언하는 방법입니다. 람다식이 하나의 메서드만 호출하는 경우 간략히 할 수 있습니다. 일반 메소드를 참조하기 위해서는 다음의 3가지 조건을 만족해야 합니다.

  • 함수형 인터페이스의 매개변수 타입 = 메소드의 매개변수 타입
  • 함수형 인터페이스의 매개변수 개수 = 메소드의 매개변수 개수
  • 함수형 인터페이스의 반환형 = 메소드의 반환형

참조가능한 메소드는 일반 메소드, Static 메소드, 생성자가 있으며 클래스이름::메소드이름 으로 참조할 수 있습니다. 이렇게 참조를 하면 함수형 인터페이스로 반환됩니다.

 

1. 일반 메소드 참조(인스턴스)

예를 들어 위에서 보여준 Function에 메소드 참조를 적용한다고 하면 우선 해당 메소드(length)가 위의 3가지 조건을 만족하는지 살펴보아야 합니다.

  • 매개변수 없음
  • 매개변수 개수 = 0개
  • 반환형 = int

String의 length 함수는 매개변수가 없으며, 반환형이 int로 동일하기 때문에 String::length로 다음과 같이 메소드 참조를 적용할 수 있다.

// 기존의 람다식
Function<String, Integer> function = (str) -> str.length();
function.apply("Hello World");

// 메소드 참조로 변경
Function<String, Integer> function = String::length;
function.apply("Hello World");

 

추가로 예시를 살펴보자. System.out.println() 메소드는 반환형이 void이며, 파라미터로 String을 받는 메소드이다. 그렇기 때문에 우리는 Consumer에 System.out.println() 메소드를 참조시킬 수 있다.

// 일반 메소드를 참조하여 Consumer를 선언한다.
Consumer<String> consumer = System.out::println;
consumer.accept("Hello World!!");

// 메소드 참조를 통해 Consumer를 매개변수로 받는 forEach를 쉽게 사용할 수 있다.
List<String> list = Arrays.asList("red", "orange", "yellow", "green", "blue");
list.forEach(System.out::println);

//interface Iterable<T>
default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

 

2. Static 메소드 참조

Static 메소드 역시 메소드 참조가 가능하다. 예를 들어 Objects의 isNull은 반환값이 Boolean이며, 매개변수 값은 1개이고, 매개 변수가 Object이므로 Predicate로 다음과 같이 메소드 참조가 가능하다.

Predicate<Boolean> predicate = Objects::isNull;

// isNull 함수
public static boolean isNull(Object obj) {
    return obj == null;
}

 

3. 생성자 참조

생성자도 메소드 참조를 할 수 있다. 생성자는 new로 생성해주므로 클래스이름::new로 참조할 수 있다. Supplier는 매개변수가 없이 반환값만을 갖는 인터페이스이기 때문에, 매개변수 없이 String 객체를 새롭게 생성하는 String의 생성자를 참조하여 Supplier로 선언할 수 있다.

Supplier<String> supplier = String::new;

 

메서드 참조 규칙 정리

  람다식 메서드 참조
static 메소드 a ->클래스이름.메소드(a) 클래스이름::메소드이름
인스턴스 메소드 (a, b) -> a.메소드(b) 클래스이름::메소드이름
(a) -> 객체.메소드(a) 객체::메소드이름
생성자 (a) -> new 생성자(a) 생성자이름::new
배열 생성자 (a) -> new 타입[a] 타입::new

 

배열 생성자

Employee[] employees = names.stream().map(Employee::new).toArray(Employee[]::new);
System.out.println(Arrays.toString(employees));

 

 

참조
[Java8] Method Reference (메서드 참조) - 생성 방법
[Java] 람다식(Lambda Expression)과 함수형 인터페이스(Functional Interface) - (2/5)
java8-functional-interface

 

 

728x90

'programming_kr > java' 카테고리의 다른 글

JAVA EE란?  (0) 2023.01.28
JDBC, DBCP 그리고 JNDI  (0) 2022.12.09
열거형(enum)  (0) 2022.10.29
JAVA8 STREAM(1)  (1) 2022.10.15
제네릭(generic)  (1) 2022.09.24

댓글