mapXXX() 메소드

mapXXX() 메소드는 요소를 대체하는 요소로 구성된 새로운 스트림을 리턴합니다.
아래 예제 코드를 통해서 이름, 점수를 인스턴스 변수로 가지고 있는 Student 객체를 타입 파라미터로 가지고 있는 리스트 객체에서 학생의 점수를 요소로 하는 새토룬 스트림을 생성하고 점수를 순차적으로 콘솔에 출력해보겠습니다.

import java.util.Arrays;
import java.util.List;

public class MapExample {

    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student("홍길동", 90),
                new Student("신용권", 40),
                new Student("유미선", 50)
        );


        students.stream()
                .map(Student::getScore)
                .forEach(System.out :: println);
    }
}

asDoubleStream(), asLongStream(), boxed() 메소드

asDoubleStream() 메소드는 IntStream의 int 요소 또는 LongStream의 long 요소를 double 요소로 타입 변환해서 DoubleStream을 생성합니다. 마찬가지로 asLongStream() 메소드는 IntStream의 int 요소를 long 요소로 타입 변환해서 LongStream을 생성합니다. boxed() 메소드는 int, long, double 요소를 Integer, Long, Double 요소로 박싱해서 Stream을 생성합니다.

import java.util.Arrays;
import java.util.stream.IntStream;

public class AsDoubleStreamBoxedExample {
    public static void main(String[] args) {
        int[] intArray = {1, 2, 3, 4, 5};

        IntStream intStream = Arrays.stream(intArray);

        intStream
                .asDoubleStream()
                .forEach(System.out :: println);

        System.out.println();

        // 스트림은 한번 사용후에 재사용이 불가능 하기 때문에 다시 사용하려면 스트림 객체를 생성해줘야 합니다.
        intStream = Arrays.stream(intArray);

        intStream
                .boxed()
                .forEach(obj -> System.out.println(obj.intValue()));

    }
}

정렬(sorted())

스트림은 요소가 최종 처리되기 전에 중간 단계에서 요소를 정렬해서 최종 처리 순서를 변경할 수 있습니다.

객체 요소일 경우에는 클래스가 Comparable을 구현하지 않으면 sorted() 메소드를 호출했을때 ClassCastException이 발생하기 때문에 Comparable을 구현한 요소에만 sorted() 메소드를 호출해야 합니다. 다음은 점수를 기준으로 Student 요소를 오름차순으로 정렬하기 위해 Comparable을 구현해야 합니다.

public class Student implements Comparable<Student> {

    private String name;
    private int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }

    @Override
    public int compareTo(Student obj) {
        return Integer.compare(score, obj.score);
    }
}

객체 요소가 Comparable을 구현한 상태에서 기본 비교방법으로 정렬하고 싶다면 다음 세가지 방법 중 하나를 선택해서 sorted()를 호출하면 됩니다.

sorted();
sorted((a,b) -> a.compareTo(b));
sorted( Comparator.naturalOrder());

만약 객체 요소가 Comparable을 구현하고 있지만, 기본 비교 방법과 정반대 방법으로 정렬하고 싶다면 다음과 같이 sorted()를 호출하면 됩니다.

sorted( (a,b) -> b.compareTo(a));
sorted( Comparator.reverseOrder());

중괄호 {} 안에는 a와 b를 비교해서 a가 작으면 음수, 같으면 0, a가 크면 양수를 리턴하는 코드를 작성하면 됩니다.

// 정렬
public class SortingExample {

    public static void main(String[] args) {

        IntStream intStream = Arrays.stream(new int[] {5, 3, 2, 1, 4});

        //오름 차순으로 정렬하는 중간 스트림 생성 후 최종처리에서 출력합니다.
        intStream
                .sorted()
                .forEach(n -> System.out.println(n + ","));
        System.out.println();

        List<Student> studentList = Arrays.asList(
                new Student("홍길동", 30),
                new Student("신용권", 10),
                new Student("유미선", 20)
        );

        // 정수를 기준으로 오름차순으로 Student 정렬
        studentList.stream()
                .sorted()
                .forEach(s -> System.out.print(s.getScore() + ","));

        System.out.println();

        // 정수를 기준으로 내림차순으로 Student 정렬
        studentList.stream()
                .sorted(Comparator.reverseOrder())
                .forEach(s -> System.out.print(s.getScore() + ","));

    }
}

