Java의 정석 - 스트림

원태연·2022년 5월 30일
0

Java의 정석

목록 보기
17/19
post-thumbnail

스트림

데이터를 저장하는 Collections, Arrays, Map등 여러 타입들이 존재한다. 이를 모두 포괄하여 다룰 수 있는 Stream이라는 인터페이스가 등장하였다. 데이터소스들을 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 두었다.

다양한 데이터 소스를 표준화된 방법으로 다루기 위한 것

스트림으 사용 순서는 다음과 같다.

  1. 스트림 만들기
  2. 중간 연산 : 중간 연산에 해당하는 메서드들을 여러번 수행 할 수 있다.
  3. 최종 연산 : 최종 연산에 해당하는 메서드는 0, 1 번 수행 가능하며, 수행 후엔 스트림이 닫힌다.

특징

  • 스트림은 데이터 소스를 변경하지 않는다
    public class playGround{
        public static void play() {
          Integer[] intArr = new Integer[]{5, 2, 3, 1, 4, 7};
          List<Integer> arrayList = Arrays.asList(intArr);
          Stream<Integer> integerStream = arrayList.stream(); //arrayList에 대한 스트림 생성
          
          integerStream.sorted().forEach(System.out::print); //스트림을 정렬하고, 출력
          System.out.println();
          arrayList.forEach(System.out::print);
        }
    }
    //123457
    //523147

    실행 결과처럼 arrayList를 통해 스트림을 생성한 뒤 정렬을 하였지만, arrayList의 데이터는 달라지지 않았다.

    스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐, 변경하지 않는다.

    스트림의 결과가 필요하다면,

    List<Integer> sortedList = integerStream.sorted().collect(Collectors.toList());
    //123457

    collect라는 메서드를 통해 저장할 수 있다.

  • 스트림은 일회용이다.
    public class playGround{
        public static void play() {
            Integer[] intArr = new Integer[]{5, 2, 3, 1, 4, 7};
            List<Integer> arrayList = Arrays.asList(intArr);
            Stream<Integer> integerStream = arrayList.stream();
            List<Integer> sortedList = integerStream.sorted().collect(Collectors.toList());
          //최종연산 .collect후 스트림이 닫힘
    
            integerStream.sorted().forEach(System.out::print);
          //ERROR
          //stream has already been operated upon or closed
        }
    }

    최종 연산이 이루어진 스트림은 다시 사용할 수 없다.

    새로 생성하여 사용하여 사용할 수 있다.

스트림의 연산

stream //스트림 생성
  .distinct() //				 //
  .limit(5)   // 중간 연산 //
  .sorted     //				 //
  .forEach(System.out::print); // 최종 연산 //

//stream.distinct().limit(5).sorted.forEach(System.out::print); 
  • 중간 연산 메소드
    • distinct() : 중복 제거

    • filter(Predicate<T> predicate) : predicate조건에 안 맞는 요소 제외

    • limit(long maxsize) : 스트림의 일부를 잘라낸다. 최대 크기가 maxsize

    • skip(long n) : 스트림의 일부를 건너 뛰기

    • peek(Consumer<T> action) : 스트림의 요소에 action작업 수행

    • sorted() : 정렬
      sorted(Comparator<T> comparator) : comparator에 맞게 정렬

    • map() : 스트림의 요소 반환
      flatMap()

  • 최종 연산 메소드
    • forEach(Comsumer<? super T> action)
      forEachOrdered(Comsumer<? super T> action) : action을 수행

    • count() : 요소의 개수 반환

    • max() : 스트림의 최대값

      min() : 최솟값 반환

    • collect() : 스트림의 요소를 수집

    • reduce() : 요소를 하나씩 줄여가면서 계산

스트림은 중간 연산과 최종 연산이 존재한다. 하지만 연산의 순서가 존재한다. 스트림의 중간 연산들은 할 일 목록처럼 저장되었다가 최종 연산이 수행되면서 요소들이 저장된 중간 연산들을 거치고 최종 연산에서 소모된다. 이러한 흐름을 지연된 연산이라고 한다.

기본형 스트림

보통 스트림을 생성할 땐, 아래 코드처럼 T라는 타입 변수를 통해 타입을 선언 한 뒤 생성 할 수 있다.

Stream<T> Collection.stream();

스트림은 다양한 타입의 요소를 지원하기 때문에, 특정 타입에서는 효율적인 부분이 떨어 질 수 있다. 기본형 (int, Long, double, ...)을 사용하려면, 래퍼클래스를 통해 생성되어야 하고, 오토박싱과 언박싱으로 인한 비효율이 증가할 수 있다.

