티스토리 뷰

동작 파라미터화를 이용하면 더 유연하고 재상용할 수 있는 커드를 만들 수 있다. 그리고 익명 클래스를 사용하여 다양한 동작을 구현할 수 있지만 코드가 깔끔하지 않았다. 람다 표현식을 사용하면 좀 더 깔끔한 코드를 작성할 수 있다. 람다 표현식은 익명 클래스 처럼 이름 없는 함수면서 메서드를 인수로 전달할 수 있다.


람다란 무엇인가?


람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이다. 람다의 특징은 아래와 같다.

  • 익명: 보통 메서드와 달리 이름이 없다.
  • 함수: 람다는 메서드 처럼 특정 클래스에 종속되지 않으므로 함수이다.
  • 전달: 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
  • 간결성: 익명 클래스처럼 부가적인 코드가 필요없다.

사과의 무게를 기준으로 오름차순하는 코드를 익명 클래스와 람다 표현식을 사용한 예는 아래와 같으며, 람다 표현식이 매우 간결하다.

inventory.sort(new Comparator<>(){
	public int compare(Apple a1, Apple a2) {
    	return a1.getWeight().compareTo(a2.getWeight());
    }
});
inventory.sort((Apple a1, Apple a2) -> a1.getWeight.compareTo(a2.getWeight()));

 

람다 표현식은 파라미터, 화살표, 바디로 이루어진다.

그리고 자바 8에서 지원하는 다섯 가지 람다 표현식의 예는 아래와 같다.

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


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

 

함수형 인터페이스

오직 하나의 추상 메서드만을 지정하는 인터페이스를 함수형 인터페이스라고한다. 자바가 제공하는 함수형 인터페이스는 아래와 같다.

함수형 인터페이스를 사용하면, 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할수 있다.

 

 

함수 디스크립터

함수형 인터페이스의 추상 메서드 시그니처(Signature)는 람다 표현식의 시그니처를 가리킨다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터(Function discriptor)라고 부른다.

 

- 함수 디스크립터: () -> "Hello World"

- 시그니처: () -> String

 

 

함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있는 이유는, 자바에 함수 형식을 추가하는 경우 언어를 더 복잡하게 만들기 때문이다. 기능의 추가 보다는 이미 익숙한 함수형 인터페이스를 사용한 것.

 

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


자원 처리에 사용하는 순환 패턴은 자원을 열고, 처리한 다음, 자원을 닫는 순서로 이루어 진다. 아래와 같은 형식의 코드를 실행 어라운드 패턴이라한다.

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

 

1단계: 동작 파라미터화를 기억하라

processFile만 다른 동작을 수행할 수 있도록 명령하면 좋을 것이다. 즉, processFile 동작을 파라미터화 하는 것이다.

람다를 이용해서 동작을 전달 할 수 있다. 두 라인을 읽는 예는 아래와 같다.

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

2단계: 함수형 인터페이스를 이용해서 전달

BufferedReader -> String과 IOException을 던질 수 있는 함수형 인터페이스를 만들어야 한다. 이 인터페이스를  BufferedReaderProcessor 이라 정의한다.

@FunctionalInterface
public interface BufferedReaderProcessor {
	String process(BufferedReader br) throws IOException;
}
public String processFile(BufferedReaderProcessor p) throws IOException {
	...
}

3단계: 동작 실행

이제 BufferedReaderProcessor에 정의된 process 메서드의 시그니처 BufferedReader -> String과 일치하는 람다를 전달할 수 있다. processFile의 내부 코드는 아래와 같다.

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

4단계: 람다 전달

한 행을 처리하는 코드

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

두 행을 처리하는 코드

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

 

함수형 인터페이스 사용


자바 8의 java.util.function 패키지에서는 여러 가지 함수형 인터페이스를 제공한다. 이번에는 Predicate, Consumer, Function 인터페이스를 소개한다.

Predicate

java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며, test는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환한다.

 

@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);

Comsumer

java.util.function.Consumer<T> 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다. T 형식의 객체를 인수로 받아, 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있다.

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

public <T> List<T> forEach(List<T> list, Comsumer<T> c) {
	List<T> results = new ArrayList<>();
    for(T t : list) {
    	c.accept(t);
    }
}

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

Function

java.util.function.Function<T> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 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> results = new ArrayList<>();
    for(T t : list) {
    	results.add(f.apply(t));
    }
    return results;
}

List<Integer> l = map(
	Arrays.asList("lambdas", "in", "action"),
    (String s) -> s.length()
);
//[7, 2, 6]

기본형 특화

자바에서는 기본형을 참조형으로 변환하는 기능을 제공하는데, 이를 박싱이라하고, 그 반대를 언박싱이라한다. 지금 까지 알아본 3 가지 함수형 인터페이스는 제네릭 함수형 인터페이스이므로, 기본형 타입을 추상 메소드의 인수로 전달하면 자동 박싱이 발생할 것이다.

 

하지만 박싱과 언박싱과 같은 변환 과정은 비용이 소모되므로, 자바는 기본형 데이터에 특화된 함수형 인터페이스를 제공한다. 보통 함수형 인터페이스 명 앞에 기본 자료형의 이름을 붙인다.

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


