Programming/Java \ Spring

🔥자바스터디🔥 자바의 정석 CH14 람다와 스트림

1. 람다식

1.1 람다식이란?

 

람다식은 메서드를 하나의 식(expression)으로 표현한 것.

- 객체 지향 언어보다는 함수 지향 언어에 가깝다.
- 함수를 간략하면서도 명확한 식으로 표현할 수 있도록 해준다.
- 메서드를 람다식으로 표현하면 메서드의 이름 및 반환 값이 없어지므로 익명 함수 라고도 한다.
- 람다식의 형태는 매개 변수를 가진 코드 블록이지만 런타임 시에는 익명 구현 객체를 생성한다.

1.2 람다식 작성하기

(타입 매개변수) -> { 실행문; ... }

ex)
- ExFunctionInterfaceTest.java

interface ExFunctionInterface
{
    public void method();
}


public class ExFunctionInterfaceTest
{
    public static void main(String[] args)
    {
        ExFunctionInterface test = new ExFunctionInterface() {

            public void method() {
                System.out.println("test");
            }
        };

        test.method();
    }
}

public class ExFunctionInterfaceTest
{
    public static void main(String[] args)
    {
        ExFunctionInterface test = () -> System.out.println("test");

        test.method();
    }
}

반환 값이 있는 메서드의 경우 return 대신 expression 으로 대신할 수 있다. (expression인 경우 ; 를 붙이지 않는다.)

람다식에 선언된 매개변수 타입은 추론이 가능한 경우 생략 가능(대부분 생략가능)

매개 변수가 하나인 경우 ()를 생략할 수 있다.

{} 안 문장이 하나인 경우 생략할 수 있다.

 

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

 

함수형 인터페이스는 람다식을 다루기 위한 인터페이스로 하나의 추상 메서드만 정의되어 있어야 한다.

단, static 메서드와 default 메서드의 개수에는 제약이 없다.

 

- 함수형 인터페이스 타입의 매개변수 및 반환 타입이 함수형 인터페이스 타입이라면

  람다식을 참조하는 참조변수를 매개변수로 지정하고 람다식을 가리키는 참조변수를 반환하거나 또는 람다식 자체를 반환할 수 있다.

- 람다식은 Object 타입으로 형변환 할 수 없으며, 오직 함수형 인터페이스로만 형변환이 가능하다.

- 람다식 내에서 참조하는 지역변수는 final이 붙어 있지 않아도 상수로 간주되며,

  외부 지역변수와 같은 이름의 매개변수를 허용하지 않는다.

- 함수형 인터페이스는 @FuntionalInterface 라는 어노테이션을 붙일 수 있다.

  (컴파일러에서 추상메서드를 갖춘 인터페이스인지 검사,

   javadoc 페이지에서 해당 인터페이스가 함수형 인터페이스임을 알 수 있도록 한다)

 

1.4 java.util.function패키지 

 

      - 메서드 이름 통일

      - 재사용성

      - 유지보수

 

1.5 Function의 합성과 Predicate의 결합

 

- Function의 합성 andThen :

           a.andThen(b) : a함수 적용 후 b함수 적용

           compose : a.compose(b) : b함수 적용 후 a함수 적용

           identity : 항등 함수 (잘 사용되지 않는 편이나 map()으로 변환 작업할 때 변환없이 그대로 처리하고자할 때 사용)

 

- Predicate의 결합

           and() : and 조건

           or() : or 조건

           negate() : not

           isEqual() : 두 대상 비교

 

1.6 메서드 참조

 

메서드를 참조해서 매개변수의 정보 및 리턴 타입을 알아내어 람다식에서 불필요한 매개 변수를 제거하는 것이 목적.

람다식의 매개 변수는 메서드의 매개값을 전달하는 역할만 하기 때문에 메서드 참조를 이용하면 깔끔하게 처리할 수 있다.

(a, b) -> Math.max(a, b);

Math::max

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

 

생성자의 메서드 참조

Supplier<MyClass> s = () -> new MyClass();
Supplier<MyClass> s = MyClass::new;

Function<Integer, MyClass> f = (i) -> new MyClass(i);
Function<Integer, MyClass> f2 = MyClass::new;

Function<Integer, int[]> f = x -> new int[x];
Function<Integer, int[]> f2 = int[]::new;

 

2. 스트림(stream)

