본문 바로가기

책 📖

모던자바인액션 / 6장 스트림으로 데이터 수집(처리) 정리

6장 스트림으로 데이터 수집(처리) 정리

스트림은 중간 연산과 최종 연산으로 구성되어있다.

List<Dish> vegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(toList());

위 코드는 중간연산 filter최종 연산 collect 으로 이루어져 있다.

collect()에 toList() 를 적용하여 리스트 컬렉션으로 만드는데 방법에는 익숙하다.

이번 장에서는 추가적으로 다른 사용 가능한 방식에 대해 공부해본다.

목차

그룹화

분할

리듀싱


1. 그룹화

groupingBy 메서드를 이용하면 데이터를 하나 이상의 특성으로 분류할 수 있다.

이번 포스팅에서는 예시를 위해 다음과 같은 데이터를 이용한다는 점을 참고하자.

예시 데이터

public class Vehicle {
    private String name;
    private int wheelCount;
    private Power power;

    enum Power {
        다리, 기름, 전기
    }

    public Vehicle(final String name, final int wheelCount, final Power power) {
        this.name = name;
        this.wheelCount = wheelCount;
        this.power = power;
    }

    public Power getPower() {
        return power;
    }

    public String getName() {
        return name;
    }

    public int getWheelCount() {
        return wheelCount;
    }

    @Override
    public String toString() {
        return name;
    }
}


public class Main {
    public static void main(String[] args) {
        List<Vehicle> vehicles = Arrays.asList(
                new Vehicle("두발자전거", 2, Vehicle.Power.다리),
                new Vehicle("세발사전거", 3, Vehicle.Power.다리),
                new Vehicle("오토바이", 2, Vehicle.Power.전기),
                new Vehicle("자동차", 4, Vehicle.Power.기름),
                new Vehicle("전기자동차", 4, Vehicle.Power.전기),
                new Vehicle("지하철", 10, Vehicle.Power.전기)
        );
    }
}

1.1 타입에 따른 분류

Vehicle 을 동력(Power) 에 따라 분류하기

public class Main {
    public static void main(String[] args) {
        Map<Vehicle.Power, List<Vehicle>> group =
            vehicles.stream().collect(Collectors.groupingBy(Vehicle::getPower));

        System.out.println(group);
    }
}

//출력결과
//{다리=[두발자전거, 세발사전거], 전기=[오토바이, 전기자동차, 지하철], 기름=[자동차]}

1.2 조건에 따른 분류

Vehicle 의 바퀴 개수가 4개 이상인 것들을 타입에 따라 분류한다고 하자.

먼저 우리가 익히 알고 있는 방법으로는 스트링에서 필터를 이용 후 groupingBy로 묶어주는 방법이 있다.

public class Main {
    public static void main(String[] args) {
        Map<Vehicle.Power, List<Vehicle>> group = vehicles.stream()
                .filter(vehicle -> vehicle.getWheelCount() >= 4)
                .collect(Collectors.groupingBy(Vehicle::getPower));

        System.out.println(group);
    }
}

//출력결과
//{기름=[자동차], 전기=[전기자동차, 지하철]}

그런데 위의 출력 결과를 가만히 보니 문제가 있다.

동력이 '다리'인 부분들이 groupingBy 되기 전에 모두 필터링되어 버려서 출력 결과에 '다리' 에 관한 카테고리 자체가 나오지 않는다.

이러한 문제는 filter의 프리티케이터를 groupingBy 안으로 이동시키면 해결할 수 있다.

public class Main {
    public static void main(String[] args) {
        Map<Vehicle.Power, List<Vehicle>> group = vehicles.stream()
            .collect(Collectors.groupingBy(Vehicle::getPower,
                    Collectors.filtering(vehicle -> vehicle.getWheelCount() >= 4, 
                    Collectors.toList())));

        System.out.println(group);
    }
}

//출력결과
//{기름=[자동차], 다리=[], 전기=[전기자동차, 지하철]}

1.3 여러개의 조건

여러 조건으로 그룹핑

public class Main {
        Map<Vehicle.Power, Map<Object, List<Vehicle>>> group = vehicles.stream()
            .collect(Collectors.groupingBy(Vehicle::getPower,
                    Collectors.groupingBy(vehicle -> {
                        if (vehicle.getWheelCount() > 3) {
                            return "3개초과";
                        } else {
                            return "3개이하";
                        }
                    })));

        System.out.println(group);
    }
}

//출력결과
//{기름={3개초과=[자동차]}, 다리={3개이하=[두발자전거, 세발사전거]}, 전기={3개초과=[전기자동차, 지하철], 3개이하=[오토바이]}}

1.4 그외

그 외의 기능으로는 그룹핑된 항목들을 가지고 연산을 할 수 있다.

- counting

    public static void main(String[] args) {
        Map<Vehicle.Power, Long> group = vehicles.stream()
        .collect(Collectors.groupingBy(Vehicle::getPower, Collectors.counting()));

        System.out.println(group);
    }
}

