반응형

1. 서론

이번 포스팅에서는 Chapter3의 람다 표현식 에 대해 진행하도록 하겠습니다.

 

2. 람다란 무엇인가?

람다 표현식은 메서드로 전달할 수 있는 익명함수를 단순화한 것입니다.

 

아래는 람다의 특징입니다.

 

  1. 익명 : 보통 메서드와 달리 이름이 없어 익명함수입니다.
  2. 함수 : 람다는 메서드처럼 특정 클래스에 종속되지 않아 함수라고 부릅니다.
  3. 전달 : 람다는 메서드 인수로 전달하거나 변수로 저장이 가능합니다.
  4. 간결성 : 익명 클래스처럼 자질구레한 코드가 필요 없습니다.

람다는 자바 8 이전에 할 수 없었던 것을 제공하는 것이 아니라 간결한 코드를 만들어 준다고 이해하면 되겠습니다.

 

아래는 Comparator의 compare 메서드를 람다로 표현한 예제 입니다.

  • 파라미터 리스트 :  compare 메서드 파라미터입니다.
  • 화살표 : 람다의 파라미터 리스트와 바디를 구분하는 역할입니다.
  • 람다 바디 : 람다의 반환값에 해당하는 표현식입니다.

메서드는 인자 파라미터가 없을수도 있고 반환값이 없거나 반환값을 위해서 다양한 일을 수행해야 할수도 있습니다.

람다는 이를 모두 지원하기 위해 아래와 같이 총 5가지의 형태가 모두 가능합니다.

 

// 1 - String 파라미터 하나에 int 반환을 하는 람다
(String s) -> s.length()

// 2 - Apple 파라미터 하나에 boolean 반환을 하는 람다
(Apple a) -> a.getWeight() > 150

// 3 - int 파라미터 두개에 반환값이 없는 람다
(int x, int y) -> {
	System.out.println("Result: ");
    System.out.println(x+y);
}

// 4 - 파라미터는 없고 int를 반환하는 람다
() -> 42

// 5 - Apple 파라미터 두개에 int를 반환하는 람다
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

 

3. 어디에, 어떻게 람다를 사용할까?

람다는 함수형 인터페이스라는 문맥에서 사용할 수 있습니다.

 

1. 함수형 인터페이스

 

함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스를 의미합니다.

 

인터페이스에는 많은 디폴트 메서드가 있을 수 있습니다.

하지만 추상 메서드가 오직 하나라면 이도 함수형 인터페이스라고 부를 수 있습니다.

 

이 함수형 인터페이스의 추상 메서드를 람다 표현식을 통해 간결하게 만들 수 있는 것입니다.

 

2. 함수 디스크립터

 

함수 디스크립터란 람다 표현식의 시그니처를 서술하는 메서드입니다.

 

여기서 시그니처란 함수의 설명이라고 이해하시면 됩니다.

 

예를 들어, Runnable 인터페이스는 인수와 반환값이 없는 시그니처 라고 말할 수 있습니다.

 

3. @FunctionallInterface

 

이 어노테이션은 함수형 인터페이스를 가리키는 어노테이션입니다.

만약 함수형 인터페이스가 아니라면 컴파일단에서 에러를 내뱉게 되어있습니다.

 

4. 람다 활용 : 실행 어라운드 패턴

람다를 활용하는 예제를 간단하게 진행해 보겠습니다.

 

아래와 같이 파일을 한줄씩 읽는 작업을 하는 메소드가 있습니다.

 

public String processFile() throws IOException {
	try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
		return br.readLine();
	}
}

 

 

현재는 파일에서 한번에 한줄만 읽을 수 있습니다.

만약, 파일에서 두줄씩 읽어야하는 요구사항이 생긴다면?

파일을 open해서 읽는 작업은 동일하되, processFile의 읽는 코드만 수정하면 좋을 것입니다.

 

이를 위해 동작 파라미터화를 적용하겠습니다.

 

1. 동작 파라미터화

 

String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());

 

2. 함수형 인터페이스를 이용해서 동작 전달

 

