-
[ch14 Stream] 스트림 생성과 연산, Optional<T>프로그래밍 언어/JAVA 2022. 11. 1. 18:07
📑 스트림(Stream) 이란?
지금까지 많은 수의 데이터를 다룰 때, 컬렉션이나 배열에 데이터를 담고 원하는 결과를 얻기 위해 for문과 Iterator를 이용해 코드를 작성해왔다. 그러나 이런 방식으로 작성된 코드는 너무 길고 알아보기 어렵다.
또 문제는 데이터 소스마다 다른 방식으로 다뤄야한다. 예를 들어 List를 정렬할때는 Collection.sort()를 사용해야하고, 배열을 정렬할 때는 Arrays.sort()를 사용해야한다. 이런 문제점들을 해결하기 위해 만든 것이 '스트림(stream)'이다.
스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았다. 데이터 소스를 추상화하였다는 것은, 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되었다는 것과 코드의 재사용성이 높아진다는 것을 의미한다.
스트림을 이용하면 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.
예를들어, 문자열 배열과 같은 내용의 문자열을 저장하는 List가 있을 때,
String[] strArr = {"aaa", "bbb", "ccc"};
List<String> strList = Arrays.asList(strArr);이 두 데이터 소스를 기반으로 하는 스트림은 다음과 같이 생성한다.
Stream<String> strStream1 = strList.stream();
Stream<String> strStream2 = Arrays.stream(strArr);이 두 스트림으로 데이터 소스의 데이터를 읽어서 정렬하고 화면에 출력하는 방법은 다음과 같다. 두 스트림의 데이터 소스는 서로 다르지만, 정렬하고 출력하는 방법은 완전 같다.
strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);📑 스트림의 특징
📌 1. 스트림은 데이터 소스를 변경하지 않는다.
: 스트림은 데이터 소스로 부터 데이터를 읽기만 할 뿐, 데이터 소스를 변경하지 않는다는 차이가 있다. 필요하다면 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수도 있다.
📌 2. 스트림은 일회용이다.
: 스트림은 Iterator처럼 일회용이다. Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼, 스트림도 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하다면 다시 생성해야한다.
📌 3. 스트림은 작업을 내부 반복으로 처리한다.
: 스트림을 이용한 작업이 간결할 수 있는 비결중의 하나가 바로 '내부 반복'이다. 내부 반복이라는 것은 반복문을 메서드의 내부에 숨겼다는 것을 의미한다. forEach()는 스트림에 정의된 메서드 중 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다.
즉) forEach()는 메서드 안으로 for문을 넣는 것이다.
더보기하나의 메서드만 호출하는 람다식은 '클래스이름::메서드이름' 또는 '참조변수::메서드이름'으로 바꿀 수 있다.
ex)
Function<String,Integer> f = (String s) -> Integer.parseInt(s);
메서드 참조로 바꾸면
Function<String,Integer> f = Integer::parseInt;
📌 4. 지연된 연산
: 스트림 연산에서 한 가지 중요한 점은 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다는 것이다. 스트림에 대해 distinct()나 sort()같은 중간 연산을 호출해도 즉각적인 연산이 수행되지 않는다. 중간 연산을 호출하는 것은단지 어떤 작업이 수행되어야하는지를 지정해주는 것일 뿐이다. 최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에 소모된다.
📌 5. Stream<Integer>와 IntStream
: 에 소모된다. 요소의 타입이 T인 스트림은 기본적으로 Stream<T>이지만, 오토박싱 & 언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 IntStream, LongStream, DoubleStream이 제공된다. 일반적으로 Stream<Integer> 사용보다 IntStream을 사용하는 것이 더 좋다.
📑 스트림 만들기
📌 1. 배열
package ch14.stream; import java.util.Arrays; import java.util.stream.IntStream; import java.util.stream.Stream; public class Ex1_createStreamFromArray { public static void main(String[] args) { /* List<Integer> list = Arrays.asList(1,2,3,4,5); Stream<Integer> intStream = list.stream(); //리스트를 스트림으로 바꿀 수 있다. Iterator it = intStream.iterator(); while(it.hasNext()) { System.out.println(it.next()); } System.out.println(); */ /* intStream.forEach(System.out::print); */ // ★ 1. 스트림 만들기 - 배열 String [] strArr = new String[] {"a","b","c","d"}; // String [] strArr = {"a","b","c","d"}; 위의 코드를 줄이면 이렇게 된다. //Stream<String> strStream = Stream.of(strArr); Stream<String> strStream = Arrays.stream(strArr); strStream.forEach(System.out::print); System.out.println(); int[] intArr = {1,2,3,4,5}; IntStream intStream = Arrays.stream(intArr); intStream.forEach(System.out::print); /* * Integer[] intArr = {1,2,3,4,5}; Stream<Intger> intStream = * Arrays.stream(intArr); */ } }
📌 2. 컬렉션
컬렉션의 최고 조상인 Collection에 stream()이 정의되어 있다. 그래서 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림을 생성할 수 있다.
Stream<E> stream() // Collection 인터페이스의 메서드
예를 들어 List로부터 스트림을 생성하는 코드는 다음과 같다.
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> intStream = list.stream();forEach()는 지정된 작업을 스트림의 모든 요소에 대해 수행한다. 아래 문장은 스트림의 모든 요소를 화면에 출력한다.
intStream.forEach(System.out::println);
📌 3. 람다식 iterator(). generate()
Stream 클래스의 iterate()와 generate()는 람다식을 매개변수로 받아서, 이 람다식에 의해 계산되는 값을 요소로 하는 무한 스트림을 생성한다.
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
static <T> Stream<T> generate(Supplier<T> s)iterate()는 씨앗값(seed)으로 지정된 값으로 시작해서 람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복한다. 아래 evenStream은 0부터 시작해 값이 2씩 계속 증가한다.
Stream<Integer> evenStream = Stream.iterator(0, n->n+2); //0, 2, 4, 6
package ch14.stream; import java.util.Random; import java.util.stream.IntStream; import java.util.stream.Stream; public class Ex2_createStream { public static void main(String[] args) { // ★ 2. 스트림 만들기 - 임의의 수 IntStream intStream = new Random().ints(1,10); //무한 난수 스트림 intStream.limit(5).forEach(System.out::println); // ★ 3. 스트림 만들기 - 람다식 Stream<Integer> intStream2 = Stream.iterate(1, n->n+2); intStream2.limit(10).forEach(System.out::println); } }
📑 스트림의 연산
스트림이 제공하는 다양한 연산을 이용하면 복잡한 작업들을 간단히 처리할 수 있다. 마치 데이터베이스에 SELECT문으로 질의(쿼리, query)하는 것과 같은 느낌이다.
스트림이 제공하는 연산은 중간 연산과 최종 연산으로 분류할 수 있는데, 중간 연산은 연산결과를 스트림으로 반환하기 때문에 주간 연산을 연속해서 연결할 수 있다. 반면 최종 연산은 스트림의 요소를 모두 소모하면서 연산을 수행하므로 단 한번만 연산이 가능하다.
<중간연산>
중간연산은 n개 사용할 수 있다. 또한 반환값이 모두 Stream이므로 중간에 이어서 쓰는게 가능하다.
map()과 flatMap()이 핵심인데 이는 뒤에서 설명.
<최종연산>
최종연산은 reduce()와 collect()가 중요한데 이거또한 뒤에서 설명하겠다.
📑 스트림의 중간연산
📌 skip(), limit()
: skip은 요소를 건너뛰고, limit은 요소를 제한한다는 의미이다.
IntStream intStream1 = IntStream.rangeClosed(1, 10);
intStream1.skip(3).limit(5).forEach(System.out::print); //45678📌 filter(), distinct()
: distinct는 중복된 요소를 제거하고, filter()는 주어진 조건에 맞지 않는 요소를 걸러낸다.
IntStream intStream2 = IntStream.of(1, 2, 2, 3, 3, 3, 4, 5, 5, 6);
intStream2.distinct().forEach(System.out::print); //123456
IntStream intStream3 = IntStream.rangeClosed(1, 10);//1~10
intStream3.filter(i->i%2==0).forEach(System.out::print);//2,4,6,8,10📌 sorted()
: 스트림을 정렬할 땐 sorted를 사용하면 된다. sorted()는 지정된 Comparator로 스트림을 정렬하는데, Comparator 대신 int 값을 반환하는 람다식을 사용하는 것도 가능하다.
만약 Stream에 dd aaa CC cc b의 데이터를 넣었을 때 정렬하면 아래와 같다.
package ch14.stream; import java.util.Comparator; import java.util.stream.Stream; public class Ex4_comparator { public static void main(String[] args) { Stream<Student> studentStream = Stream.of( new Student("김",3,300), new Student("이",1,200), new Student("박",2,100), new Student("신",2,150), new Student("정",1,200), new Student("장",3,290), new Student("한",3,180) ); studentStream.sorted(Comparator.comparing(Student::getBan) .thenComparing(Comparator.naturalOrder())) .forEach(System.out::println); } } class Student implements Comparable<Student>{ String name; int ban; int totalScore; public Student(String name, int ban, int totalScore) { super(); this.name = name; this.ban = ban; this.totalScore = totalScore; } public String toString() { return String.format("[%s,%d,%d]", name,ban,totalScore); } String getName() {return this.name;} int getBan(){return this.ban;} int totalScore() {return this.totalScore;} //총점을 내림차순으로 기본 정렬 public int compareTo(Student s) { return s.totalScore - this.totalScore; } }
📌 map()
스트림의 요소에 저장된 값 중 원하는 필드만 뽑아내거나 특정 형태로 변환해야 할 때가 있다. 이 때 사용하는 것이 바로 map이다.
Stream<R> map(Function<? super T, ? extends R> mapper)
예를 들어 File의 스트림에서 파일의 이름만 뽑아서 출력하고 싶을 때, 아래와 같이 map()을 이용하면 File 객체에서 파일의 이름(String)만 간단히 뽑아낼 수 있다.
Stream<File> fileStream = Stream.of(new File("Ex1.java"), new File("Ex1"), new File("Ex1.bak"));
Stream<String> filenameStream = fileStream.map(File::getName);
filenameStream.forEach(System.out::println);map 역시 중간 연산이므로, 연산 결과는 String을 요소로 하는 스트림이다. map()으로 Stream<File>을 Stream<String>으로 변환했다고 볼 수 있다. 그리고 map도 filter()처럼 하나의 스트림에 여러 번 적용할 수 있다.
아래 문장은 File 스트림에서 파일의 확장자만을 뽑은 다음 중복을 제거해서 출력한다.
<예제로 살펴보기>
package part_14; import java.io.File; import java.util.Arrays; import java.util.List; import java.util.stream.IntStream; import java.util.stream.Stream; public class Ex14_5_Stream { public static void main(String[] args) { File[] fileArr={new File("Ex1.java"), new File("Ex1"), new File("Ex1.bak") ,new File("Ex2.java"),new File("Ex1.txt")}; Stream<File> fileStream = Stream.of(fileArr); //map으로 Stream<File>을 Stream<String>으로 변환 Stream<String> filenameStream = fileStream.map(File::getName); //모든 파일 이름 출력 filenameStream.forEach(System.out::println); //스트림 다시 생성 fileStream = Stream.of(fileArr); //indexOf는 찾는 문자열 위치 반환 //만약 입력값이 문자열에 없다면 리턴 값은 -1이다 //substring(number) : number 숫자 위치부터 값 출력 fileStream.map(File::getName)//map으로 Stream<File>을 Stream<String>으로 변환 .filter(s->s.indexOf('.')!=-1) //확장자가 없는 것은 제외 .map(s->s.substring(s.indexOf('.')+1)) //확장자만 추출 .map(String::toUpperCase)//대문자로 변경 .distinct()//중복제거 .forEach(System.out::print); //JAVABAKTXT } }
📑 Optional<T>
Optional<T>은 'T 타입의 객체'를 감싸는 래퍼 클래스이다. 그래서 Optional타입의 객체는 모든 타입의 객체를 담을 수 있다. 최종 연산의 결과를 그냥 반환하는 게 아닌 Optional 객체에 담아서 반환 하면, 반환된 결과가 null인지 매번 if문으로 체크하는 대신 Optional에 정의된 메서드를 통해서 간단히 처리할 수 있다. Optional<T>를 이용하면, 널 체크를 위한 if문이 없이도 NullPointerException이 발생하지 않는 보다 간결하고 안전하게 작성할 수 있다.
Optional 객체를 생성할 때는 of() 또는 ofNullable()을 사용한다.
String str = "abc"; //특정 객체를 요소로 갖는 Stream을 생성하고 싶을 때 Stream.of()를 사용할 수 있다. Optional<String> optVal = Optional.of(str); Optional<String> optVal1 = Optional.of("abc"); Optional<String> optVal2 = Optional.of(new String("abc")); //만약 참조 변수 값이 null일 가능성이 있음 ofNullable(null) 사용 Optional<String> optVal3 = Optional.ofNullable(null); //기본 값으로 초기화 empty() Optional<String> optVal4 = Optional.<String>empty();
Optinal 객체에 저장된 값을 가져올 때는 get()을 사용한다. 값이 null일때는 NoSuch ElementException이 발생하며, 이를 대비해 orElse()로 대체할 값을 지정할 수 있다.
Optional<String> optVal1 = Optional.of("abc");
String str1 = optVal1.get();
String str2 = optVal1.orElse("");isPresent()는 Optional 객체 값이 null이면 false, 아니면 true를 반환한다. ifPresent(Consumer<T> block) 은 값이 있으면 주어진 람다식을 실행하고, 없으면 아무 일도 하지 않는다.
Optional.ofNullable(str).ifPresent(System.out::println);
반응형'프로그래밍 언어 > JAVA' 카테고리의 다른 글
[JDK] 설치 후 환경변수 설정 (path, JAVA_HOME) (0) 2022.12.07 [JAVA] toString, (String), String.valueOf() 문자열 형변환 살펴보기 (0) 2022.11.17 [JAVA] 열거형(enum)이란? (0) 2022.10.24 [JAVA] TreeSet과 이진 탐색 트리 (1) 2022.10.15 [JAVA] Object 클래스의 equals()와 hashCode() 재정의 (1) 2022.10.15