본문 바로가기
Java

[CS] Java 8 Lambda

by codeok 2022. 7. 27.
반응형

오늘은 Java 8 Lambda란 어떤 것이고 문법과 람다식을 사용하면 어떻게 코드가 간결해지는지 살펴본다.

목차

  1. Lambda란?
  2. Lambda 문법
  3. Lambda 작성 주의사항
  4. 함수형 인터페이스(Functional Interface)
  5. interface 생성 방법 vs 익명 클래스 사용 방법 vs 람다 사용 방법 

 

람다(Lambda)란?

  1. 람다란 대용량 병렬 처리와 이벤트 처리를 위해서 Java 8부터 등장한 표현식이다.
  2. 함수(메서드)를 간단한 식으로 표현하는 방법이다.
  3. 람다는 익명 함수(이름이 없는 함수)이다

람다의 형태는 람다 파라미터와 람다 바디로 구성되어 있다.

람다 파라미터 -> 람다 바디

 

람다는 하나의 추상(abstract) 메서드만 가진 함수형 인터페이스일 때만 사용이 가능하다.

 

람다는 기존에 별도의 interface를 만들거나 익명 클래스를 사용하는 방법보다 코드의 양을 줄이고 코드의 재사용성을 높일 수 있는 방법이다.

 

Lambda 문법

  1. 메서드의 이름과 반환타입 제거
  2. 함수 마지막 파라미터 뒤에 화살표 함수(->) 사용  
  3. 반환값이 있는 경우, 식이나 값만 적고 return문 생략 가능(끝에 세미클론 ; 생략가능)
  4. 블록 안의 문장이 하나이면 괄호{} 생략 가능
  5. 매개변수 타입이 추론 가능하면 생략 가능(대부분의 경우 생략)

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

 

람다 사용 주의사항

  1. 매개변수가 하나인 경우 소괄호 () 생략가능
  2. 매개변수가 없는 경우 소괄호 () 생략 불가
  3. 블록 안에 문장이 하나일 때 중괄호 {} 생략 가능

 

매개변수가 하나인 경우 소괄호 () 생략 가능

// 기존 코드
(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
  1. 별도 파일을 통한 클래스 정의 방법
  2. 익명 클래스 방법
  3. 람다 방법

 

세 가지 방법을 테스트 코드를 통해서 살펴본다.

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