Java Lambda (1) 기본

🗓 ⏱ 소요시간 6 분

이번 포스트부터 Java 8 에서 새로 도입된 람다(Lambda)와 Java 9 의 모듈 프로그래밍까지 쭉 다뤄보려고 합니다. 사실 람다도 몇 번 사용해보면 쉽게 익숙해질 수 있는 기술이지만, 내부적인 동작 원리까지 알아보려고 합니다.

Lambda

람다 대수는 1930년대 알론조 처치Alonzo Church가 소개한 함수의 수학적 표기 방식입니다. 람대 대수는 함수를 사용해 수학을 탐구하는 방식이었습니다. 이러한 방식은 리스프에서 람다 함수를 적용한 이후로 프로그래밍 분야에서도 발전해왔습니다.

1
2
3
4
5
6
7
# 수학에서의 람다 대수 표현식
# x 인자를 받아서 더하는 함수(람다)
λx.x+1

# Lisp 람다 표현식
# x 인자를 받아서 더하는 함수(람다)
(lambda (arg) (+ arg 1))

람다는 익명 함수 (이름이 없고 내용만 있는 함수)이고, 함수(Funciton)를 정의하는 간편한 방법입니다. 특히 다른 함수에 함수를 인자로 전달할 때 유용합니다.

Function

자바의 람다(함수)와 익명 클래스와 유사하게 사용되지만 기술적으로 차이가 있습니다. 단순히 익명 클래스를 쉽게 사용하기 위한 syntactic sugar 가 아닙니다.

  • 익명 클래스는 인스턴스를 생성해야 하지만, 함수는 평가될 때마다 새로 생성되지 않습니다. 함수를 위한 메모리 할당은 자바 힙의 Permanent 영역에 한 번 저장됩니다.
  • 객체는 데이터와 밀접하게 연관해서 동작하지만, 함수는 데이터와 분리되어 있습니다. 상태를 보존하지 않기 때문에 연산을 여러 번 적용해도 결과가 달라지지 않습니다(멱등성).
  • 클래스의 스태틱 메소드가 함수의 개념과 가장 유사합니다.

이론 상 차이점

이 anonymousClass 메소드는 Condition 의 구현체를 인자로 넘겨주며 waitFor 메소드를 호출합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 클로저
void anonymousClass() {
final Server server = new HttpServer();
waitFor(new Condition(){
@Override
public Boolean isSatisfied() {
return !server.isRunning();
}
})
}

// 람다로 표현
void closure() {
Server server = new HttpServer();
waitFor(() -> !server.isRunning());
}

변수 server 는 Condition 클래스의 익명 인스턴스로 복사되어야 합니다. 여기서 사용될 때와 넘겨질 때의 시간 차이 동안 변경되지 않도록 하기 위해 final 로 선언됩니다(최신 버전에서는 갱신되지 않는다고 판단되면 final 로 자동 처리). 반면 람다에서는 실행 환경이나 다른 조건들이 복사되지 않습니다.

점유 문법 Capture

  • 익명함수(익명클래스의 메소드)에서 this 는 익명 클래스의 인스턴스를 참조합니다.
  • 람다에서 this는 그것을 둘러싼 범위를 참조합니다.
1
2
3
4
5
6
7
8
9
10
public class Example {
private String firstName = "Jack";

public void example() {
Function<String, String> addSurname = surname -> {
// this.firstName 과 동일함
return firstName + " " + surname;
}
}
}

예제에서 this 는 람다를 둘러싼 범위 -> Example 클래스를 참조하기 때문에 Jack 을 가리킵니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Example {
private String firstName = "Charlie";

public void anotherExample() {
Function<String, String> addSurname = new Function<String, String>() {
@Override
public String apply(String surname) {
// this.firstName 은 컴파일 에러
return Example.this.firstName + " " + surname;
}
}
}
}

익명 클래스에서는 this 키워드는 익명 클래스의 인스턴스를 의미하기 때문에 firstName 이 없습니다. 따라서 Example.this.firstName 과 같이 접근해야 합니다.

