케빈 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();
}
});
케빈님 강좌가 참 좋은게 람다는 이렇게 쓰는 거다! 라고 정답을 바로 말해주는 것이 아니라
원래는 ~~이런데 요렇게 요렇게 바뀌어서 이렇게 된거다! 라는 식으로
이전버전과 현재버전을 비교해주시면서 설명해주신다는 것!
이해하는 데 참 많은 도움이 된다.