본문 바로가기

코딩/JAVA 공부

[JAVA] 자바의 정석 _ 스트림(Stream)

 * 스트림(Stream)

- 다양한 데이터 소스를 표준화된 방법으로 다루기 위한 것 / 데이터의 연속적인 흐름 이란 의미를 가지고 있음.

CollectionFrameWork에서 List, Set, Map 사용 방식을 표준화하려 했으나 실패!

이유: List, Set과 Map 은 사용 방식이 다르기 때문.

이를 Stream으로 통일! 진정한 표준화.

다양한 데이터 소스로부터 Stream을 만들 수 있고, 작업 방식이 같음.

중간 연산을 n번 거치고 최종 연산을 통해 결과를 얻을 수 있음.

- 스트림으로 변환(생성)

List<Integer> list = Arrays.asList(1,2,3,4,5);
Stream<Integer> intStream 	 = list.stream(); // 컬렉션.
Stream<String> 	strStream	 = Stream.of(new String[]{"a", "b", "c"}); // 배열
Stream<Integer> evenStream	 = Stream.interate(0, n->n+2);	// 0,2,4,6..
Stream<Double> 	randomStream 	 = Stream.generate(Math::random);	// 람다식
IntStream 	intStream	 = new Random().ints(5);	// 난수 스트림(크기가 5)

- 작업순서

1. 스트림 만들기

2. 중간 연산 (여러 번 가능) (연산 결과가 스트림, 반복적으로 적용 가능)

3. 최종 연산 (1 번만 가능) (연산 결과가 스트림이 아님, 단 한 번만 적용 가능, 스트림의 요소를 소모)

String[] strArr = {"dd", "aaa", "CC", "cc", "b"};
Stream<String> stream = Stream.of(strArr); // 스트림 생성
Stream<String> filteredStream	= stream.filter();   // 걸러내기 (중간 연산)
Stream<String> distinctedStream	= stream.distinct(); // 중복제거 (중간 연산)
Stream<String> sortedStream	= stream.sort();     // 정렬    (중간 연산)
Stream<String> limitedStream	= stream.limit(5);   // 스트림 자르기 (중간 연산)
int total = stream.count(); // 요소 개수 세기 (최종연산)

 * 스트림의 특징

1. 데이터를 읽기만 할 뿐 변경하지 않는다.

2. 스트림은 Iterator처럼 일회용이다. (필요하면 다시 스트림을 생성해야 함)

3. 최종 연산 전 까지 중간 연산이 수행되지 않는다. - 지연된 연산

IntStream intStream = new Random().ints(1,46);	// 1~45 범위의 무한 스트림
intStream.distinct().limit(6).sorted()	// 중간 연산
// 무한 스트림에서 distinct() (중복제거)를 하는게 말이 안되지만, 가능하다! 지연된 연산을 하기 때문..
	 .forEach(i-> System.out.print(i)); // 최종 연산

4. 작업을 내부 반복으로 처리한다.

for (String str : strList)
    System.out.println(str);
// 이런 반복문을 내부 반복으로 처리할 수 있음
stream.forEach(System.out::println);
// 이유: forEach 메서드에 for문이 들어있어서.

