📚 Study/JAVA
[JAVA] 스트림(Stream)
0_ch4n
2022. 5. 12. 01:39
반응형
✔️ Stream이란?
- 스트림은 데이터 소스를 추상화하고 데이터를 다루는데 특화되어있다
- 데이터 소스를 읽거나 배열에 담기만할 뿐 데이터 소스를 변경하지 않는다
- 스트림은 Iterator처럼 일회용이므로 재사용하려면 다시 생성해야한다
- 스트림은 메서드 내부에 반복문을 가지고 있어 작업을 내부 반복으로 처리한다
Stream<Integer> VS IntStream
- 래퍼 클래스 타입과 기본형 타입 간의 오토박싱&언박싱으로 인해 비효율이 발생한다
- BaseStream를 상속받는 IntStream, DoubleStream, LongStream을 이용하는게 더 효율적이다
병렬 스트림
- 스트림의 장점 중 하나로 fork&join 프레임워크를 사용하여 병렬처리를 지원한다
- 기본적으론 순차처리이므로 parallel()로 병렬 연산을 지시하고 sequential()로 취소할 수 있다
📌 스트림의 생성
- 컬렉션
- Collection 인터페이스에 정의된 stream()은 컬렉션을 소스로 하는 스트림을 반환한다
List<Integer> list = Arrays.asList(1,2,3,4,5); //컬렉션 클래스 객체 생성
//Stream<T> streamName = Collection.stream()
Stream<Integer> intStream = list.stream(); //list를 소스로 하는 스트림 생성
- 배열
- Stream과 Arrays에 static으로 정의된 메서드를 활용해서 배열을 소스로 하는 스트림을 반환한다
//Stream<T> Stream.of(T... values)
Stream<Integer> intStream = Stream.of(1,2,3,4,5);
//Stream<T> Stream.of(T[])
Stream<Integer> intStream1 = Stream.of(new Integer[]{1,2,3,4,5});
//Stream<T> Arrays.stream(T[])
Stream<Integer> intStream2 = Arrays.stream(new Integer[]{1,2,3,4,5});
//Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)
Stream<Integer> intStream3 = Arrays.stream(new Integer[]{1,2,3,4,5}, 0, 5);
- 특정 범위의 정수
- IntStream과 LongStream에는 지정된 범위의 연속된 정수를 스트림으로 생성하는 메서드를 정의한다
- range()는 end를 미포함, rangeClosed()는 end를 포함한다
//IntStream streamName = IntStream.range(int begin, int end)
IntStream intStream = IntStream.range(1, 5); //1,2,3,4
//IntStream streamName = IntStream.rangeClosed(int begin, int end)
IntStream intStream1 = IntStream.rangeClosed(1, 5); //1,2,3,4,5
- 임의의 수
- Random 클래스는 난수 스트림을 반환하는 ints(), longs(), doubles()를 정의한다
- ints()의 범위 : Integer.MIN_VALUE ~ Integer.MAX_VALUE
- longs()의 범위 : Long.MIN_VALUE ~ Long.MAX_VALUE
- doubles()의 범위 : 0.0 ~ 1.0 (1은 미포함)
//IntStream Random().ints()
IntStream intStream = new Random().ints(); //무한 스트림
intStream.limit(5).forEach(System.out::println); //limit()으로 개수 지정해야함
//IntStream Random().ints(long streamSize)
IntStream intStream1 = new Random().ints(5); //유한 스트림
//IntStream Random().ints(int begin, int end)
IntStream intStream2 = new Random().ints(1, 6); //1~5의 난수 무한 스트림
//IntStream Random().ints(long streamSize, int begin, int end)
IntStream intStream3 = new Random().ints(5, 1, 6); //1~5의 난수 5개
- 람다식
- Stream 클래스의 iterate()와 generate()는 람다식을 매개변수로 받아 계산된 값으로 무한 스트림을 생성
- 기본형 스트림 타입(IntStream 등)으로 다룰 수 없으므로 메서드로 변환해야 한다
//static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
Stream<Integer> evenStream = Stream.iterate(0, n -> n + 2); //0, 2, 4, 6, ...
//static <T> Stream<T> generate(Supplier<T> s)
Stream<Integer> oneStream = Stream.generate(() -> 1); //1, 1, 1, 1, ...
- 파일
- java.nio.file.Files는 파일을 다루는 메서드들을 제공한다
//Stream<Path> Files.list(Path dir)
Stream<Path> fileStream = Files.list(Path.of("C:\\")); //해당 위치 파일들 URL을 요소로 스트림 생성
- 빈 스트림
Stream emptyStream = Stream.empty(); //빈 스트림
long count = emptyStream.count(); //0
- 두 스트림의 연결
- 두 스트림을 연결하기 위해선 서로 같은 타입이어야 한다
//String 배열 생성
String[] str1 = { "123", "456", "789" };
String[] str2 = { "abc", "def", "ghi" };
//String 배열로 Stream 생성
Stream<String> stream1 = Stream.of(str1);
Stream<String> stream2 = Stream.of(str2);
//Stream1,2 연결
Stream<String> unionStream = Stream.concat(stream1, stream2);
📌 스트림의 연산
- 중간연산 : 연산 결과가 스트림인 연산으로 스트림에 연속해서 중간 연산할 수 있음
- 최종연산 : 연산 결과가 스트림이 아닌 연산으로 스트림의 요소를 소모하므로 단 한번만 가능
- 지연된 연산 : 중간 연산은 작업 지정만 해주고 최종 연산이 수행되기 전까지 수행되지 않는다
📌 스트림의 중간연산
- 스트림 자르기 - skip(), limit()
- 기본형 스트림 타입에도 사용 가능하며 둘 다 매개변수의 위치를 포함하지 않는다
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);
//Stream<T> skip(long n)
Stream<Integer> skipStream = itgStream.skip(3); //4,5
//Stream<T> limit(long maxSize)
Stream<Integer> limitStream = itgStream.limit(3); //1,2,3
- 스트림의 요소 걸러내기 - filter(), distinct()
- filter() : 람다식을 통해 주어진 조건(Predicate)에 맞지 않는 요소를 걸러낸다 (여러 번 사용가능)
- distinct() : 중복된 요소를 걸러낸다
Stream<Integer> itgStream = Stream.of(1,2,3,4,4,5);
//Stream<T> filter(Predicate<? super T> predicate)
Stream<Integer> filterStream = itgStream.filter(i -> i%2 == 0); //2,4,4
//Stream<T> distinct()
Stream<Integer> distinctStream = itgStream.distinct(); //1,2,3,4,5
- 정렬 - sorted()
- comparing()을 주로 사용하며 요소가 Comparable을 구현하지 않으면 따로 Comparator를 지정해야 한다
- 정렬 조건을 추가할 때는 thenComparing()을 사용한다
//Stream<T> sorted()
//Stream<T> sorted(Comparator<? super T> comparator)
//Comparator를 쓰지 않는 정렬
Stream<String> sortedStream = itgStream.sorted(); //기본 정렬
Stream<String> sortedStream = itgStream.sorted((i1, i2) -> i1.compareTo(i2)); //람다식
Stream<String> sortedStream = itgStream.sorted(String::compareTo); //메서드 참조
Stream<String> sortedStream = itgStream.sorted(String.CASE_INSENSITIVE_ORDER); //String 클래스의 Comparator
//Comparator의 static 메서드
.sorted(Comparator.naturalOrder());
.sorted(Comparator.reverseOrder());
.sorted(Comparator.comparing(Function<T, R> keyExtractor, Comparator<U> keyComparator));
.sorted(Comparator.comparingInt(ToIntFunction<T> keyExtractor));
.sorted(Comparator.nullsFirst(Comparator<T> comparator));
.sorted(Comparator.nullsLast(Comparator<T> comparator));
//Comparator의 default 메서드
.sorted(Comparator.comparing()
.thenComparing(Comparator<T> other));
.thenComparing(Function<T, U> keyExtractor, Comparator<U> keyComp));
.thenComparingInt(ToIntFunction<T> keyExtractor));
- 조회 - peek()
- forEach()와 달리 요소를 소모하지 않고 요소를 사용할 수 있다
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);
itgStream.filter(i -> i%2 == 0)
.peek(i -> System.out.println(i + "[중간확인]"))
.forEach(System.out::println);
- 변환 - map()
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);
//map()
Stream<String> mapStream = itgStream.map(i -> i + ", "); //1, 2, 3, 4, 5
//mapToInt(), mapToLong(), mapToDouble()
IntStream intStream = itgStream.mapToInt(i -> i); //Integer -> int
//기본형 스트림 타입의 메서드 (호출 후 스트림 닫힘)
int sum = intStream.sum();
OptionalDouble avg = intStream.average();
OptionalInt max = intStream.max();
OptionalInt min = intStream.min();
//summaryStatistics() (호출 후 스트림 안닫힘)
IntSummaryStatistics stat = intStream.summaryStatistics();
long count = stat.getCount();
long sum = stat.getSum();
double avg = stat.getAverage();
int min = stat.getMin();
int max = stat.getMax();
//형변환
Stream<String> objStream = intStream.mapToObj(i -> i + "[str]"); //int -> String
Stream<Integer> itgStream2 = intStream.mapToObj(i -> i); //int -> Integer
- 원자화 변환 - flatMap()
Stream<String[]> strArrStream = Stream.of(
new String[] { "abc", "def", "ghi" },
new String[] { "ABC", "DEF", "GHI" }
);
//map() 사용
Stream<Stream<String>> strStrStream = strArrStream.map(Arrays::stream);
//flatMap() 사용
Stream<String> strStream = strArrStream.flatMap(Arrays::stream);
📌 Optional<T>
- java.util에 제네릭 클래스로 T타입의 객체를 감싸는 래퍼 클래스이므로 모든 타입의 참조변수를 담을 수 있다
- 최종 연산의 결과를 Optional 객체에 담아서 반환하면 null 체크에 용이하다
//Optional의 생성
String str = "abc";
Optional<String> optVal = Optional.of(str);
Optional<String> optVal1 = Optional.of("abc");
Optional<String> optVal2 = Optional.of(new String("abc"));
//null 생성
Optional<String> optVal3 = Optional.of(null); //NullPointerException
Optional<String> optVal4 = Optional.ofNullable(null); //OK
//초기화
Optional<String> optVal5 = null; //null로 초기화
Optional<String> optVal6 = Optional.empty(); //빈 객체로 초기화
//값 가져오기
String str1 = optVal.get(); //저장된 값 반환, null이면 예외발생
String str2 = optVal.orElse(""); //null일 때 "" 반환
String str3 = optVal.orElseGet(String::new); //null일 때 람다식 반환
String str4 = optVal.orElseThrow(NullPointerException::new); //null일 때 예외발생
//null 체크
boolean check = optVal.isPresent(); //null이면 false, 아니면 true
optVal.ifPresent(System.out::println); //값이 있으면 람다식 실행, 없으면 아무것도 안함
//stream(). 메서드
Optional<T> findAny()
Optional<T> findFirst()
Optional<T> max(Comparator<? super T> comparator)
Optional<T> min(Comparator<? super T> comparator)
Optional<T> reduce(BinaryOperator<T> accumulator)
//OpitonalInt, OptionalLong, OptionalDouble
int OptionalInt.getAsInt();
long OptionalLong.getAsLong();
double OptionalDouble.getAsDouble();
📌 스트림의 최종연산
- forEach()
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);
itgStream.forEach(System.out::println); //1,2,3,4,5
- 조건 검사 - allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);
//allMatch()
boolean allM = itgStream.allMatch(i -> i > 0); //모든 요소가 0보다 큰지
//anyMatch()
boolean anyM = itgStream.anyMatch(i -> i > 0); //0보다 큰 요소가 있는지
//noneMatch()
boolean noneM = itgStream.noneMatch(i -> i > 0); //0보다 큰 요소가 없는지
//findFirst()
Optional<Integer> opt = itgStream.filter(i -> i < 3).findFirst(); //3보다 작은 요소 중 첫번째
//findAny()
Optional<Integer> opt2 = itgStream.filter(i -> i < 3).findAny(); //병렬 스트림일 때 사용
- 통계 - count(), sum(), average(), max(), min()
IntStream intStream = IntStream.of(1,2,3,4,5);
intStream.count(); //5
intStream.sum(); //15
intStream.average(); //3.0
intStream.max(); //5
intStream.min(); //1
- 리듀싱 - reduce()
- 요소를 하나씩 소모해 연산을 수행하고 최종결과를 반환한다
- 처음 두 요소를 가지고 연산한 결과를 다음 요소와 연산한다. (초기값이 있을 때 초기값과 첫 요소를 연산)
- 요소가 없는 경우 초기값이 그대로 반환되므로 반환타입은 T이다
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);
System.out.println(itgStream.reduce(0, (i1, i2) -> i1+i2)); //15
- collect()
- collect() : 스트림의 최종연산으로 매개변수로 컬렉터를 필요로 한다
- Collector : 인터페이스로 collect()의 collector는 이 인터페이스를 구현해야한다
- Collectors : 클래스로 미리 작성된 컬렉터를 static 메서드로 제공한다
📌 스트림의 최종연산 - collect()
- 스트림을 컬렉션과 배열로 변환 - toList(), toSet(), toMap(), toCollection(), toArray()
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);
Stream<Person> mapStream = Stream.of(
new Person("987654-1111111", "홍길동", 20),
new Person("987654-1111112", "김자바", 21)
);
//toList()
List<Integer> list = itgStream.collect(Collectors.toList());
//toSet()
Set<Integer> set = itgStream.collect(Collectors.toSet());
//toMap()
Map<String, Person> map = mapStream.collect(Collectors.toMap(p -> p.regId, p -> p));
//toCollection()
ArrayList<Integer> arrList = itgStream.collect(Collectors.toCollection(ArrayList::new));
//toArray()
Integer[] itgArr = itgStream.toArray(Integer[]::new);
Object[] objArr = itgStream.toArray(); //타입 지정 없으면 Object[] 반환
- 통계 - counting(), summingInt(), averagingInt(), maxBy(), minBy(), summarizingInt()
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);
//counting()
itgStream.count(); //5
//summingInt()
itgStream.collect(Collectors.summingInt(Integer::intValue)); //15
//averagingInt()
itgStream.collect(Collectors.averagingInt(Integer::intValue)); //3.0
//maxBy()
itgStream.collect(Collectors.maxBy(Integer::compareTo)); //5
//minBy()
itgStream.collect(Collectors.minBy(Integer::compareTo)); //1
//summarizingInt()
System.out.println(itgStream.collect(Collectors.summarizingInt(Integer::intValue)));
//IntSummaryStatistics{count=5, sum=15, min=1, average=3.000000, max=5}
- 리듀싱 - reducing()
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);
//Collector reducing(BinaryOperator<T> op)
itgStream.collect(Collectors.reducing((i1, i2) -> i1+i2)); //Optional[15]
//Collector reducing(T identity, BinaryOperator<T> op)
itgStream.collect(Collectors.reducing(1, (i1, i2) -> i1+i2)); //16
//Collector reducing(U idnetity, Function<T, U> mapper, BinaryOperator<U> op)
itgStream.collect(Collectors.reducing(1, Integer::intValue, Integer::sum)); //16
- 문자열 결합 - joining()
- 하나의 문자열로 반환하므로 요소가 문자열이 아닌 경우 map()을 이용해 문자열로 변환해야 한다
- 구분자를 지정해줄 수 있고 접두사와 접미사도 지정 가능하다
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);
String itgs = itgStream.map(i -> i + "").collect(Collectors.joining(",")); //1,2,3,4,5
- 그룹화와 분할 - groupingBy(), partitioningBy()
- 그룹화 : 스트림의 요소를 특정 기준으로 그룹화하는 것
- 분할 : 스트림의 요소를 두 가지(지정된 조건에 일치하는 그룹, 일치하지 않는 그룹)으로 분할하는 것
- 그룹화는 Function으로 분할은 Predicate로 분류한다
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);
//groupingBy()
//itgStream에서 각 값이 키가 되고 값은 각 값을 List로 해서 Map에 저장
//Collector groupingBy(Function classifier, Supplier mapFactory, Collector downstream)
Map<Integer, List<Integer>> map = itgStream.collect(Collectors.groupingBy(i -> i, Collectors.toList()));
System.out.println(map.values()); //[[1], [2], [3], [4], [5]]
//partitioningBy()
//itgStream에서 짝수는 true 홀수는 false 키로 Map에 값을 합산해 저장
//Collector partitioningBy(Predicate predicate, Collector downstream)
Map<Boolean, Long> map2 = itgStream.collect(Collectors.partitioningBy(i -> (i%2 == 0), Collectors.summingLong(i -> i)));
System.out.println(map2.get(true)); //6
System.out.println(map2.get(false)); //9
📌 Collector 구현하기
- Collector 를 구현한다는 것은 Collector 인터페이스를 구현하는 것으로 5개의 메서드를 구현해야 한다
- Suppllier<A> supplier() : 작업 결과를 저장할 공간을 제공
- BiConsumer<A, T> accumulator() : 스트림의 요소를 수집(collect)할 방법을 제공
- BinaryOperator<A> combiner() : 두 저장공간을 병합할 방법을 제공(병렬 스트림)
- Function<A, R> finisher() : 결과를 최종적으로 변환할 방법을 제공 (변환 필요없으면 identity 반환)
- Set<Characteristics> characteristics() : 컬렉션의 특성이 담긴 Set을 반환 (지정 필요없으면 emptySet 반환)
- Characteristics.CONCURRENT : 병렬로 처리할 수 있는 작업
- Characteristics.UNORDERED : 스트림의 요소의 순서가 유지될 필요가 없는 작업
- Characteristics.IDENTITY_FINISH : finisher()가 항등 함수인 작업
class Ex {
public static void main(String[] args) {
String[] strArr = { "aaa", "bbb", "ccc" };
Stream<String> strStream = Stream.of(strArr);
String result = strStream.collect(new concatCollector());
System.out.println(Arrays.toString(strArr));
System.out.println(result);
}
}
class concatCollector implements Collector<String, StringBuilder, String> {
@Override
public Supplier<StringBuilder> supplier() {
return () -> new StringBuilder();
}
@Override
public BiConsumer<StringBuilder, String> accumulator() {
return (sb, s) -> sb.append(s);
}
@Override
public BinaryOperator<StringBuilder> combiner() {
return (sb, sb2) -> sb.append(sb2);
}
@Override
public Function finisher() {
return sb -> sb.toString();
}
@Override
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
}
✔️ 스트림 변환 표
📄 참고
자바의 정석 3rd Edition
반응형