그래서 기본형에 대해 사용할 수 있는 기본형 스트림이 존재한다.

IntStream, LongStream, DoubleStream, ...

기본형 스트림은 해당 기본형타입의 요소에서만 사용가능하지만, 더 효율적이고 해당 기본형에 사용하기 유용한 메서드들이 포함되어 있으므로 사용하는 것이 좋다.

스트림 생성

Stream<String> strStream = Stream.of("a","b","c");
Stream<String> strStream = Stream.of(new String[]{"a","b","c"});
Stream<String> strStream = Arrays.stream(new String[]{"a","b","c"});

Stream<String> strStream = Arrays.stream(new String[]{"a","b","c"}, 0, 3);// 0,1,2번째 요소만 {"a", "b", "c"}

//기본형 스트림
IntStream intStream = IntStream.of(1,2,3,4);
IntStream intStream = IntStream.of(new int[]{1,2,3,4});
IntStream intStream = Arrays.stream(new int[]{1,2,3,4});

IntStream intStream = Arrays.stream(new int[]{1,2,3,4}, 0, 2); //0, 1번째 요소만 {1,2}

IntStream intStream = IntStream.range(1,5); //1 ~ 4 {1,2,3,4}
IntStream intStream = IntStream.rangeClosed(1,5); //1 ~ 5 {1,2,3,4,5}

IntStream intStream = Random.ints(); //난수 생성
IntStream intStream = Random.ints(1, 9); //1 ~ 9 사이의 난수 생성

//.ints()는 무한 스트림이므로 stream을 연산하여 크기를 제한하여야 한다.
intStream.limit(5).forEach(System.out::println);

iterate(), generate()

Stream클래스의 iterate(), generate()는 람다식을 받아 계산되는 값들을 요소로 하는 무한 스트림을 생성.

예시)

//iterate()
Stream<Integer> eventStream1 = Stream.iterate(0, n -> n + 2);
//0을 시작으로, n -> n + 2 라는 람다의 반환값을 저장하는 요소로하는 무한 스트림
//반환된 값을 다시 사용.
//0 + 2 -> 2
//2 + 2 -> 4
//4 + 2 -> 6
//6 + 2 -> ...

//0, 2, 4, 6, 8, ...

eventStream1.limit(5).forEach(System.out::println);
//0 2 4 6 8 

//generate
Stream<Integer> eventStream2 = Stream.generate(() -> 1);
//1을 반환하는 무한 스트림
//generate는 매개변수가 없는 람다식만 허용한다.

eventStream2.limit(5).forEach(System.out::println);
//1 1 1 1 1

스트림의 중간연산

스트림의 중간연산은 여러번 가능하다.

제공되는 여러 메서드들을 통해 스트림을 조작해보도록 하자

.skip()

Stream<T> skip(long n)
//n만큼 건너뛰어 스트림 생성
IntStream intStream = IntStream.range(1, 10); // 1~9
intStream.skip(4).forEach(System.out::print);
//5 6 7 8 9

.limit()

Stream<T> limit(long maxsize)
//스트림의 요소를 maxsize로 제한
Stream<String> stringStream = Arrays.stream(new String[]{"aa", "bb", "cc", "dd", "ff"});
stringStream.limit(4).forEach(System.out::print);
//aabbccdd

.filter()

Stream<T> filter(Predicate<? super T> predicate)
//predicate식에 맞지 않는 식 거르기
Stream<String> stringStream = Arrays.stream(new String[]{"ab", "cde", "f", "gh", "ij"});
stringStream.filter(s -> s.length() == 2).forEach(System.out::println);
//ab
//gh
//ij

.distinct()

Stream<T> distinct()
//중복 제거
Stream<String> stringStream = Arrays.stream(new String[]{"A", "C", "B", "C", "C", "A"});
stringStream.distinct().forEach(System.out::println);
//A
//C
//B

.sorted()

Stream<T> sorted()
//기본 정렬
  
Stream<T> sorted(Comparator<? super T> comparator)
//comparator기준에 맞는 정렬
//기본 정렬
Stream<String> stringStream = Arrays.stream(new String[]{"A", "C", "B", "C", "C", "A"});
stringStream.sorted().forEach(System.out::print);
//AABCCC

//역순 정렬
Stream<String> stringStream = Arrays.stream(new String[]{"A", "C", "B", "C", "C", "A"});
stringStream.sorted(Comparator.reverseOrder()).forEach(System.out::print);
//CCCBAA
//길이로 정렬
Stream<String> stringStream = Arrays.stream(new String[]{"ab", "cde", "f", "gh", "ij"});

