람다
익명 클래스로 다양한 동작을 구현할 수 있지만, 인터페이스를 구현하고 해당 인터페이스를 상속받는 클래스를 만들고 사용하던지 아님 익명 클래스를 사용해야 했습니다.
[람다의 특징]
익명 클래스를 단순화 한 것
- 파라미터 리스트, 바디, 반환형식, 예외리스트를 가질 수 있다.
- 익명성 : 메서드의 이름이 없다.
- 함수 : 특정 클래스에 종속되지 않는다.
- 전달 : 람다 표현식 자체를 메서드 인수로 전달하거나 변수로 저장 가능
- 간결성 : 간결하게 코드 가능
[함수 디스크립터]
함수의 선언에서
반환타입 함수이름 (파라미터 타입, 파라미터 타입2);
와 같이 지정할 수 있다. 이와 같이 람다도 이러한 타입을 부를 수 있는데 바로 함수 디스크립터이다.
() -> void
(Apple, Apple) -> int
(int, String) => void
와 같이 표현한 것이 함수 디스크립터이다
[함수형 인터페이스]
하나의 추상 메서드만 가지고 있으며 디폴트 메서드는 상관 없다.
- @FunctionalInterface: 자바에서 사용되는 어노테이션
- 함수형 인터페이스가 아니라면 에러를 발생시키기 때문에 붙이는 것이 좋다.
- 기존 자바에서 Compareable, Runnable, Callable 등 다양한 함수형 인터페이스를 제공한다
기존 인터페이스를 상속받아서 익명클래스를 통해 사용을 했다.
하지만 자바8에서는 다양한 함수형 인터페이스를 제공하는데 대표적으로 Predicate, Consumer, Function등이 있다.
- 이러한 인터페이스를 제공함으로써 람다를 제공할때 받는 메서드 자료형을 새로 만들지 않아도 된다.
1. Predicate
@FunctionalInterface
interface Predicate<T> {
boolean test(T t);
}
함수 디스크럽트 : (T) -> boolean;
public static void predicateTest() {
List<String> nonEmpty = predicateFilter(new ArrayList<>(), (String s) -> !s.isEmpty());
}
public static <T> List<T> predicateFilter(List<T> list, Predicate<T> pred) {
List<T> result= new ArrayList<>();
for (T t: list) {
if (pred.test(t)) {
result.add(t);
}
}
return result;
}
람다의 시그니처 (T)-> boolean 이므로 Predicate를 사용해서 받았다.
또한 해당 메서드의 test기능을 통해 test를 한다.
2. Consumer
@FunctionalInterface
interface Consumer<T> {
void accept(T t);
}
함수 디스크럽트 : (T) -> void;
public static <T> void acceptTest() {
foreach(new ArrayList<>(), (i) -> System.out.println(i));
}
public static <T> void foreach(List<T> list, Consumer<T> consumer) {
for (T t : list) {
consumer.accept(t);
}
}
Consumer는 입력 값을 받고 반환하지 않는다. 해당 기능에서 구현한 accept()를 실행하여 한다.
3. Function
@FunctionalInterface
interface Function<T, R> {
R apply(T t);
}
함수 디스크럽트 : (T) -> R;
public static void functionTest() {
List<Integer> result = map(new ArrayList<>(), (String s)->s.length());
}
public static <T,R> List<R> map(List<T> list, Function<T,R> f) {
List<R> result = new ArrayList<>();
for (T t : list) {
result.add(f.apply(t));
}
return result;
}
람다의 시그니처(T) -> R 이므로 Function을 사용
[기본형 특화]
- Int / Double / Long Predicate
- Int / Double / Long Consumer
- Int / Double / Long / ToInt / ToDouble / ToLong Function
자바의 기본 타입이 박싱되어 래퍼런스 타입이 된다. 이때 자바는 박싱과 언박싱을 자동으로 해주는 오토박싱이 존재하는데 이런 변화 과정에는 비용이 소모된다.
- 박싱된 값 = 래퍼런스타입이며, 이는 힙에 저장된다. 따라서 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색해야 한다.
[그 외 함수형 인터페이스]
- Supplier<T> = () -> T
- UnaryOperator<T> = T -> T
- BinaryOperator<T, T> = (T, T) => T
- BiPredicate<L, R> = (T, U) -> boolean
- BiConsumer<T, U> = (T, U) -> void
- BiFunction<T, U, R> = (T, U) -> R
[예외, 람다, 함수형 인터페이스]
함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않는다.
따라서 예외를 선언하는 함수형 인터페이스를 직접 정의하거나 람다함수를 try / catch 해야한다.
[람다 표현식의 이해]
1. 형식 검사
람다가 사용되는 context를 통해 람다의 형식을 추론할 수 있다.
Context란?
- 람다가 전달될 메서드 파라미터
- 람다가 할당되는 변수
target type은 람다 표현식의 형식
형식 검사 동작 과정
- 람다를 받는 메서드의 선언을 확인
- 전달하는 파라미터로 target type을 지정 : 따라서 Predicate<T> => Predicate<Apple>
- 인터페이스가 함수형 인터페이스(추상 메서드가 1개)이므로 해당 추상 메서드 지정
- Apple -> boolean인 함수 디스크립터 완성
- 해당 함수 디스크립터와 람다의 시그니처가 같은지 확인
* void 효환
람다 바디에 일반 표현식있는 경우 void를 반환하는 함수 디스크립터와 호환된다.
Predicate<String> p = s-> list.add(s);
Consumer<String> c = s -> list.add(s);
Predicate는 (T) -> boolean, Consumer는 (T) -> void 이지만 서로 호환된다.
2, 형식 추론
자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 이용해 추론 ( 형식 검사 동작 과정 확인)
여기서 컴파일러가 람다 표현식의 파라미터 형식과 메서드 선언의 파라미터 형식을 비교할 수 있다는 것은 람다 표현식의 파라미터 형식을 알고 있다는 것이다. 따라서 파라미터를 생략 가능하다.
Predicate<String> pred = (String s) -> !s.isEmpty();
Predicate<String> pred = (s) -> !s.isEmpty();
3. 지역 변수 사용
람다 표현식에서도 지역 변수를 지정할 수 있다. 이렇게 파라미터로 넘겨진 변수가 아닌 외부 정의 변수를 활용하는 것을 람다 캡쳐링이라 한다.
- 단, `fianl`변수만 가능하며 캡처는 동일한 상태를 유지하는 것과 같다고 생각하면 된다.
*왜 그럴까?
바로 인스턴스 변수는 힙에 저장되는 반면, 지역 변수는 스택에 저장된다.
람다가 스레드에서 실행되다가 변수를 할당한 스레드가 사라져도 다른 스레드에서 해당 변수를 접근하려할 수 있다.
따라서 동일한 동작 수행을 위해 자바에서는 값이 변하지 않게 복사본을 제공하는 것으로 지역 변수를 사용하는 것을 허용
[메서드 참조]
기존의 메서드 정의를 재활해서 람다처럼 전달할 수 있다.
이를 통해 가독성을 높일 수 있다.
메서드 참조를 하는 방법
- 정적 메서드 참조 : Integer::parseInt
- 다양한 형식의 인스턴스 메서드 참조 : String::length
- 기존 객체의 인스턴스 참조 : instance::getValue
예를 들어 생성자를 메서드 참조로 생성하려 한다하면
Supplier<Apple> instance = Apple::new;
Apple a = instance.get();
Function<String, Apple> instance2 = Apple::new;
Apple b = instance2.apply("100");
- supplier : 함수 디스크립터는 () -> t이므로 Apple을 반환한다 보면 됩니다.
- Function : (T,R) -> R 이므로 파라미터가 1개인 생성자 호출
만약 2개 이상 여러개의 파라미터를 가진 생성자를 호출하고 싶으면? => 새로 인터페이스를 만들어야합니다.
public interface AutoFunction<T, U, V, R> {
R apply(T t,U u, V v);
}
[람다 표현식의 조합]
inventory.sort(Comparator
.comparing(Apple::getCountry)
.reversed()
.thenComparing(Apple::getA));
람다식을 조합하면 위와 같이 만들 수 있습니다.
먼저 비교는 Compartor.comparing(Apple::getCountry)로 처음 비교할 대상을 정하고
이후 reversed()로 뒤집었습니다. 마지막으로 동일한 경우 A를 대상으로 비교하라고 작성했습니다.
이것이 가능한 이유는 결국 Default Method덕분입니다. 함수형 인터페이스의 경우 하나의 추상 메서드를 가지고 있지만 default method의 갯수에는 정해진 것이 없고 구현까지 해놓을 수 있어서 이러한 연속 동작이 가능합니다.
1. Predicate 조합
java.util.function.Predicate<Apple> pred = new java.util.function.Predicate<>(){
@Override
public boolean test(Apple apple) {
return false;
}
};
java.util.function.Predicate<Apple> pred2 = (Apple a) -> a.getA().isEmpty();
pred.negate();
pred.and(apple -> apple.getA().isEmpty())
.or(apple->apple.getA().length()==4);
2. Function 조합
java.util.function.Function<Integer, Integer> f = x->x+1;
java.util.function.Function<Integer, Integer> g = x->x*2;
java.util.function.Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1); // 4
int result2 = f.andThen(g).apply(1); // 4
int result3 = f.compose(g).apply(1); //3 g먼저하고 f
'JAVA > Java in action' 카테고리의 다른 글
[자바 인 액션] 6. 스트림 데이터 수집 (0) | 2023.03.23 |
---|---|
[자바 인 액션] 5. 스트림 활용 (0) | 2023.03.23 |
[자바 인 액션] 4. 스트림 (0) | 2023.03.23 |
[자바 인 액션] 2. 동작 파라미터화 (0) | 2023.03.22 |
[자바 인 액션] 1. 자바의 변화 (0) | 2023.03.22 |