위 정의한 동작을 함수형 인터페이스를 통해 전달하도록 수정해보겠습니다.

 

코드는 아래와 같습니다.

 

@FunctionalInterface
public interface BufferedReaderProcessor {
	String process(BufferedReader b) throws IOException;
}

public String processFile(BufferedReaderProcessor p) throws IOException {
	
}

 

3. 동작 실행

 

위 처럼 구조를 잡고 이젠 processFile의 메서드를 구현해 보겠습니다.

 

public String processFile(BufferedReaderProcessor p) throws IOException {
	try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
		return p.process(br);
	}
}

 

4. 람다 전달

 

이제 processFile을 사용하는 쪽에서는 람다를 통해 실행을 전달할 수 있습니다.

 

String oneLine = processFile((BufferedReader br) -> br.readLine());
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());

 

위와 같은 작업을 적용한 패턴을 실행 어라운드 패턴이라고 부릅니다.

 

 

 

 

 

반응형

 

 

 

 

 

5. 함수형 인터페이스 사용

자바 8 에서는 기본적인 함수형 인터페이스를 java.util.function 패키지에 제공하고 있습니다.

 

대표적인 Predicate, Consumer, Function 에 대해 소개하도록 하겠습니다.

 

1. Predicate

 

test 라는 추상메서드가 있고, 시그니처는 아래와 같습니다.

제네릭 타입의 인자 한개를 받아 boolean 값을 반환.

 

아래는 Predicate를 사용하는 예제 코드입니다.

 

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

public <T> List<T> filter(List<T> list, Predicate<T> p) {
	List<T> results = new ArrayList<>();
	for (T t: list) {
		if(p.test(t)) {
			results.add(t);
		}
	}
	return results;
}

Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

 

2. Consumer

 

accept라는 추상메서드가 있고, 시그니처는 아래와 같습니다.

제네릭 타입의 인자 한개를 받고 반환 값은 없는 void.

 

아래는 Consumer를 사용하는 예제 코드입니다.

 

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

public <T> void forEach(List<T> list, Consumer<T> c) {
	for (T t: list) {
		c.accept(t);
	}
}

forEach(
    Arrays.asList(1,2,3,4,5,),
    (Integer i) -> System.out.println(i)
);

 

3. Function

 

apply라는 추상메서드가 있고, 시그니처는 아래와 같습니다.

제네릭 타입의 인자 한개를 받고 제네릭 타입을 반환.

 

아래는 Function을 사용하는 예제 코드입니다.

 

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

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

List<Integer> l = map(
		Arrays.asList("lambdas", "in", "action"),
		(String s) -> s.length()
);

 

대표적인 3개의 함수형 인터페이스를 알아봤습니다.

 

java.util.function에는 이 3개 말고도 아래와 같이 더 있습니다.

 

  • Supplier<T>
  • UnaryOperator<T>
  • BinaryOperator<T>
  • BiPredicate<L, R>
  • BiConsumer<T, U>
  • BiFunction<T, U, R>

6. 형식 검사, 형식 추론, 제약

이번에는 자바 컴파일러가 람다를 어떻게 처리하는지 알아보도록 하겠습니다.

 

1. 형식 검사

 

람다가 사용되는 콘텍스트를 통해 람다의 형식을 추론할 수 있습니다.

 

이전에 예제로 했던 filter 를 통해 설명하겠습니다.

 

List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);

 

 

 

위 예제의 람다는 아래와 같은 순으로 형식검사가 이루어 집니다.

 

  1. 람다가 사용된 콘텍스트인 filter 메서드를 확인
  2. 확인 결과 Predicate<Apple>로 대상 형식 확인
  3. Predicate<Apple>의 추상메서드 확인
  4. 람다의 디스크립터와 Predicate<Apple>의 추상메서드가 동일한지 확인

 

 

2. 같은 람다, 다른 함수형 인터페이스

 

위 형식검사에서 본듯이 하나의 람다는 여러개의 함수형 인터페이스와 호환이 가능합니다.

 

아래는 그 예제입니다.

 

Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

 

3. 형식 추론

 