2.1 스트림이란?

 

    다양한 데이터 소스를 표준화된 방법으로 다루기 위한 라이브러리이다.

    스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해놓았다.

    스트림을 이용하면, 배열이나 컬렉션 뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.

/*
문자열 배열 'strArr'과 같은 내용의 문자열을 저장하는 리스트 'strList'가 있을 때, 
각각의 데이터를 정렬하고 출력하는 방법
*/

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

/* 방법1. 예전의 방법 */

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

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

for(String str : strList)
  System.out.println(str);
  
/* 방법2. 스트림을 활용한 방법 */

Stream<String> strStreamArr = Arrays.stream(strArr);
Stream<String> strStreamList = strList.stream();

strStremaArr.sorted().forEach(System.out::println);
strStreamList.sorted().forEach(System.out::println);

 

스트림의 연산

 

      1. 중간연산 : 연산결과가 스트림. 연속해서 수행 가능.

      2. 최종연산 : 연산결과가 스트림이 아님. 스트림의 요소를 소모하기 때문에 단 한번만 가능

 

중간연산

     

      모든 중간연산의 결과는 스트림이지만, 연산 전의 스트림과 같은 것은 아니다.

String[] strArr = {"dd", "aaa", "cc", "cc", "e"}

Stream<String> Stream = Stream.of(strArr);
Stream<String> distenctedStream = stream.distinct();

최종연산

 

 

 

reduce() : 스트림의 요소를 하나씩 줄여가면서(reducing) 계산한다.

collect() : 스트림의 요소를 수집한다. 주로 요소를 그룹화하거나 결과를 컬렉션에 담아 반환하는데 사용된다.

List<String> sortedList = strStreamList.sorted().collect(Collectors.toList());

 

스트림의 특징

 

1. 스트림은 데이터 소스를 변경하지 않는다.

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

   정렬된 결과가 필요할 경우, collect를 활용해서 컬렉션이나 배열에 담아 return할 수 있다.

 

2. 스트림은 일회용이다.

   스트림은 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하다면 스트림을 다시 생성해야 한다.

 

3. 스트림은 작업을 내부 반복으로 처리한다.

  내부 반복이란, 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미한다. 

  forEach() 는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터소스의 모든 요소에 적용한다.

  즉, forEach()는 메서드 안에 for문을 넣어버린 것이다.

 

4. 지연된 연산

  스트림 연산에서는 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다. 

  스트림에 대해 sort()나 distinct()같은 중간 연산을 호출해도 즉각적으로 수행되지 않는다는 것이다.

  중간연산을 호출하는 것은 단지 어떤 작업이 수행되어야하는지를 지정해주는 것일 뿐이다.

  최종연산이 수행되어서야 스트림의 요소들이 중간연산을 거치고 최종연산에 소모된다.

 

5. 기본형 스트림

  오토박싱, 언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 

  IntStream, LongStream, DoubleStream 이 제공된다.

  일반적으로 Stream< Integer > 대신 IntStream을 사용하는 것이 더 효율적이고,

  IntStream에는 int타입으로 작업하는데 유용한 메서드들이 포함되어있다.

 

6. 병렬스트림

  앞서 13장에서 fork&join framework으로 병렬처리하는 것에 대해 배웠는데,

  스트림은 내부적으로 이 framework를 이용해서 연산을 자동적으로 병렬로 수행한다.

  parallel() 메서드를 호출하면 병렬로 연산이 수행되고, sequential() 메서드를 호출하면 병렬로 처리되지 않게 된다.

  모든 스트림은 기본적으로 병렬 스트림이 아니기 때문에 sequential() 메서드는 parallel()를 취소할 때만 사용한다.

 

2.2 스트림 만들기

 

/* 컬렉션 */

Stream <T> Collection.stream()

List<Integer> list = Arrays.asList(1,2,3,4,5);
Stream<Integer> intStream = list.stream();

/* 배열 */

String[] strArr = {"aaa", "ddd", "ccc"};

Stream<String> strStream = Stream.of(strArr); 
Stream<String> strStream = Arrays.stream(strArr);

/* 특정범위의 정수 */

IntStream intStream = IntStream.range(1, 5); // 1,2,3,4 
IntStream intStream = IntStream.rangeClosed(1, 5); // 1,2,3,4,5

/* 난수 */

IntStream ints()
LongStream longs()
DoubleStream doubles()