형식 검사

람다가 사용되는 컨텍스트(Context)를 이용해서 람다의 형식을 추론할 수 있다. 어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 대상 형식이라 한다.

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

위 코드의 대상 형식 과정은 아래와 같다.

  1. filter 메서드의 선언을 확인한다.
  2. filter 메서드는 두 번째 파라미터로 Predicate<Apple> 형식을 기대한다.
  3. Predicate<Apple>은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스이다.
  4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사한다.
  5. filter 메서드로 전달된 인수는 이와 같은 요구 사항을 만족해야한다.

형식 추론

자바 컴파일러는 람다 표현식이 사용된 컨텍스트(대상 형식)을 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다. 결과적으로 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로, 람다 문법에서 이를 생략할 수 있다.

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

 

지역 변수 사용

람다 표현식에서는 파라미터로 넘겨진 변수 뿐 아니라 자유 변수( 외부에서 정의된 변수) 또한 사용할 수 있다. 이를 람다 캡쳐링이라 한다. 하지만 사용할 수 있는 자유 변수에는 제약이있다. 사용할 자유 변수가 final로 선언 되어 있거나, final과 같이 사용 되어야 한다. 즉, 람다식은 한 번만 할당할 수 있는 지역변수를 캡처할 수 있다.

 

메서드 참조


메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다. 때로는 람다 표현식보다 메서드 참조를 사용하는 것이 더 가독성이 좋으며 자연스러울 수 있다.

 

기존 코드

inventory.sort((a1, a2) -> a1.getWeight.compareTo(a2.getWeight));

java.util.Comparator.comparing 을 활용한 코드

inventory.sort(comparing(Apple::getWeight));

메서드 참조를 만드는 방법

1. 정적 메서드 참조

EX) Integer.parseInt() -> Integer::parseInt

 

2. 다양한 형식의 인스턴스 메서드 참조

EX) "String".length() -> String::length

 

3. 기존 객체의 인스턴스 메서드 참조

EX) apple.getWeight() -> Apple::getWeight

 

생성자 참조

Classname::new 처럼 클래스명과 new 키워드를 이용하여 기존 생성자의 참조를 만들 수 있다. 

Supplier의 () -> Apple과 같은 시그니처를 갖는 생성자가 있다고 가정한다.

 

// 기존 코드
Supplier<Integer, Apple> c1 = () -> new Apple();
Apple a1 = c1.get();

// 생성자 참조
Supplier<Integer, Apple> c1 = Apple::new
Apple a1 = c1.get();

/**
 * Supplier의 get 메서드를 이용해서 새로운 Apple 객체를 만들 수 있다.
 */

Apple(Integer weight) 라는 시그니처를 갖는 생성자는 Function 인터페이스의 시그니처와 같다. 따라서 다음과 같이 구현할 수 있다.

// 기존 코드
Function<Integer, Apple> f1 = (weight) -> new Apple(weight);
Apple a2 = f1.apply(110) 

// 생성자 참조
Function<Integer, Apple> f1 = Apple::new
Apple a2 = f1.apply(110);

/**
 * Function의 apply 메서드에 무게를 인수로 전달하여 새로운 Apple 객체를 만들 수 있다.
 */
List<Integer> weights = Arrays.asList(1, 2, 3, 4, 5);
List<Apple> apples = map(weights, Apple::new);

public <Apple> map(List<Integer> list, Function<Integer, Apple> f) {
	List<Apple> result = new ArrayList<>();
    for(Integer i : list) {
    	result.add(new Apple(i));
    }
    
    return result;
}

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


Comparator

// 역정렬
inventory.sort(comparing(Apple::getWeight).reversed());

// Comparator 연결
inventory.sort(comparing(Apple::getWeight) // 무게로 정렬
	.reversed() // 역정렬
        .thenComparing(Apple::getCountry)); // 무게가 같으면 국가별로 정렬

Predicate

Predigate 인터페이스는 복잡한 프레디게이트를 만들 수 있도록 negate, or, and 세 가지 메서드를 제공한다. 반전에는 negate를 사용한다. 그리고 and 와 or를 연결할 수 있다.

// red가 아닌 사과
Predicate<Apple> notRedApple = redApple.negate()

// red 면서 무거운 사과
Predicate<Apple> redAndHeavyApple = 
redApple.and(apple -> apple.getWeight() > 150);

// red면서 무거운 사과 혹은 그냥 초록색 사과
Predicate<Apple> redAndHeavyOrGreenApple = 
redApple.and(apple -> apple.getWeight() > 150);
	.or(apple -> GREEN.equals(a.getColor()));

Function

Function 인터페이스는 Function 인스턴스를 반환하는 andThen, compose 두 가지 디폴트 메서드를 제공한다.

 

Fuction<Integer, Integer> f = x -> x+1;
Fuction<Integer, Integer> g = x -> x*2;
Fuction<Integer, Integer> h = f.andThen(g);

int result = h.apply(1); // 4 -> f 다음 g

Fuction<Integer, Integer> f = x -> x+1;
Fuction<Integer, Integer> g = x -> x*2;
Fuction<Integer, Integer> h = f.compose(g);

int result = h.apply(1); // 3 -> g 다음 f
728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함