//출력결과
//{기름=1, 다리=2, 전기=3}

- maxBy

    public static void main(String[] args) {
        Map<Vehicle.Power, Optional<Vehicle>> group = vehicles.stream()
            .collect(Collectors.groupingBy(Vehicle::getPower,
            Collectors.maxBy(Comparator.comparingInt(Vehicle::getWheelCount))));

        System.out.println(group);
    }
}

//출력결과
//{전기=Optional[지하철], 기름=Optional[자동차], 다리=Optional[세발사전거]}

동력으로 분류한 후 각 동력별로 바퀴 개수가 가장 많은 객체를 반환하고 있다.

여기서 주목해야 할 부분은 반환형이 Optional 이라는 점인데 Optional 을 제거하는 방법으로는 collectingAndThen 을 사용하는 방법이 있다.

...
import static java.util.Comparator.*;
import static java.util.stream.Collectors.*;

public class Main {
    public static void main(String[] args) {
        Map<Vehicle.Power,Vehicle> group = vehicles.stream()
        .collect(groupingBy(Vehicle::getPower,
            collectingAndThen(maxBy(comparingInt(Vehicle::getWheelCount)),
                Optional::get)));

        System.out.println(group);
    }
}

//출력결과
//{다리=세발사전거, 전기=지하철, 기름=자동차}

(Collectors 를 static 으로 import 해준후 생략한 모습)

2. 분할

분할은 프리디케이트로 true 와 false 로 분류하는 기능이다.

public class Main {
    public static void main(String[] args) {
        Map<Boolean, List<Vehicle>> group = vehicles.stream()
            .collect(Collectors.partitioningBy(
                vehicle -> vehicle.getPower() == Vehicle.Power.다리));

        System.out.println(group);
    }
}

//출력결과
//{false=[오토바이, 자동차, 전기자동차, 지하철], true=[두발자전거, 세발사전거]}

참 값을 가져오고 싶다면 group.get(true) 를 통해 가져올 수 있다.

우리가 많이 사용해왔던 스트림에서 필터링 후 리스트에 결과를 수집하는 방법과 비교했을 때, 분할은 false 값까지 정보를 저장한다는 장점이 있다.

예를 들어 동력이 다리인 것 중 바퀴 수가 가장 많은 것다리가 아닌 것 중 바퀴 수가 가장 많은 것을 확인하고 싶다면 다음과 같이 할 수 있다.

...
import static java.util.Comparator.*;
import static java.util.stream.Collectors.*;

public class Main {
    public static void main(String[] args) {
        Map<Boolean, Vehicle> group = vehicles.stream()
            .collect(partitioningBy(vehicle -> vehicle.getPower() 
                                        == Vehicle.Power.다리,
            collectingAndThen(maxBy(comparingInt(Vehicle::getWheelCount)),
                Optional::get)));

        System.out.println(group);
    }
}

//출력결과
//{false=지하철, true=세발사전거}

3. 리듀싱

3.1 최댓값, 최솟값 검색

collect 함수의 인자로 maxBy 혹은 minBy 를 사용하여 최댓값 혹은 최솟값을 구할 수 있다.

이때 maxBy 와 minBy의 인자로는 Compatator 를 입력한다.

public class Main {
    public static void main(String[] args) {
        List<String> numbers = Arrays.asList("1","2","3","4","5");

        Comparator<String> stringComparator = 
                Comparator.comparingInt(Integer::parseInt);

        Optional<String> maxNumber = numbers.stream()
                .collect(Collectors.maxBy(stringComparator));

        System.out.println(maxNumber);
    }
}
//출력결과
//Optional[5]

3.2 문자열 연결

joining 메서드를 사용하여 하나의 문자열로 반환할 수 있다.

public class Main {
    public static void main(String[] args) {
        List<String> numbers = Arrays.asList("1","2","3","4","5");

        Comparator<String> stringComparator = 
                Comparator.comparingInt(Integer::parseInt);

        String numberString = numbers.stream()
                .collect(Collectors.joining());

        System.out.println(numberString);
    }
}
//출력결과
//12345

3.3 reducing 메서드

reducing 메서드를 사용하여 사용자가 하고 싶은 동작을 직접 작성할 수 있다.

public class Main {
    public static void main(String[] args) {
        List<String> numbers = Arrays.asList("1", "2", "3", "4", "5");

        int sum = numbers.stream()
            .collect(Collectors.reducing(0, Integer::parseInt, (x, y) -> x + y));

        System.out.println(sum);
    }
}
//출력결과
//15

reducing 메서드는 3개의 인자를 받고 있다.

첫 번째 인자는 리듀싱 연산의 시작 값이거나 스트림에 인수가 없을 때의 반환 값이다.

두 번째 인자는 변환 함수다.

세 번째 인자는 두 항목을 하나의 값으로 더하는 Operator 이다.

반응형