스트림

이정연·2023년 1월 10일

자바기초

목록 보기
3/25

스트림 핵심 개념

  • 자바8부터 도입된 문법으로
  • 배열 및 컬렉션의 저장요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 하는 반복자
  • 데이터의 흐름을 의미한다. 각 데이터를 흐름에 따라 우리가 원하는 결과로 가공하고 처리하는 일련의 과정
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

// Iterator 생성
Iterator<Integer> it = list.iterator();

// 리스트를 순회하며 값 출력
while (it.hasNext()) {
   int num = it.next();
   System.out.print(num);
}

원래는 위와 같이 표현되어야 함. 하지만 스트림을 이용하면 아래와 같이 됨.

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = list.stream();
stream.forEach(System.out::print);

말그대로 선언형을 사용함으로 써 더 간결하게, 알아보기 쉽게 표현할 수 있다.

예를하나 더 들어보자!

		// List에 있는 숫자들 중에서 4보다 큰 짝수의 합계 구하기
        List<Integer> numbers = List.of(1, 3, 6, 7, 8, 11);
        int sum = 0;

        for(int number : numbers){
            if(number > 4 && (number % 2 == 0)){
                sum += number;
            }
        }

위는 명령형 프로그래밍 방식! 아래는 선언형 프로그래밍 방식

		// List에 있는 숫자들 중에서 4보다 큰 짝수의 합계 구하기
        List<Integer> numbers = List.of(1, 3, 6, 7, 8, 11);

        int sum =
                numbers.stream()
                        .filter(number -> number > 4 && (number % 2 == 0))
                        .mapToInt(number -> number)
                        .sum();

mapToInt는 스트림에서 걸러진 값들을 int 타입의 정수로 바꿔주고 있다.

  • 기존방식의 데이터 처리방법은 데이터 소스를 각기 다른 방식으로 다뤄야하는 불편함이 있었다.
  • 즉 표준화된 하나의 방식이 아니라 데이터 소스에 맞는 방식의 메서드를 각각 다르게 적용해야 했었다.
  • 예를 들면, 배열의 경우는 Arrays.sort() 를 써야하고, List의 경우엔 Collections.sort() 를 써야 했었다.
  • 스트림은 데이터 소스에 관계없이 같은 방식으로 데이터를 가공/처리하는데에 의미가 있다.

예제

		// ArrayList
        List<String> fruitList = new ArrayList<>();
        fruitList.add("바나나 ");
        fruitList.add("사과 ");
        fruitList.add("오렌지 ");

        // 배열
        String[] fruitArray = {"바나나 ", "사과 ", "오렌지 "};

        // 각각 스트림 생성
        Stream<String> ListStream = fruitList.stream();
        Stream<String> ArrayStream = Arrays.stream(fruitArray);

        // 출력
        ListStream.forEach(System.out::print);
        ArrayStream.forEach(System.out::print);

각각 데이터 소스에 따라 스트림을 생성해준 다음 forEach() 동일한 메서드를 이용하여 각 요소들을 순회하며 출력해주고 있다.
List와 배열의 스트림화에대해선 외워두자!
참고로 forEach() 메서드는 람다식 안에 정의된 어떤 명령을 실행하는 데 사용하는 최종 연산자임.

스트림 특징

1. 스트림 파이프 라인

  • 스트림생성, 중간연산, 최종연산 세단계로 구성됨.
  • 중간연산에는 필터링, 매핑, 정렬 등의 작업이 포횜되며, 연산의 결과는 또 다른 스트림이기때문에 계속 연결해서 연산을 진행하게 된다.
  • 중간연산이 완료되면 최종연산을 진행한다.
  • 최종연산에는 총합, 평균, 카운팅 등으로 데이터를 처리한다.

2. 스트림은 원본 데이터 소스를 변경하지 않는다(read-only)

  • 데이터에 대한 변경과 처리는 생성된 스트림 안에서만 수행된다.

