본문 바로가기
얄코의 제대로 파는 자바/섹션9 함수형 프로그래밍

스트림 연산

by 백엔드 개발자 2024. 8. 16.

Stream 연산 예시


        IntStream
                .range(1, 100)//1~99까지 생성
                .filter(i -> i % 2 == 0)//짝수인 값만 통과시키는 필터.Predicate 함수를 받음.
                //  💡 아래의 중간과정을 하나하나 주석해제해 볼 것
                .skip(10)//앞에서 부터 숫자 10개 제거
                .limit(10)//10개만 스트림으로 반환
                .map(i -> i * 10)//인자별로 각각 10을 곱한 값을 반환
                .forEach(System.out::println);//각 요소 별로 주어진 Consumer(여기서는 출력) 실행

  • 그림처럼 필터는 특정조건이 true인 경우에만 값을 내보낸다.
  • map은 모든 인자에 대해서 함수를 적용시키고 내보낸다.

 

 

 

				System.out.println("\n- - - - -\n");

        String str1 = "Hello World! Welcome to the world of Java~";

        str1.chars().forEach(System.out::println);
  • chars는 문자열을 대응하는 숫자로 바꾸는 메서드. 그후 각 요소들 출력하는 스트림.

 

				System.out.println("\n- - - - -\n");

        char str1MaxChar = (char) str1.chars()
                .max() // OptionalInt 반환 - 이후 배울 것
                //.min() // 변경해 볼 것
                .getAsInt();

 

  • max(), min()은 뒤에 배울 OptionalInt를 반환하고, 최종값 반환이라 값수정을 위한 메서드 체이닝은 불가능하다.

 

//  사용되는 모든 알파벳 문자들을 정렬하여 프린트
str1.chars()
        .sorted()//       !HJWWaacddeeeefhlllllmoooooorrttvw~  빈칸이 제일 적은 숫자와 대응되므로 맨앞에 정렬된다.
        .distinct()// !HJWacdefhlmortvw~ 중복제거
        //.filter(i -> (i >= 'A' && i <= 'Z') || (i >= 'a' && i <= 'z'))
        .filter(Character::isLetter)// isLetter는 정수에 대응되는 문자열 존재여부를 boolean으로 리턴한다.
        .forEach(i -> System.out.print((char) i));

 

  • 문자열만 추출해서 출력하는 코드
  • 정렬 -> 중복 제거 -> filter로 문자열과 대응되는 숫자만 통과 -> foreach로 각각 요소 char형으로 쭉 이어서 출력(print)

 

 

 

 

//  대소문자 구분 없이 중복 제거한 뒤 정렬하고 쉼표로 구분
String fromStr1 = str1.chars()
        .boxed()
        // 💡 boxed를 사용하여 Stream<Integer>으로 변환
        //  요소를 다른 타입으로 바꾸려면 Stream<T>을 사용해야 함
        .map(i -> String.valueOf((char) i.intValue()))
        //.map(String::toUpperCase)
        //.filter(s -> Character.isLetter(s.charAt(0)))
        //.sorted()
        //.distinct()
        .collect(Collectors.joining(", "));

 

  • chars()는 IntStream 자료형을 가진다. map은 스트림의 각 요소를 변환하는데 사용된다. 그래서 요소를 문자열 타입 스트림으로 바꾸려고 하는데, IntStream은 다른 자료형으로 변경이 안된다. 그래서 변경이 가능한 Stream<Integer>로 변환하기 위해 boxed()를 사용한다.

 

 

 

 

peek

//  💡 peek으로 중간 과정 체크하기
//  - 스트림이나 요소를 변경하지 않고 특정 작업 수행 - 디버깅에 유용
String oddSquaresR = IntStream.range(0, 10).boxed()
        .peek(i -> System.out.println("초기값: " + i))
        .filter(i -> i % 2 == 1)
        .peek(i -> System.out.println("홀수만: " + i))
        .map(i -> i * i)
        .peek(i -> System.out.println("제곱: " + i))
        .sorted((i1, i2) -> i1 < i2 ? 1 : -1)
        .peek(i -> System.out.println("역순: " + i))
        .map(String::valueOf)
        .collect(Collectors.joining(", "));

 

  • 물레방아처럼 코드에는 영향을 끼치지 않으면서 특정작업이 가능하다(Consumer사용). 중간중간 값 확인하기 좋다
  • 디버깅에 탁월해보인다.

 

