Java : 함수형 인터페이스

cad·2022년 4월 24일
0

Study

목록 보기
5/5

그게 뭐야?


함수형 인터페이스(Functional Interface)는 오직 1개의 추상 메소드를 갖는 인터페이스를 말한다.

예제

public interface FunctionalInterface{
	// abstract 메서드는 무조건 하나만!!
	public abstract void doSomething(String text);
    
    // default는 있어도 무관
    default void a(){
    	...
    }
    
    // static도 괜츈
    static void b(){
    	...
    }
}
  • 오직 1개의 추상 메서드만을 가지기 때문에 Single Abstract Method(SAM) 이라고 불리기도 한다.
  • 추상 메서드가 하나만 존재한다면 이외 여러개의 default method, static method가 있어도 된다.
  • interface를 봤을 때 추상 메서드가 하나 밖에 없다면 알아서 함수형 인터페이스라고 판단한다.

함수형 인터페이스를 사용하는 이유?


함수형 인터페이스를 사용하는 이유는 자바의 람다식은 함수형 인터페이스로만 접근이 되기 때문이다.

  • 우선 두 예제 코드를 보자
public interface FunctionalInterface {
    public abstract void doSomething(String text);
}

...
// 1번 코드
FunctionalInterface func = new FunctionalInterface() {
    @Override
    public void doSomething(String text) {
        System.out.println(text);
    }
};
func.doSomething("hello world");

// 2번 코드
FunctionalInterface func2 = text -> System.out.print(text);
func2.doSomething("hello world");

  • 두 코드의 실행 결과는 같다.
  • 언뜻 봐도 1번 코드보다 2번코드가 라인 수도 적고 직관적이며 가독성이 좋다.
  • 1번 코드는 익명 클래스의 사용으로 표현했다.
  • 2번 코드에서는 람다식으로 표현된 FunctionalInterface 형 text 변수를 선언하고 메서드를 실행했다.

java 8 에서는 함수형 인터페이스를 제공해주고 있다.

기본 함수형 인터페이스

  1. Function<T, R>
  2. Consumer
  3. Predicate
  4. Supplier

메서드 참조 표현식

  • 이중 콜론(::)을 가지는 형태로, 함수형 인터페이스에서 자주 쓰이는 표현식
  • Integer::parseInt 등 인스턴스와 메서드(혹은 new) 사이에 이중 콜론이 위치하는 모습을 보인다.
List<String> lists = Arrays.asList("1", "2", "3", "4", "5");
  • lists 를 하나씩 출력하고 싶다고 가정하자.
lists.forEach(num -> System.out.println(num));
  • forEach 문으로 각 숫자를 num으로 받아 println으로 출력해줄 수 있다. 이를 메서드 참조 표현식으로 나타낸다면,
lists.forEach(System.out::println);
  • 어차피 num을 받아 그대로 출력할게 뻔하기 때문에 굳이 num를 할당하지 않아도 된다는 것이 메서드 참조 표현식의 장점이다.

다만 개인적으로 이중 콜론이 익숙하지 않은 상태에서 코드를 보면 가독성 면에서 썩 좋을 것 같진 않다..

간단한 예제

Functional Interface는 @FunctionalInterface 어노테이션을 사용하는데, 이 어노테이션은 해당 인터페이스가 함수형 인터페이스 조건에 맞는지 검사해준다.

@FunctionalInterface 어노테이션이 없어도 함수형 인터페이스로 동작하고 사용하는 데 문제는 없지만, 인터페이스 검증과 유지보수를 위해 붙여주는 게 좋다.

Function<T, R>

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
    
    ...
}

//같은 타입이면서 같은 값을 반환하는 것을 identity function이라 한다.
Function<String, Integer> toInt = Integer::parseInt;

final Integer number = toInt.apply("100");

System.out.println(number);

