스트림

박민수·2023년 2월 18일
0

자바의 정석

목록 보기
16/17
post-thumbnail

1. 스트림이란?

스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았다.

1. 스트림을 사용하는 이유

스트림을 사용하는 이유는 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있기 때문이다.

데이터를 다룰 때, 컬렉션이나 배열에 담고 for문과 Iterator를 이용했지만, 이러한 방식은 코드가 길고 재사용성이 떨어진다는 단점이 있다.

또한 데이터 소스마다 다른 방식으로 다뤄야 한다.
Collection이나 Iterator와 같은 인터페이스를 이용해서 표준화시키긴 했지만 각 컬렉션마다 같은 동작을 위해 다른 메서드를 사용하는 경우가 있다.

예를 들어 List를 정렬할때는 Collections.sort()를 사용하고 배열을 정렬할 때는 Arrays.sort()를 사용해야 한다.

스트림은 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 해줘 코드의 재사용성을 높일 수 있다.

2. 스트림의 특징

(1) 스트림은 데이터 소스를 변경하지 않는다.

스트림은 데이터를 읽기만 할 뿐, 데이터 소스를 변경하지 않는다.
필요하다면 정렬된 결과를 컬렉션이나 배열에 담아 반환할 수 있다.

(2) 스트림은 일회용이다.

스트림은 Iteator처럼 일회용이다.
다시 사용하려면 스트림을 다시 생성해야 한다.

(3) 스트림은 작업을 내부 반복으로 처리한다.

스트림을 이용한 작업이 간결할 수 있는 이유는 '내부 반복'이다.
내부 반복이란 반복문을 메서드의 내부에 숨길 수 있다는 것이다.
(그냥 메서드 자체 안에 for문이 적용되어 있다고 이해)
forEach()는 메서드 안에 for문을 넣은 것이다.

for(String str : strList){System.out.printLn(str)}
				↓
stream.forEach(System.out::printLn);

3. 스트림 연산

스트림이 제공하는 연산은 중간 연산최종 연산 으로 분류가능하다.

중간연산은 연산결과를 스트림으로 반환하기 때문에 중간 연산을 연속해서 사용가능하다.(쉼표)

반면에 최종연산은 스트림의 요소를 소모하면서 연산을 수행하므로 단 한번만 가능하다.(마침표)

아래는 중간 연산 관련 목록이다.

Stream<T> distinct()
중복제거

filter(Prediacte<T> predicate)
filter는 요소들을 조건에 따라 걸러내는 작업을 해줍니다.
길이의 제한, 특정문자포함 등 의 작업을 하고 싶을때 사용 가능합니다.
조건에 안맞는 요소 제외
여러번 쓰기 가능(&&)

limit(maxsize)
스트림의 일부를 잘라낸다

skip(n)
스트림의 일부를 건너뛴다

peek(Consumer<T> action)
스트림 요소의 작업수행
forEach와 비슷하지만 중간연산임
중간중간 확인용으로 사용

sorted()
sorted(Comparator<T> comparator)
지정된 Comparator로 스트림을 정렬
Comparator가 없을 경우(그냥 sorted()) 기본 정렬 기준으로 정렬

int값을 반환하는 람다식을 매개변수로 사용하는 것도 가능하다.

Stream<R> map(Function<T,R> mapper)
DoubleStream mapToDouble(ToDoubleFunction<T> mapper)
IntStream mapToInt(ToIntFunction<T> mapper)
LongStream mapToLong(ToLongFunction<T> mapper)

Stream<R> flatMap(Function<T, Stream<R>> mapper)
DoubleStream flatMapToDouble(Function<T,DoubleStream> m)
IntStream flatMapToInt(Function<T, IntStream> m)
LongStream flatMapToLong(Function<T, LongStream> m)
스트림의 요소를 변환
map은 요소들을 특정조건에 해당하는 값으로 변환해 줍니다.
요소들을 대,소문자 변형 등 의 작업을 하고 싶을떄 사용 가능합니다.