자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 이용해 람다 표현식과 관련된 함수형 인터페이스를 추론 할 수 있습니다.

 

그로인해, 람다 사용시 이 추론을 이용해 더욱 간결한 코드작성이 가능합니다.

 

아래는 그 예제입니다.

 

Comparator<Apple> c1 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

 

컴파일러는 파라미터인 Apple을 추론하기 때문에 위와 같이 생략이 가능합니다.

 

4. 지역변수 사용

 

람다에서는 외부 변수를 조작하는 경우도 있습니다.

이 경우 가능은 하지만 외부 변수는 final로 선언되거나 final처럼 취급되는 것에 한해서 가능합니다.

 

이유로는 인스턴수 변수는 힙에, 지역 변수는 스택에 저장되기 때문입니다.

람다가 실행되는 스레드에서 지역 변수를 참조할 때는 지역변수의 복사본(읽기 전용)을 생성하고 참조하게 됩니다.

하지만, 람다가 직접 지역변수가 위치한 스택영역에 접근하게 되면 지역변수를 할당한 스레드가 끝나면 변수 할당이 해제되는 시점과 겹칠 수 있습니다.

그래서, 람다는 복사본(읽기 전용)을 참조하기 때문에 해당 변수의 값은 변경되어서는 안되는 제약사항이 생기게 된것입니다.

 

7. 메서드 참조

메서드 참조는 특정 메서드만을 호출하는 람다의 축약형입니다.

 

메서드 참조는 코드의 가독성을 높인다는 장점을 가지고 있습니다.

 

또한, 람다의 축약형으로 동일하게 콘텍스트의 형식과 일치해야 하며, 생성자 참조도 지원하고 있습니다.

 

아래는 생성자 참조의 예제 입니다.

 

Supplier<Apple> c1 = Apple::new

 

8. 람다, 메서드 참조 활용하기

이제 배운 람다와 메서드 참조를 통해 사과를 무게순으로 정렬하는 코드를 단계별로 적용하는 예제를 진행하겠습니다.

 

1. 코드 전달

 

@Getter
class Apple {
	private Integer weight;
}

public class AppleComparator implements Comparator<Apple> {
	@Override
	public int compare(Apple o1, Apple o2) {
		return o1.getWeight().compareTo(o2.getWeight());
	}
} 
inventory.sort(new AppleComparator());

 

2. 익명 클래스 사용

 

inventory.sort(new Comparator<Apple>() {
	@Override
	public int compare(Apple o1, Apple o2) {
		return o1.getWeight().compareTo(o2.getWeight());
	}
});

 

3. 람다 표현식 사용

 

inventory.sort((Comparator<Apple>) (o1, o2) -> o1.getWeight().compareTo(o2.getWeight()));

 

4. 메서드 참조 사용

 

Arrays.asList().sort(Comparator.comparing(Apple::getWeight));

 

9. 람다 표현식을 조합할 수 있는 유용한 메서드

람다 표현식을 조합할 수 있는 유용한 메서드는 바로 디폴트 메서드입니다.

 

함수형 인터페이스는 디폴트 메서드가 있더라도 추상 메서드만 한개를 가지고 있으면 되기 때문에, 이 규칙에도 어긋나지 않습니다.

 

대표적인 함수형 인터페이스들의 디폴트 메서드를 소개하겠습니다.

 

1. Comparator

 

  • comparing - 비교에 사용할 키를 추출하는 Function
  • reversed - 오름차순이 아니라 내림차순으로 정렬.
  • thenComparing - 정렬을 위한 비교자 추가

2. Predicate

 

  • negate - 기존 Predicate 객체 결과를 반전시킨 객체 생성
  • and - 두 Predicate를 and로 연결하여 새로운 Predicate 객체 생성
  • or - 두 Predicate를 or로 연결하여 새로운 Predicate 객체 생성

3. Function

 

  • andThan - 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수
  • compose - 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공

 

10. 마무리

이번 포스팅에서는 Chapter 3인 람다 표현식에 대해 진행하였습니다.

다음에는 Chapter 4인 스트림 소개에 대해 포스팅하겠습니다.

반응형

+ Recent posts