자바 람다식(Lambda Expressions in Java)

자바 8에서 람다식(Lambda Expressions)이 추가 되었습니다. 이번 포스팅은 간단하게 람다식에 대해 알아보고자 합니다.

1. Lambda Expressions

람다식을 간단히 정의하면 다음과 같습니다.
식별자 없이 실행 가능한 함수 표현식
요즘은 정의만 보면 잘 모르겠어요. 부연 설명을 조금 해봅시다. 람다식은 자바 8의 가장 특징적인 기능입니다. 또한 기존의 불필요한 코드를 줄이고 가독성을 향상시키는것에 목적을 두고 있습니다.
대표적으로 반복문이나 비교문이 있겠습니다. 어떤식으로 코드를 줄이는지 한번 알아봅시다.

2. Lambda Expressions Example

자바에서 람다식을 사용하려면 다음과 같은 방법으로 사용이 가능합니다.
( parameters ) -> expression body
( parameters ) -> { expression body }
() -> { expression body }
() -> expression body
...
너무 단순한가요? 실제 코드로 확인을 해봅시다.

2.1. Basic

"Hello World."라는 단어를 출력하고 종료하는 쓰레드를 만들어봅시다. 처음으로는 기존에 존재하던 방식으로 작성해보고 다음은 람다식으로 작성해봅니다.

2.1.1. Traditional Code

기존에 있던 방식으로 만든 코드는 다음과 같습니다.
// Thread - traditional
new Thread(new Runnable() {
 @Override
 public void run() {
  System.out.println("Hello World.");
 }
}).start();
쓰레드를 돌리기 위해 Runnable 인터페이스를 새롭게 작성해서 매개변수로 넣었습니다. 이 코드를 람다식으로 바꾸면 어떻게 될까요?

2.1.2. Lambda Expression Code

아래에 있는 코드는 위에 있던 코드를 람다식으로 다시 작성한 것입니다.
// Thread - Lambda Expression
new Thread(()->{
 System.out.println("Hello World.");
}).start();
기존에 있던 코드량이 줄어든 것을 볼 수 있습니다. 인자가 없기 때문에 ()로 작성하고 실제로 동작할 코드를 ->{ ... }의 내부에 작성했습니다. 처음에 적어두었던 () -> { expression body }구조입니다. 하지만 이것은 꽤나 단순한 예제이고 이것으로 람다식이 끝은 아닙니다.

2.2. Using @FunctionalInterface

객체지향 언어인 자바에서 값이나 객체가 아닌 하나의 함수(Funtion)을 변수에 담아둔다는 것은 이해가 되지 않을 것입니다. 하지만 자바 8에서 람다식이 추가 되고 나서는 하나의 변수에 하나의 함수를 매핑할 수 있습니다.
실제로 다음과 같은 구문을 실행시키고자 한다면 어떻게 해야할까요?
Func add = (int a, int b) -> a + b;
분명히 int형 매개 변수 a,b를 받아 그것을 합치는 것을 람다식으로 표현한것입니다. 그러면 Func는 무엇이어야 할까요?
답은 interface입니다. 위와 같은 람다식을 구현하려면 Func 인터페이스를 아래처럼 작성합니다.
interface Func {
 public int calc(int a, int b);
}
이 인터페이스에서는 하나의 추상 메소드를 가지고 있습니다. 바로 calc라는 메소드입니다. 이 메소드는 int형 매개 변수 2개를 받아 하나의 int형 변수를 반환합니다. 아직 내부 구현은 어떻게 할지 정해지지 않았죠.
이 내부 구현을 람다식으로 만든것이 처음에 보셨던 코드입니다. 아래의 코드죠.
Func add = (int a, int b) -> a + b;
여기까지는 진행에 무리가 없어보입니다. 그러면 혹시 Func 인터페이스에 메소드를 추가하게 되면 어떻게 될까요?
람다식으로 구현했던 add 함수 코드에서 오류가 납니다. 기본적으로 람다식을 위한 인터페이스에서 추상 메소드는 단 하나여야 합니다. 하지만 이러한 사실을 알고 있다 하더라도 람다식으로 사용하는 인터페이스나 그냥 메소드가 하나뿐인 인터페이스나 구별을 하기 힘들뿐더러 혹시라도 누군가 람다식으로 사용하는 인터페이스에 메소드를 추가하더라도 해당 인터페이스에서는 오류가 나지 않습니다.
따라서 이 인터페이스는 람다식을 위한 것이다라는 표현을 위해 어노테이션 @FunctionalInterface을 사용합니다. 실제로 저 어노테이션을 선언하면 해당 인터페이스에 메소드를 두 개 이상 선언하면 유효하지 않다는 오류를 냅니다. 즉, 컴파일러 수준에서 오류를 확인 할 수 있습니다.
다음처럼 Func 인터페이스의 코드가 변경됩니다.
@FunctionalInterface
interface Func {
 public int calc(int a, int b);
}