루핑(peek(), forEach())

루핑은 요소 전체를 반복하는 것을 말합니다. 루핑하는 메소드에는 peek(), forEach()가 있습니다. 이 두 메소드는 루핑한다는 기능에서는 동일하지만, 동작 방식은 다릅니다. peek()은 중간 처리 메소드이고, forEach()는 최종 처리 메소드입니다.

peek()는 중간 처리 단계에서 전체 요소를 루핑하면서 추가적인 작업을 하기 위해 사용합니다. 최종 처리 메소드가 실행되지 않으면 지연되기 때문에 반드시 최종 처리 메소드가 호출되어야 동작합니다.

예를 들어 필터링 후 어떤 요소만 남아있는지 확인하기 위해 다음과 같이 peek()를 마지막에서 호출할 경우, 스트림은 전혀 동작하지 않습니다.

요소 처리의 최종 단계가 합을 구하는 것이라면, peek() 메소드 호출 후 sum()을 호출해야만 peek()가 정상적으로 동작합니다.

intStream
    .filter(a -> a%2 == 0)
    .peek(a -> Systemp.out.println(a))
    .sum()

하지만 forEach()는 최종 처리 메소드이기 때문에 파이프라인 마지막에 루핑하면서 요소를 하나씩 처리합니다. forEach()는 요소를 소비하는 최종 처리 메소드이므로 이후에 sum()과 같은 다른 최종 메소드를 호출하면 안됩니다.

public class LoopingExample {
    public static void main(String[] args) {

        int[] intArr = {1, 2, 3, 4, 5};

        System.out.println("[peek()를 마지막에 호출한 경우]");

        Arrays.stream(intArr)
                .filter(a -> a % 2 == 0)
                .peek(n -> System.out.println(n)); // 동작하지 않습니다.


        System.out.println("[최종처리 메소드를 마지막에 호출한 경우]");

        int total = Arrays.stream(intArr)
                .filter(a -> a % 2 == 0)
                .peek(n -> System.out.println(n)) // 동작함
                .sum();                           // 최종 메소드

        System.out.println("총합: " + total);



        Arrays.stream(intArr)
                .filter(a -> a % 2 == 0)
                .forEach(n -> System.out.println(n)); // 최종 메소드로 동작합니다.

    }
}

매칭(allMatch(), anyMatch(), noneMatch())

스트림 클래스는 최종 처리 단계에서 요소들이 특정 조건에 만족하는지 조사할 수 있도록 세가지 메소드를 제공하고 있습니다. allMatch() 메소드는 모든 요소들이 매개값으로 주어진 Predicate의 조건을 만족하는지 조사하고, anyMatch() 메소드는 최소한 한 개의 요소가 매개 값으로 주어진 Predicate의 조건을 만족하는지 조사합니다. 그리고 noneMatch()는 모든 요소들이 매개값으로 주어진 Predicate의 조건을 만족하지 않는지 조사합니다.

스크린샷 2019-12-15 오후 11 25 28

public class MatchExample {
    public static void main(String[] args) {

        int[] intArr = {2, 4, 6};

        boolean result = Arrays.stream(intArr)
                .allMatch(a -> a%2==0);

        System.out.println("모두 2의 배수인가?"+ result);

        result = Arrays.stream(intArr)
                .anyMatch(a -> a%3==0);
        System.out.println("하나라도 3의 배수가 있는가? " + result);

        result = Arrays.stream(intArr)
                .noneMatch(a -> a%3==0);
        System.out.println("3의 배수가 없는가? " + result);

    }
}

기본 집계(sum(), count(), average(), max(), min())

