[JAVA] 람다와 스트림 ( Lambda & Stream ) ⑧

DongGyu Jung·2022년 5월 3일
0

자바(JAVA)

목록 보기
56/60
post-thumbnail

🏃‍♂️ 들어가기 앞서..

본 게시물은 스터디 활동 중에 작성한 게시물로 자바의 정석-기초편 교재를 학습하여 정리하는 글입니다.
※ 스터디 Page : 〔투 비 마스터 : 자바〕

*해당 교재의 목차 순서와 구성을 참고하여 작성하며
각 내용마다 부족할 수 있는 내용이나 개인적으로 궁금한 점은
추가적인 검색을 통해 채워나갈 예정입니다.



🧵 스트림(Stream) 연산

스트림에 정의된 메서드 중에서
데이터 소스를 다루는 작업을 수행하는 것을 "연산(Operation)"이라고 한다.

스트림 연산에는
이전 게시물 에서 잠깐 다뤘지만
스트림 만들기 단계를 제외하고
총 2개의 단계가 진행된다.

  • 중간 연산 : 연산 결과스트림인 연산 _ 연속해서 중간 연산 가능
    [ ex. filter(), distinct(), sort(), limit() 등 등 ]

  • 최종 연산 : 연산 결과가 스트림이 아닌 연산 _ 단 한번만 최종 연산 가능 : " 스트림 소모(closed) "
    [ ex. count(), forEach() 등 등 ]




🚀 중간 연산

반환값의 타입 : 무조건 Stream

🔅 skip()

말 그대로 Stream 내 요소를 건너뛰는 중간 연산 메서드이다.

  • 앞에서부터 long n건너뛰기
Stream<T> skip( long n )
// 기본형 스트림에서의 skip _ 반환타입만 다름
IntStream skip( long n )


🔅 limit()

반환값/결과값으로 구성된 Stream에 있는 요소에 대해 원하는 갯수만큼만 요소 가져오는 중간 연산 메서드이다.

  • 앞에서부터 순서대로 long maxSize만큼만 제한해서 가져오기
    ( maxSize 이후의 요소는 잘라내는 것 )
Stream<T> limit( long maxSize )
// 기본형 스트림에서의 limit _ 반환타입만 다름
IntStream limit( long maxSize )


🔅 filter()

주어진 조건에 맞는 요소만 가져오는 중간 연산 메서드이다.

  • 다른 조건으로 여러번 filter() 메서드 적용 가능
  • Predicate 인터페이스가 아닌 " boolean 반환 람다식 " 사용 가능
Stream<T> filter( Predicate<? super T> predicate )

IntStream intStream = IntStream.rangeClosed(1,10) ; // 1~10

intStream.filter(i -> i%2 == 0).forEach(System.out::print); // 246810
intStream.filter(i -> i%2 != 0 && i%3 != 0).forEach(System.out::print); // 157
intStream.filter(i -> i%2 != 0).filter(i -> i%3 != 0).forEach(System.out::print); // 157


🔅 distinct()

Stream내 중복된 요소들을 하나씩만 남겨놓고 중복값 제거하는 중간 연산 메서드이다.

Stream<T> distinct()

IntStream intStream = IntStream.of(1,2,2,3,3,3,4,5,5,6) ; 
intStream.distinct().forEach(System.out::print); // 123456


🔅 sorted()

Stream 요소들을 정렬하는 중간 연산 메서드이다.

  • Comparator<? super T> comparator 를 통해서 정렬 기준 적용 가능
Stream<T> sorted()
Stream<T> sorted( Comparator<? super T> comparator )

이전에 공부했던 것처럼 Comparator를 지정하지 않으면
스트림의 요소 클래스에서 구현해놓은 Comparable 기준으로 정렬된다.
( 당연히 Comparable을 구현해놓지 않은 클래스 요소라면 " 지정하지 않을 경우 " 예외가 발생한다. )

※ 문자열 스트림 정렬하기

CASE_INSENSITIVE_ORDER 같은 경우는
미리 static 으로 구현되어 있다.

static Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
Stream<String> strStream = Stream.of("dd", "aaa", "CC", "cc", "b");

