[java] 스트림

이동엽·2023년 1월 9일
0

스트림이란 무엇인가?

  • 배열 및 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 하는 반복자다.
  • 람다식을 사용하여 데이터 처리한다.
  • 데이터들을 처리하는데 for문이나 Iterator를 사용하는 경우에 많은 경우 코드가 길어지고 복잡해집니다.
  • 스트림을 사용하면 선언형 프로그래밍 방식으로 데이터를 처리할 수 있어 보다 사람이 보기 편안하고 직관적인 코드 작성이 가능합니다.

    선언형 프로그래밍 : 내부 동작 원리를 모르더라도 어떤 코드가 어떤 역할 하는 지 직관적으로 볼수 있다. ex) Filter()

예를들어
Iterator를 사용했을때 반복처리는

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

public class PrintNumberOperator {
    public static void main(String[] args) {
        // 각 숫자를 배열화
        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);
        }
    }
}

이 똑같은 결과를 출력하는 코드를 스트림으로 사용하면

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class PrintNumberOperatorByStream {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        Stream<Integer> stream = list.stream();
        stream.forEach(System.out::print);
    }
}

이렇게 나타낼수 있다.

그리고 장점중 하나는
원래 방식은 데이터 타입에 따라 메소드가 각자 다르게 적용해야했었습니다.
데이터 소스를 각자 다른 방식으로 다뤄야하기에 불편함이 있습니다.

하지만, 스트림을 쓰면 데이터 소스 관계없이 데이터를 하나의 통합된 방식으로 할수 있습니다.

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

        // 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);
    }
}

스트림의 특징

4가지 핵심 특징있습니다.
1. 스트림 처리 과정은 생성,중간 연산, 최종 연산 세 단계의 파이프라인으로 구성할수 있다.
2. 스트림은 원본 데이터 소스를 변경하지 않는다.
3. 일회용이다.
4. 내부 반복자이다.

1번 특징 : 파이프라인


출처 : https://steady-coding.tistory.com/311
파이프 라인은 1)스트림 생성 2) 중간 연산 3) 최종 연산 이라는 세가지 단계로 구성되어 있습니다.

1) 다양한 데이터 소스를 일원화 하여 스트림으로 작업하기 위해선 스트림을 생성해야합니다.

2) 여기는 필터링,매핑,정렬 등의 작업이 포함되며, 이 중간 연산 결과는 또 다른 스트림 이기 때문에 계속 연결해서 연산할수 있습니다. 여러번 할수 있는거죠.

3) 마지막으로 최종 연산은 총합,평균,카운팅 등을 끝으로 스트림은 닫히고 데이터 처리는 완료됩니다.
최종 연산은 단 한번의 연산만 가능하며, 다시 데이터를 처리 해야한다면 다시 스트림을 생성해야합니다.

2번 원본 데이터 소스를 변경하지 않는다.

스트림은 그 원본이 되는 데이터 소스의 데이터들을 변경하지 않습니다. 무조건 읽기만 하고 데이터 처리는 생성된 스트림 안에서만 수행 됩니다.
따라서 원본 데이터가 임의로 변경되거나 데이터가 손상되는 일은 없습니다.

3번 일회용이다.

스트림이 생성되고 여러가지 중간 연산을 거쳐 최종 연산까지 수행되면 스트림은 닫히고 다시 그 스트림을 사용할수 없습니다.
만약 추가 작업이 필요하면 다시 스트림을 생성해야합니다.

4번 내부 반복자이다.

내부 반복자의 반대 되는 개념인 외부 반복자는
개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴 입니다.
따라서 for문,Iterator을 사용하는 while문이 대표적이다.
요소가 필요할 때마다 순차적으로 컬렉션에 필요한 요소들을 불러오는 방식.

내부 반복자는 데이터 처리 코드만 컬렉션 내부로 주입해서 그안에서 모든 데이터 처리가 이뤄지도록 합니다.

스트림은 컬렉션 내부에 람다식을 주입시켜 요소를 반복 처리하는 방식입니다.
따라서 좀더 효율적인 데이터 처리가 가능합니다.

스트림의 생성

데이터 소스에 따라 스트림의 생성 방법이 조금 차이가 있습니다. 예를들어 배열,컬렉션 등등

배열 스트림 생성

먼저 Arrays 클래스의 stream()메소드 또는 Stream 클래스의 of()메소드를 사용할수 있습니다.

public class test {
   
