[Java] Stream

LDH·2021년 3월 23일
1

☕ Java

목록 보기
1/2
post-thumbnail

스트림?

  • JDK 8 에서 추가된 Stream API
  • 데이터의 흐름

스트림을 사용하는 이유

  • 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있다.
    -> 람다(함수형 인터페이스) 이용 가능

  • 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓은것이다.
    -> 데이터 소스가 무엇이든 같은 방식으로 다룰 수 있게 되었다는것과 코드의 재사용성이 높아진다는 것

  • 병렬 처리가 가능하다.
    -> 하나의 작업을 둘 이상의 작업으로 잘게 나눠서 동시에 진행하는 것
    코드 간결성과 추상화

    🔎 병렬 스트림
    스트림으로 데이터를 다룰 때의 장점 중 하나가 바로 병렬 처리가 쉽다는 것이다. 병렬 스트림은 내부적으로 fork&join 프레임웍을 이용해서 자동적으로 연산을 병렬로 수행한다. 우리가 할일이라고는 그저 스트림에 parallel()이라는 메서드를 호출해서 병렬로 연산을 수행하도록 지시하면 될 뿐이다. 반대로 병렬로 처리되지 않게 하려면 sequential()을 호출하면 된다. 모든 스트림은 기본적으로 병렬 스트림이 아니므로 sequential()을 호출할 필요가 없다. 이 메서드는 parallel()을 호출한 것을 취소할 때만 사용한다.

특징

  • 스트림은 데이터 소스로 부터 데이터를 읽기만할 뿐, 변경하지 않는다.

  • 스트림은 일회용이다.
    -> 스트림의 요소를 소모해서 결과를 만든다. 그래서 최종 연산후에는 스트림이 닫혀서 더이 상 사용할 수 없다. 필요하다면 스트림을 다시 생성해야한다.

  • 스트림은 작업을 내부 반복으로 처리한다.
    -> 내부 반복이라는 것은 반복문을 메서드 내부에 숨길 수 있다는 것을 의미한다. forEach()는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다.

스트림 사용법

스트림의 구조는 크게 3가지로 나뉜다.

  1. 생성하기 : 스트림 인스턴스 생성
  2. 가공하기 : 필터링(filtering) 및 맵핑(mapping) 등 원하는 결과를 만들어가는 중간 작업
  3. 결과 만들기 : 최종적으로 결과를 만들어내는 작업

👍 불필요한 코딩을 걷어낼 수 있고, 직관적이기 때문에 가독성이 좋아진다.


1. 스트림생성

  • 컬렉션
    : 컬렉션의 최고 상위조상인 Collection에 stream()이 정의되어있다. Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림을 생성할수있다.
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> listStream = list.stream();
  • 배열
  Stream<String> arrStream = Stream.of("a","b","c");
  Stream<String> arrStream = Stream.of(new String[]{"a","b","c"});
  Stream<String> arrStream = Arrays.stream(new String[]{"a","b","c"});
  Stream<String> arrStream = Arrays.stream(new String[]{"a","b","c"}, 0, 3);//0부터 (3-1)까지

2. 가공하기 (중간연산)

중간 연산은 연산결과를 스트림으로 반환하기 때문에 중간 연산을 연속해서 연결할 수 있다.

✔ skip(), limit() >> 스트림 자르기
: 스트림의 일부를 잘라낼 때 사용하며, skip(n)은 처음 n개의요소를 건너뛰고, limit(m)는 스트림의 요소를 m개로 제한한다.

intStream.skip(1).limit(3).forEach(System.out::print); //1번째 요소부터 3개의 요소를 가진 스트림이 반환된다.

✔ filter(), distinct() >> 스트림의 요소 걸러내기
distinct()는 스트림에서 중복된 요소들을 제거하고, filter()는 주어진 조건(Predicate)에 맞지 않는 요소를 걸러낸다.

IntStream intStream = IntStream.of(1,2,2,3,3,3,4,5,5,6);
itnStream.distinct().forEach(System.out::print); //중복을 제거
intStream.filter(i -> i%2 == 0).forEach(System.out::print); //2로 나눈 나머지가 0 

intStream.filter(i -> i%2).filter(i->i%3 !=0)... 식으로 연속으로 사용도 가능하다.

✔ sorted() >> 스트림 정렬
: sorted()는 지정된 Comparator로 스트림을 정렬하는데, Comparator대신 int값을 반환하는 람다식을 사용하는 것도 가능하다. Comparator를 지정하지 않으면 스트림 요소의 기본 정렬 기준(Comparable)으로 정렬한다. 단, 스트림의 요소가 Comparable을 구현한 클래스가 아니면 예외가 발생한다.

Stream<String> strStream = Stream.of("dd", "aaa", "CC", "cc", "b");
strStream.sorted().forEach(System.out::print); //CCaaabccdd

정렬 조건을 추가할 때는 thenComparing()을 사용한다. 예를 들어 학생 스트림(studentSteam)을 반별, 성적순(내림차순), 그리고 이름순으로 정렬하여 출력하려면 다음과 같다.