allMatch/AnyMatch

        Integer[] ints = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        //  💡 allMatch/anyMatch : (모든 요소가/어느 한 요소라도)
        //  - 주어진 Predicate에 부합하는가 여부 반환
        boolean intsMatch = Arrays.stream(ints)
                .allMatch(i -> i > 0); //모두 0보다 크니 true
        //.allMatch(i -> i % 2 == 0); // 전부짝수는 아니니 false
        //.anyMatch(i -> i < 0); // 0보다 작은게 1개도 없으니 false
        //.anyMatch(i -> i % 2 == 0); // 짝수가 1개이상 있으니 true

 

  • 모든 요소, 어느 한 요소라도 조건으로 최종결과를 boolean값으로 반환하는 메서드.

 

 

 

takeWhile/dropWhile

System.out.println("\n- - - - -\n");

//  💡 takeWhile/dropWhile 주어진 Predicate을 충족시킬 때까지 취함/건너뜀
//  💡 count : 중간과정을 거친 요소들의 개수를 반환
long afterWhileOp = Arrays.stream(ints)
        .takeWhile(i -> i < 4) // 1,2,3
        //.dropWhile(i -> i < 4) // 4..9
        .peek(System.out::println)
        .count();
  • takeWhile : 조건을 충족하는 동안만 아래로 통과시킨다.
  • dropWhile : 조건을 충족하는 동안만 건너뛴다.

 

sum

//  💡 sum : IntStream, LongStream, DoubleStream 요소의 총합 반환

int intsSum = IntStream.range(0, 100 + 1)
        //.filter(i -> i % 2 == 1)
        //.filter(i -> i % 2 == 0)
        //.filter(i -> i % 3 == 0)
        .sum();

 

  • +연산을 직관적으로 사용가능하다.

 

 

 

 

reduce

				System.out.println("\n- - - - -\n");

				//  💡 reduce : 주어진 BiFunction으로 값을 접어나감
        //  seed 값이 없을 때 : Optional 반환 (빈 스트림일 수 있으므로)
        int intReduce = IntStream.range(1, 10)
                //.filter(i -> i % 2 == 1)
                .reduce((prev, curr) -> {
                    System.out.printf("prev: %d, cur: %d%n", prev, curr);
                    return prev * curr;
                })
                .getAsInt(); // 필요함

 

  • reduce는 파라미터 2개를 받아서 람다식을 실행한 후, 결과값을 첫번째 인자로 둔다. 계속해서 누적하여 처리한다.
    1   ->   2
              ↓
            2   ->   3
                     ↓
                   6   ->   4
                            ↓
                          24   ->   5
                                   ↓
                                 120   ->   6
                                          ↓
                                        720   ->   7
                                                 ↓
                                               5040   ->   8
                                                        ↓
                                                      40320   ->   9
                                                                 ↓
                                                               362880

 

  • 초기값이 없으면 스트림값 첫번째, 2번째 값으로 연산을 진행한다.
  • 초기값이 있으면 초기값과 스트림내 첫번째 값으로 연산을 진행한다.
  • 초기값이 없으면, 빈값으로 나올 수 있기 때문에 오류를 방지하기 위해 OptionalInt로 반환한다. 여기서 int값을 뽑아내기 위해 getAsInt()를 사용한다.
  • 초기값이 있으면 빈값으로 나오지 않기 때문에, OptionalInt를 사용하지 않는다. 그래서 getAsInt()도 사용하지 않는다

 

 

좀 더 복잡한 예시


 

		//  내부 정적 클래스
		public static class StringStat {
        int length = 0;
        int upperCases = 0;
        int lowerCases = 0;
        int nonLetters = 0;
    }
    
StringStat stringStat = "Hello World! Welcome to the world of Java~"
                .chars()
                .boxed()
                .reduce(
                        new StringStat(),
                        (ss, i) -> {
                            ss.length++;
                            if (i >= 'A' && i <= 'Z') { ss.upperCases++; }
                            else if (i >= 'a' && i <= 'z') { ss.lowerCases++; }
                            else { ss.nonLetters++; }
                            return ss;
                        },
                        //  아래의 combiner 인자는 병령 연산에서만 작용 (여기서는 무의미)
                        //  요소와 다른 타입을 반환하는 오버로드의 필수인자이므로 임의로 넣음
                        (ss1, ss2) -> ss1
                );

 

  • 위 예제는 초기값을 StringStat이라는 클래스의 인스턴스로 사용한다.
  • 람다식연산은 초기값 인스턴스와 stringStat 문자열의 각각 문자 1개당 조건문을 진행한다. 그후 연산이후 업데이트된 초기값 인스턴스와 동일한 연산을 반복한다.

 

 

