자바

[모던자바] Function, The Transformer, Consumer, Predicate, Supplier

모디(modi) 2020. 1. 9. 12:10

케빈 TV (자바8) 못다한 이야기를 들으며 내용을 정리해보았습니다.

https://www.youtube.com/watch?v=Ql9car-IjR0&t=2s

 

 

최대한 잘 정리해서 실무에 적용하고 싶을 때 참고할 수 있는 자료가 되었으면 좋겠네요 ^^

 

 

  • Functional Interface
    • 인터페이스인데 그 안에 구현해야 될 Abstract Method가 하나만 존재하면 그것을 Functional Interface라고 부른다.
    • 이 인터페이스는 람다표현식으로 구현 가능하다. 
  • 중요한 이유 ? 
    • Functional Interface를 사용하는 코드는 Functional Interface의 Object, Instance에 해당하는 Anonymous class를 생성할 필요없이 Lambda Expression으로 대체할 수 있음
    • Lambda Expression을 사용하기에 꼭 필요한 Interface라고 볼 수 있다. 
    • 사실, Lambda Expression의 type이 Functional Interface
    • 구현해야하는 Abstract Method가 1개여야만 하는 이유는 메소드 이름, 매개변수의 타입 지정 같은 것이 없어야 컴파일러가 인터페이스와 메서드, 인자를 추론할 수 있기 때문이다. 

 

 

 

 

 

자바 8에 추가된 Functional Interface Type을 알아보자!

 

 

Function

@FunctionalInterface
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));
    }
    
    static <T> Function<T, T> identity() { return t -> t;}
}
  • @FunctionalInterface라는 애노테이션을 추가하여 이 인터페이스는 추상 메서드 하나만을 가질 수 있는 함수형 인터페이스라는 걸 알려준다. -> 추상메서드 개수 제한!
  • FunctionalInterface 애노테이션이 있는 인터페이스는 람다 표현식으로 구현할 수 있다. 
    • 애노테이션 반드시 달아야 하는 것은 아니나 나중에 누가 메서드를 추가하거나 하는 상황을 막기 위해서는 이 애노테이션을 달고 그런 문제를 사전에 방지하도록 하자!
  • R apply (T t)를 보면 다른 전달받은 Type과 다른 Type을 반환하는 것을 알 수 있다.
    • 같은 Type을 리턴하는 함수를 Identity 함수라고 한다. -> 입력값과 같은 Type의 Value를 리턴하는 함수

람다 적용 전

Function<String, Integer> toInt = new Function<String, Integer>() {
  @Override
  public Integer apply(final String value) {
  return Integer.parseInt(value);
  }
};

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

람다 적용중

 

Function<String, Integer> toInt1 = (final String value) -> {
   return Integer.parseInt(value);
};

람다 적용 완성!

Function<String, Integer> toInt2 = value -> Integer.parseInt(value);

 

 

 


 

 

Consumer

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

 

람다적용 전

final Consumer<String> print = new Consumer<String>() {
	@Override
    public void accept(final String value) {
    	System.out.println(value);
    }
}

print.accept("Hello");

 

람다적용 후

final Consumer<String> pringt = value -> System.out.println(value);
print.accept("Hello");

 

  • Consumer를 Function으로 바꾸면 ? 
final Consumer<String> print = value -> System.out.println(value); // OK
final Function<String, Void> print2 = value -> System.out.println(value); //ERROR!!

Function은 반드시 입력값과 출력값이 있어야 한다. 리턴값을 Void로 명시해주더라도 에러가 발생한다. 

따라서, 리턴되는 값이 없는 경우에는 Consumer Function Interface를 사용해주도록 한다. 

 

 

 

 


 

Predicate

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

입력값 T만 있고 리턴되는 값은 항상 Boolean이다. 

 

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

System.out.println(isPositive.test(1)); //  true

 

Predicate을 사용한 번외편 ? 

 

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

List<Integer> positiveNumbers = Arrays.asList(-3,-2,-1,0,1,2,3,4,5);

List<Integer> positiveNumbersResult = new ArrayList<>();
for (Integer num : numbers) {
	if (isPositive.test(num)) {
    	positiveNumbers.add(num);
    }
}

List<Integer> lessThan3Result = new ArrayList<>();
for (Integer num : numbers) {
	if (lessThan.test(num)) {
    	lessThan3Result.add(num);
    }
}