/*
최종 연산 forEach(System.out::print) 를 수행시켰다고 가정한다.
*/
// 기본 정렬 -> CCaaabccdd
strStream.sorted() ; // String 클래스의 Comparable
strStream.sorted( Comparator.naturalOrder() ) ; // Comparator 클래스에 구현된 기본 정렬 기준 
strStream.sorted( (s1, s2) -> s1.compareTo(s2) ) ; // 람다식
strStream.sorted( String::compareTo ) ;

// 역순 정렬 -> ddccbaaaCC
strStream.sorted( Comparator.reverseOrder() )
strStream.sorted( Comparator.<String>naturalOrder().reversed() ) // 잘안씀

// 기본 정렬 (대소문자 구분 X) -> aaabCCccdd
strStream.sorted( String.CASE_INSENSITIVE_ORDER )

// 역순 정렬 (대소문자 구분 X) -> ddCCccbaa _ 주의(대문자가 오히려 앞에  위치)
strStream.sorted( String.CASE_INSENSITIVE_ORDER.reversed() )

// 별도 기준 정렬 : comparing
strStream.sorted( Comparator.comparing( String::length ) )
strStream.sorted( Comparator.comparingInt( String::length ) )

// 별도 기준 역순 정렬 : comparing().reversed()
strStream.sorted( Comparator.comparing( String::length ).reversed() )

❓❗ Comparator 메서드

JDK1.8 부터 「 Comparator 인터페이스 」에 static메서드와 디폴트 메서드가 많이 추가됨.
해당 메서드들은 모두 Comparator<T>를 반환 ( 가장 기본 메서드 : comparing() )

스트림에서는 sorted() 메서드의 매개변수 값으로 주로 쓰인다

스트림의 요소가 만약 Comparable을 구현한 경우라면
매개변수가 하나짜리인 comparing을 사용하면 되고

Comparable을 구현하지 않은 경우
추가적인 정렬 기준(Comparator)을 따로 지정해 줘야한다.

comparing( Function<T, U> keyExtractor )
comparing( Function<T, U> keyExtractor, Comparator<U> keyComparator )

또한 스트림과의 비교 대상기본형이라면

comparingInt( ToIntFunction<T> keyExtractor )
comparingLong( ToLongFunction<T> keyExtractor )
comparingDouble( ToDoubleFunction<T> keyExtractor )

같은 메서드를 통해
" 오토박싱 & 언박싱 " 과 같은 과정을 줄여서 효율적으로 기본형 비교대상을 제공할 수 있다.


만약 정렬 조건이 2개 이상일 경우엔,
정렬 조건 누적을 thenComparing() 메서드로 할 수 있다.
( 사용하는 것은 comparing()메서드와 크게 다르지 않다. )

thenComparing( Comparator<T> other )
thenComparing( Function<T, U> keyExtractor )
thenComparing( Function<T, U> keyExtractor, Comparator<U> keyComparator )

※ 구조 = sorted( Comparator.comparing().thenComparing().thenComparing()...thenComparing() )



🔅 map()

중간연산 메서드들 중에서 핵심적인 메서드 라고 할 수 있다.

스트림을 사용할 때,
스트림 요소에 저장된 값 중에서

원하는 필드만 뽑아내거나
특정 형태 변환을 해야 할 때가 있는데

이 때, 사용하는 메서드가 map() 메서드이다.

매개변수로는
" T타입 값을 R타입 값으로 변환 & 반환 "하는 함수를 지정해야한다.

// Stream<T> ---> Stream<R>
Stream<R> map( Function<? super T, ? extends R> mapper )

예를 들어
File 타입 값들
즉, File 클래스 객체들로 이루어진 Stream에서

" 파일 이름(String)만 가져와서 이름으로만 구성된 Stream<String> 으로 변환 "
을 하고 싶을 때

Stream<File> fileStream = Stream.of( new File("F1.java"), new File("F2.java"), new File("F3.java") );
// 각 파일 객체 요소에서 파일 이름(String) 값만 가져와 보자.
// fileStream.map( File::getName ).forEach(System.out::print) // F1.javaF2.javaF3.java
Stream<String> fileNameStream = fileStream.map( File::getName );
fileNameStream..forEach(System.out::print) // F1.javaF2.javaF3.java

이 또한 중간 연산 메서드이기 때문에
앞서 알아본 다른 메서드들을 활용해서 같이 사용할 수 있고