섀도잉 Shadowing

섀도잉은 외부, 내부에 동일한 이름의 변수가 존재할 때 내부 범위의 변수가 우선되기 때문에, 외부 범위의 변수가 덮어씌워지는 것을 말합니다. 가려진다고 해서 섀도잉이라고 합니다.

1
2
3
4
5
6
7
8
9
10
11
public class ShadowingExample {
private String firstName = "Charlie";

public void shadowingExample(String firstName) {
Function<String, String> addSurname = surname -> {
// firstName -> 매개변수
// this.firstName -> "Charlie"
return this.firstName + " " + surname;
};
}
}

람다 내부에서 this 가 사용되었기 때문에 그것을 둘러싸고 있는 범위를 참조합니다. this 사용 시에는 Charlie 를 가리키고 this 없이 firstName 은 매개변수를 가리키게 됩니다.

기본 문법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Arrays.sort() 와 익명 클래스
Arrays.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer first, Integer second) {
return first.compareTo(second);
}
});

// Arrays.sort() 와 람다
Arrays.sort(numbers, (first, second) -> first.compareTo(second));

// 사실 익명 클래스 인스턴스인 것처럼 처리하고 있다
Comparator<Integer> asc = (first, second) -> first.compareTo(second);
Arrays.sort(numbers, asc);

람다는 기본적으로 기능을 가지는 익명의 코드 블록입니다. 위 예제에서 보면 람다를 이용해 훨씬 간결하게 표현한 것을 볼 수 있습니다. 여기서 람다가 Comparator<Integer> 타입으로 처리됩니다. Comparator 는 하나의 추상 메소드만 가지고 있기 때문에, 컴파일러가 봤을 때 이 람다가 그 추상 메소드를 구현한 내용이라고 보고 Comparator<Integer> 타입으로 처리한 것입니다.

이렇게 언제든지 하나만 있는 추상 메소드는 람다로 교체할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
interface Example {
R apply(A arg);
}

// 인스턴스 생성 방식
new Example() {
@Override
public R apply(A args) {
// body
}
}

이를 람다로 바꾸는 방법은 인자 목록과 함수 내용(body)만 남기고 화살표 부호(->)로 연결해주면 됩니다.

1
2
3
(args) -> {
// body
}

Arrays.sort() 예제를 한 번 더 살펴봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Arrays.sort() 와 익명 클래스
Arrays.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer first, Integer second) {
return first.compareTo(second);
}
});

// 인스턴스 생성과 메소드 시그니처를 간략화
Arrays.sort(numbers, (Integer first, Integer second) {
return first.compareTo(second);
});

// 인자의 타입을 생략하고 중괄호를 삭제해 간략화
Arrays.sort(numbers, (first, second) -> first.compareTo(second));

마지막에 간략화된 모습을 보면 return 키워드도 생략되었습니다. 이렇게 생략할 수 있는 것은 컴파일러가 충분히 알아차릴 수 있기 때문입니다. 인터페이스에 따라서 int 값을 반환해야 하는데 first.compareTo() 의 값이 int 이기 때문에 유추가 가능합니다.

함수의 인자가 하나라면 인자를 둘러싼 괄호도 생략할 수 있습니다.

1
2
3
4
5
// x 를 받아서 x + 1 을 리턴하는 람다
(x) -> x + 1

// 인자 괄호 생략
x -> x + 1

문법 요약

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 인자 -> 바디
(int x, int y) -> { return x + y; }

// 인자 타입 생략 - 컴파일러가 추론
(x, y) -> { return x + y; }

// return 및 중괄호 생략
(x, y) -> x + y

// 인자가 하나인 경우 인자 괄호 생략
x-> x * 2

// 인자가 없으면 빈 괄호로 표시
() -> System.out.println("Hey there!")

// 메소드 참조 Method reference
// (value -> System.out.println(value)) 의 축약형
System.out::println

참고