Java/Grammer

Stream API 정리

2022. 6. 4. 17:06

오늘은 간단하게  기초적인 예제와 함께 Stream API를 정리해보도록 하겠습니다. 사실 개념 설명이라기보단 단순 정리에 가까우니 참고용으로 봐주시길 바랍니다.

 

스트림 생성

//배열 스트림 생성
String[] arr = {"a", "b", "c"};
Stream<String> arrStream = Stream.of(arr);

//리스트 스트림 생성
List<String> list = Arrays.asList(arr);
Stream<String> listStream = list.stream();

스트림을 만드는 방법은 다양하고 배열이나 컬렉션이냐에 따라 생성하는 방법도 다릅니다.

여기선 가장 많이 사용되는 방법으로 스트림을 생성해보았습니다.

 

스트림의 중간 연산

⦁ 자르기 (skip, limip) 

String[] ex1 = {"a", "b", "c", "d", "e"};

Stream.of(ex1)
    .skip(1).limit(3)              //1개는 건너뛰고 3개 출력	
    .forEach(System.out::print);   //b, c, d

필터링 (distinct, filter)

String[] ex2 = {"a", "a", "moong", "뭉", "a"};

Stream.of(ex2)
    .distinct()                     //중복제거
    .filter(s -> s.matches("\\W"))  //영어나 숫자는 제외
    .forEach(System.out::print);    //뭉

정렬과 출력 (sorted, peek)

String[] ex3 = {"b", "a", "c", "e", "d"};

Stream.of(ex3)
    .sorted()                           //Comparable로 정렬
    .peek(System.out::print)            //abcde
    .sorted(Comparator.reverseOrder())  //역순정렬
    .forEach(System.out::print);        //edcba

변환 (map, flatMap)

String[][] ex5 = {{"jax", "shen", "darius"}, {"lulu", "nami"}};

Stream.of(ex5)
    .flatMap(Arrays::stream)      //2뎁스를 1뎁스로
    .map(String::toUpperCase)     //대문자로 변경
    .forEach(System.out::print);  //JAX, SHEN, DARIUS, LULU, NAMI

 

스트림의 최종 연산

⦁ 갯수 (count)

String[] ex7 = {"a", "b", "c", "", " "};
		
Long count = Stream.of(ex7)
        .filter(s -> !s.isBlank())   //빈 문자열 거르기
        .count();   //갯수 구하기

System.out.println(count);  //3

 조건 판별 (allMatch, anyMatch, noneMatch)

tring[] ex8 = {"가", "a", "A", "1"};
		
boolean isAllTrue = Stream.of(ex8)
        .allMatch(s -> (s.length() == 1));  //모두 길이가 1

boolean isAnyTrue = Stream.of(ex8)
        .anyMatch(s -> s.matches("\\d"));   //적어도 하나는 숫자

boolean isNoneTrue = Stream.of(ex8)
        .noneMatch(s -> s.matches("\\s"));  //모두 길이가 1

System.out.println(isAllTrue);   //true
System.out.println(isAnyTrue);   //true
System.out.println(isNoneTrue);  //true

누산 (reduce)

Integer[] ex9 = {1, 2, 3};

int multiple = Stream.of(ex9)
        .reduce(1, (r, a) -> r * a);   //초기값 1에 * 1 * 2 * 3

System.out.println(multiple);   //6

자료형 담기 (collect, toArray)

String[] data = {"one", "two", "three"};
		
List<String> list = Stream.of(data)
        .collect(Collectors.toList());   //리스트에 담기

String[] arr = list.stream()
        .toArray(String[]::new);   //배열에 담기

Map<String, Integer> map = Stream.of(data)
        .collect(Collectors.toMap(s -> s, String::length));   //맵에 담기

String text = Stream.of(data)
        .collect(Collectors.joining(", "));   //문자열에 담기

System.out.println(list);                  //[one, two, three]
System.out.println(Arrays.toString(arr));  //[one, two, three]
System.out.println(map);                   //{one=3, two=3, three=5}
System.out.println(text);                  //one=3, two=3, three=5

그룹핑 - 2분할 [collect(Collectors.partitioningBy)]

String[] names = {"junho", "jinho", "hoya", "lee", "lulu", "darius", "jax"};
		
Map<Boolean, List<String>> nameByStart = Stream.of(names)
        .collect(Collectors.partitioningBy(s -> s.startsWith("j")));   //2분할(j로 시작인지)

System.out.println(nameByStart.get(true));    //[junho, jinho, jax]
System.out.println(nameByStart.get(false));   //[hoya, lee, lulu, darius]

위 소스는 사람들의 이름이 저장된 배열을 j로 시작하는 이름과 j로 시작하지 않는 이름 둘로 나눈 것입니다.

partitioningBy매개변수로 Predicate 타입의 함수를 매개변수로 받습니다.