집계(Aggregate)는 최종 처리 기능으로 요소들을 처리해서 카운팅, 합계, 평균값, 최대값, 최소값 등과 같이 하나의 값으로 산출하는 것을 말합니다. 집계는 대량의 데이터들을 가공해서 축소하는 리덕션이라고 볼 수 있습니다.

스트림이 제공하는 기본 집계

스크린샷 2019-12-15 오후 11 25 39

이 집계 메소드에서 리턴하는 OptionalXXX는 자바 8에서 추가한 java.util 패키지의 Optional, OptionalDouble, OptionalInt, OptionalLong 클래스 타입을 말합니다. 이들은 값을 저장하는 값 기반 클래스들입니다. 이 객체에서 값을 얻기 위해서는 get(), getAsDouble(), getAsInt(), getAsLong()를 호출하면 됩니다.

public class AggregateExample {
    public static void main(String[] args) {
        long count = Arrays.stream(new int[]{1, 2, 3, 4, 5})
                .filter(n -> n % 2 == 0)
                .count();

        System.out.println("2의 배수 개수: " + count);

        long sum = Arrays.stream(new int[]{1, 2, 3, 4, 5})
                .filter(n -> n % 2 == 0)
                .sum();

        System.out.println("2의 배수의 합: " + sum);


        double avg = Arrays.stream(new int[]{1, 2, 3, 4, 5})
                .filter(n -> n % 2 == 0)
                .average()
                .getAsDouble();

        System.out.println("2의 배수의 평균: " + avg);


        int max = Arrays.stream(new int[]{1, 2, 3, 4, 5})
                .filter(n -> n % 2 == 0)
                .max()
                .getAsInt();

        System.out.println("2의 배수의 최대 값: " + max);


        int min = Arrays.stream(new int[]{1, 2, 3, 4, 5})
                .filter(n -> n % 2 == 0)
                .min()
                .getAsInt();

        System.out.println("2의 배수의 최소 값: " + min);


        int first = Arrays.stream(new int[]{1, 2, 3, 4, 5})
                .filter(n -> n % 3 == 0)
                .findFirst()
                .getAsInt();


        System.out.println("첫번째 3의 배수: " + first);
    }
}

실행 결과

스크린샷 2019-12-04 오후 11 05 00

Optional 클래스

Optional, OptionalDouble, OptionalInt, OptionalLong 클래스에 대해서 좀 더 알아보기로 하자. 이 클래스들은 저장하는 값의 타입만 다를 뿐 제공하는 기능은 거의 동일합니다. Optional 클래스는 단순히 집계 값만 저장하는 것이 아니라, 집계 값이 존재하지 않을 경우 디폴트 값을 설정할 수도 있고, 집계 값을 처리하는 Consumer도 등록할 수 있습니다. 다음은 Optional 클래스들이 제공하는 메소드들입니다.

스크린샷 2019-12-15 오후 11 25 52

컬렉션의 요소는 동적으로 추가되는 경우가 많습니다. 만약 컬렉션의 요소가 추가되지 않아 저장된 요소가 없을 경우 요소가 없기 때문에 평균값도 있을 수 없습니다.

List<Integer> list = new ArrayList<>();

double avg = list.stream()
                .mapToInt(Integer::intValue)
                .average()
                .getAsDouble();
        System.out.println("평균: " + avg);

그렇기 때문에 NoSuchElementException 예외가 발생합니다. 요소가 없을 경우 예외를 피하는 세 가지 방법이 있습니다. 첫 번째는 Optional 객체를 얻어 isPresent()메소드로 평균값 여부를 확인하는 것입니다. isPresent() 메소드가 true를 리턴할 때만 getAsDouble() 메소드로 평균값을 얻으면 됩니다.

OptionalDouble optional = list.stream()
                .mapToInt(Integer::intValue)
                .average();

    if(optional.isPresent()){
        System.out.println("평균: " + optional.getAsDouble());
    }else {
        System.out.println("평균: 0.0");
    }

두 번째 방법은 orElse() 메소드로 디폴트 값을 정해 놓습니다. 평균값을 구할 수 없는 경우에는 orElse()의 매개값이 디폴트 값이 됩니다.

