솜이의 데브로그

Chapter 14 ) 람다와 스트림 본문

책을 읽자/Java의 정석

Chapter 14 ) 람다와 스트림

somsoming 2022. 11. 4. 16:26

출처 : Java의 정석

 

1. 람다식 (Lambda expression)

  • 람다식의 도입으로 인해 자바는 객체지향언어인 동시에 함수형 언어가 되었다.

 

람다식이란?

  • 메서드를 하나의 '식(expression)'으로 표현한 것이다.
  • 메서드의 이름과 반환값이 없어지므로, 람다식을 '익명 함수'라고도 한다.
  • 메서드의 매개변수로 전달되어지는 것이 가능하고, 메서드의 결과로 반환될 수도 있다.

ex)

int[] arr = new int[5];
Arrays.setAll(arr, (i) -> (int)(Math.random()*5) + 1);

 

함수형 인터페이스 (Functional Interface)

  • 람다식을 다루기 위한 인터페이스
  • 단, 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다.
    • 그래야 람다식과 인터페이스의 메서드가 1:1로 연결
  • static 메서드와 default 메서드의 개수에는 제약 X
  •  ex) 내가 자주 쓰는 list 정렬 방법
Collections.sort(list, (s1, s2) -> s2.compareTo(s1));

 

람다식의 타입과 형변환

  • 함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다.
  • 람다식은 익명 객체이고 익명 객체는 타입이 없다.
  • 따라서 대입 연산자의 양변의 타입을 일치시키기 위해 형 변환이 필요하다.
  • ex) 
MyFunction f = (MyFunction) (() -> {});

 

 

메서드 참조

  • 람다식이 하나의 메서드만 호출하는 경우에는 메서드참조로 람다식을 간략히 할 수 있다.
  • ex)
//원래 메서드
Integer wrapper(String s) {
	return Integer.parseInt(s);
}

// 람다식
Function<String, Integer> f = (String s) -> Integer.parseInt(s);

//메서드 참조
Function<String, Integer> f = Integer::parseInt;

★ 하나의 메서드만 호출하는 람다식은 '클래스이름::메서드이름' 또는 '참조변수::메서드이름'으로 바꿀 수 있다.

 

 

2. 스트림(stream)

스트림이란?

  • 데이터소스마다 다른 방식으로 다뤄야한다.
  • 자바에서 데이터를 다룰 때, 컬렉션이나 배열에 데이터를 담고 for문이나 Iterator 를 이용했었는데, 이러한 방식은 너무 길고 재사용성이 떨어진다.
  • 이러한 문제점들을 해결하기 위해 Stream 을 사용한다.
  • 스트림은 데이터소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해두었다.
    • 데이터소스를 추상화했다는 것은, 데이터 소스가 무엇이든간에 같은 방식으로 다룰 수 있게 되었다는 것과 코드의 재사용성이 높아진다는 것을 의미한다.

 

예시를 살펴보자

String[] strArr = {"aaa", "ddd", "ccc"};
List<String> strList = Arrays.asList(strArr);

위의 두 배열과 List가 있을 때, 두 데이터소스를 기반으로하는 스트림을 생성하고, 정렬후 화면에 출력하는 코드는 다음과 같다.

 

(스트림을 쓰지 않은 경우)

Arrays.sort(strArr);
Collections.sort(strList);

for(String str : strArr)
	System.out.println(str);
   
for(String str : strList)
	System.out.println(str);

 

스트림 사용한 경우

Stream<String> strStream1 = strList.stream();
Stream<String> strStream2 = Arrays.stream(strArr);

strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);

훨씬 간결해졌다!

 

  • 스트림은 데이터소스를 변경하지 않는다.
    • 데이터소스로부터 데이터를 읽기만할 뿐, 데이터소스를 변경하지 않는다.
  • 스트림은 일회용이다.
  • 스트림은 작업을 내부 반복으로 처리한다.
    • 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미한다. 
    • forEach()는 스트림에 정의된 메서드 중 하나로 매개변수에 대입된 람다식을 데이터소스의 모든 요소에 적용한다.

 

중간연산 : 연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산할 수 있음

ex) distinct, filter, limit, skip, peek, sorted, map, flatMap ...

 

최종 연산 : 연산결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능

ex) forEach, count, max, min, findAny, findFirst, allMatch, anyMatch, toArray, reduce, collect

 

 

  • 지연된 연산
    • 최종연산이 수행되기 전까지는 중간 연산이 수행되지 않는다.
    • 즉, 중간 연산을 호출하는 것은 최종 연산이다
  • Stream<Integer> 보다는 IntStream 이 효율적
  • 병렬 스트림
    • parallel() 메서드 호출

 

스트림 만들기

컬렉션

  • Collection에 stream() 이 정의되어 있다.
  • 따라서 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림을 생성할 수 있다.
  • stream()은 해당 컬렉션을 소스로 하는 스트림을 반환한다.
Stream<T> Collection.stream()

 

배열

  • Stream<T> Stream.of(T... values)
  • Stream<T> Stream.of(T[])
  • Stream<T> Arrays.stream(T[])
  • Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)
    • 예시
    • Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"});

 

특정 범위의 정수

  • IntStream과 LongStream은 다음과 같이 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()와 rangeClosed()를 가지고 있다.
  • IntStream.range(int begin, int end) : end 가 범위에 포함되지 않음
  • IntStream.rangeClosed(int begin, int end) : end 가 범위에 포함됨

 

 

