-
[JAVA] 스트림(Stream) 생성 / 중간연산 map,flatMap / 최종연산 reduce, collect / Optional (2)DEV/JAVA 2024. 3. 26. 16:16
1. 스트림 만들기
- 컬렉션 : Stream<T> Collection.stream()
- ex : Stream<Integer> intStream = list.stream();
- 배열
- Stream<T> Stream.of(val)
- Stream<T> Arrays.stream(val)
- ex 1 : Stream<String> strStream = Stream.of( "a","b","c" );
- ex 2 : Stream<String> strStream = Arrays.stream(new String[]{ "a","b","c" });
- 특정 범위의 정수 : 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환
- IntStream.range(int begin, int end) : end가 범위에 포함 X
- IntStream.rangeClosed(int begin, int end): end가 범위에 포함
- ex 1 : IntStream intstr = IntStream.range(1,5); //1,2,3,4
- ex 2 : IntStream intstr = IntStream.rangeClosed(1,5); //1,2,3,4,5
- 임의의 수 : Random클래스에 난수들로 이루어진 스트림을 반환하는 클래스가 존재
- 아래 메서드들에 매개변수를 주지 않으면, 스트림의 크기가 정해지지 않은 무한 스트림을 반환하므로 limit()도 같이 사용하여 스트림의 크기를 제한해야한다.
- IntStream ints()
- LongStream longs()
- DoubleStream doubles()
IntStream intStr = new Random().ints(); //무한스트림 intStr.limit(5).forEach(System.out::println); //5개 요소만 출력 IntStream intStr2 = new Random().ints(5); //유한 스트림 : 크기가 5인 난수 스트림 반환 //지정된 범위의 난수를 발생시키는 스트림을 반환하는 것도 가능 //단, end 는 범위에 포함되지 않음 IntStream intStr3 = new Random().ints(1, 10)
- 람다식 - iterate(), generate() : 람다식을 매개변수로 받아서, 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성
- static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
- seed값으로 지정된 값부터 시작해서 람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복
- ex ) Stream<Integer> evenStream = Stream.iterate(0, n→n+2); //0,2,4,6, …
- static <T> Stream<T> generate(Supplier<T> s)
- iterate()와 같이 람다식에 의해 계산되는 값을 요소로하는 무한 스트림을 생성해서 반환하지만, 다른점은 이전 결과를 이용해서 다음 요소를 계산하지않음
- 매개변수 타입은 Supplier<T>이므로 매개변수가 없는 람다식만 허용됨
- ex ) Stream<Double> randomStream = Stream.generate(Math::random);
- 두 메서드에 의해 생성된 스트림은 기본형 스트림 타입의 참조변수로 다룰 수 없다.
- static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
- 파일 : list()는 지정된 디렉토리에 있는 파일의 목록을 소스로하는 스트림을 생성해서 반환한다.
- Stream<Path> Files.list(Path dir)
- 빈 스트림 : 요소가 하나도 없는 비어있는 스트림을 생성할 수 도 있다.
- Stream emptyStream = Stream.empty();
- 두 스트림의 연결 : concat() 메서드를 사용하면 같은 타입의 두 스트림을 하나로 연결할 수 있다.
- Stream<String> strs = Stream.concat(strs1, strs2);
2. 스트림의 중간 연산
: 다양한 연산자 중 자세한 설명이 필요한 map, flatMap만 다룸
- map() : 스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환해야 할때 사용
ex) File 스트림에서 파일의 이름만 뽑기
Stream<File> fileStream = Stream.of(new File("ex1.java"),new File("ex2.java"), new File("ex.bak"),new File("ex1.txt")); Stream<String> fileNameStream = fileStream.map(File::getName);
* map은 중간연산이므로 연산결과는 스트림이다.
* 하나의 스트림에 여러번 적용할 수 있다.
- mapToInt(), mapToLong(), mapToDouble() : 스트림의 요소를 숫자로 변환하는 경우 기본형 스트림으로 변환하는 것이 더 유용할 수 있음.
* 기본형 스트림은 숫자를 다루는데 편리한 메서드를 제공
- int sum() : 스트림 모든 요소의 총합
- OptionalDouble average() : sum() / (double) count()
- OptionalInt max() : 스트림 요소 중 가장 큰 값
- OptionalInt min() :스트림 요소 중 가장 작은 값
- 해당 메서드들은 최종 연산이기 때문에 호출 후에 스트림이 닫힌다.
→ 하나의 스트림에서 sum() , average()를 연속으로 호출 할 수 없음. 그래서 제공하는 summaryStatistics() 메서드
IntSummaryStatistics stat = scoreStream.summaryStatistics(); long totalCount = stat.getCount(); long totalScore = stat.getSum(); double avgScore = stat.getAverage(); int min = stat.getMin(); int max = stat.getMax();
* IntStream을 Stream<T>로 변환 시 mapToObj() 사용
* IntStream을 Stream<Integer>로 변환 시 boxed() 사용
- flatMap() : Stream<T[]>를 Stream<T>로 변환
Stream<String[]> strArrStream = Stream.of( new String[]{"abc","def","ghi"}, new String[]{"ABC","GHI","JKLMN"} ); //flatMap Stream<String> strStream = strArrStream.flatMap(Arrays::stream); //일반 map()사용시 스트림의 스트림 형태로 변환 됨 Stream<Stream<String>> strStream2 = strArrStream.map(Arrays::stream);
3. Optional<T>와 OptionalInt
- Optional<T>는 지네릭 클래스로 ‘T타입의 객체’를 감싸는 래퍼클래스. 그래서 Optional타입의 객체에는 모든 타입의 참조변수를 담을 수 있다.
- 최종연산의 결과를 Optional객체에 담아서 반환하는 것이다.
→ Optional에서 정의된 메서드를 통해서 널 체크를 위한 if문 없이도 NullPointerException이 발생하지 않는 간결하고 안전한 코드를 작성할 수 있게 해줌
* Optional객체 생성하기
- of() 또는 ofNullable() 사용
- 참조변수의 값이 null일 가능성이 있으면 ofNullable() 사용 (NullPointerException 방지)
- 초기화 시 empty() 사용
- 객체의 값을 가져올 때는 get(), 값이 null일 것을 대비해서 orElse()로 대체할 값 지정 가능
- orElse()의 변형으로 람다식을 지정할 수 있는 orElseGet(), 예외를 발생시키는 orElseThrow() 존재
- isPresent()는 Optional 객체에 값이 null이면 false, 아니면 true 반환
- ifPresent()는 값이 있으면 주어진 람다식을 실행, 없으면 아무 일도 하지않음
/* 생성 */ String str = "abc"; Optional<String> optVal = Optional.of(str); /* Null일 경우 */ Optional<String> optVal = Optional.of(null); // NullPointerException 발생 Optional<String> optVal = Optional.ofNullable(null); /* 초기화 */ Optional<String> optVal = null; //권장하지않음 Optional<String> optVal = Optional.empty(); /* 값 가져오기 */ String str1 = optVal.get(); //저장 값 반환. null이면 예외발생 String str2 = optVal.orElse(""); //값이 null일때, "" 반환 /* orElse() 변형 */ String str3 = optVal.orElseGet(String::new); //String 객체 생성 String str4 = optVal.orElseThrow(NullPointerExcetopn::new); //NPE 발생 /* isPresent(), ifPresent() */ //기존의 null 체크 if(str != null) { System.out.println(str); } //isPresent() 사용 if(Optional.ofNullable(str).isPresent()) { System.out.println(str); } //ifPresent() 사용 Optional.ofNullable(str).ifPresent(System.out::println);
* Stream클래스에 정의된 메서드 중에 Optional<T>를 반환하는 메서드
- findAny()
- findFirst()
- max(Comparator)
- min(Comparator)
- reduce()
→ 기본형 스트림과 다르게, max()와 min()의 매개변수로 Comparator 보냄
* 기본형 스트림에는 Optional도 기본형을 값으로 하는 OptionalInt, OptionalLong, OptionalDouble을 반환한다.
아래는 InputStream에 정의된 메서드이다.
- OptionalInt findAny()
- OptionalInt findFirst()
- OptionalInt reduce()
- OptionalInt max()
- OptionalInt min()
- OptionalDouble average()
* 기본형 int의 기본값은 0이다. 아무런 값도 갖지 않는 OptionalInt에 저장되는 값도 0이다.
아래 두 객체는 같을까 ?
OptionalInt opt = OptionalInt.of(0); OptionalInt opt2 = OptionalInt.empty();
→ 저장된 값이 없는 것과 0이 저장된 것은 isPresent로 구분이 가능하다.
- opt.isPresent(); → true
- opt2.isPresent(); → false
- opt.getAsInt(); → 0
- OptionalInt[0]
- opt2.getAsInt(); → NoSuchElementException 발생
- OptionalInt.empty
- opt.equals(opt2); → false
- Optional객체에 null을 저장하면 비어있는 것과 동일하게 취급한다.
Optional<String> opt = Optional.ofNullable(null); Optional<String> opt2 = Optional.empty(); System.out.println(opt.equals(opt2)); //true
4. 스트림의 최종 연산
: 다양한 연산자 중 자세한 설명이 필요한 reduce, collect만 다룸
- reduce() : 스트림의 요소를 줄여나가면서 연산을 수행하고 최종결과를 반환. 처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다. 이 과정에서 스트림 요소를 하나씩 소모하게 되며 모든 요소를 소모하게 되면 그 결과를 반환한다.
- Optional<T> reduce(BinaryOperator<T> accumulator)
- 초기 값을 갖는 reduce()도 있음 : 초기값과 스트림의 첫 번째 요소로 연산 시작
- T reduce(T identity, BinaryOperator<T> accumulator)
- U reduce(U identity, BiFunction<U,T,U> accumulator, BinaryOperator<U> combiner)
→ combiner는 병렬 스트림에 의해 처리된 결과를 합칠 때 사용
ex) 최종연산의 count(), sum() 등은 다음과 같이 내부적으로 reduce()를 이용해 만들어진것.
//Stream<Integer> 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); /* max와 min의 경우 초기값이 필요 없음. 다음과 같이 매개변수가 하나짜리인 reduce를 사용하는 것이 나음. 변수 intStream의 타입이 기본형 IntStream인 경우 사용 */ OptionalInt max = intStream.reduce( (a,b) -> a > b ? a : b); OptionalInt min = intStream.reduce( (a,b) -> a < b ? a : b); //위 람다식을 메서드 참조로 변환 OptionalInt max = intStream.reduce(Integer::max); OptionalInt min = intStream.reduce(Integer::min);
- collect() : 스트림의 요소를 수집하는 연산. 리듀싱(reduce())과 유사.
collect()가 스트림의 요소를 수집하려면 어떻게 수집할 것인가에 대한 방법이 정의되어있어야하는데 이것이 바로 컬렉터(collector)이다.
collect() : 스트림의 최종연산. 매개변수로 컬렉터를 필요로 함.
Collector : 인터페이스. 컬렉터는 이 인터페이스를 구현해야함.
Collectors : 클래스. static 메서드로 미리 작성된 컬렉터를 제공.- Object collect(Collector collector)
- Object collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)
* 스트림을 컬렉션과 배열로 변환
: 스트림의 모든 요소를 컬렉션에 수집하려면, Collectors 클래스의 toList() 같은 메서드를 사용하면된다.
List나 Set이 아닌 특정 컬렉션을 지정하려면 toCollection()에 해당 컬렉션의 생성자 참조를 매개변수로 넣는다.
- toList(), toSet(), toMap(), toCollection(), toArray()
//toList() 사용하여 수집 List<String> names = studentStream.map(Student::getName).collect(Collectors.toList()); //toCollection으로 ArrayList 수집 ArrayList<String> list = names.stream().collect(Collectors.toCollection(ArrayList::new)); //toMap() : key - 학번, value - 학생 객체 지정 Map<String,Student> map = studentStream.collect(Collectors.toMap(s->s.getHakbun(), s->s)); /* toArray() : 스트림에 저장된 요소 T[]타입의 배열로 반환 단, 해당 타입의 생성자 참조를 매개변수로 지정해줘야함. default : Object[] */ Student[] stuNames = studentStream.toArray(Student[]::new); //OK Student[] stuNames = studentStream.toArray(); //Error Object[] stuNames = studentStream.toArray(); //OK
* 통계
: 최종연산에서 제공하는 통계 정보를 collect()를 사용해서 구하기. 주로 groupingBy()와 함께 사용할 때 필요
- counting(), summingInt(), averagingInt(), maxBy(), minBy()
// 최종연산 count() 메서드 long count = studentStream.count(); // collect()에서 제공하는 counting() long count = studentStream.collect(Collectors.counting());
* 리듀싱
: 리듀싱도 마찬가지로 collect()로 사용이 가능하다
- reducing()
IntStream intStream = new Random().ints(1,46).distinct().limit(6); // 최종연산 reduce() OptionalInt max = intStream.reduce(Integer::max); // collect()의 reducing() Optional<Integer> max = intStream.boxed().collect(Collectors.reducing(Integer::max));
* 문자열 결합
: 문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환. 구분자, 접두사, 접미사 지정 가능
스트림의 요소가 String이나 StringBuffer 처럼 CharSequence의 자손인 경우에만 결합이 가능
- joining()
String studentNames = studentStream.map(Student::getName).collect(Collectors.joining()); String studentNames = studentStream.map(Student::getName).collect(Collectors.joining(",")); String studentNames = studentStream.map(Student::getName).collect(Collectors.joining(",","[","]"));
* 그룹화와 분할
: 그룹화는 스트림의 요소를 특정기준으로 그룹화하는 것을 의미. 분할은 스트림의 요소를 두 가지, 지정된 조건에 일치하는 그룹과 일치하지 않는 그룹으로의 분할을 의미
- groupingBy(), partitioningBy()
- groupingBy()는 스트림의 요소를 Function으로, partitioningBy()는 Predicate로 분류
- 스트림을 두 개의 그룹으로 나누어야한다면 partitioningBy(), 그 외는 groupingBy()
- 그룹화와 분할의 결과는 Map에 담겨 반환됨
ex ) Student.class : 학생 클래스 정의
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; } public String getName() { return name; } public boolean isMale() { return isMale; } public int getHak() { return hak; } public int getBan() { return ban; } public int getScore() { return score; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", isMale=" + isMale + ", hak=" + hak + ", ban=" + ban + ", score=" + score + '}'; } }
partitioningBy()에 의한 분류
Student[] students = { new Student("김자바", false, 1,2,200), new Student("이자바", true, 2,2,100), new Student("박자바", false, 3,3,300), new Student("최자바", true, 1,4,200) }; //1. 분할 : 성별로 기본 분할 후 각각 리스트에 담기 Map<Boolean, List<Student>> stuBySex = Stream.of(students).collect(partitioningBy(Student::isMale)); List<Student> maleStudent = stuBySex.get(true); List<Student> femaleStudent = stuBySex.get(false); //2. 분할 + 통계 : counting() 메서드로 남학생,여학생 수 구하기 Map<Boolean, Long> stuNumBySex = Stream.of(students).collect(partitioningBy(Student::isMale, counting())); System.out.println("남학생 수 : " + stuNumBySex.get(true)); System.out.println("여학생 수 : " + stuNumBySex.get(false)); //3. 분할 + 통계 : maxBy() 메서드로 남학생, 여학생 각각 성적 1등 구하기 Map<Boolean, Optional<Student>> topScoreBySex = Stream.of(students).collect(partitioningBy(Student::isMale, maxBy(comparingInt(Student::getScore)))); System.out.println("남학생 1등 : " + topScoreBySex.get(true)); System.out.println("여학생 1등 : " + topScoreBySex.get(false)); //3-1. 같은 결과 Optional이 아닌 Student객체 얻고 싶을 때 collectingAndThen()과 Optional::get 사용 Map<Boolean, Student> topScoreBySex2 = Stream.of(students).collect(partitioningBy(Student::isMale, collectingAndThen(maxBy(comparingInt(Student::getScore)), Optional::get))); System.out.println("남학생 1등 : " + topScoreBySex2.get(true)); System.out.println("여학생 1등 : " + topScoreBySex2.get(false)); /*4. 다중 분할 1) 성별로 분할 (남/여) 2) 성적으로 분할 (불합격/합격) */ Map<Boolean, Map<Boolean, List<Student>>> failedStuBySex = Stream.of(students).collect(partitioningBy(Student::isMale, // 1. 성별로 분할 partitioningBy(s -> s.getScore() < 150))); //2. 성적으로 분할 List<Student> failedMaleStudent = failedStuBySex.get(true).get(true); //남자 불합격자 List<Student> failedFemaleStudent = failedStuBySex.get(false).get(true); //여자 불합격자
결과
남학생 수 : 2 여학생 수 : 2 남학생 1등 : Optional[Student{name='최자바', isMale=true, hak=1, ban=4, score=200}] 여학생 1등 : Optional[Student{name='박자바', isMale=false, hak=3, ban=3, score=300}] 남학생 1등 : Student{name='최자바', isMale=true, hak=1, ban=4, score=200} 여학생 1등 : Student{name='박자바', isMale=false, hak=3, ban=3, score=300}
groupingBy()에 의한 분류
//1. 학생을 반별로 그룹화 Map<Integer, List<Student>> stuByBan = Stream.of(students).collect(groupingBy(Student::getBan, toList())); //toList생략 가능 System.out.println("===반별 그룹화==="); for(List<Student> ban : stuByBan.values()) { for(Student s : ban) { System.out.println(s); } } /*2. 다중 그룹화 1) 학년별 그룹화 2) 반별 그룹화 */ Map<Integer, Map<Integer, List<Student>>> stuByHakAndBan = Stream.of(students).collect(groupingBy(Student::getHak, groupingBy(Student::getBan))); System.out.println("===다중 그룹화 1.학년 / 2.반 ==="); for(Map<Integer, List<Student>> hak : stuByHakAndBan.values()) { for(List<Student> ban : hak.values()) { for(Student s : ban) { System.out.println(s); } } }
결과
===반별 그룹화=== Student{name='김자바', isMale=false, hak=1, ban=2, score=200} Student{name='이자바', isMale=true, hak=2, ban=2, score=100} Student{name='박자바', isMale=false, hak=3, ban=3, score=300} Student{name='최자바', isMale=true, hak=1, ban=4, score=200} ===다중 그룹화 1.학년 / 2.반 === Student{name='김자바', isMale=false, hak=1, ban=2, score=200} Student{name='최자바', isMale=true, hak=1, ban=4, score=200} Student{name='이자바', isMale=true, hak=2, ban=2, score=100} Student{name='박자바', isMale=false, hak=3, ban=3, score=300}
'DEV > JAVA' 카테고리의 다른 글
[JAVA] 쓰레드(Thread)의 동기화 / synchronized / Lock과 Condition / fork & join 프레임워크 (0) 2024.04.23 [JAVA] 쓰레드 (Thread)의 구현 / 싱글쓰레드, 멀티쓰레드 / 우선순위 / 쓰레드 그룹 / 데몬 쓰레드 (2) 2024.04.05 [JAVA] 제네릭스(Generics) / 열거형 (Enum) (2) 2024.04.03 [JAVA] 스트림(Stream) 기본 / 특징 / 연산 (1) (0) 2024.03.20 [JAVA] 람다식 (Lambda expression) (2) 2024.03.17 - 컬렉션 : Stream<T> Collection.stream()