double avg = list.stream()
                .mapToInt(Integer::intValue)
                .average()
                .orElse(0.0);
System.out.println("평균: " + avg);

세 번째 방법은 ifPresent() 메소드로 평균값이 있을 경우에만 값을 이용하는 람다식을 실행합니다.

list.stream()
        .mapToInt(Integer::intValue)
        .average()
        .ifPresent(a -> System.out.println("평군: " + a));

수집(collect())

스트림은 요소들을 필터링 또는 매핑한 후 요소들을 수집하는 최종 처리 메소드인 collect()를 제공하고 있습니다. 이 메소드를 이용하면 필요한 요소만 컬렉션으로 담을 수 있고, 요소들을 그룹핑한 후 집계할 수 있습니다.

필터링한 요소 수집

Stream의 collect(Collector<T,A,R> collector) 메소드는 필터링 또는 매핑된 요소들을 새로운 컬렉션에 수집하고, 이 컬렉션을 리턴합니다.

스크린샷 2019-12-15 오후 11 26 03

매개값인 Collector(수집기)는 어떤 요소를 어떤 컬렉션에 수집할 것인지를 결정합니다. Collector의 타입 파라미터 T는 요소이고 A는 누적기이고, 그리고 R은 요소가 저장될 컬렉션입니다. 풀어서 해석하면 T요소를 A누적기가 R에 저장한다는 의미입니다. Collector의 구현 객체는 다음과 같이 Collectors 클래스의 다양한 정적 메소드를 이용해서 얻을 수 있습니다.

스크린샷 2019-12-15 오후 11 26 14

리턴값인 Collector를 보면 A(누적기)가 ?로 되어 있는데, 이것은 Collector가 R(컬렉션)에 T(요소)를 저장하는 방법을 알고 있어 A(누적기)가 필요 없기 때문입니다.
Map과 ConcurrentMap의 차이점은 Map는 스레드에 안전하지 않고, ConcurrentMap은 스레드에 안전합니다. 멀티 스레드 환경에서 사용하려면 ConcurrentMap을 얻는 것이 좋습니다. 다음 코드는 전체 학생 중에서 남학생들만 필터링해서 별도의 List로 생성합니다.

// 전체 학생 List에서 Stream을 얻습니다.
Stream<Student> totalStream = totalList.stream();
//남학생만 필터링해서 Stream을 얻습니다.
Stream<Student> maleStream = totalStream.filter(s -> s.getSex() == Student.Sex.MALE);
//List에 Student를 수집하는 Collector를 얻습니다.
Collector<Student, ?, List<Student>> collector = Collectors.toList();
// Stream에서 collect() 메소드로 Student를 수집해 새로운 List를 얻습니다.
List<Student> maleList = maleStream.collect(collector);

위의 코드를 아래와 같이 간단하게 작성할 수 있습니다.

List<Student> maleList = totalList.stream()
        .filter(s -> s.getSex() == Student.Sex.MALE)
        .collect(Collectors.toList());

다음 코드는 전체 학생 중에서 여학생들만 필터링해서 별도의 HashSet으로 생성합니다.

// 전체 학생 List에서 Stream을 얻습니다.
Stream<Student> totalStream = totalList.stream();
// 여학생만 필터링해서 Stream을 얻습니다.
Stream<Student> femaleStream = totalStream.filter(s -> s.getSex()== Student.Sex.FEMALE);
// 새로운 HashSet을 공급하는 Supplier를 얻습니다. 
Supplier<HashSet<Student>> supplier = HashSet :: new;
// Supplier가 공급하는 HashSet에 Student를 수집하는 Collector를 얻습니다.
Collector<Student, ? , HashSet<Student>> collector = 
Collectors.toCollection(supplier);
// Stream에서 collect() 메소드로 Student를 수집해서 새로운 HashSet을 얻습니다.
Set<Student> femaleSet = femaleStream.collect(collector);

위의 코드도 마찬가지로 아래처럼 간단하게 작성이 가능합니다.