중간연산 사용예제


//skip(), limit()
IntStream intStream1 = IntStream.range(1, 10); //1~10의 요소를 가진 스트림
intStream1.skip(3).limit(5).forEach(System.out :: print); //3개를 건너뛰고, 5개 출력

//distinct()
IntStream intStream2 =IntStream.of(1,2,2,3,3,3);   
intStream2.distinct().forEach(System.out :: print); / 123 중복제거

//filter()
IntStream intStream1 = IntStream.rangeClosed(1, 10);
intStream1.filter(i -> i%2 == 0).forEach(System.out :: println); //246810 주어진 조건에 맞는 요소

intStream1.filter(i -> i%2 == 0 && i%3 !=0).forEach(System.out :: println); //157
또는
intStream1.filter(i -> i%2 == 0).filter(i -> i%3 !=0).forEach(System.out :: println);
filter 여러번 사용 가능

//sorted
Stream<String> strStream = Stream.of("dd", "aaa", "cc", "CC")
strStream.sorted().forEach(System.out :: println); //CCaaabccdd 기본 정렬(사전순)
      
기본 정렬 외
strStream.sorted((s1,s2) -> s1.comparTo(s2))// 람다식 매개변수, CCaaabccdd

strStream.sorted(Comparator.reverseOrder()) - 역순, ddccbaaaCC

strStream.sorted(Comparator.comparing(String::length)) - 길이 순 정렬, bddCCccaaa

strStream.sorted(Comparator.comparing(String::length).reversed()) - 길이 순 정렬 역순, aaaddCCccb

comparing()은 새로운 정렬 기준을 제공 가능
ex) 학생 스트림(studentStream)을 반별로 정렬할때
studentStream.sorted(Comparator.comparing(Student::getBan)) - 반별로 정렬