       public static void main(String[] args) {
           // 문자열 배열 선언 및 할당
           String[] arr = new String[]{"김코딩", "이자바", "박해커"};
   
           // 문자열 스트림 생성
           Stream<String> stream = Arrays.stream(arr);
   
           // 출력
           stream.forEach(System.out::println);
   		
        	// 문자열 스트림 생성
       	 	Stream<String> stream = Stream.of(arr);

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

컬렉션 스트림 생성

컬렉션 타입(List,Set 등)은 컬렉션의 최상위 클래스인 Collection에 정의된 stream() 메소드를 이용해 스트림을 생성할수 있습니다.

public class test {

    public static void main(String[] args) {
				// 요소들을 리스트
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
        Stream<Integer> stream = list.stream();

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

임의의 수 스트림 생성

난수를 생성하는 자바 기본 내장 클래스 Random 클래스안에 해당 타입의 난수들을 반환하는 스트림을 생성하는 메소드들이 정의 되어있습니다.

public class StreamCreator {

    public static void main(String[] args) {
        //         난수 생성
        IntStream ints = new Random().ints();
        ints.forEach(System.out::println);
    }
}

이 같은 경우는 무한대로 생성됩니다.
이렇게 스트림의 크기가 정해지지 않은것은 무한 스트림이라고 부릅니다.
그래서 limit()메소드를 사용하거나 range(),rangeClosed()를 사용해 범위를 설정합니다.

public class test {

    public static void main(String[] args) {
			
			// 스트림 생성의 범위를 5개로 제한
        IntStream ints = new Random().ints(5);
				IntStream ints = new Random().ints().limit(5); 
        ints.forEach(System.out::println);
        
        //특정 범위의 정수
        IntStream intStream = IntStream.rangeClosed(1, 10);
        intStream.forEach(System.out::println);
    }
}

rangeClosed()와 range()의 차이는
범위가 포함 여부로 구분할수 있습니다. rangeClosed()는 1~10까지 출력되면, range()은 1~9까지 출력됩니다.

스트림의 중간 연산

중간 연산은 생성 이후 데이터 처리를 수행 할수 있습니다.
주로 필터링,매팅,정렬 등 여러가지 사용하며,데이터를 가공하고 최종 연산자를 통해서 스트림 작업을 종료합니다.

필터링(filter().distince())

조건에 맞는 데이터들을 정제하는 연산이다.

  • distince() : Stream요소들의 중복을 제거하는 연산이다.
  • filter() : 조건에 맞는것만 필터링해서 더 작은 컬렉션을 만들어 냅니다. 조건은 람다식을 사용합니다.
public class test {
    public static void main(String[] args) throws Exception {

        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));
    }
}

// 출력값
김치맨
김초딩
박해커

김치맨
김코딩
김코딩

김치맨
김코딩

매핑(map())

원하는 필드만 추출하거나 특정 형태로 변환할때 사용하는 연산입니다.

public class test {
    public static void main(String[] args) {
    //1번 코드
        List<String> names = Arrays.asList("kimcoding", "javalee", "hackerna", "luckyguy");
        names.stream()
                .map(element -> element.toUpperCase()) // 요소들을 하나씩 대문자로 변환
                .forEach(element->System.out.println(element));
                
     //2번 코드
     	List<Integer> list = Arrays.asList(1, 3, 6, 9);

        // 각 요소에 3을 곱한 값을 반환
        list.stream().map(number -> number * 		3).forEach(System.out::println);
    }
}
//
// 1번 출력값
KIMCODING
JAVALEE
HACKERNA
LUCKYGUY

//2번 출력값
3
9
18
27

만약 이중 배열인데 그 값들을 출력할때flatMap()을 활용할수 있다.

String[][] namesArray = new String[][]{{"박해커", "이자바"}, {"김코딩", "나박사"}};
Arrays.stream(namesArray).flatMap(Arrays::stream).forEach(System.out::println);

정렬(sorted())

정렬을 하면 괄호 안에 아무 값도 안넣은 상태면 오름차순으로 정렬됩니다.
sorted() 메소드 사용할때 광호안에 Comparator 라는 인터페이스에 정의된 static 메소드와 디폴트 메소드를 사용해서 간편하게 정렬 작업을 할수 있습니다.
예시

// 동물들의 이름을 모아둔 리스트 
        List<String> animals = Arrays.asList("Tiger", "Lion", "Monkey", "Duck", "Horse", "Cow");
				
				// 인자값 없는 sort() 호출
        animals.stream().sorted().forEach(System.out::println);

역순으로하면 Comparator 인터페이스 안에 정의된 reverseOrder() 라는 이름의 메소드를 인자로 넘겨 호출하면 쉽게 역순으로 정렬하느 기능을 실행 할수 있습니다.

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

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

다른 여러가지 연산자

  • skip() : 스트림의 일부 요소들을 건너뛴다.
// 1~10 범위의 정수로 구성된 스트림 생성
            IntStream intStream = IntStream.rangeClosed(1, 10);
    
            // 앞의 5개의 숫자를 건너뛰고 숫자 6부터 출력
            intStream.skip(5).forEach(System.out::println);
출력값
6
7
8
9
10
  • limit() : 스트림 일부를 자릅니다.
// 1~10 범위의 정수로 구성된 스트림 생성
            IntStream intStream = IntStream.rangeClosed(1, 10);
    
            // 앞에서부터 5개의 숫자만 출력
            intStream.limit(5).forEach(System.out::println);
 출력값
 1
 2
 3
 4
 5
  • peek() :forEach()처럼 요소들을 순회하며 작업을 수행합니다.
    차이점은 forEach()는 최종연산자 peek()는 중간 연산자입니다.
    그래서 peek()는 주로 코드의 에러 찾기 위한 디버깅 용도로 주로 활용합니다.
// 요소들을 사용하여 IntStream 생성
        IntStream intStream3 = IntStream.of(1, 2, 2, 3, 3, 4, 5, 5, 7, 7, 7, 8);

        // 짝수만 필터링하여 합계 구하기
        int sum = intStream3.filter(element -> element % 2 == 0)
                .peek(System.out::println)
                .sum();

        System.out.println("합계 = " + sum);

최종 연산

기본 숫자 연산(sum,count.average,max,min)

int[] intArray = {1,2,3,4,5,6,7,8,9,10};

        // 카운팅
        long count = Arrays.stream(intArray).count();
        System.out.println("intArr의 전체 요소 개수 " + count);

        // 합계
        long sum = Arrays.stream(intArray).sum();
        System.out.println("intArr의 전체 요소 합 " + sum);

        // 평균
        double average = Arrays.stream(intArray).average().getAsDouble();
        System.out.println("전체 요소의 평균값 " + average);

        // 최대값
        int max = Arrays.stream(intArray).max().getAsInt();
        System.out.println("최대값 " + max);

        // 최소값
        int min = Arrays.stream(intArray).min().getAsInt();
        System.out.println("최소값 " + min);

        // 배열의 첫 번째 요소 
        int first = Arrays.stream(intArray).findFirst().getAsInt();
        System.out.println("배열의 첫번째 요소 " + first);
//출력밧
intArr의 전체 요소 개수 10
intArr의 전체 요소 합 55
전체 요소의 평균값 5.5
최대값 10
최소값 1
배열의 첫번째 요소 1

여기서 getAsInt(),getAsDouble() 하는 이유는
저 연산자들은 반환하는 값의 반환 타입이 Optional, 래퍼 클래스 객체로 되어 있기 때문에 기본형으로 변환해야 우리가 원하는 값이 나옵니다.
따라서 기본형으로 변환하기 위해 사용하는 메소드입니다.

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

match() 메소드 사용하면 조건식 람다 Predicate를 매개변수로 넘겨 스트림의 각 데이터들을 검사 해서 그 결과를 boolean값으로 반환한다.

allMatch() : 모든 요소들이 조건을 만족하는 지 판단
anyMatch() : 모든 요소들이 조건을 만족하지 않는 지 판단
noneMatch() : 하나라도 조건을 만족하는 요소가 있는지 판단

// int형 배열 생성
        int[] intArray = {2,4,6};

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

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

        // noneMatch()// 3의 배수 없으면 false
        result = Arrays.stream(intArray).noneMatch(element -> element % 3 == 0);
        System.out.println(result);
//출력값
true
true
flase

요소 소모(reduce())

reduce() 연산자는 스트림의 요소를 줄여나가면서 연산을 수행하고 최종적인 결과를 반환합니다.
반복된 패턴을 추상화 할수 있습니다.
reduce() 메소드의 매개변수 타입은 BinaryOperator로 정의 되어 있어 연산이 끝날때까지 반복합니다
Optinal.reduce(BinaryOperator accumulator)
T reduce(T identity, BinaryOperator accumulator)
최대 3개까지 매개변수 받을수 있다.
identity는 초기값 accumulator는 각요소들을 연산하여 나온 누적된 결과값을 생성하는데 사용하는 조건식입니다.

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

        // sum()
        long sum = Arrays.stream(intArray).sum();
        System.out.println(sum);

        // 초기값이 없는 reduce()
        int sum1 = Arrays.stream(intArray)
                .map(element -> element * 2)
	                .reduce((a , b) -> a + b)
                .getAsInt();
        System.out.println(sum1);

        // 초기값이 있는 reduce()
        int sum2= Arrays.stream(intArray)
                .map(element -> element * 2)
                .reduce(5, (a ,b) -> a + b);
        System.out.println(sum2);
  

첫번째는 15가 나왔습니다
2번째는 30이 나오고 3번째는 35가 나왔습니다.
여기서 accumulator : (a,b) -> a는 누적값, b는 새로 더해질 값입니다.

요소 수집(collect())

collect() 메소드는 단순히 요소를 수집하는 기능 이외에도 요소그룹핑 및 분할 등 다른기능들을 제공합니다.
collector는 Collector 인터페이스를 구현한 것이며, Collextors 클래스는 미리 작성된 다양한 collector를 반환하는 static 메소드도 가지고 있다.

// 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}

이뿐만 아니라 여러가지 collect 활용들이 많지만,(toList(),등등) 그때그때 필요할때 검색해서 쓰자.

profile
씨앗

0개의 댓글