Set<Student> femaleSet = totalList.stream()
        .filter(s -> s.getSex() == Student.Sex.FEMALE)
        .collect(Collectors.toCollection(HashSet :: new));
// 필터링해서 새로운 컬렉션 생성 예제
public class StudyMember {

    public enum Sex { MALE, FEMALE }
    public enum City { Seoul, Pusan}


    private String name;
    private int score;
    private Sex sex;
    private City city;


    public StudyMember(String name, int score, Sex sex) {
        this.name = name;
        this.score = score;
        this.sex = sex;
    }

    public StudyMember(String name, int score, Sex sex, City city) {
        this.name = name;
        this.score = score;
        this.sex = sex;
        this.city = city;
    }

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }

    public Sex getSex() {
        return sex;
    }

    public City getCity() {
        return city;
    }
}

public class ToListExample {
    public static void main(String[] args) {
        List<StudyMember> totalList = Arrays.asList(
                new StudyMember("홍길동", 10, StudyMember.Sex.MALE),
                new StudyMember("김수애", 6, StudyMember.Sex.FEMALE),
                new StudyMember("신용권", 10, StudyMember.Sex.MALE),
                new StudyMember("박수미", 6, StudyMember.Sex.FEMALE)
        );

        //남학생들만 묶어 List 생성
        List<StudyMember> maleList = totalList.stream()
                .filter(s -> s.getSex() == StudyMember.Sex.MALE)
                .collect(Collectors.toList());

        maleList.stream()
                .forEach(s -> System.out.println(s.getName()));



        // 여학생들만 묶어 HashSet 생성
        Set<StudyMember> femaleList = totalList.stream()
                .filter(s -> s.getSex() == StudyMember.Sex.FEMALE)
                .collect(Collectors.toCollection(HashSet::new));
        femaleList.forEach(s -> System.out.println(s.getName()));
    }
}

요소를 그룹핑해서 수집

collect() 메소드는 단순히 요소를 수집하는 기능 이외에 컬렉션의 요소들을 그룹핑해서 Map객체를 생성하는 기능도 제공합니다. collect()를 호출할 때 Collectors의 groupingBy() 또는 groupingByConcurrent()가 리턴하는 Collector를 매개값으로 대입하면 됩니다. groupingBy()는 스레드에 안전하지 않는 Map을 생성하지만, groupingByConcurrent()는 스레드에 안전한 ConcurrentMap을 생성합니다.

아래 코드는 학생들을 성별로 그룹핑하고 나서, 같은 그룹에 속하는 학생 List를 생성한 후, 성별을 키로, 학생 List를 값으로 갖는 Map을 생성합니다. collect()의 매개값으로 groupingBy(Function<T,K> classifier)를 사용하였습니다.

// 전체 학생 List에서 Stream을 얻습니다.
Stream<Student> totalStream = totalList.stream();
// Student를 Student.Sex로 매핑하는 Function을 얻습니다.
Function<Student, Student.Sex> classifier = Student :: getSex;
// Student.Sex가 키가 되고,  그룹핑된 List<Student>가 값인 Map을 생성하는 Collector를 얻습니다.
Collector<Student, ?, Map<Student.Sex, List<Student>>> collector = Collectors.groupingBy(classifier);
// Stream의 collect() 메소드로 Student를 Student.Sex 별로 그룹핑해서 Map을 얻습니다.
Map<Student.Sex, List<Student>> mapBySex = totalStream.collect(Collector);

또 다른 코드는 학생들을 거주 도시별로 그룹핑하고 나서, 같은 그룹에 속하는 학생들의 이름 List를 생성한 후, 거주 도시를 키로, 이름 List를 값으로 갖는 Map을 생성합니다. collect()의 매개값으로 groupingBy(Function<T,K> classifier, Collector<T,A,D> collector)를 사용하였습니다.

// 전체 학생 List에서 Stream을 얻습니다.
Stream<Student> totalStream = totalList.stream();
// Student를 Student.City로 매핑하는 Function을 얻습니다.
Function<Student, Student.City> classifier = Student :: getCity;

