오늘은 Java 8 Lambda란 어떤 것이고 문법과 람다식을 사용하면 어떻게 코드가 간결해지는지 살펴본다.
목차
- Lambda란?
- Lambda 문법
- Lambda 작성 주의사항
- 함수형 인터페이스(Functional Interface)
- interface 생성 방법 vs 익명 클래스 사용 방법 vs 람다 사용 방법
람다(Lambda)란?
- 람다란 대용량 병렬 처리와 이벤트 처리를 위해서 Java 8부터 등장한 표현식이다.
- 함수(메서드)를 간단한 식으로 표현하는 방법이다.
- 람다는 익명 함수(이름이 없는 함수)이다
람다의 형태는 람다 파라미터와 람다 바디로 구성되어 있다.
람다 파라미터 -> 람다 바디
람다는 하나의 추상(abstract) 메서드만 가진 함수형 인터페이스일 때만 사용이 가능하다.
람다는 기존에 별도의 interface를 만들거나 익명 클래스를 사용하는 방법보다 코드의 양을 줄이고 코드의 재사용성을 높일 수 있는 방법이다.
Lambda 문법
- 메서드의 이름과 반환타입 제거
- 함수 마지막 파라미터 뒤에 화살표 함수(->) 사용
- 반환값이 있는 경우, 식이나 값만 적고 return문 생략 가능(끝에 세미클론 ; 생략가능)
- 블록 안의 문장이 하나이면 괄호{} 생략 가능
- 매개변수 타입이 추론 가능하면 생략 가능(대부분의 경우 생략)
int max() 함수가 람다 문법을 거치게 되면 마지막에 어떻게 되는지 확인한다.
// 기존 코드
int max(int a, int b){
return a > b ? a : b;
}
// 1. 메서드의 이름과 반환타입 제거 int max
(int a, int b){
return a > b ? a : b;
}
// 2. 함수 마지막 파라미터 뒤에 -> 사용
(int a, int b) -> {
return a > b ? a : b;
}
// 3. 반환값이 있는 경우, 식이나 값만 적고 return, 세미클론(;) 생략 가능
(int a, int b)-> {
a > b ? a : b
}
// 4. 블록 안의 문장이 하나이면 괄호{} 생략 가능
(int a, int b) -> a > b ? a : b
// 5. 매개변수 타입 생략가능 - (int a, int b) -> (a, b)
(a, b) -> a > b ? a : b
람다 사용 주의사항
- 매개변수가 하나인 경우 소괄호 () 생략가능
- 매개변수가 없는 경우 소괄호 () 생략 불가
- 블록 안에 문장이 하나일 때 중괄호 {} 생략 가능
매개변수가 하나인 경우 소괄호 () 생략 가능
// 기존 코드
(a) -> a * a
// 실행가능 코드
a -> a * a
// 에러 코드
int a -> a * a
자주 사용하는 정렬 예제를 통해 익명 클래스를 만드는 방법과 람다가 적용된 코드가 어떻게 다른지 확인해본다.
List<String> words = Arrays.asList("bbb", "aaa", "ccc");
// 람다를 사용하지 않고 익명 클래스 생성 코드
Collections.sort(words, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.compareTo(o1);
}
});
// 람다 적용 코드
Collections.sort(words, (o1, o2) -> o2.compareTo(o1));
함수형 인터페이스(Functional Interface)
함수형 인터페이스란 하나의 추상(abstract) 메서드만 가진 인터페이스이다.
하단의 코드처럼 하나의 추상 메소드만 가지고 있으면 함수형 인터페이스이다.
Java의 어노테이션에서 @FunctionalInterface를 사용하면 함수형 인터페이스라고 한눈에 알아볼 수 있고, 컴파일 시점에 두 개의 추상 메서드가 들어오는 것을 방지할 수 있다.
하단의 그림을 보면 @FunctionalInterface 어노테이션 작성 시 두 개의 추상 메서드를 선언하면 여러 개의 추상 메서드는 선언하면 안 된다고 에러가 발생한다.
Multiple non-overriding abstract methods found in interface lambda.product.ProductPredicate
interface를 생성 방법 vs 익명 클래스 사용 방법 vs 람다 사용 방법
세 가지 코드를 통해서 람다를 사용하면 어떻게 간결해지고 재사용성을 높이는지 한 번 보겠습니다.
- 첫 번째는 별도의 interface를 만들어서 사용하는 방법
- 1억 원 이상인 값들만 가져오는 AmountPredicate 인터페이스를 직접 생성해서 구현
- 두 번째는 익명 클래스를 사용하는 방법
- 3억 원 이상인 값들만 가져오는 익명 클래스를 생성해서 구현
- 마지막 람다를 사용하는 방법
- 5억 원 이상인 값들만 가져오는 람다식으로 구현
Ex) 상품(Product)의 금액이 특정 금액 이상인 배열을 List에 담는 예제를 통해서 살펴보겠습니다.
- Product.java
Product에는 id와 price만 필드만 존재
package lambda.product;
public class Product {
private int productId;
private int price;
public Product(int productId, int price) {
this.productId = productId;
this.price = price;
}
public int getProductId() {
return this.productId;
}
public int getPrice() {
return this.price;
}
public String toString() {
return "Product{productId=" + this.productId
+ ", price=" + this.price + "}";
}
}
- ProductPredicate.java
상품(Product)이 특정 금액에 부합하는지 test 하기 위한 함수
package lambda.product;
public interface ProductPredicate {
boolean test(Product product);
}
- AmountPredicate.java
기존에 별도의 interface를 만들어서 사용하는 방법은 상품의 가격이 1억 원 이상이면 true를 반환하는 메서드이다.
package lambda.product;
public class AmountPredicate implements ProductPredicate{
@Override
public boolean test(Product product) {
return product.getPrice() >= 100_000_000;
}
}
- ProductTest.java
- 별도 파일을 통한 클래스 정의 방법
- 익명 클래스 방법
- 람다 방법
세 가지 방법을 테스트 코드를 통해서 살펴본다.
package lambda.product;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class ProductTest {
List<Product> products = new ArrayList<>();
// 각 테스트 실행할 때 마다 prodcutId가 1 ~ 5, amount가 1억 ~ 5억인 값 생성
@BeforeEach
void beforeEach() {
for (int i = 1 ; i <= 5; i++){
Product product = new Product(i , 100_000_000 * i);
products.add(product);
}
}
// products 리스트를 ProductPredicate interface의 test() 메소드로
// 조건에 일치하는 상품을 리스트에 담고 반환하는 메소드
public static List<Product> filterProduct(List<Product> products, ProductPredicate productPredicate) {
List<Product> filterProducts = new ArrayList<>();
for (Product product : products){
if (productPredicate.test(product)){
filterProducts.add(product);
}
}
return filterProducts;
}
@DisplayName("1. 별도의 파일을 통한 클래스 정의 방법")
@Test
void fileTest(){
List<Product> filterProducts = filterProduct(products, new AmountPredicate());
// new AmountPredicate()에서는 1억원 이상의 값에 대해 true를 반환
// filterProducts에 product 5개가 담겨져 있다.
assertEquals(filterProducts.size(), 5);
}
@DisplayName("2. 익명 클래스 사용 방법")
@Test
void anonymousClassTest(){
List<Product> filterProducts = filterProduct(products, new ProductPredicate() {
@Override
public boolean test(Product product) {
return product.getPrice() >= 300_000_000;
}
});
// 익명 클래스의 test 메소드에서는 3억원 이상의 값에 대해 true 반환
// filterProducts에 prodcut 3개가 담겨져 있다.
assertEquals(filterProducts.size(), 3);
}
@DisplayName("3. 람다 사용 방법")
@Test
void lambdaTest(){
List<Product> filterProducts = filterProduct(products,
product -> product.getPrice() >= 500_000_000);
// 람다식에서는 5억원 이상의 값에 대해 true 반환
// filterProducts에 prodcut 1개가 담겨져 있다.
assertEquals(filterProducts.size(), 1);
}
}
위 테스트 코드를 실행하면 정상적으로 모두 통과한다.
첫 번째로 별도 파일을 통한 클래스 정의 방법은 상품 가격이 1억 원 이상인 것들을 필터링하기 위해서 AmountPredicate interface를 만들어야 하는 불편함이 있다.
두 번째 익명 클래스를 만드는 방법은 별도 파일을 만드는 것보다 편리하지만 매 번 객체를 생성하고 오버 라이딩되는 코드를 작성해줘야 한다.
마지막으로 람다를 사용하면 함수형 인터페이스의 메서드에 맞게 람다 파라미터와 실행할 구문인 람다 바디를 작성만 해주면 되기에 앞 선 두 가지 방법보다는 단순하며 간결해진다.
결론
람다 사용법을 정리해보면서 기존의 인터페이스 파일을 생성하고, 익명 클래스를 만드는 방식보다 간결하게 사용할 수 있게 되는 것을 느꼈다.
람다의 문법과 람다식을 사용하면 어떻게 코드가 간결해지는지 비교에 초점을 맞추느라 장단점에 대해서는 다루지 못했다.
람다의 장단점은 하단의 우아한 테크 코스의 스컬님이 람다에 대해서 잘 정리해주셔서 참고하면 좋을 거 같다.
참고
'Java' 카테고리의 다른 글
[CS] Java 8 Stream (2) | 2022.07.09 |
---|