IntStream intStream = new Random().ints(); // 무한 스트림
intStream.limit(5).forEach(System.out::println); // 5개의 요소만 출력

IntStream intStream = new Random().ints(5); // 크기가 5인 난수 스트림을 반환

 

람다식

 

Stream 클래스의 iterate(), generate()는 람다식을 매개변수로 받아서,

이 람다식에 의해 계산되는 결과값들을 요소로 하는 무한 스트림을 생성한다.

 

iterate() : 이전 결과에 대해 종속적

generate() : 이전 결과에 대해 독립적

 

iterate()와 generate()에 의해 생성된 스트림은 아래와 같이 기본형 스트림 타입의 참조변수로 다룰 수 없다.

IntStream evenStream = Stream.iterate(0, n->n+2); //error
DoubleStream randomStream = Stream.generate(Math::random); //error

/* Stream, IntStream 변환하면 사용가능 */
IntStream evenStream = Stream.iterate(0, n->n+2).mapToInt(Integer::valueOf);
Stream<Integer> stream = evenStream.boxed();

 

/* 파일 */

Stream<Path> Files.list(Path dir) // Path는 파일 또는 디렉토리

/* 빈 스트림 */

//요소가 하나도 없는 비어있는 스트림을 생성할 수도 있다.
//스트림에 연산을 수행한 결과가 하나도 없을 때 null보다는 빈 스트림을 return하는 것이 낫다.

Stream emptyStream = Stream.empty();


/* 두 스트림 연결 */

String[] str1 = {"123", "456" };
String[] str2 = {"aaa", "bbb", "cc"};

Stream<String> strs1 = Stream.of(str1);
Stream<String> strs2 = Stream.of(str2);
Stream<String> strs3 = Stream.concat(strs1, strs2);

 

2.3 스트림의 중간연산

 

중간연산

 

sorted() 는 지정된 Comparator로 스트림을 정렬하는데, int 값을 반환하는 람다식을 사용하는 것도 가능하다.

 

변환 : map()

스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑다내거나 특정 형태로 변환해야될 때 사용한다.

 

mapToInt(), mapToLong(), mapToDouble()

 

스트림의 요소를 숫자로 반환하는 경우, IntStream같은 기본형 스트림으로 변환하는 것이 더 유용할 수 있다.

기본형 스트림은 숫자를 다루는 데 편리한 메서드들을 제공하기 때문이다.

IntStream studentScoreStream = studentStream.mapToInt(Student::getTotalScore);
int allTotalScore = studentScoreStream.sum(); // IntStream의 sum()

추가적으로 이 메서드들은 최종연산이기 때문에 호출 후 스트림이 닫힌다. 따라서 sum(), average() 를 연속해서 호출할 수 없다.

이러한 경우에 스트림을 또 생성해야하므로 불편하다. 그래서 summarystatics()라는 메서드가 따로 제공된다.

IntSummaryStatistics stat = scoreStream.summaryStatistics();

long totalCount = stat.getCount();
long totalScore = stat.getSum();
double avgScore = stat.getAverage();

mapToObj(), boxed()

 

반대로 IntStream을 Stream < T >로 변환할 때는 mapToObj()를, Stream로 변환할 때는 boxed()를 사용한다.

 

flatMap()

 

스트림의 요소가 배열이거나 map의 연산결과가 배열인경우, Stream < T [] >를 Stream < T >로 다루는 것이 더 편리할 때 flatMap()을 사용한다.

 

faltMap()은 map()과 달리 스트림의 스트림이 아닌 스트림으로 만들어준다.

 

2.4 Optional< T >와 OptionalInt

 

Optional은 지네릭클래스로, T타입의 객체를 감싸는 래퍼클래스이다.

Optional 타입의 객체에는 모든 타입의 참조변수를 담을 수 있다.

최종 연산의 결과를 그냥 반환하는게 아니라 Optional객체에 담아서 반환을 하면,

반환된 결과가 null인지 매번 if문으로 체크하는 대신 Optional에 정의된 메서드를 통해 간단히 처리할 수 있다.

 

Optional 객체생성

 

참조변수의 값이 null일 가능성이 있으면 of()대신 ofNullable()을 사용해야 한다.

of()는 매개변수의 값이 null이면 NullPointerException을 발생시키기 때문이다.

 

// Optional < T > 참조변수의 초기화