2.2.1. Various uses

물론 add 함수처럼 내부 구현을 할 수 있지만 조금 더 다양하게 구현을 해봅시다. 다음의 코드는 조금씩 내부 구현을 바꿔본 예제 코드입니다.
Func sub = (int a, int b) -> a - b;
Func add2 = (int a, int b) -> { return a + b; };
뺄셈을 위한 sub 함수도 만들었고 add 함수와 비슷하지만 expression body에 return 키워드를 붙인 add2 함수도 있습니다.

2.2.2. Using Function

그러면 실제로 람다식을 통해 내부를 구현한 함수 add는 어떻게 사용할까요? 다음의 코드를 봅시다.
int result = add.calc(1,2) + add2.calc(3, 4); // 10
위에서 만들어뒀던 add 함수와 add2 함수를 사용했습니다. 따라서 result 변수의 결과값으로 10이라는 것을 알 수 있습니다.

3. Stream API

람다식을 소개하면서 Stream API를 소개 하지 않을 수 없겠지요. 람다식을 추가하면서 같이 추가된 Stream API를 살펴봅시다. 이번 포스팅은 람다식이 주요 내용이었기 때문에 간단하게 사용법만 알아봅시다.

3.1. Get Stream

먼저 Stream API를 사용하려면 stream을 얻어와야 합니다. 얻는 방법은 다음과 같습니다.
Arrays.asList(1,2,3).stream(); // (1)
Arrays.asList(1,2,3).parallelStream(); // (2)
콜렉션 관련 객체라면 stream을 얻어올 수 있습니다. (1)번 방법은 일반적인 stream을 가져오는 것이고 (2)번 방법은 병렬로 stream을 가져옵니다.

3.2. Working Stream

실제로 얻어온 stream에 연산을 해봅시다. 주요하게 쓰이는 몇가지 API만 살펴봅시다.

3.2.1. forEach

stream의 요소를 순회해야 한다면 forEach를 활용할 수 있습니다.
Arrays.asList(1,2,3).stream()
     .forEach(System.out::println); // 1,2,3

3.2.2. map

stream의 개별 요소마다 연산을 할 수 있습니다. 아래의 코드는 리스트에 있는 요소의 제곱 연산을 합니다.
Arrays.asList(1,2,3).stream()
     .map(i -> i*i)
     .forEach(System.out::println); // 1,4,9

3.2.3. limit

stream의 최초 요소부터 선언한 인덱스까지의 요소를 추출해 새로운 stream을 만듭니다.
Arrays.asList(1,2,3).stream()
     .limit(1)
     .forEach(System.out::println); // 1

3.2.4. skip

stream의 최초 요소로부터 선언한 인덱스까지의 요소를 제외하고 새로운 stream을 만듭니다.
Arrays.asList(1,2,3).stream()
     .skip(1)
     .forEach(System.out::println); // 2,3

3.2.5. filter

stream의 요소마다 비교를 하고 비교문을 만족하는 요소로만 구성된 stream을 반환합니다.
Arrays.asList(1,2,3).stream()
     .filter(i-> 2>=i)
     .forEach(System.out::println); // 1,2

3.2.6. flatMap

stream의 내부에 있는 객체들을 연결한 stream을 반환합니다.
Arrays.asList(Arrays.asList(1,2),Arrays.asList(3,4,5),Arrays.asList(6,7,8,9)).stream()
     .flatMap(i -> i.stream())
     .forEach(System.out::println); // 1,2,3,4,5,6,7,8,9

3.2.7. reduce

stream을 단일 요소로 반환합니다.
Arrays.asList(1,2,3).stream()
     .reduce((a,b)-> a-b)
     .get(); // -4
이 코드는 조금 설명이 필요할 것 같습니다. 우선, 첫번째 연산으로 1과 2가 선택되고 계산식은 앞의 값에서 뒤의 값을 빼는 것이기 때문에 결과는 -1이 됩니다. 그리고 이상태에서 -1과 3이 선택되고 계산식에 의해 -1-3이 되기 때문에 결과로 -4가 나옵니다. 뒤로 추가 요소가 있다면 차근차근 앞에서부터 차례대로 계산식에 맞춰 계산하면 됩니다.

3.2.8. collection

아래의 코드들은 각각의 메소드로 콜렉션 객체를 만들어서 반환합니다.
Arrays.asList(1,2,3).stream()
     .collect(Collectors.toList());
Arrays.asList(1,2,3).stream()
     .iterator();

4. Closing Remarks

람다식과 간단한 Stream API 사용법을 알아 봤습니다. :3

댓글

가장 많이 본 글