3. 일회용이다(onetime-only)

  • 스트림이 최종연산을 수행한 후에는 스트림이 닫히고 다시 사용할 수없다.
  • 추가적인 작업이 필요하다면 다시 스트림을 생성해야 한다.

4. 스트림은 내부반복자

  • 개발자가 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴(for, while)이 아닌
  • 컬렉션 내부에 데이터 요소 처리방법을 주입시켜 데이터를 반복적으로 처리하는 방식
  • 외부반복자는 필요할때만 순차적으로 컬렉션의 요소들을 불러온다.
  • 내부 반복자는 데이터 처리 코드만 컬렉션 내부로 주입시켜 그 안에서 모든 데이터가 처리됨.
int sum =
          numbers.stream()
                        .filter(number -> number > 4 && (number % 2 == 0))
                        .mapToInt(number -> number)
                        .sum();

스트림 생성

  • 배열을 데이터 소스로 하는 스트림 생성은 Arrays 클래스의 stream() 메서드 또는 stream 클래스의 of()메서드를 사용할 수 있다.
			// 문자열 배열 선언 및 할당
           String[] arr = new String[]{"김코딩", "이자바", "박해커"};
   
           // 문자열 스트림 생성
           Stream<String> stream = Arrays.stream(arr);
   
           // 출력
           stream.forEach(System.out::println);
		// 문자열 배열 선언 및 할당
        String[] arr = new String[]{"김코딩", "이자바", "박해커"};

        // 문자열 스트림 생성
        Stream<String> stream = Stream.of(arr);

        // 출력
        stream.forEach(System.out::println);

두가지 방법으로 배열에서 스트림을 생성할 수 있다.

스트림을 생성할 때 문자열 배열이 숫자로 되어있다면 int형 배열로 스트림을 생성하는 것이 좋다.
IntStream 의 경우에는 숫자와 관련한 여러 유용한 메서드들이 정의되어 있기 때문임.

		// int형 배열로 스트림 생성
        int[] intArr = {1,2,3,4,5,6,7};
        IntStream intStream = Arrays.stream(intArr);

        // 숫자와 관련된 경우 intStream을 사용하는 것을 권장
        System.out.println("sum=" + intStream.sum());
//        System.out.println("average=" + intStream.average());

IntStream을 이용하여 sum()이나 average() 메서드를 간편하게 이용할 수 있다.

1. 컬렉션 스트림 생성

  • Collection 으로부터 확장된 하위클래스 List 와 set 을 구현한 컬렉션 클래스들은 stream() 메서드를 이용하여 스트림 생성
		List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
        Stream<Integer> stream = list.stream();

        stream.forEach(System.out::print);

2. 임의의 수 스트림 생성

  • 난수를 생성하는 자바의 기본 내장 클래스인 Random 클래스 안에는 난수들을 반환하는 스트림을 생성하는 메서드들이 정의되어있음.
  • ints() 메서드의 경우 int 형 범위 안에 있는 난수들을 무한대로 생성하여 IntStream 타입의 스트림으로 반환해준다.

예제

		IntStream ints = new Random().ints();
        ints.forEach(System.out::println);

범위를 제한 하려면 ints() 에 매개변수를 넣어주거나, Random().ints().limit(5)로 해결가능함.

  • 추가적으로 IntStream 에는 range() 나 rangeClosed() 메서드를 사용하여 특정범위의 정수값을 스트림으로 생성해서 반환할 수 있다.
		//특정 범위의 정수
        IntStream intStream = IntStream.rangeClosed(1, 10);
        intStream.forEach(System.out::println);
        
//출력값
12345678910

rangeClosed() 는 해당 매개변수의 범위를 포함하여 출력하고, range()는 매개변수의 범위를 제외하고 출력한다.

스트림 중간 연산자

  • 스트림의 중간연산자의 결과는 스트림을 반환하기때문에 여러개의 연산자를 연결하여 우리가 원하는데이터 처리를 수행할 수 있다.

