람다식(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 한개가 되자 소괄호가 생략됨

람다를 사용하면 어떤 개선점을 얻을 수 있나요?

  1. 코드가 간결해짐 (가독성)
  2. 함수를 만들 필요 없어 생산성이 좋아진다.
  3. 병렬 프로그램에 용이하다. - 일반적으로 다중 cpu를 활용하는 형태로 구현되어 병렬 처리에 유리(Parallel)

람다식 관련하여 검색하면 병렬 처리에 유리하다는 내용이 존재한다.
람다식이 왜 병렬처리에 유리한가 찾아본 내용은 아래와 같다.

  1. 빅데이터 때문에 멀티코어를 활용한 분산처리, 즉 병렬화 기술이 필요해짐
  2. 기존의 CPU는 내부에 코어 하나만 가지고 있었음
  3. JAVA8 부터는 이를 대응하기 위해 병렬화 컬렉션(배열, List, Set, Map) 등을 강화했고,
    이를 더 효율적으로 쓰기 위해 최종적으로 람다를 사용하게 됨

히스토리 : 빅데이터 지원 -> 병렬화 강화 -> 컬렉션 강화 -> 스트림 강화 -> 람다 도입 -> 인터페이스 명세 변경 -> 함수형 인터페이스 도입
즉, 람다 자체가 병렬에 유리한 게 아니고, 병렬에 유리해진 stream을 사용하는데 람다식이 유용한 것 같음


람다 사용 시 유의사항 및 단점

  1. 가독성이 떨어질 수 있다.
  2. 디버깅이 어렵다 - 람다 자체가 내부적으로 수행하는게 많아 오류 메시지가 많아지며, 수행하는게 많은 이유로 성능이 떨어질 수 있다.
  3. 함수의 중복이 많을 수 있다. (익명 함수이기 때문에)
  4. 재귀에 부적합 (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의 값을 가져왔기 때문에
		}
	}
}

더블콜론(::)

  1. 람다 표현식이 단 하나의 메서드만을 호출하는 경우
  2. 람다 표현식이 단순히 객체를 생성하고 반환하는 경우

문법

클래스이름::메소드이름
참조변수이름::메소드이름


메서드를 참조하는 경우

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;
}