stringStream
  .sorted(Comparator
          .comparing(String::length)
          .reversed())
  .forEach(System.out::println);

//cde
//ab
//gh
//ij
//f
class Person {
    private int age;
    private String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

public class playGround{
    public static void play() {
      //Perons 스트림 생성
        Stream<Person> personStream = Stream.of(
                new Person(23, "Tom"),
                new Person(23, "Alice"),
                new Person(38, "Royce"),
                new Person(25, "Andrew")
        );

        personStream
          .sorted(Comparator.comparing((Person s) -> s.getAge()) //나이순으로 정렬
                .thenComparing((Person s) -> s.getName())) //같은 나이면, 이름 순으로 정렬
          .forEach(System.out::println);
    }
}

/*
Person{age=23, name='Alice'}
Person{age=23, name='Tom'}
Person{age=25, name='Andrew'}
Person{age=38, name='Royce'}
*/

.map()

Stream<R> map(Function<? super T, ? extends R> mapper)
//mapper수행 후 R타입으로 반환
IntStream intStream = IntStream.of(new int[]{1, 2, 3, 4, 5, 6, 7, 8});
intStream.map(a -> a * 2).forEach(System.out::print);
//2 4 6 8 10 12 14 16

스트림의 최종연산

스트림을 반환하는 중간연산과는 달리, 최종연산을 수행하면 그 스트림을 소모한다.

그래서 최대 한 번만 가능하다.

.forEach()

void forEach(Consumer<? super T> action)
void forEachOrdered(Consumer<? super T> action) //병렬스트림인 경우 순서를 보장해서 실행 함
IntStream.range(1,10).forEach(System.out::print);  //123456789
IntStream.range(1,10).forEachOrdered(System.out::print); //123456789

IntStream.range(1,10).parallel().forEachOrdered(System.out::print); //682149357

.allMatch(), .anyMatch(), .nonMatch()

boolean allMatch (Predicate<? super T> predicate)  //모든 요소가 조건을 만족하면 true
boolean anyMatch (Predicate<? super T> predicate)  //하나 이상의 요소가 조건을 만족하면 true
boolean noneMatch (Predicate<? super T> predicate)  //모든 요소가 조건을 만족하지 않으면 true
boolean isUnderAll = IntStream.range(1,10).allMatch(num -> num <= 10); //true
boolean isUnderAny = IntStream.range(1,10).anyMatch(num -> num <= 5); //true
boolean isUnderNone = IntStream.range(1,10).noneMatch(num -> num >= 10); //true

.reduce()

스트림의 요소를 줄여나가면서 누적 연산을 수행하고 최종결과 반환.

T reduce(T identity, BinaryOperator<T> accumulator) //identity라는 초기값에 대해 accumulator 작업 수행
Optional<T> reduec(BinaryOperator<T> accumulator)  //identity가 null의 가능성이 있기 때문에 Optional로도 처리할 수 있다.
//.count()
int count = intStream.reduce(0, (a,b) -> a + 1);

//.sum()
/*
int a = identity; //identity
for(int b : stream) { a = a + b; } // accumulator
*/
int sum = intStream.reduce(0, (a,b) -> a + b);



//.max
int max = intStream.reduce(Integer.MAX_VALUE, (a,b)-> a > b ? a : b);

//.min
int min = intStream.reduce(Integer.MIN_VALUE, (a,b)-> a < b ? a : b);

.collect()

그룹별 reducing이 필요한 경우에 사용. Collector인터페이스를 구현한 Collectors클래스의 메서드를 자주 사용한다.

Object collect(Collector collector) //Collector를 구현한 클래스의 객체를 매개변수로 사용
  • 스트림을 컬렉션으로 변환
List<String> names = studentStream.map(Student::getName)
  											.collect(Collectors.toList()); 
											//Collectors.toList()를 통해 List로 변환

ArrayList<String> list = names.stream()
  													.collect(Collectors.toCollection(ArrayList::new));
											//Collectors.toCollection(ArrayList::new)을 통해 컬렉션으로 저장
Map<String,Person> map = personStream
  													.collect(Collectors.toMap(p -> p.getRegId(), p -> p));
											//Collectors.toMap()을 통해 Map의 key와 value를 선택, 저장
  • 스트림을 배열로 변환
Student[] stuNames = studentStream.toArray(Student[]::new);
//.toArray() 처럼 매개변수 없이 사용 가능하지만 Object타입으로 변환함
profile
앞으로 넘어지기

0개의 댓글