void forEach(Consumer<? super T> action){
    Objects.requireNonNull (action); // 매개변수의 널 체크
    for (T t: src) // 내부 반복(fo문을 메서드 안으로 넣음)
    	action.accept(T);
// 어찌보면 비효율적일 수 도 있지만, 코드가 간결해져서 알아보기 편해짐!

5. 스트림의 작업을 병렬로 처리 - 병렬 스트림 (멀티쓰레드)

Stream<String> strStream = stream.of("dd", "aaa", "CC", "cc", "b");
int sum = strStream.parallel() // 병렬 스트림으로 전환 (속성만 변경)
		.mapToInt(s -> s,length()).sum();	// 모든 문자열의 길이의 합

6. 기본형 스트림 - IntStream, LongStream, DoubleStream

Stream<T> 에서 T에 기본형은 못 들어가고, 참조형 만 들어갈 수 있음.

그래서 기본형을 넣으면 오토 박싱으로 변환해서 Stream에 넣게 됨.

이런 비효율을 제거하고자 Stream<Integer> 대신 IntStream 사용

숫자와 관련된 유용한 메서드를 Stream<T> 보다 더 많이 제공!

 

 * 스트림 만들기

- Collection의 Stream 생성: Collection 인터페이스의 stream()으로 컬렉션을 스트림 생성.

Stream<E> stream() // Collection인터페이스의 메서드

- 배열의 Stream 생성: 객체 배열로부터 스트림 생성하기

Stream<T> Stream.of(T...values)	// 가변인자
Stream<T> Stream.of(T[])
Stream<T> Arrays.stream(T[])
Stream<T> Arrays.stream(T[] array, int startInclusive, int endExculsive)

Stream<String> strStream = Stream.of("a", "b", "c");	// 가변인자
Stream<String> strStream = Stream.of(new String[]{"a", "b"});
Stream<String> strStream = Arrays.stream(new String[]{"a", "b"});
Stream<String> strStream = Arrays.stream(new String[]{"a", "b"},0, 2);

- 기본형 배열의 Stream 생성: 기본형 배열로부터 스트림 생성하기

IntStream InStream.of(int...values) ... 이하 Stream 과 동일

- 난수를 요소로 갖는 스트림 생성하기

IntStream intStream = new Random().ints();	// 무한 스트림
intStream.limit(5).forEach(System.out::println); // 5개만 출력

IntStream intStream = new Random().ints(5); // 크기 5인 난수 스트림 생성

// 지정된 범위의 난수를 요소로 갖는 스트림을 생성하는 메서드(Random 클래스)
IntStream	ints(int begin, int end)		 // 무한 스트림
IntStream	ints(long streamSize, int begin, int end) // 유한 스트림

- 특정 범위의 정수를 요소로 갖는 스트림 생성하기(IntStream, LongStream)

IntStream IntStream.range(int begin, int end)
IntStream IntStream.rangeClosed(int begin, int end)
// 사용법
IntStream intStream = IntStream.range(1, 5);       // 1,2,3,4
IntStream intStream = IntStream.rangeClosed(1, 5); // 1,2,3,4,5

- 람다식을 소스로 하는 스트림 생성하기 (iterate(), generate())

static <T> Stream<T> iterate(T seed, UnaryOperator<T> f) // 이전 요소에 종속적
static <T> Stream<T> generate(Supplier<T> s)		 // 이전 요소에 독립적

iterate()는 seed를 이전 요소로 사용해 다음 요소를 계산한다.

Stream<Integer> evenStream = Stream.iterate(0, n->n+2); //0, 2, 4, 6...

generate는 seed를 사용하지 않는다.

Stream<Double>	randomStream 	= Stream.generate(Math::random);
Stream<Integer>	oneStream	= Stream.generate(() -> 1);

- 파일을 소스로 하는 스트림 생성하기

Stream<Path> Files.list(Path dir)	// Path는 파일 또는 디렉토리

 

 * 스트림의 연산

- 중간 연산: 연산 결과가 스트림인 연산, 반복 적용 가능

- 최종 연산: 연산 결과가 스트림이 아님. 단 한 번만 적용 가능

중간 연산 설명
Stream<T>  distinct() 중복을 제거
Stream<T> filter(Perdicate<t> perdicate) 조건에 안맞는 요소 제외
Stream<T> limit(ling maxSize) 스트림의 일부 잘라냄
Stream<T> skip(long n) 스트림의 일부 건너뜀
Stream<T> peek(Consumer<T> action) 스트림 요소에 작업 수행
Stream<T> sorted()
Stream<T> sorted(Comparator<T> comparator)
스트림의 요소를 정렬
Stream<R> map(Function<T,R> mapper) 스트림의 요소를 변환
Stream<R> flatMap(Function<T, Stream<R>> mapper)
최종 연산 설명
void forEach(Consumer<? super T> action
void forEachOrdered(Consumer <? supet T> action)
각 요소에 지정된 작업 수행
순서를 유지할 때, 병렬 스트림 사용할 때 사용
long count() 스트림의 요소 개수 반환
Optional<T> max(Comparator<? super T> comparator)
Optional<T> min(Comparator<? super T> comparator)
스트림의 최대값, 최소값 반환
Optional<T> findAny()  // 아무거나 하나 - 병렬
Optional<T> findFirst() // 첫 번째 요소   - 직렬
스트림의 요소 하나 반환
boolean allMatch(Perdicate<T> p)     // 모두 만족하는지  
boolean anyMatch(Perdicate<T> p)   // 하나라도 만족하는지
boolean noneMatch(Perdicate<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, BiFunction<U, T, U> accumulator,
                  BinartOperator<U> combiner)
스트림의 요소를 하나씩 줄여가면서 계산
R collect(Collector<T,A,R> collector)
R collect(Supplier<R> supplier, BiConsumer<R,T> accumulator,
                  Biconsumer<R,R> comviner)
스트림의 요소를 수집.
주로 요소를 그룹화하거나 분할한 결과를 컬렉션에 담아 반환

 

 * 스트림의 중간 연산

1. 스트림 자르기 - skip(), limit()

IntStream IntStream = IntStream.rangeClosed(1,10); // 1,2,3,4,5,6,7,8,9,10
intStream.skip(3).limit(5).forEach(System.out::print); // 45678

2. 스트림의 요소 걸러내기 - filter(), distinct()

IntStream inStream - IntStream.of(1,2,2,3,3,3,4,5,5,6);
intStream.distinct().forEach(System.out::print); // 123456
IntStream intStream = IntStream.rangeClosed(1,10);		//12345678910
intStream.filter(i->i%2 ==0).forEach(System.out::print);	// 246810

3. 스트림 정렬하기 - sorted()

studentStream.sorted(Comparator.comparing(Student::getBan))
		.forEach(System.out::println);
        
// 람다식으로 변경하기
studentStream.sorted(Comparator.comparing((Student s) -> s.getBan())

- 추가 정렬 기준을 제공할 때는 thanComparing()을 사용

studentStream.sorted(Comparator.comparing(Student::getBan)	// 반별로 정렬
		    .thenComparing(Student::getTotalScore)	// 총점별로 정렬
                    .thenComparing(Student::getName))		// 이름별로 정렬
                    .forEach(System.out::println);

4. 스트림의 요소 변환하기 - map()

Stream<File> fileStream = Stream.of(... 파일 객체들);
Stream<String> filenameStream = fileStream.map(File::getName);	// 파일 이름으로 스트림 요소 변환

fileStream.map(File::getName)				// Stream<File> --> Stream<String>
	.filter(s -> s.indexOf('.') != -1)		// 뒤에 확장자 없으면 제외
    	.map(s -> s.substring(s.indexOf('.')+1))	// 뒤쪽만 잘라서 넣기
    	.map(String::toUpperCase)			// 대문자로 변경
    	.distinct()					// 중복 제외
    	.forEach(System.out::println);			// 출력

5. 스트림의 요소를 소비하지 않고 엿보기 - peek()

- forEach와 기능은 같음. 중간중간 작업 결과 확인하기 위해 사용.

Stream<File> fileStream = Stream.of(... 파일 객체들);
Stream<String> filenameStream = fileStream.map(File::getName);	// 파일 이름으로 스트림 요소 변환

fileStream.map(File::getName)				// Stream<File> --> Stream<String>
	.filter(s -> s.indexOf('.') != -1)		// 뒤에 확장자 없으면 제외
    	.map(s -> s.substring(s.indexOf('.')+1))	// 뒤쪽만 잘라서 넣기
        .peek(s->System.out.println(s);			// 변환 상태 확인
    	.map(String::toUpperCase)			// 대문자로 변경
    	.distinct()					// 중복 제외
    	.forEach(System.out::println);			// 출력

6. 스트림의 스트림을 스트림으로 변환 -flatMap()

Stream<String[]> strArrStrm = Stream.of(new String[]{"abc", "def", "ghi"},
					new String[]{"ABD", "DEF", "GHI"});

위 스트림에서 map을 사용하면,

Stream<Stream<String>> strStrStrm = strArrStrm.map(Arrays::stream);

스트림 속에 스트림이 있는 형태로 변환됨. 이게 아니라, 두 배열이 합쳐진 스트림을 얻고 싶을 때 flatMap() 사용

Stream<String> strStrStrm = strArrStrm.flatMap(Arrays::stream);

 

 * 스트림의 최종 연산

1. 스트림의 모든 요소에 지정된 작업을 수행 - forEach(), forEachOrdered()

IntStream.range(1, 10).sequential() .forEach(System.out::print); //123456789 (직렬스트림)
IntStream.range(1, 10).parallel()   .forEach(System.out::print); //683295714 (병렬스트림)
IntStream.range(1, 10).parallel()   .forEachOrdered(System.out::print); //123456789 (병렬스트림)

2. 조건 검사 - allMatch(), anyMatch(), nonMatch()

boolean hasFailedStu = stuStream.anyMatch(s->s.getTotalScore()<=100);

3. 조건에 일치하는 요소 찾기 - findFirst(), findAny()

Optional<Student> result = stuStream.filter(s->s.getTotalScore() <= 100).findFirst();

4. 스트림의 요소를 하나씩 줄여가며 누적 연산 수행 (accumulate) - reduce()

Optional<T> reduce(BinaryOperator<T> accumulator)
T	    reduce(T identity, BinaryOperator<T> accumulator)
U           reduce(U identity, BiFunction<U,T,U> accumulator, BinaryOperator<U> combiner)
/*
identity 	- 초기값
accumulator	- 이전 연산결과와 스트림의 요소에 수행할 연산 
combiner 	- 병렬처리된 결과를 합치는데 사용할 연산 (병렬 스트림)
int count	= intStream.reduce(0, (a,b) -> a+1);
int sum		= intStream.reduce(0, (a,b) -> a+b);
int max		= intStream.reduce(Integer.MIN_VALUE, (a,b) -> a>b ? a:b);
int min		= intStream.reduce(Integer.MAX_VALUE, (a,b) -> a<b ? a:b);

5. collect()와 Collectors

- collect()는 Collector를 매개변수로 하는 스트림의 최종 연산

Object collect(Collector collector)	// Collector를 구현한 클래스의 객체를 매개변수로

 ** reduce와 collect의 차이 : Stream 전체에 작업할 땐 reduce /  Stream을 그룹별로 작업할 땐 collect

- Collector는 collect에 필요한 메서드를 정의해 놓은 인터페이스

public interface Collector<T, A, R>{ // T를 A에 누적한 다음, 결과를 R로 변환해 반환
    Supplier<A>		 supplier();
    BiConsumer<A, T>	 accumulator();
    BinartOperator<A> 	 combiner();
    Function<A, R>	 finisher();
    Set<Characterustucs> characteristics();
    ...
}

accumulator() 연산 (누적 방법)을 supplier()에 저장한 다음 combiner()로 합치고, finisher()로 변환.

 

- Collectors 클래스는 다양한 기능의 컬렉터를 제공

 변환 - mapping(), toList(), toSet(), toMap(), toCollection()

 통계 - counting(), summingInt(), averagingInt(), maxBy(), minBy(), summarizingInt()

 문자열 결합 - joining()

 리듀싱 - reducing()

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

 

 * Collectors 활용

- 스트림을 컬렉션으로 변환 - toList(), toSet(), toMap(), toCollection()

List<String> names = stuStream.map(Student::getName)
			.collect(Collectors.toList());
ArrayList<String> list = names.stream()
			.collect(Collectors.toCollection(ArrayList::new));
Map<String, Person> map = personStream
			.collect(Collectors.toMap(p->p.getRegId(), p->p));

- 스트림을 배열로 변환 - toArray()

Student[] stuNames = studentStream.toArray(Student[]::new);

- 스트림의 통계정보 제공 - counting(), summingInt(), averagingInt(), maxBy(), minBy(), summarizingInt()

long count = stuStream.count();
long count = stuStream.collect(counting()); // 그룹별로 작업 수행 가능함! .count 와는 다르다

- 스트림을 리듀싱 - reducing()

reduce()와 동일한 기능이지만, Collectors의 reducing() 은 그룹별로 가능해짐.

 

- 문자열 스트림의 요소를 모두 연결 - joining()

String studentNames = stuStream.map(Student::getName).collect(joining());
String studentNames = stuStream.map(Student::getName).collect(joining(","));	// 구분자
String studentNames = stuStream.map(Student::getName).collect(joining(",";"[","]")); 
String studentInfo 	= stuStream.collect(joining(","));	// student의 toString()으로 결합

 

 *  스트림의 그룹화와 분할

- partitioningBy()는 스트림을 2분할한다. (Collectors 에 있는 메서드!)

Collector partitioningBy(Predicate predicate)
Collector partitioningBy(Predicate predicate, Collector downstream)
Map<Boolean, List<Student>> stuBySex = stuStream
			.collect(partitioningBy(Student::isMale)); // 남학생은 true
List<Student> maleStudent = sutBySex.get(true);	// Map 에서 남학생 목록 얻기
Map<Boolean, Long> stuBySex = stuStream
		.collect(partitioningBy(Student::isMale, counting())); // true 에 남학생 수 저장
Long maleStudent = sutBySex.get(true);	// Map 에서 남학생 수 얻기
Map<Boolean, Optional<Student>> stuBySex = stuStream
	.collect(partitioningBy(Student::isMale, maxBy(comparingInt(Student::getScore)))); 
    // 남,여학생 1등 Map 에 저장
Map<Boolean, Map<Boolean, List<Student>>> stuBySex = stuStream
		.collect(partitioningBy(Student::isMale, // 1. 성별로 분할
        	partitioningBy(s-> s.getScore() <150))); // 2. 성적으로 분할
List<Student> failedMaleStu   = stuBySex.get(true).get(true);  // 불합격 남자
List<Student> failedFemaleStu = stuBySex.get(false).get(true); // 불합격 여자

- groupingBy() 는 스트림을 n분할한다. (Collectors 에 있는 메서드!)

Collector groupingBy(Function classifier)
Collector groupingBy(Function classifier, Collector downstream)
Collector groupingBy(Function classifier, Supplier mapFactory, Collector downstream)
Map<Integer, List<Student>> stuByBan = stuStream
		.collect(groupingBy(Student::getBan, toList()));
Map<Integer, Map<Integer, List<Student>>> stuByHakAndBan = stuStream
		.collect(groupingBy (Student::getHak,	// 1. 학년별 그룹화
        		groupingBy(Student::getBan)));	// 2. 반별 그룹화
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()) // mapping()
    ))
);