Optional <String> optVal = null;                     //null로 초기화
Optional <String> optVal = Optional.<String>empty(); //빈 객체로 초기화

// Optional 객체의 값 가져오기

Optional<String> optVal = Optional.of("abc");

String str1 = optVal.get(); 
// optVal에 저장된 값을 반환. null이면 예외발생 

String str2 = optVal.orElse(""); 
// optVal에 저장된 값이 null일 때는, ""를 반환 

String str3 = optVal.orElseGet(String::new); 
// 람다식 사용가능 () -> new String()

String str4 = optVal.orElseThrow(NullPointerException::new); 
// 널이면 예외발생

// isPresent()

if(Optional.ofNullable(str).isPresent()) { 
  System.out.println(str); //Optional 객체의 값이 null이면 false, 아니면 true를 반환한다.
}

// ifPresent()
Optional.ofNullable(str).ifPresent(System.out::println);

// OptionalInt, OptionalLong, OptionalDouble

public final class OptionalInt {
  ...
  private final boolean isPresent; // 값이 저장되어 있으면 true
  private final int value; // int타입의 변수
}

 

2.5 스트림의 최종연산

최종연산

 

reduce()

     스트림의 요소를 하나씩 줄여가면서(reducing) 계산하고 최종결과를 반환한다.

     처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다.

     이 과정에서 스트림의 요소를 하나씩 소모하게 되며, 스트림의 모든 요소를 소모하게 되면 그 결과를 반환한다.

 

** 최종연산 count(), sum() 등은 내부적으로 모두 reduce를 이용해서 작성된 것이다.

 

2.6 collect()

collect()

    스트림의 요소를 수집한다.

    주로 요소를 그룹화하거나 결과를 컬렉션에 담아 반환하는데 사용된다.

 

Collector

    collect에 필요한 메서드를 정의해놓은 인터페이스이다

public interface Collector <T, A, R> {     
//T(요소)를 A에 누적한 다음 결과를 R로 변환해서 return

  Supplier < A >   supplier();   
  //StringBuilder::new   
  //결과를 저장할 공간(A)을 제공

  BiConsumer < A, T > accumulator();   
  //(sb, s) -> sb.append(s) 
  //스트림의 요소를 수집(collect)할 방법을 제공

  BinaryOperator < A > combiner();   
  //(sb1, sb2) -> sb1.append(sb2) 
  //두 저장공간(A)을 결합할 방법을 제공(병렬 스트림)

  Function < A, R > finisher();   
  //sb -> sb.toString()  
  //최종변환
  //변환할 필요가 없는 경우, x->x

  Set<Characteristics> characteristics;  
  //컬렉터의 특성이 담긴 set return
}

 

Collectors

    다양한 기능의 메소드를 제공한다.

 

   - 변환 : mapping(), toList(), toSet(), toMap(), toCollection(), ...

   - 통계 : counting(), summingInt(), averagingInt(), maxBy(), minBy(), summarizingInt(), ...

   - 문자열 결합 : joining()

   - 리듀싱 : reducing()

   - 그룹화와 분할 : partitioningBy() (스트림요소 2분할), groupingBy()(스트림요소 그룹화), collectingAndThen()

 

2.7 Collector구현하기

    Collector인터페이스를 구현하는 클래스를 작성한다.

public interface Collector < T, A, B > {
  //T(요소)를 A에 누적한 다음, 결과를 R로 변환해서 return

  Supplier < A >   supplier();   
  //StringBuilder::new   
  //결과를 저장할 공간(A)을 제공

  BiConsumer < A, T > accumulator();   
  //(sb, s) -> sb.append(s) 
  //스트림의 요소를 수집(collect)할 방법을 제공

  BinaryOperator < A > combiner();   
  //(sb1, sb2) -> sb1.append(sb2) 
  //두 저장공간(A)을 결합할 방법을 제공(병렬 스트림)

  Function < A, R > finisher();   
  //sb -> sb.toString()  
  //최종변환
  //변환할 필요가 없는 경우, x->x

  Set<Characteristics> characteristics;  
  //컬렉터의 특성이 담긴 Set return
  }

characteristics()

   컬렉터가 수행할 작업의 속성정보를 제공한다.

   지정할 특성이 없으면 빈 set을 return한다.

 

Example. ConcatCollector

    문자열 스트림의 모든 요소를 연결하는 컬렉터이다.

 

2.8 스트림의 변환