본문 바로가기
Java

[점프 투 자바] 7장 자바 날개달기2

by seonggu 2024. 1. 1.

자바는 Java 8 버전부터 함수형 프로그래밍을 지원하기 위해

람다(lambda)와 스트림(stream)을 도입했다.

람다와 스트림을 사용하면 함수형 프로그래밍 스타일로 자바 코드를 작성할 수 있다.

물론 람다와 스트림을 사용하여 작성한 코드를 일반 스타일 자바 코드로 바꾸어 작성하는 것이 불가능하지 않다.

그런데도 람다와 스트림을 사용하는 이유는 작성하는 코드의 양이 줄어들고

읽기 쉬운 코드를 만들 수 있기 때문이다.

 

이 책에서는 람다와 스트림이 무엇인지 어떻게 사용하는지만 알아본다.

 

  • 람다(lambda)

익명 함수(anonymous function)을 의미한다.

일반적인 코드와 람다를 적용한 코드를 비교하여 알아보자

 

일반적인 코드

두 개의 정수를 입력받아 정수의 결괏값을 리턴하는 sum 함수를 정의한 인터페이스이다.

interface Calulator {
	int sum(int a, int b);
}

 

앞서 정의한 인터페이스를 사용하려면 Calculator인터페이스를 구현해야한다.

class MyCalculator implements Calculator {
    public int sum(int a, int b) {
        return a+b;
    }
}

 

아래는 완성한 프로그램이다.

interface Calculator {
    int sum(int a, int b);
}

class MyCalculator implements Calculator {
    public int sum(int a, int b) {
        return a+b;
    }
}

public class Sample {
    public static void main(String[] args) {
        MyCalculator mc = new MyCalculator();
        int result = mc.sum(3, 4);
        System.out.println(result);  // 7 출력
    }
}

 

람다를 적용한 코드

interface Calculator {
    int sum(int a, int b);
}

public class Sample {
    public static void main(String[] args) {
        Calculator mc = (int a, int b) -> a +b;  // 람다를 적용한 코드
        int result = mc.sum(3, 4);
        System.out.println(result);
    }
}

이 코드에서 사용한 람다함수는 다음과 같다

(int a, int b) -> a + b

괄호 사이의 int a, int b는 Calculator 인터페이스 sum함수의 입력항목에 해당하고

→ 뒤의 a+b가 리턴값에 해당한다.

이렇게 람다함수를 사용하면 MyCalculator와 같은 실제 클래스 없이도 Calculator 객체를 생성할 수 있고,

일반적인 코드보다 훨씬 간단해진다.

 

  • 인터페이스 사용 시 주의사항

Calculator 인터페이스의 메서드가 1개 이상이면 람다함수를 사용할 수 없다.

interface Calculator {
    int sum(int a, int b);
    int mul(int a, int b);  // mul 메서드를 추가하면 컴파일 에러가 발생한다.
}

람다 함수로 사용할 인터페이스는 처음처럼 @FunctionalInterface 어노테이션을 사용하는 것이 좋다.

@FunctionalInterface 어노테이션을 사용하면 2개 이상의 메서드를 가진 인터페이스를 작성하는 것이 불가능해진다.

@FunctionalInterface
interface Calculator {
    int sum(int a, int b);
    int mul(int a, int b);  // @FunctionalInterface 는 두 번째 메서드를 허용하지 않는다.
}

@FunctionalInterface 는 인터페이스가 함수형 인터페이스임을 표시하며,

단 하나의 메서드만 가질 수 있게 한다. 주로 람다표현식과 같이 사용한다.

 

  • 람다 축약하기

람다를 적용한 자바코드는 다음처럼 좀 더 축약이 가능하다.

인터페이스에 이미 입출력에 대한 타입이 정의되어 있으므로 입력값의 자료형인 int를 생략할 수 있다.

interface Calculator {
    int sum(int a, int b);
}

public class Sample {
    public static void main(String[] args) {
        Calculator mc = (a, b) -> a +b;
        int result = mc.sum(3, 4);
        System.out.println(result);
    }
}

 

  • 더 축약하기

두 수를 더하여 그 결과를 리턴하는 함수 (a,b) → a+b는

Integer.sum(int a , int b)와 동일하기 때문에 더 축약이 가능해진다

Integer::sum

어떤 클래스의 메서드를 사용할 때는 이와 같이 :: 기호를 사용하여 클래스와 메서드를 구분하여 표기한다.

 

적용한 코드 :

@FunctionalInterface
interface Calculator {
    int sum(int a, int b);
}

public class Sample {
    public static void main(String[] args) {
        Calculator mc = Integer::sum;
        int result = mc.sum(3, 4);
        System.out.println(result);
    }
}

 

  • 스트림

스트림(stream)은 글자 그대로 해석하면 ‘흐름’이라는 뜻이다.

데이터가 물결처럼 흘러가면서 필터링 과정을 통해 여러 번 변경되어 반환되기 때문에

이러한 이름을 가지게 됐다.

예제를 통해 이해해보자.

다음과 같은 배열이 존재한다.

int[] data = {5, 6, 4, 2, 3, 1, 1, 2, 2, 4, 8};

 

이 배열에서 짝수만 뽑아 중복을 제거한 후 역순으로 정렬하는 프로그램을 만들어보자.

int[] result = {8, 6, 4, 2};

 

스트림이 없을 땐 이렇게 만들 수 있다

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);
        }
    }
}

정수 배열에서 짝수만 찾아서 ArrayList에 넣고

Set을 이용하여 중복을 제거한 후

Set을 리스트로 변환하고 리스트의 sort를 사용하여 역순으로 정렬하고

정렬된 값으로 다시 정수 배열을 생성했다.

 

이 문제는 스트림을 사용하면 간단하게 해결할 수 있다.

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[] 배열로 반환한다.
                ;
    }
}
  1. Arrays.stream(data)로 정수 배열을 IntStream으로 생성한다.
  2. .boxed()로 IntStream을 Integer의 Stream으로 변경한다. 이렇게 하는 이유는 뒤에서 사용할 Comparator.reverseOrder 메서드는 원시 타입인 int 대신 Integer를 사용해야 하기 때문이다.
  3. .filter((a) -> a % 2 == 0)로 짝수만 필터링한다. 이때 사용한 (a) -> a % 2 == 0 구문은 앞에서 공부한 람다 함수이다. 입력 a가 짝수인지를 조사하는 람다 함수로 짝수에 해당되는 데이터만 필터링한다.
  4. .distinct()로 스트림에서 중복을 제거한다.
  5. .sorted(Comparator.reverseOrder())로 역순으로 정렬한다.
  6. .mapToInt(Integer::intValue)로 Integer의 Stream을 IntStream으로 변경한다. 왜냐하면 최종적으로 int[] 타입의 배열을 리턴해야 하기 때문이다.
  7. .toArray()를 호출하여 IntStream의 배열인 int[] 배열을 리턴한다.

스트림 방식은 일반적인 코드보다 확실히 간결하고 가독성이 좋다는 것을 확인할 수 있을 것이다.

 

참고자료 :
위키독스 점프 투 자바 https://wikidocs.net/book/31