정렬기준이 여러개 일때는 thenComparing()을 사용하여 추가
ex) 반과 이름순으로 정렬
studentStream.sorted(Comparator.comparing(Student::getBan).thenComparing(Student::getName)

//map
Stream<File> fileStream = Stream.of(new File(“Ex1.java”), new File("Ex1"), new File("Ex2.txt");
			
//Stream<File>을 Stream<String>으로 변환 
				↓
Stream<String> fileStream = fileStream.map(File::getName);
로 변환(파일의 이름만 String으로)

//다음은 스트림에서 파일의 확장자만을 뽑아 출력

fileStream.map(File::getName) // Stream<File>을 Stream<String>으로 변환 
.filter(s-> s.indexOf('.') != -1) //확장자 없는것 제외
.map(s-> s.substring(s.indexOf('.')+1) // Stream<String>을 Stream<String>으로 변환 
.map(String::toUpperCase) //대문자로 변환
.distinct() //중복 제거
.forEach(System.out::print); JAVATXT

//peek
fileStream.map(File::getName) 
.filter(s-> s.indexOf('.') != -1)
.peek(s->System.out.printf("filename = %s%n, s)) // 파일명을 출력
.map(s-> s.substring(s.indexOf('.')+1)
.peek(s->System.out.printf("extension = %s%n, s)) // 확장자를 출력
.map(String::toUpperCase) 
.distinct() 
.forEach(System.out::print); 

아래는 스트림 최종연산 목록이다.

void forEach(Consumer<? super T> action)
병렬스트림인 경우 순서가 보장되지 않음(53124)

void forEachOrdered(Consumer<? super T> action)
순서가 보장됨(12345)
각 요소에 지정된 작업 수행

long count()
스트림 요소 개수 반환

*Optional<T> max(Comparator<? super T> comparator)
Optional<T> min(Comparator<? super T> comparator)
스트림의 최대값/최소값을 반환

Optional<T> findAny() //아무거나 하나
Optional<T> findFirst() //첫번째 요소
스트림의 요소 하나를 반환

boolean allMatch (predicate<T> p) // 모두 만족하는지
boolean anyMatch (predicate<T> p) // 하나라도 만족하는지
boolean noneMatch (predicate<T> p) // 모두 만족하지 않는지
주어진 조건을 만족하는지 확인

Object[] toArray()
A[] toArray(IntFunction<A[]> generator)
스트림의 모든 요소를 배열로 반환

Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
U reduce(U identity, BinaryOperator<U, T, U>
accumulator,BinaryOperator<U> combiner)
스트림의 요소를 하나씩 줄여가면서 계산한다.

R collect(Collector<T,A,R> collector)
R collect(Supplier<R> supplier, BiConsumer<R,T>
accumulator, BiConsumer<R,R> combiner)
스트림의 요소를 수집한다.
주로 그룹화하거나 분할한 결과를 컬렉션에 담아 반환하는데 사용된다.
Collect는 Stream의 데이터를 변형 등의 처리를 하고 원하는 자료형으로 변환해 줍니다.

최종연산 중
collect()가 스트림의 요소를 수집하려면, 어떻게 수집할 것인가에 대한 방법이 정의되어 있어야 하는데, 이 방법을 정의한 것이 컬렉터(collector)이다.

컬렉터(collector)는 Collector인터페이스를 구현한 것으로 직접 구현할수도, 미리 작성된 것을 사용할수도 있다.

collect()
스트림의 최종연산, 매개변수로 컬렉터를 필요
Collector
인터페이스, 컬렉터는 이 인터페이스를 구현해야 한다.
Collectors
클래스, static메서드로 미리 작성된 컬렉터를 제공한다.

즉 sort()할 때, Comparator가 필요한 것처럼 collect()할 때는 Collector가 필요하다.

Collectors(미리 작성된 컬렉터)의 메서드 사용예제

스트림을 컬렉션과 배열로 변환하는 메서드
toList() , toSet(), toMap(), toCollection(), toArray()

List<String> names = 
  stuStream.map(Student::getName).collect(Collectors.toList()); //list로 변환

ArrayList<String> list = 
  names.stream().collect(Collectors.toCollection(ArrayList::new)); //collection으로 변환
  
Map<String, Person> map = 
  personStream.collect(Collectors.toMap(p->p.getRegId(), p->p)); //regId를 키값으로
  
 //통계
 counting(), summingInt(), averagingInt(), maxBy(), minBy()

long count = stuStream().collect(Collectors.counting());

long totalScore = stuStream().collect(Collectors.summingInt(Student::getTotalScore));

//문자열 결합 joining()
//스트림의 요소가 문자열이 아닌경우 map으로 먼저 문자열로 변환시킨 후 결합
String studentNames = stuStream.map(Student::getName).collect(joining(","));

기존의 연산을 collect()으로 대체할 수 있는 것만으로는 collect()의 필요성을 모를 것이다.

그러나 collect()의 유용함은 groupingBy(), partitioningBy() 메서드를 통해 알 수 있다.

groupingBy() - 그룹화, partitioningBy() - 분할

class Student {
    String name; // 이름
    boolean isMale; // 성별
    int hak; //학년
    int ban; //반
    int score; //점수

    Student(String name , boolean isMale, int hak, int ban, int score){
        this.name = name;
        this.isMale = isMale;
        this.hak = hak;
        this.ban = ban;
        this.score = score; 
    }

    String getName(){
        return name;
    }
    boolean isMale(){
        return isMale;
    }
    int getHak(){
        return hak;
    }
    int getBan(){
        return ban;
    }
    int getScore(){
        return score;
    }
    public String toString() {
        return String.format("[%s, %s, %d학년, %d반, %3d점]", name, isMale ? "남":"여", hak, ban, score);
    }
    enum level { HIGH, MID, LOW} // 성적을 상, 중, 하로 분류
}

//학생 집합
 Student[] stuArr = {
	new Student("박씨", true, 1, 1, 300),
    new Student("김씨", false, 1, 1, 250),
    new Student("이씨", true, 1, 1, 200),
    new Student("최씨", false, 1, 2, 150)
            };

Stream<Student> stuStream = Stream.of(stuArr);

//partitioningBy예제
// 1. 기본 분할
Map<Boolean, List<Student>> stuBySex 
              = stuStream.collect(partitioningBy(Student::isMale));  // 학생들을 성별로 분할
List<Student> maleStudent = stuBySex.get(true);   // Map에서 남학생 목록을 얻는다.
List<Student> femaleStudent = stuBySex.get(false);  // 여학생 목록

// 2. 기본 분할 + 통계 정보
Map<Boolean, Long> stuNumBySex = stuStream.collect(partitioningBy(Student::isMale, counting()));
System.out.println(stuNumBySex.get(true));  // 8 (남학생수)
System.out.println(stuNumBySex.get(false));  // 10 (여학생수)


// 남학생 1등 구하기, mapBy()의 반환타입은 Optional<Student>
Map<Boolean, Optional<Student>> topScoreBySex
                = stuStream.collect(partitioningBy(Student::isMale, maxBy(comparingInt(Student::getScore))));
System.out.println(topScoreBySex.get(true));  // Optional{[남일등, 남, 1, 1, 300]}


// mapBy()의 반환타입이 Optional<Student>가 아닌 Student를 반환 결과로 얻으려면,  
// collectiongAndThen()과 Optional::get 함께 사용
Map<Boolean, student> topScoreBySex 
            = stuStream.collect(
                        partitioningBy(
                            Student::isMale, collectingAndThen(
                                              maxBy(comparingInt(Student::getScore))
                                              , Optional::get)));
                                              
// 3. 이중 분할
Map<Boolean, Map<Boolean, List<Student>>> failedStuBySex 
                         = stuStream.collect(
                                      partitioningBy(Student::isMale, partitioningBy(s->s.getScore()<150)));
List<Student> failedMaleStu = failedStuBySex.get(true).get(true);

------------------------------------------------------------
//groupingBy예제
// 1. 학생 스트림을 반 별로 그룹지어 Map에 저장 
Map<Integer, List<Student>> stuByBan 
                  = stuStream.collect(groupingBy(Student::getBan, toList()));   //toList()생략가능

Map<Integer, HashSet<Student>> stuByHak 
                  = stuStream.collect(groupingBy(Student::getHak, toCollection(HashSet::new)));


// 2. 학생 스트림을 성적의 등급(Student.Level)으로 그룹화
Map<Student.Level, Long> stuByLevel
            = stuStream.collect(
                        groupingBy(s-> { if(s.getScore()>=200)      return Student.Level.HIGH;
                                         else if(s.getScore()>=100) return Student.Level.MID;
                                         else                       return Student.Level.LOW;
                                       }, counting()));

// 3. groupingBy() 다중 사용하기.
// 학년별로 그룹화하고 다시 반별로 그룹화
Map<Integer, Map<Integer, List<Student>>> stuByHakAndBan
            = stuStream.collect(groupingBy(Student::getHak,
                                groupingBy(Student::getBan)));
                                

// 4. 각 반별 1등 추출
Map<Integer, Map<Integer, Student>> topStuByHakAndBan
            = stuStream.collect(groupingBy(Student::getHak,
                               groupingBy(Student::getBan, 
                                           collectingAndThen(
                                                        maxBy(comparingInt(Student::getScore)),
                                                                                      Optional::get))));

// 5. 학년별, 반별 그룹화한 후에 성적그룹으로 변환하여 Set에 저장
Map<Integer, Map<Integer, Set<Student.Level>>> stuByHakAndBan
            = stuStream.collect(groupingBy(Student::getHak,
                                groupingBy(Student::getBan,
                                           mapping(s-> {
                                                        if(s.getScore()>=200)      return Student.Level.HIGH;
                                                        else if(s.getScore()>=100) return Student.Level.MID;
                                                        else                       return Student.Level.LOW;
                                                       } , toSet()))));

자바의 정석 스트림

최종연산 사용예제

//조건 검사
boolean allMatch (predicate<T> p) // 모두 만족하는지
boolean anyMatch (predicate<T> p) // 하나라도 만족하는지
boolean noneMatch (predicate<T> p) // 모두 만족하지 않는지
Optional<T> findAny() //조건에 맞는 아무거나 하나
Optional<T> findFirst() //조건에 맞는 첫번째 요소

Stream<String> strStream = Stream.of("dd", "aaa", "cc", "CC", "b");
boolean noFailed = strStream.anyMatch(s->s.equals("b"));
System.out.println(noFailed); //true

//reduce()
int count = intStream.reduce(0, (a,b) -> a+1); // 0부터 요소가 몇 개있는지 개수(count()와 동일)
int sum = intStream.reduce(0, (a,b) -> a+b); //sum()과 동일
int max = intStream.reduce(Integer.MAX_VALUE, (a,b) -> a>b ? a:b); //max()와 동일

Optional은 JDK1.8부터 추가되었다.
개발을 할 때 가장 많이 발생하는 예외 중 하나가 바로 NPE(NullPointerException)이다. NPE를 피하려면 null 여부를 검사해야 하는데, null 검사를 해야하는 변수가 많은 경우 코드가 복잡해지고 번거롭다.
이때 Optional는 null이 올 수 있는 값을 감싸는 Wrapper 클래스로, 값이 null이더라도 바로 NPE가 발생하지 않게해준다.
최종 연산의 결과를 Optional객체에 담아서 반환하면 결과가 null인지 매번 체크하지 않아도 되며, 보다 안전한 코드를 작성하는 것이 가능하다.

4. 스트림 생성

(1) 컬렉션 스트림 생성
스트림은 컬렉션의 최고 조상인 Collectionstream()이 정의 되어있다.

그래서 Collection의 자손 클래스들은 스트림을 생성할 수 있다.

Stream Collection.stram()
stream()은 해당 컬렉션을 소스로 하는 스트림을 반환

List<Integer> list = Arrays.asList(1,2,3,4,5);
Stream<Integer> iStream = list.stream(); //스트림 생성
 iStream.forEach(System.out :: println); //1 2 3 4 5  
 iStream.forEach(System.out :: println);//에러. 위에 언급했듯이 스트림은 일회용

(2) 배열 스트림 생성

Stream Stream.of(T...values)
Stream Stream.of(T[])
Stream Arrays.stream(T[])
Stream Arrays.stream(T[] array, int 시작 범위, int 끝 범위)
배열을 소스로 하는 스트림을 생성하는 메서드는 Stream과 Arrays에 정의되어 있다.

 예를 들면
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이 아니라 `IntStream, LongStream, DoubleStream을 반환할 수도 있다.(반환타입을 정해서)
 ex)  IntStream IntStream.of(int...values)

그 중 IntStreamLongStream은 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()rangeClosed()를 가지고 있다.

range()은 끝범위를 포함 X, rangeClosed()는 포함한다.

IntStream intStream1 = IntStream.range(1, 5); //1 2 3 4 
IntStream intStream2 = IntStream.rangeClosed(1, 5); 1, 2, 3, 4, 5

(3) 무한 스트림

IntStream ints()
LongStream longs()
DoubleStream doubles()
크기가 정해지지않은 '무한 스트림' 반환

IntStream intStream = new Random().ints() // 무한
intStream.limit(5).forEach(System.out :: println) // 임의의 수 5개 출력
  
이걸 
IntStream intStream = new Random().ints(5); 
로 변환 가능

(4) 파일

java.nio.file.Files는 파일을 다루는 데 유용한 메서드들을 제공

list()는 지정된 디렉토리(dir)에 있는 파일의 목록을 소스로 하는 스트림을 생성해서 반환

Stream<Path> Files.list(Path dir)  
profile
쉽게 쉽게

0개의 댓글