1. 필터링(filter() , distinct() )

  • 조건에 맞는 데이터 정제
  • distinct() : 중복된 요소 제거
  • filter() : 조건에 맞는 데이터만을 정제해서 새로운 컬렉션을 만들어서 반환해줌.
		List<String> names = Arrays.asList("김코딩", "이자바", "박해커", "김코딩", "박해커");

        names.stream()
                .distinct() //중복 제거
                .forEach(element -> System.out.println(element));
        System.out.println();

        names.stream()
                .filter(element -> element.startsWith("김")) // 김씨 성을 가진 요소만 필터링 
                .forEach(element -> System.out.println(element));
        System.out.println();

        names.stream()
                .distinct() //중복제거
                .filter(element -> element.startsWith("김")) // 김씨 성을 가진 요소만 필터링 
                .forEach(element -> System.out.println(element));

forEach 안의 문법의 경우

forEach(element -> System.out.println(element))
forEach(System.out::println)

둘중에 원하는걸 써도 된다.

2. 매핑(map())

  • 스트림 내 요소중에서 원하는 필드만 추출하거나 특정 형태로 반환할 때 사용함.
		List<String> names = Arrays.asList("kimcoding", "javalee", "hackerna", "luckyguy");
        names.stream()
                .map(element -> element.toUpperCase()) // 요소들을 하나씩 대문자로 변환
                .forEach(element->System.out.println(element));
                
                
// 출력값
KIMCODING
JAVALEE
HACKERNA
LUCKYGUY

toUpperCase() 는 대문자로 변환해줌

map(number -> number * 3)

과 같이 이용할 수 도 있음

flatMap()

  • 배열이 2차, 3차 로 깊어지는 경우에서 중첩구조를 제거하고 단일 컬렉션으로 만들어준다.
String[][] namesArray = new String[][]{{"박해커", "이자바"}, {"김코딩", "나박사"}};

// 기대하는 출력값
박해커
이자바
김코딩
나박사

Arrays.stream(namesArray).flatMap(Arrays::stream).forEach(System.out::println);

3. 정렬(sorted())

  • 정렬할때 사용하는 중간연산자
  • Comparator 라는 인터페이스에 정의된 메서드를 사용하여 간편하게 정렬가능
  • 아무값도 넣지 않으면 기본(오름차순)으로 정렬된다.
List<String> animals = Arrays.asList("Tiger", "Lion", "Monkey", "Duck", "Horse", "Cow");
				
// 인자값 없는 sort() 호출
animals.stream().sorted().forEach(System.out::println);

// 출력값
Cow
Duck
Horse
Lion
Monkey
Tiger

List<String> animals = Arrays.asList("Tiger", "Lion", "Monkey", "Duck", "Horse", "Cow");

// 인자값에 Comparator 인터페이스에 규정된 메서드 사용
animals.stream().sorted(Comparator.reverseOrder()).forEach(System.out::println);

// 출력값
Tiger
Monkey
Lion
Horse
Duck
Cow

4. skip()

  • 일부 요소들을 건너뛴다.
			// 1~10 범위의 정수로 구성된 스트림 생성
            IntStream intStream = IntStream.rangeClosed(1, 10);
    
            // 앞의 5개의 숫자를 건너뛰고 숫자 6부터 출력
            intStream.skip(5).forEach(System.out::println);
// 출력값
6
7
8
9
10

5. limit()

  • 앞에서부터 차례로 갯수대로 출력
// 1~10 범위의 정수로 구성된 스트림 생성
IntStream intStream = IntStream.rangeClosed(1, 10);
    
// 앞에서부터 5개의 숫자만 출력
intStream.limit(5).forEach(System.out::println);

// 출력값
    1
    2
    3
    4
    5

6. peek()

  • forEach()와 비슷하지만 중간연산자이기 때문에 여러번 연결하여 사용할 수 있다.
  • 주로 에러를 찾기위한 디버깅용도로 사용함.