Predicate 조건에 따라 스트림이 2분할로 나누어 지고 해당 데이터들은 리스트안에 저장됩니다.  그리고 해당 리스트는 Map안에 저장되어 Boolean 타입의 값들로 꺼낼 수 있습니다. 

 

 그룹핑 - 재가공하여 자료형 담기 [collect(Collectors.mapping)]

String[] names = {"junho", "jinho", "hoya", "lee", "lulu", "darius", "jax"};
		
Map<Boolean, Set<String>> nameByStartMap = Stream.of(names)
        .collect(Collectors.partitioningBy(
            s -> s.startsWith("j"),   //2분할(j로 시작하는지)
            Collectors.mapping(String::toUpperCase, Collectors.toSet())   //재가공
        ));   

System.out.println(nameByStartMap.get(true));    //[JUNHO, JAX, JINHO]
System.out.println(nameByStartMap.get(false));   //[LULU, DARIUS, LEE, HOYA]

이전에 봤던 2분할과 같은 예시입니다만 partitioningBy 안에 매개변수가 하나 더 추가되었습니다.

위와 같이 mapping을 사용하면 자료를 재가공하여 원하는 자료형에 담을 수 있습니다. 분할된 이름들을 대문자로 재가공한 후 Set에 담아 마찬가지로 Map 안에 저장됩니다.

 

 그룹핑 - n분할 [collect(Collectors.groupingBy)]

String[] names = {"junho", "jinho", "hoya", "lee", "lulu", "darius", "jax"};
		
Map<Integer, List<String>> nameByLength = Stream.of(names)
        .collect(Collectors.groupingBy(String::length));   //n분할(길이에 따른 분할)

System.out.println("3글자 이름 리스트 : " + nameByLength.get(3));   //[lee, jax]
System.out.println("4글자 이름 리스트 : " + nameByLength.get(4));   //[hoya, lulu]
System.out.println("5글자 이름 리스트 : " + nameByLength.get(5));   //[junho, jinho]

groupingBy는 n분할하여 데이터를 저장합니다. partitioningBy와 차이점은 매개변수가 Predicate가 아닌 Function 타입의 함수를 받으며 위 소스는 길이에 따라 n분할하여 마찬가지로 맵과 리스트에 저장됩니다. 

이때 키는 Boolean 타입이 아닌 매개변수 Function의 리턴타입이 키로 지정됩니다.

 

그룹핑 - 통계값 얻기 (Collectors.counting, Collectors.maxBy, Collectors.averagingInt)

분할한 뒤에 요소들의 갯수를 얻거나 최대값/최소값 그리고 평균같은 통계값을 얻을 수도 있습니다. 밑의 예제들은 자연수와 자연수가 아닌 수로 분류하고 해당 값들을 각각 구하는 예제입니다. 

//요소 갯수 구하기
Integer[] numbers = {3, -25, 54, 1, -6, 20, -90, -1, 1000, 50};

Map<Boolean, Long> byNaturalCount = Stream.of(numbers)
        .collect(Collectors.partitioningBy(
            n -> n > 0,            //2분할(자연수인지 아닌지)
            Collectors.counting()  //분할된 원소 갯수
        ));   

System.out.println("자연수 갯수 : " + byNaturalCount.get(true));   //6
System.out.println("자연수가 아닌 수의 갯수 : " + byNaturalCount.get(false));  //4

반환값이 Long이므로 Map을 선언하실 때 값의 타입을 Long으로 설정해주셔야 합니다.

 

//최대값 구하기
Integer[] numbers = {3, -25, 54, 1, -6, 20, -90, -1, 1000, 50};

Map<Boolean, Optional<Integer>> byNaturalMax = Stream.of(numbers)
        .collect(Collectors.partitioningBy(
            n -> n > 0,                                  //2분할(자연수인지 아닌지)
            Collectors.maxBy(Comparator.naturalOrder())  //최대값...naturalOrder 생략 불가능
        ));   

System.out.println("50 미만인 숫자 중 최대값 : " + byNaturalMax.get(true).orElse(0));   //6
System.out.println("50 이상인 숫자 중 최소값 : " + byNaturalMax.get(false).orElse(0));  //4

최대값/최소값을 구할 때 MaxBy는 매개변수로 Comparator 함수를 받으며 이때 naturalOrder는 생략 불가능합니다.

또한 리턴타입이 Optional<T>인 것을 주의하셔야 합니다. 

 

//평균 구하기
Integer[] numbers = {3, -25, 54, 1, -6, 20, -90, -1, 1000, 50};
		
Map<Boolean, Double> byNaturalAvg = Stream.of(numbers)
        .collect(Collectors.partitioningBy(
            n -> n > 0,                      //2분할(자연수인지 아닌지)
            Collectors.averagingInt(n -> n)  //평균값
        ));   

System.out.println("50 미만인 숫자 평균 : " + byNaturalAvg.get(true));   //188.0
System.out.println("50 이상인 숫자 평균 : " + byNaturalAvg.get(false));  //-30.5

averagingInt는 매개변수로 ToIntFunction 함수를 받으며 리턴 타입이 Double인 것에 주의하셔야 합니다.