map()으로 다른 자료형으로 바꾸는 예시


 

     String[] names = {
                "강백호", "서태웅", "채치수", "송태섭", "정대만",
                "윤대협", "변덕규", "황태산", "안영수", "허태환",
                "이정환", "전호장", "신준섭", "고민구 ", "홍익현",
                "정우성", "신현철", "이명헌", "최동오", "정성구"
        };

        Stream<String> nameStream = Arrays.stream(names);

        Random random = new Random();
        random.setSeed(4); // 균일한 결과를 위해 지정된 시드값
        List<Person> people = nameStream
                .map(name -> new Person(
                        name,
                        random.nextInt(18, 35),
                        random.nextDouble(160, 190),
                        random.nextBoolean()
                ))//이름 가지고 새 객체들을 만든다.
                .sorted()
                //.sorted((p1, p2) -> p1.getHeight() > p2.getHeight() ? 1 : -1)
                //.sorted((p1, p2) -> Boolean.compare(p1.isMarried(), p2.isMarried()))
                .toList();

 

  • 배열이었던 인스턴스를 map(..)을 통해서 Person객체들의 Stream으로 바꿔보는 예제. Stream<Person> 자료형이 될 것이고, 그걸 toList()를 써서 리스트로 바꾼다.

 

 

내가 원하는 자료형으로 바꾸는 방법 ( collect 활용)


 

  • collect안에서 Collectors.toList().. 등을 쓰면 원하는 기본 컬렉션(리스트,맵,셋)으로 최종 자료형을 변경할 수 있다.
  • Collectors.toCollection(..)을 쓰면 내가 원하는 컬렉션(ex : LinkedList)같은 걸 사용할 수 있다.
var peopleLastNameSet = people.stream()
                .map(p -> p.getName().charAt(0))

                //  💡 아래 중 원하는 컬렉션으로 택일
                .collect(Collectors.toList());
        //.collect(Collectors.toSet());
        //.collect(Collectors.toCollection(ArrayList::new));
        //.collect(Collectors.toCollection(LinkedList::new));
        //.collect(Collectors.toCollection(TreeSet::new));

 

 

 

특정 기준으로 key value 맵 만드는 방법 (Collectors.groupingBy)


  • Collectors.GroupingBy를 사용하면 특정 조건을 키값, 스트림에서 그 조건을 만족 하는 요소를 value로 한 맵을 리턴한다.
  • 어떤 조건에 대해 분류할때 유용해보인다.
Map<Boolean, List<Person>> peopleHMapByMarried = people.stream()
        .collect(Collectors.groupingBy(Person::isMarried));
List<Person> marrieds = peopleHMapByMarried.get(true);
System.out.println(marrieds);

 

key : true

value : Person객체.....

 

key : false

value : Person 객체...

 

이런식으로 Map이 만들어질 수 있는 것이다.

 

 

 

 

수의 통계를 인스턴스로 가지는 클래스를 만드는 방법(Collectors.summarizingInt,Double)


 

  • Collectors.sumarizing을 사용하면 최대값,최소값등 통계값들을 바로 얻어낼수 있다.
  • 값들 중에서 통계를 낼때, 최대,최소,평균등의 값이 필요할 때 아주 유용하겠다.

 

				//  💡 수의 통계를 인스턴스 형태로 갖는 클래스
        IntSummaryStatistics ageStats = people.stream()
                .map(Person::getAge)
                .collect(Collectors.summarizingInt(Integer::intValue));
                
				DoubleSummaryStatistics heightStats = people.stream()
                .map(Person::getHeight)
                .collect(Collectors.summarizingDouble(Double::doubleValue));

 

 

 

 

 

[Java] Stream API 연습문제 풀이 (5/5)

이번에는 Stream API를 연습해볼만한 문제를 제공해보고자 합니다. 자동화된 테스트를 통해 정답을 확인하도록 제공하고 있으니 직접 문제를 풀어서 정답을 확인해보실 분들은 아래 내용을 참고

mangkyu.tistory.com

 

 

 

 

 

 

출처 

  • 사이트, 검색명 (날짜)

'얄코의 제대로 파는 자바 > 섹션9 함수형 프로그래밍' 카테고리의 다른 글

스트림 문제 풀이  (0) 2024.08.17
스트림  (0) 2024.08.12
메서드 참조  (0) 2024.08.11
java.util.function 패키지  (0) 2024.08.04
람다식과 함수형 인터페이스  (0) 2024.07.28