[Java] Lambda

·2022년 10월 16일

JAVA

목록 보기
10/10

함수형 프로그래밍

Java 8부터 람다(Lambda)가 도입되었다. 이를 활용하면 함수형 프로그래밍 스타일로 자바 코드를 작성할 수 있다. 이를 이용해 코드를 작성하면 코드의 양이 줄어들고, 가독성을 높이는 데 유리하다.

 

람다(Lambda)


람다를 사용하면 인터페이스 내부 메서드를 별도의 구현 클래스 없이 사용하거나, 인터페이스도 구현하지 않고 익명의 함수를 만들어 바로 사용할 수 있다.

먼저 구현 없이 인터페이스 내부 메서드를 사용해 보자.

@FuntionalInterface // 람다 함수로 쓸 인터페이스는 어노테이션을 사용하는 것이 좋다.
interface AddTwoInt {
	int sum(int a, int b);
}

public class Example {
    public static void main(String[] args) {
        AddTwoInt ati= (a, b) -> a + b; // 람다로 바로 구현해서 쓴다.
        int result = ati.sum(3, 4);
        System.out.println(result);  // 7 출력
    }
}

람다함수로 사용하기 위해서는 해당 인터페이스에 메서드가 하나만 있어야 한다.
@FuntionalInterface 어노테이션을 사용하면 인터페이스에 메서드를 하나만 작성할 수 있도록 강제해 람다 함수로 안정적으로 쓸 수 있다.
(a, b) -> a + b;(int a, int b) -> a + b; 로도 쓸 수 있으나 자료형은 인터페이스에 선언되어 있으므로 생략가능하다.
또한 두 int의 합을 구하는 것은 Integer.sum(int a, int b)와 동일하기 때문에, 이미 존재하는 어떤 클래스의 메서드를 가져와 사용한다면 AddTwoInt ati = Integer::sum; 즉, 클래스명::메서드명 으로도 작성할 수 있다.

 

굳이 인터페이스를 따로 작성하지 않고 더 쉽게 익명 함수를 만들 방법은 없을까?
Java에서 제공되는 BiFunction 인터페이스를 사용하면 아래처럼 더 쉽게 작성할 수 있다.

	BiFunction<Integer, Integer, Integer> ati = (a, b) -> a + b;
    BiFunction<입력, 입력, 출력> 이름 = 람다식;
    
    // BiFunction은 입출력 타입을 다양하게 설정할 수 있지만,
    // 위처럼 모두 같다면 BinaryOperator을 사용하면 더 간단하다.
    
    BinaryOperator<Integer> ati = (a, b) -> a + b;
    
    int result = ati.apply(3, 4); // apply로 호출한다.

이처럼 람다식을 사용하면 조금 더 간결한 코드 작성이 가능하다.
Java에서는 위에서 소개한 것보다 더 다양한 람다 인터페이스를 제공하고 있어, 필요에 맞게 사용할 수 있다.

스트림(Stream)


스트림은 데이터의 흐름을 의미한다. 배열, 리스트와 같은 데이터셋을 필터링하고 가공할 때 물 흐르듯 제어할 수 있도록 해 준다.

주어진 정수 배열에서 짝수만 찾아 중복을 제거한 다음 역순으로 정렬해야 하는 문제가 있다고 가정해 보자.

import java.util.*;

public class Sample {
    public static void main(String[] args) {
        int[] data = {5, 6, 4, 2, 3, 1, 1, 2, 2, 4, 8};

        // 짝수만 포함하는 ArrayList 생성
        ArrayList<Integer> dataList = new ArrayList<>();
        for(int i=0; i<data.length; i++) {
            if(data[i] % 2 == 0) {
                dataList.add(data[i]);
            }
        }

        // Set을 사용하여 중복을 제거
        HashSet<Integer> dataSet = new HashSet<>(dataList);

        // Set을 다시 List로 변경
        ArrayList<Integer> distinctList = new ArrayList<>(dataSet);

        // 역순으로 정렬
        distinctList.sort(Comparator.reverseOrder());

        // Integer 리스트를 정수 배열로 변환
        int[] result = new int[distinctList.size()];
        for(int i=0; i< distinctList.size(); i++) {
            result[i] = distinctList.get(i);
        }
    }
}

스트림을 활용하지 않으면 위처럼 기나긴 과정을 거쳐 결과를 얻어야 한다.
하지만 스트림을 사용하면 아래처럼 간단하게 표현할 수 있다.

import java.util.Arrays;
import java.util.Comparator;

public class Sample {
    public static void main(String[] args) {
        int[] data = {5, 6, 4, 2, 3, 1, 1, 2, 2, 4, 8};
        int[] result = Arrays.stream(data)  // IntStream을 생성한다.
                .boxed()  // IntStream을 Stream<Integer>로 변경한다.
                .filter((a) -> a % 2 == 0)  //  짝수만 걸러낸다.
                .distinct()  // 중복을 제거한다.
                .sorted(Comparator.reverseOrder())  // 역순으로 정렬한다.
                .mapToInt(Integer::intValue)  // Stream<Integer>를 IntStream으로 변경한다.
                .toArray()  // int[] 배열로 반환한다.
                ;
    }
}

스트림은 적힌 순서대로 데이터를 처리한다. 가독성이 훨씬 뛰어난 것을 확인할 수 있다.
스트림은 위처럼 기존 데이터를 통해 만들 수도 있지만 빌더를 이용해 원하는 값을 넣어 만들 수도 있고, 빈 스트림을 만들 수도 있다. 스트림에서 제공되는 작업 역시 매우 다양하므로 필요에 맞게 찾아 사용하면 데이터 처리를 한결 수월하게 진행할 수 있다.

 

 


이 글은 점프 투 자바를 읽고 스터디한 글입니다.

0개의 댓글