final Function<Integer, Integer> identity = Function.identity();
final Function<Integer, Integer> identity2 = t -> t;
System.out.println(identity.apply(999));
System.out.println(identity2.apply(999));
  • Function은 T타입 인자를 받고 R타입 객체를 리턴한다.
  • apply() 를 통해 사용할 수 있다.

Consumer

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
    ...
}

final Consumer<String> print = System.out::println;
final Consumer<String> greetings = s -> System.out.println(s + " World");

print.accept("jaja");
greetings.accept("Hello");
greetings.accept("CAD");
  • T 를 입력 받아 accept로 사용한다.
  • Consumer는 반환 타입이 없다(void).

Prediate

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
    ...
}

Predicate<Integer> isPositive = i -> i > 0;

System.out.println(isPositive.test(1));
System.out.println(isPositive.test(0));
System.out.println(isPositive.test(-1));

true
false
false
  • Prediate는 T타입 객체를 넘겨받아 bool형 결과를 반환한다.
  • test() 를 통해 사용한다.

Supplier

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}
  • Supplier는 입력 값은 없고 T 타입 객체를 리턴한다.
  • Supplier의 장점을 하나 말하자면 Lazy Evalution 을 사용할 수 있다.
  • 무슨 말이냐 하면 일단 코드부터 보자
	// Supplier<String> valueSupplier
	private static void printIfValidIndexSupplier(int number, Supplier<String> valueSupplier) {
		if (number >= 0) {
			System.out.println("The Supplier value is " + valueSupplier.get() + ".");
		} else {
			System.out.println("Invalid");
		}
	}

	// 비교해보기 위해 일반 String value
	private static void printIfValidIndex(int number, String value) {
		if (number >= 0) {
			System.out.println("The value is " + value + ".");
		} else {
			System.out.println("Invalid");
		}
	}
  • number가 0 이상, 0 이하일 때 다른 문자열을 출력하는 코드이다.
private static String getVeryExpensiveValue() {
	try {
    	// 데이터를 가져오기 위해 2초가 걸린다고 가정...
		TimeUnit.SECONDS.sleep(2);
	} catch (Exception e) {
		e.printStackTrace();
	}
	return "Expensive Value!!!";
}
  • 위 함수는 문자열(무거운 데이터)을 얻기 위해 2초 가량 소모된다고 가정하자.

  • 그리고 일반 String과 Supplier 를 통해 데이터를 가져오는 시간을 비교해보자.

		long start = System.currentTimeMillis();
		printIfValidIndex(0, getVeryExpensiveValue());
		printIfValidIndex(-1, getVeryExpensiveValue());
		printIfValidIndex(-2, getVeryExpensiveValue());
		System.out.println("It took " + ((System.currentTimeMillis() - start) / 1000) + " seconds");


		System.out.println();
		System.out.println();

		long start2 = System.currentTimeMillis();
		printIfValidIndexSupplier(0, SupplierExamples::getVeryExpensiveValue);
		printIfValidIndexSupplier(-1, () -> getVeryExpensiveValue());
		printIfValidIndexSupplier(-2, () -> getVeryExpensiveValue());
		System.out.println("It took " + ((System.currentTimeMillis() - start2) / 1000) + " seconds");


//결과

The value is Expensive Value!!!.
Invalid
Invalid
It took 6 seconds


The Supplier value is Expensive Value!!!.
Invalid
Invalid
It took 2 seconds
  • 일반 String의 경우 getVeryExpensiveValue()를 호출하자 마자 즉시 데이터를 가져온다.
  • 하지만 Supplier는 조건을 만족하는(.get()이 호출되는) 시점 까지 대기하고 있다가 필요해질 때 데이터를 가져온다.

Ref.

https://codechacha.com/ko/java8-functional-interface/
https://www.youtube.com/watch?v=7e7FCMFrwcg&list=PLRIMoAKN8c6O8_VHOyBOhzBCeN7ShyJ27&index=7
https://bcp0109.tistory.com/313

profile
Dare mighty things!

0개의 댓글