studentStream.sorted(Comparator.comparing(Student::getBan)
			.thenComparing(Student::getTotalScore)
            .thenComparing(Student::getName)
            .forEach(System.out.println);
//총점 내림차순을 기본정렬로 한다.
public int CompareTo(Student s) {
    return s.totalScore - this.totalScore;
}

Comparable을 구현해서 성적별 내림차순 정렬이 Student 클래스의 기본 정렬이 되도록 했다.

✔ map() >> 변환
: 스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환해야 할 때 사용한다.
스트림에서 파일의 이름만 뽑아서 출력하고 싶을 때, 아래와 같이 map()을 이용하면 File객체에서 파일의 이름(String)만 간단히 뽑아낼 수 있다.

Stream<File> fileStream = Stream.of(new File("Ex1.java"), new File("Ex1"), new File("Ex1.bak"), new File("Ex2.java"), new File("Ex1.txt"));
Stream<String> filenameStream = fileStream.map(File::getName);
filenameStream.forEach(System.out::println); //스트림의 모든 파일이름을 출력

✔ peek() >> 조회
: 연산과 연산 사이에 올바르게 처리되었는지 확인한다. forEach()와 달리 스트림의 요소를 소모하지 않으므로 연산 사이에 여러 번 끼워 넣어도 문제가 되지 않는다.

fileStream.map(File::getName)
        .filter(s -> s.indexOf('.') != -1) //확장자가 없는 것은 제외
        .peek(s->System.out.print("filename = "+s)) //파일명을 출력한다
        .map(s -> s.substring(s.indexOf('.')+1)) //확장자만 추출
        .peek(s -> System.out.print("extension = "+s)) //확장자만 출력
        .forEach(System.out::println); 

✔ mapToInt(), mapToLong(), mapToDouble() >> 조회
: 스트림의 요소를 숫자로 반환하는 경우 IntStream과 같은 기본형 스트림으로 변환하는 것이 더 유용할 수 있다. 스트림에 포함된 모든 학생의 성적을 합산해야 한다면, map()으로 학생의 총점을 뽑아서 새로운 스트림을 만들어 낼 수 있다.

Stream<Integer> studentScoreStream = studentStream.map(Student::getTotalScore);
IntStream studentScoreStream = studentStream.mapToInt(Student::getTotalScore);

count()만 지원하는 Stream와 달리 IntStream과 같은 기본형 스트림은 숫자를 다루는데 편리한 메서드들을 제공한다.
int sum() / OptionalDouble average() / OptionalInt max() / OptionalInt min()

✔ flatMap() >> Stream<T[]>를 Stream<T>로 변환
: 각 요소의 문자열들을 합쳐서 문자열이 요소인 스트림으로 만들경우

Stream<String[]> strArrStrm = Stream.of(new String[]{"abc", "def", "ghi"}, new String[]{"ABC", "GHI", "JKLMN"});
Stream<Stream<String>> strStrStream = strArrStrm.map(Arrays:stream);

Stream<String[]>을 map(Arrays::stream)으로 변환한 결과는 Stream이 아닌 Stream<Stream>이다. 스트림의 스트림인 것이다. 이때 간단히 map()을 아래와 같이 flatMap()으로 바꾸기만하면 우리가 원하는 결과를 얻을 수 있다.

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

3. 결과만들기 (최종연산)

최종 연산은 스트림의 요소를 소모하면서 연산을 수행하므로 단 한번만 연산이 가능하다.

✔ forEach() >> 출력
: 반환 타입이 void이므로 스트림의 요소를 출력하는 용도로 많이 사용한다.
✔ reduce() >> 출력
: 스트림의 요소를 줄여나가면서 연산을 수행하고 최종결과를 반환한다. 처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다. 이 과정에서 스트림의 요소를 하나씩 소모하게 되며, 스트림의 모든 요소를 소모하게 되면 그 결과를 반환한다.

✔ count(), sum(), average(), max(), min() >> 통계

✔ OptionalDouble average() / OptionalInt max() / OptionalInt min() >> 연산
: IntStream과 같은 기본형 스트림은 숫자를 다루는데 편리한 메서드들을 제공한다. 스트림의 요소가 하나도 없을 때, sum()은 0을 반환하면 그만이지만 다른 메서드들은 단순히 0을 반환할 수 없다. 여러 요소들을 합한 평균이 0일 수도 있기 때문이다. 이를 구분하기 위해 단순히 double 값을 반환하는 대신, double 타입의 값을 내부적으로 가지고 있는 OptionalDouble을 반환하는 것이다.

✔ collect() >> 수집
: Collector를 매개변수로 하는 스트림의 최종 연산이다.
collect()가 스트림의 요소를 수집하기 위한 수집 방법이 정의된 것이 collector이다. collector는 Collector인터페이스를 구현한 것이다.

profile
💻💻💻

0개의 댓글