for문 안의 if 조건문만을 제외하고는 두개의 for문이 중복된다는 것을 알 수 있다.

중복 부분을 제거하기 위해 공통으로 사용하는 메소드로 처리해보자. 

 

 

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

List<Integer> positiveNumbers = Arrays.asList(-3,-2,-1,0,1,2,3,4,5);

System.out.println("positive integers : "  + filter(numbers, isPositive));
System.out.println("less than 3 : "  + filter(numbers, lessThan3));

private static <T> List<T> filter(List<T> list, Predicate<T> filter) {
	List<T> result = new ArrayList<>();
    for (T input : list) {
    	if (filter.test(input)) {
        	result.add(input);
       }
    }
    return result;
}

두개가 다른 if 조건문에다가는 Functional Interface인 Predicate 을 입력했다. 

Predicate 덕분에 'i > 0', 'i < 3' 이렇게 조건문이 다른 데도 같은 동작을 할 수 있다는 것을 알 수 있다.

아니면 Lambda Expression 덕분에 메소드에 다른 메소드를 넘길 수 있게된 덕? 이라고 할 수도 있겠다.

 

 

 

 


Supplier

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

입력값이 없는데 리턴값이 있다. -> Lazy evaluation(계산의 결과값이 필요할 때까지 계산을 늦추는 기법) 가능하게 한다.

 

final Supplier<String> helloSupplier = () -> "Hello";

System.out.println(helloSupplier.get() + "world");

 

왜 그냥 "Hello"를 쓰면 되지 Supplier를 쓰는가? 예제로 알아보자

private String getVeryExpensiveValue() {
	return "Kevin";
}

private static String getVeryExpensiveValue() {
    try {
    	TimeUnit.SECONDS.sleep(3);
    } catch (InterruptionException e) {
    	e.printStackTrace();
    }
    
    return "KEVIN"
}

private static void printIfValidIndex(int number, String value) {
	if (number >= 0) {
    	System.out.println("The value is " + value + ".");
    } else {
    	System.out.println("Invalid);
    }
}

long start = System.currentTimeMills();
printIfValidIndex(0, getVeryExpensiveValue()); //3s
printIfValidIndex(-1, getVeryExpensiveValue()); //3s
printIfValidIndex(-2, getVeryExpensiveValue()); //3s

System.out.println("It took " + ((System.currentTimeMillis() - start) / 1000) + " seconds");

"It took 9 seconds" -> 결과는 9초

조건에 부합되지 않는 것 까지 계산하였으니..

 

 

Supplier를 써보자! -> Lazy Evaluation으로 불필요한 계산을 줄여 메모리, CPU 자원 낭비도 줄이고 싶다면!

private String getVeryExpensiveValue() {
	return "Kevin";
}

private static String getVeryExpensiveValue() {
    try {
    	TimeUnit.SECONDS.sleep(3);
    } catch (InterruptionException e) {
    	e.printStackTrace();
    }
    
    return "KEVIN"
}

private static void printIfValidIndex(int number, Supplier<String> valueSupplier) {
	if (number >= 0) {
    	System.out.println("The value is " + valueSupplier.get() + ".");
    } else {
    	System.out.println("Invalid);
    }
}

long start = System.currentTimeMills();
printIfValidIndex(0, () -> getVeryExpensiveValue()); // 3s
printIfValidIndex(-1, () -> getVeryExpensiveValue()); // 조건통과안되어 getVeryExpensiveValue 실행 X
printIfValidIndex(-2, () -> getVeryExpensiveValue()); // 조건통과안되어 getVeryExpensiveValue 실행 X

System.out.println("It took " + ((System.currentTimeMillis() - start) / 1000) + " seconds");

결과는 3초!

printIfValidIndex(0, new Supplier<String>() {
	@Override
    public String get() {
    	return getVeryExpensiveValue();
    }
});

 

 

 

 


 

 

케빈님 강좌가 참 좋은게 람다는 이렇게 쓰는 거다! 라고 정답을 바로 말해주는 것이 아니라

원래는 ~~이런데 요렇게 요렇게 바뀌어서 이렇게 된거다! 라는 식으로 

이전버전과 현재버전을 비교해주시면서 설명해주신다는 것! 

이해하는 데 참 많은 도움이 된다.