[JAVA] - 람다 표현식

buckshot·2024년 5월 23일

JAVA

목록 보기
5/18
post-thumbnail

람다

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

특징

  • 익명
    보통의 메소드와는 달리 이름이 없으므로 익명이라고 표현한다. 또한 구현해야 할 코드에 대한 걱정거리가 줄어든다.

  • 함수
    람다는 메소드 처럼 특정 클래스에 종속이 되지 않으므로 함수라고 칭한다. 하지만 메소드 처럼의 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다.

  • 전달
    람다 표현식을 메소드 인수로 전달하거나 변수로 저장할 수 있다.

  • 간결성
    익명 클래스 처럼 많은 자질구레한 코드를 구현할 의무는 없다.

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

이렇게 두 사과에 대하여 무게를 비교하는 가존 코드를 람다를 사용을 하면

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

이렇게 기존의 코드에 비하여 훨씬 간단하게 작성이 가능하다.

람다 표현식

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

이 코드를 보면
->을 기준으로 세 부분으로 이루어 진다.

  • 파라미터 리스트
    (Apple a1, Apple a2)
  • 화살표
    람다의 파리미터 리스트와 바디를 구분한다.
    ->
  • 람다 바디
    a1.getWeight().compareTo(a2.getWeight());

람다 기본 문법

(parameters) -> expression
(parameters) -> { statements; }

유효한 람다 표현식

(String s) -> s.length() 
(Apple a) -> a.getWeight() > 150
(int x, int y) -> { System.out.println("~~"); }
() -> 42
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

함수형 인터페이스

람다를 필터 메소드에서 사용이 가능하다. 추가적으로 함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다.

함수형 인터페이스는 정확히 하나의 추상 메소드를 지정하는 인터페이스다.
자바API의 함수형 인터페이스로는 Runnable, Comparator등이 있다.

// java.lang.Runnable
public interface Runnable {
	void run();
}

// java.util.Comparator
public interface Comparator<T> {
	int compare(T o1, T o2);
}

// java.util.concurrent.Callable
public interface Callable<V> {
	V call() throws Exception;
}

다음 인터페이스 중 어느것이 함수형 인터페이스인가?

public interface Adder {
	int add(int a, int b);
}
public interface SmartAdder extends Adder {
	int add(double a, double b);
}
public interface Nothing

이러한 질문이 있을 때 정답은 Adder만이 함수형 인터페이스다.
SmartAdder는 두 추상 add메소드(Adder 상속)를 포함하므로 함수형 인터페이스가 아니다.
마지막으로 Nothing은 아무런 추상 메소드가 없기에 함수형 인터페이스가 아니다.

그러면 이러한 함수형 인터페이스로 무엇을 할 수 있는가?

람다 표현식으로 함수형 인터페이스의 추상 메소드를 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다. 함수형 인터페이스보다는 덜 깔끔하지만 익명 내부 클래스로도 같은 기능을 구현할 수 있다.

함수 디스크립터

함수형 인터페이스의 추상 메소드 시그니처는 람다 표현식의 시그니처를 가리킨다. 람다 표현식의 시그니처를 서술하는 메소드를 함수 디스크립터라고 말 한다.

예를 들어서 Runnable인터페이스의 유일한 추상 메소드 run은 인수와 반환값이 없으므로 Runnable 인터페이스는 인수와 반환값이 없는 시그니처로 생각하면 된다.

(Apple, Apple) -> int

위 코드는 2개의 Apple를 인수로 받아 int를 반환하는 함수를 가리킨다.


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

람다와 동작 파라미터화로 유연하고 간결한 코드를 구현하는데 있어 도움을 주는 실용적인 예제를 보면 자원 처리에 사용하는 순환 패턴은 자원을 열고, 동작을 한 다음에 자원을 닫는 순서로 구성되어 있다.
즉 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는다. 이와 같은 형식의 코드를 실행 어라운드 패턴이라고 한다.

public String processFile() throws IOException {
	try (BufferedReader br =
			new BufferedReader(new FileReader("data.txt"))) {
		return br.readline(); // <- 실제 필요한 작업을 수행하는 행
	}
}

위 예제 코드는 자바 7에 추가된 try-with-resources 구문이 사용되었다. 아를 사용하면 자원을 명시적으로 닫을 필요가 없으므로 간결한 코드를 구현하는데 도움을 준다.

동작 파라미터화를 기억하라

processFile 메소드가 한 번에 두 행을 읽게 하려면 BufferedReader를 인수로 받아 String을 반환하는 람다가 필요하다.

아래의 코드는 BufferedReader에서 한 번에 두 행을 출력하는 코드다.

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

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

함수형 인터페이스 자리에 람다를 사용할 수 있다. 그렇기에 BufferedReader -> String과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다.

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

정의한 인터페이스를 processFile 메서드의 인수로 전달

public String processFile(BufferedReaderProcessor p) throws IOException{
        ...
}

동작 실행

BufferedReaderProcessor에 정의된 메소드의 시그니처 (BufferedReader -> String)와 일치하는 람다를 전달할 수 있다.

processFile 바디 내에서 BufferedReaderProcessor 객체의 process를 호출할 수 있다.

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

람다 전달

이제 람다를 이용해서 다양한 동작을 processFile 메서드로 전달할 수 있다.

// 한 행을 처리
String oneLine = processFile((BufferedReader br) -> br.readLine());

// 두 행을 처리
String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());

이렇게 함수형 인터페이스를 이용하여 람다를 전달하는 방법을 이야기 했다.

함수형 인터페이스 사용

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))
          resuls.add(t);
    }
    return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listsOfStrings, nonEmptyStringPredicate);

Consumer

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

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

public void <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); // <- Consumer의 accept 메서드를 구현하는 람다
);

Function

java.util.function.Function<T,R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply 정의한다.
입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 사용할 수 있다.

아래의 코드는 String리스트를 인수로 받아 각 String의 길이를 포함하는 Integrt 리스트로 변환하는 map메소드를 정의한다.

@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(); // <- Function의 apply 메서드를 구현하는 람다
);

기본형 특화

위 3개의 활용 예제를 보았다. 하지만 특화된 형식의 함수형 인터페이스도 있다.

자바의 모든 형식은 참조형 아니면 기본형에 해당한다.
하지만 제네릭 파라미터에는 참조형만이 가능하다. 이는 제네릭 내부 구현 때문에 어쩔 수 없다.

자바에서 기본형을 참조형으로 변환하는 기능을 제공한다. 이 기능은 박싱, 언박싱이라고 한다. 또한 개발에 있어 편리하게 자동으로 박싱, 언박싱이 이루어지는 오토박싱이라는 기능도 제공한다.

List<Integer> list = new ArrayList<>();
for (int i = 300, i < 400; i++) {
	list.add(i);
}

위 코드에서 int가 Integer로 박싱이 된다. 하지만 이런 변환 과정은 비용이 소모가 된다.

박싱한 값은 기본형을 감싸는 래퍼며 힙에 저장된다. 따라서 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요하다.

자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공한다.

public interface InPredicate {
	boolean test(int t);
}

Intpredicate evenNumbers = (int i) -> i % 2 ==0;
evenNumbers.test(1000);

Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
oddNumbers.test(1000);

위 코드는 IntPredicate는 100이라는 값을 박싱하지 않지만, Predcate<Integer>는 1000이라는 값을 Integer객체로 박싱한다.

profile
let's go insane

0개의 댓글