람다식(Lamda)
람다식 사용 방법
전제 조건
단 한개의 추상 메서드만 가지는 인터페이스 필요
interface
public interface MaxNumber {
int getMax(int num1, int num2);
}
AS-IS
MaxNumber maxNumber = new MaxNumber() {
@Override
public int getMax(int num1, int num2) {
return num1 >= num2 ? num1 : num2;
}
};
maxNumber.getMax(100, 101);
TO-BE
MaxNumber maxNumber = (x,y)->(x>=y) ? x:y;
maxNumber.getMax(100, 101);
(자료형 매개변수명,...) ->{실행문;};
으로 new 하는 과정을 짧게 생략할 수 있다.
(x,y)
: 매개변수 (매개변수의 자료형 생략됨)
(x>=y) ? x:y;
: 실행문 (함수 몸체에 실행문이 1개이고, 실행문 자체를 return했기에 중괄호와 return 생략됨-즉 실행문이 여러개이면 대괄호를 써서 여러개를 나열하면 됨)
+매개변수가 1개일경우에는 소괄호도 생략 가능
public interface MaxNumber{
int getMax(int num1);
}
//=>
MaxNumber maxNumber = x->(x>= 1) ? x:0;
이전에 매개변수가 2개였을 때 (x,y)
가 x
한개가 되자 소괄호가 생략됨
람다를 사용하면 어떤 개선점을 얻을 수 있나요?
- 코드가 간결해짐 (가독성)
- 함수를 만들 필요 없어 생산성이 좋아진다.
- 병렬 프로그램에 용이하다. - 일반적으로 다중 cpu를 활용하는 형태로 구현되어 병렬 처리에 유리(Parallel)
람다식 관련하여 검색하면 병렬 처리에 유리하다는 내용이 존재한다.
람다식이 왜 병렬처리에 유리한가 찾아본 내용은 아래와 같다.
- 빅데이터 때문에 멀티코어를 활용한 분산처리, 즉 병렬화 기술이 필요해짐
- 기존의 CPU는 내부에 코어 하나만 가지고 있었음
- JAVA8 부터는 이를 대응하기 위해 병렬화 컬렉션(배열, List, Set, Map) 등을 강화했고,
이를 더 효율적으로 쓰기 위해 최종적으로 람다를 사용하게 됨히스토리 : 빅데이터 지원 -> 병렬화 강화 -> 컬렉션 강화 -> 스트림 강화 -> 람다 도입 -> 인터페이스 명세 변경 -> 함수형 인터페이스 도입
즉, 람다 자체가 병렬에 유리한 게 아니고, 병렬에 유리해진 stream을 사용하는데 람다식이 유용한 것 같음
람다 사용 시 유의사항 및 단점
- 가독성이 떨어질 수 있다.
- 디버깅이 어렵다 - 람다 자체가 내부적으로 수행하는게 많아 오류 메시지가 많아지며, 수행하는게 많은 이유로 성능이 떨어질 수 있다.
- 함수의 중복이 많을 수 있다. (익명 함수이기 때문에)
- 재귀에 부적합 (2번과 같은 이유)
@FunctionalInterface
추상 메서드가 오직 하나인 인터페이스를 함수형 인터페이스라고 한다.
interface TestInterface() { // 추상 메서드를 오직 하나만 가지는 인터페이스
T testCall(); // 구현체가 없는 추상 메서드
}
위와같이 인터페이스를 생성 할 경우 @FunctionalInterface
어노테이션이 없어도 함수형 인터페이스가 되지만,
어노테이션을 이용하면 이후 유지보수 시 함수형 인터페이스 조건이 맞지 않게 될 때 빌드 오류로 캐치할 수 있게 된다.
함수형 인터페이스를 매번 만들어 사용 할 필요가 없다.
Java에서 제공되는 Functional Interface 들을 이용할 수 있다.
인터페이스명 | 람다표현 | 메서드 |
---|---|---|
Predicate | T -> boolean |
boolean test(T t) |
Consumer | T -> void |
void accept(T t) |
Supplier | () -> T |
T get() |
Function<T,R> | T -> R |
R apply(T t) |
Comparator | (T,T) -> int |
int compare(T o1, T o2) |
Runnable | () -> void |
void run() |
Callable | () -> V |
V call() |
매개변수가 2개인 함수형 인터페이스
(매개변수가 존재하는 함수형 인터페이스인 3가지만 종류)
인터페이스명 | 람다표현 | 메서드 |
---|---|---|
BiConsumer | (t,u) -> void |
void accept(T t, U u) |
BiFunction | (t,u) -> R |
R apply(T t, U u) |
BiPredicate | (t,u) -> boolean |
boolean test(T t, U u) |
❗만약 매개변수의 타입과 반환 타입이 동일하다면 (ex. T apply (T t) 와 같은 형태)
Function
, BiFunction
보다 UnaryOperator
, BinaryOperator
가 더 유리하다.
결론
대부분의 기능을 지원하기 때문에 Java 기본 함수형 인터페이스를 이용하면 인터페이스를 따로 생성하지 않아도 된다.
람다를 이용한 사례 ( 샘플 코드 포함 )
// 실제 사용중인 코드
userRepository.findUserById(id)
.orElseThrow(() -> new CustomThrowable("사용자 정보가 존재하지 않습니다."));
// orElseThrow 내용
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
// user정보 setting(mapping까지 한 번에)
Map<long, String> userMaps = userList.stream().collect(Collectors.toMap(
e->e.getId(), // key
e->e.getEmail()+"#"+e.getName() // value
));
// Collectors.toMap 내용
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return new CollectorImpl<>(HashMap::new,
uniqKeysMapAccumulator(keyMapper, valueMapper),
uniqKeysMapMerger(),
CH_ID);
}
외부변수를 참조하는 람다식
람다식 내에서 람다 바깥쪽 외부 변수를 참조하게되면,
해당 변수는 자동으로 상수(final)가 된다.
// 함수형 인터페이스 생성
interface MyFunction {
void print();
}
...
class A {
void method(int i) {
int val = 30; // 4. 자동으로 final int val = 30; 이 됨
i = 10; // 2. Error 발생 : (1)부분 람다식 내에서 참조되었기 때문에 final int i; 가 되어버려서 다시 주입할 수 없다.
// MyFunction f = (i) -> { 는 사용 불가능, i는 외부지역변수와 이름이 중복되기 때문
MyFunction f = (n) -> {
System.out.println("i : " + i); // 1. 람다식 외부 지역변수를 사용했기 때문에 i 변수는 자동으로 상수(final)이 됨
System.out.println("val : " + val); // 3. 외부 지역변수인 val의 값을 가져왔기 때문에
}
}
}
더블콜론(::)
- 람다 표현식이 단 하나의 메서드만을 호출하는 경우
- 람다 표현식이 단순히 객체를 생성하고 반환하는 경우
문법
클래스이름::메소드이름
참조변수이름::메소드이름
메서드를 참조하는 경우
List<String> list = Arrays.asList("first", "second", "third", "forth");
list.forEach(item -> System.out.println(item)); // 기본 람다식
list.forEach(System.out::println); // :: 문법
// 콘솔 로그 결과 동일
생성자를 참조하는 경우
Function<String, Food> function1 = (String a) -> new Food(a); // 람다식
Function<String, Food> function2 = Food::new; // :: 문법
class A {
private List<ApplicationWarmer> applicationWarmers;
private void excute() {
Optional.ofNullable(applicationWarmers) // applicationWarmers가 비어있는지 검증
.orElseGet(Collections::emptyList) // applicationWarmers가 비어있으면 빈 list를 applicationWarmers가 null이 아니게끔 생성
.forEach(this::warmup); // warmup(ApplicationWarmer warmer) 메서드를 applicationWarmers 사이즈만큼 for문 돌면서 호출
}
private void warmup(ApplicationWarmer warmer){
// do someting
}
}
...
Collections.java
// Collections::emptyList 의미는 emptyList 라는 메서드를 호출함
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}