스트림 최종연산

  • forEach() 메서드가 스트림 파이프라인에서 사용되면 해당스트림은 닫히고 모든 연산이 종료된다.

  • count() : 전체 요소 갯수

  • sum() : 전체 요소 합

  • average() : 전체 요소 평균값 - getAsDouble() 을 보통 같이씀.

  • max() : 최대값 - getAsInt() 같이씀.

  • min() : 최소값 - getAsInt() 같이씀.

  • findFirst() : 배열의 첫 요소를 찾아줌

스트림을 사용할때 정의된 이름.stream() 으로 쓰던가 아니면, stream을 정의하고 그걸로 쓴다.

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = list.stream();  // stream()을 정의하고 쓰는방법
stream.forEach(System.out::print);
으로 쓰던지
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.stream().forEach(System.out::print);   //이름+stream() 으로쓰는방법
로 쓰던지 

1. 매칭

  • allMatch() - 모든 요소들이 조건을 만족하는 지 여부를 판단
  • noneMatch() - 모든 요소들이 조건을 만족하지 않는 지 여부를 판단
  • anyMatch() - 하나라도 조건을 만족하는 요소가 있는 지 여부를 판단
import java.util.Arrays;

public class TerminalOperationExample {
    public static void main(String[] args) throws Exception {
        // int형 배열 생성
        int[] intArray = {2,4,6};

        // allMatch()
        boolean result = Arrays.stream(intArray).allMatch(element-> element % 2 == 0);
        System.out.println("요소 모두 2의 배수인가요? " + result);

        // anyMatch()
        boolean result = Arrays.stream(intArray).anyMatch(element-> element % 3 == 0);
        System.out.println("요소 중 하나라도 3의 배수가 있나요? " + result);

        // noneMatch()
        boolean result = Arrays.stream(intArray).noneMatch(element -> element % 3 == 0);
        System.out.println("요소 중 3의 배수가 하나도 없나요? " + result);
    }

}

2. 요소 소모

  • reduce() : 스트림의 요소를 줄여나가면서 연산을 수행
  • 첫번째와 두번째를 가지고 연산을하고 그 결과를 세번째요소와 다시 수행
  • 연산이 끝날때 까지 반복
  • 3개까지 매개변수를 받을 수 있음.
T reduce(T identity, BinaryOperator<T> accumulator)
  기본형은 이렇게 되는데, identity는 특정 연산을 시작하는 초기값 accumulator는 조건식임

예제

int[] intArray = {1,2,3,4,5};
int sum2= Arrays.stream(intArray)
                .map(element -> element * 2)
                .reduce(5, (a ,b) -> a + b);
        System.out.println("초기값이 있는 reduce(): " + sum2);
        
초기값이 있는 reduce(): 35

처음에 모든 배열에 *2를하여 {2,4,6,8,10}을 만들고, 초기값 5부터 계속 더해감

3. 요소 수집

  • collect() : 스트림의 요소들을 List, Set, Map 등의 타입 결과로 수집하고 싶은경우에 사용
  • 그룹핑, 분할등의 기능 제공
public class TerminalOperationExample {

    public static void main(String[] args) {
        // Student 객체로 구성된 배열 리스트 생성 
        List<Student> totalList = Arrays.asList(
                new Student("김코딩", 100, Student.Gender.Male),
                new Student("박해커", 80, Student.Gender.Male),
                new Student("이자바", 90, Student.Gender.Female),
                new Student("나미녀", 60, Student.Gender.Female)
        );
        
        // 스트림 연산 결과를 Map으로 반환
        Map<String, Integer> maleMap = totalList.stream()
                .filter(s -> s.getGender() == Student.Gender.Male)
                .collect(Collectors.toMap(
                        student -> student.getName(), // Key
                        student -> student.getScore() // Value
                ));

        // 출력
        System.out.println(maleMap);
    }
}

class Student {
    public enum Gender {Male, Female};
    private String name;
    private int score;
    private Gender gender;

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

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }

    public Gender getGender() {
        return gender;
    }
}

// 출력값
{김코딩=100, 박해커=80}
profile
반갑습니다.

0개의 댓글