임의의 수

  • 무한 스트림 -> limit() 도 같이 사용해 스트림의 크기를 제한해주어야함
    • ints()
    • longs()
    • doubles
  • 유한 스트림
    • ints(long streamSize)
    • longs(long streamSize)
    • doubles(long streamSize)

 

람다식

  • iterate()
  • generate()
  • 위 두가지로 생성된 스트림은 기본형 스트림타이의 참조변수로 다룰 수 없다. 따라서 아래와 같이 써야함
Intstream evenStream = Stream.iterate(0, n->n+2).mapToInt(Integer::valueOf);
Stream<Integer> stream = evenStream.boxed();

 

 

스트리의 중간 연산

스트림 자르기

  • skip() : 처음 요소들 건너뛰기
  • limit() : 스트림 요소 개수 제한

 

스트림의 요소 걸러내기

  • filter() : 주어진 조건에 맞지 않는 요소 걸러냄
  • distinct() : 중복된 요소 제거

 

정렬

  • sorted() : 지정된 Comparator로 스트림을 정렬
    • Comparator를 지정하지 않으면 스트림 요소의 기본 정렬 기준(Comparable)으로 정렬

예시 : 학생스트림을 반(ban) 별, 성적(totalScore)순, 그리고 이름(name)순으로 정렬하여 출력하려면 다음과 같이 한다.

sutdentStream.sorted(Comparator.comparing(Student::getBan)
							.thenComparing(Student::getTotalScore)
                            .thenComparing(Student::getName)
                            .forEach(System.out::println);

 

 

변환 - map()

  • 스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환해야할 때
  • 매개변수로 T타입을 R타입으로 변환해서 반환하는 함수 지정
Stream<R> map(Function<? super T,? extends R> mapper)

 

 

조회 - peek()

  • forEach()와 달리 스트림의 요소를 소모하지 않으므로 연산 사이에 여러번 끼워넣어도 문제가 되지 않는다.
  • filter()나 map()의 결과를 확인할 때 유용하게 사용될 수 있다.

 

mapToInt(), mapToLong(), mapToDouble()

  • Stream<T> 타입보다 효율적
  • intStream은 count()만 지원하는 Stream<T> 와 달리 다른 메서드들 제공
    • sum()
    • average()
    • max()
    • min()

 

flatMap()

  • Stream<T[]> 를 Stream<T> 로 변환

 

 

스트림의 최종 연산

최종연산은 스트림의 요소를 소모해서 결과를 만들어낸다.

따라서 최종연산 후에는 스트림이 닫히게 되고, 더 이상 사용할 수 없다.

최종연산의 결과는 스트림 요소의 합과 같은 단일 값이거나, 스트림의 요소가 담긴 배열 또는 컬렉션일 수 있다.

 

  • forEach()
  • 조건 검사 - allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()
  • 통계 - count(), sum(), average(), max(), min()
  • 리듀싱 - reduce()
    • 처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다.

 

 

collect()

컬렉터는 Collector 인터페이스를 구현한 것으로, 직접 구현할 수도 있고 미리 작성된 것을 사용할 수도 있다.

 

collect() : 스트림의 최종 연산, 매개변수로 컬렉터를 필요로 한다.

Collector : 인터페이스, 컬렉터는 이 인터페이스를 구현해야 한다.

Collectors : 클래스, static 메서드로 미리 작성된 컬렉터를 제공한다.

 

 

스트림을 컬렉션과 배열로 변환

  • toList()
  • toSet()
  • toMap()
  • toCollection()
  • toArray()
List<String> names = stuStream.map(Student::getName)
					.collect(Collectors.toList());

ArrayList<String> list = names.stream()
					.collect(Collectors.toCollection(ArrayList::new));
                    
                    
Map<String, Person> map = personStream
					.collect(Collectors.toMap(p->p.getRegId(), p->p));

 

 

문자열 결합 - joining()

  • 문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환

 

 

그룹화와 분할 - groupingBy(), partitioningBy()

  • 추가로 공부하기..

 

 

Collector 구현하기

컬렉터를 작성한다는 것은 Collector 인터페이스를 구현한다는 것을 의미한다.

public interface Collector<T, A, R> {
	Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    BinaryOperator<A> combiner();
    Function<A, R> finisher();
    
    Set<Characteristics> characteristics();
}
  • supplier() : 작업 결과를 저장할 공간을 제공
  • accumulator() : 스트림의 요소를 수집할 방법을 제공
  • combiner() : 두 저장공간을 병합할 방법을 제공 (병렬 스트림)
  • finisher() : 결과를 최종적으로 변환할 방법을 제공

 

스트림의 변환

 

 

 

햐,,, 너무 어렵다 자바...

지금까지 자바는 구구절절 언어라고 생각했는데 그냥 내가 이런것들을 사용할 줄 몰랐던것 뿐..

적절하게 사용하려면 많이 사용해보고 기억해야할 것 같다!!

언제쯤 자바의정석을 읽으면서 다 이해할 수 있을까 ㅠㅡㅠ

 

 

이번 챕터에서 더 공부해야 할 것

제네릭스 개념

java.util.function 패키지

predicate

groupingBy(), partitioningBy()

Collectors 인터페이스 구현