특히 이 map()함수가 요긴하게 쓰인다.



🔅 peek()

연산 중간 중간에 디버깅 용도로 많이 쓰이는 메서드

중간 연산과 중간 연산 사이에
스트림을 소모하는 연산

즉, 최종 연산을 사용하지 못하기 때문에
결과를 출력해서 결과를 확인하는 forEach()를 사용할 수 없다.

그래서 연산 중간 중간의 연산결과를 확인하고 싶을 때,
peek()함수를 사용한다.

peek()함수는
스트림의 요소를 소모하지 않으므로 여러 번 끼워 넣어도 문제 없다.

Stream<File> fileStream = Stream.of( new File("F1.java"), new File("F1.bak"), 
									new File("F2.java"), new File("F3.txt"),
                                    new File("F3"), new File("F4.java"));

// 요소 하나씩 하나씩 처리되는 점 주의하자.
fileStream.map( File::getName )
		.filter( s -> s.indexOf('.') != -1) // 확장자 없는 경우, 출력하지 못한다.
        .peek( s -> System.out.printf("filename = %s%n", s) )
        .map( s -> s.substring( s.indexOf('.') + 1 ) )
        .peek( s -> System.out.printf("extension = %s%n", s) )
        .map(String::toUpperCase)
        .distinct() // 확장자가 중복되는 경우, 출력하지 못한다.
        .forEach(System.out::println);

/*
filename = F1.java
extension = java
JAVA
filename = F1.bak
extension = bak
BAK
filename = F2.java
extension = java
filename = F3.txt
extension = txt
TXT
filename = F4.java 
extension = java
*/


🔅 flatMap()

" Stream의 Stream "을 Stream으로 변환

정리된 한 줄이 대체 무슨 말인지 이해가 가지 않을 수도 있다.

예를 틀어
배열 요소를 가진 Stream<String[]>이 있다고 하면
" 스트림 > 배열 > 배열 내부 값들 " 구조가 이루어져 있는데

" 각 배열 "의 내부 값들을 하나의 스트림으로 합병해서
Stream<String> 타입의 하나의 스트림으로 만들고 싶을 때,

만약 바로 위에서 배웠던 map() 메서드를 사용하게 되면
기존의 Stream<String[]>Stream<Stream<String>>으로 밖에 만들 수 없다.
( 내부 값들이 합쳐진게 아닌 그냥 단순 스트림으로 묶어준 꼴이 된다.
: Stream<Stream<String>> strStrStream = 배열스트림.map( Arrays::stream ) )

이 때,
우리가 원했던 " 각 배열들이 가진 요소들을 하나의 스트림으로 합병 " 과 같은 작업은
flatMap() 메서드를 사용하면 된다.

Stream<String[]> 스트림》.flatMap( Arrays::stream ) // ---> Stream<String> 의 스트림 반환

보통 문자열 배열의 값들이
긴 문자열 한 줄 씩으로 배열을 구성하고 있다 가정하면

"""
각 한 줄을 단어로 나눠
각 줄의 단어들을 하나의 스트림으로 모아서 취급하고 싶을 때

"""

이럴 때
사용하면 효과적이다.


🔆 중간 연산 메서드 모음 🔆

중간 연산설명
Stream<T> distinct()중복 제거
Stream<T> filter( Predicate<T> predicate )조건에 맞는 요소만
Stream<T> limit( long maxSize )스트림의 요소 제한 (잘라내기)
Stream<T> skip( long n )스트림의 요소 일부 Skip
Stream<T> peek( Consumer<T> action )스트림의 요소 작업 수행
보통 " 중간 결과를 볼 때 " 사용
Stream<T> sorted()
Stream<T> sorted( Comparator<T> comparator )
스트림의 요소 정렬
Comparator로 정렬 기준

중간 연산설명
Stream<R>map( Function<T, R> mapper )
DoubleStreammapToDouble( ToDoubleFunction<T> mapper )
IntStreammapToInt( ToIntFunction<T> mapper )
LongStreammapToLong( ToLongFunction<T> mapper )
스티림 요소 반환
Stream<R>map( Function<T, R> mapper )
DoubleStreammap( Function<T, R> mapper )
IntStreammap( Function<T, R> mapper )
LongStreammap( Function<T, R> mapper )

0개의 댓글