// Student의 이름을 List에 수집하는 Collector를 얻습니다.
Function<Student, String> mapper = Student :: getName;

Collector<String, ?, List<String>> collector1 = Collectors.toList();
// Collectors의 mapping() 메소드로 Student를 이름으로 매핑하고 이름을 List에 수집하는 Collector를 얻습니다.
Collector<Student, ?, List<String>> collector2 = Collectors.mapping(mapper, collector1);
// Student.City가 키이고, 그룹핑된 이름 List가 값인 Map을 생성하는 Collector를 얻습니다.
Collector<Student, ?, Map<Student.City, List<String>>> collector3 = Collectors.groupingBy(classifier, collector2);
// Stream의 collect() 메소드로 Student를 Student.City별로 그룹핑해서 Map을 얻습니다. 
Map<Student.City, List<String>> mapByCity = totalStream.collect(collector3);

위의 상기 코드에서 변수를 생략하면 다음과 같이 간단하게 작성이 가능합니다.

Map<Student.City, List<String>> mapByCity = totalList.stream()
        .collect(Collectors.groupingBy(
            Student :: getCity,
            Collectors.mapping(Student::getName, Collectors.toList())
        )
);

다음 코드는 위와 동일하지만, TreeMap 객체를 생성하도록 groupingBy(Function<T, K> classifier, Supplier<Map<K,D>> mapFactory, Collector<T,A,D> collector)를 사용했습니다.

Map<Student.City, List<String>> mapByCity = totalList.stream()
        .collect(Collectors.groupingBy(
            Student :: getCity,
            TreeMap :: new,
            Collectors.mapping(Student::getName, Collectors.toList())
        )    
);
//성별로 그룹핑하기
public class GroupingByExample {
    public static void main(String[] args) {
        List<StudyMember> totalList = Arrays.asList(
                new StudyMember("홍길동", 10, StudyMember.Sex.MALE, StudyMember.City.Seoul),
                new StudyMember("김수애", 6, StudyMember.Sex.FEMALE, StudyMember.City.Pusan),
                new StudyMember("신용권", 10, StudyMember.Sex.MALE, StudyMember.City.Pusan),
                new StudyMember("박수미", 6, StudyMember.Sex.FEMALE, StudyMember.City.Seoul)
        );

        Map<StudyMember.Sex, List<StudyMember>> mapBySex = totalList.stream()
                                                                    .collect(
                                                                            Collectors.groupingBy(
                                                                                    StudyMember :: getSex
                                                                            )
                                                                    );
        System.out.print("[남학생] ");
        mapBySex.get(StudyMember.Sex.MALE).stream()
                .forEach(s -> System.out.print(s.getName() + " "));

        System.out.print("\n[여학생] ");
        mapBySex.get(StudyMember.Sex.FEMALE).stream()
                .forEach(s -> System.out.print(s.getName() + " "));


        System.out.println();

        Map<StudyMember.City, List<String>> mapByCity = totalList.stream()
                                                                 .collect(
                                                                        Collectors.groupingBy(
                                                                            StudyMember :: getCity,
                                                                            Collectors.mapping(StudyMember :: getName, Collectors.toList())
                                                                        )
                                                                 );

        System.out.print("\n[서울] ");
        mapByCity.get(StudyMember.City.Seoul).stream()
                .forEach(s -> System.out.print(s + " "));
        System.out.print("\n[부산] ");
        mapByCity.get(StudyMember.City.Pusan).stream()
                .forEach(s -> System.out.print(s + " "));
    }
}

실행 결과

스크린샷 2019-12-05 오전 3 00 26

참조: 이것이 자바다

'SpringFramework > JAVA' 카테고리의 다른 글

객체지향 프로그래밍  (0) 2020.01.01
멀티 스레드의 개념  (0) 2019.12.30
스트림 처리 메소드 1편  (0) 2019.12.04
람다식을 통한 메소드 참조  (0) 2019.12.03
제네릭을 사용하는 이유?  (1) 2019.12.03

+ Recent posts