자바8(Lambda, Stream)

자바8(Lambda, Stream)

  • Lambda 표현식

    • 자바스크립트에서의 arrow function의 도입

    • 메서드로 전달할 수 있는 익명 함수를 단순화한 코드의 블록이다

      • 특정 클래스에 종속되지 않기 때문에 함수라고 한다

      • 전달 인자로 보내거나 변수에 저장하는 것이 가능하다

    • JVM이 파라미터에 입력된 익명 함수를 함수형 인터페이스의 익명 구현 클래스를 생성하고 객체화(인스턴스)하여 파라미터로 전달한다

      • 람다 표현식(익명 함수) -> 함수형 인터페이스 -> 익명 구현 클래스(인스턴스)

      • 일반적인 인스턴스 생성인 힙 메모리에 올라가는 게 아니라 스택 메모리에 생성되고 사라진다

    • 람다 표현식은 인터페이스로 대입 가능하며 이 인터페이스를 함수형 인터페이스라고 한다

      • 하나의 추상 메서드를 갖는 인터페이스는 모두 함수형 인터페이스가 될 수 있다

      • 다수의 디폴트 메서드를 갖는 인터페이스라도 추상 메서드가 하나라면 함수형 인터페이스이다

      • 함수형 인터페이스를 정의할 때 @FunctionalInterface를 이용해 컴파일 검사를 진행할 수 있다

      • 함수형 인터페이스의 추상메서드 시그니처를 함수 디스크립터라고 한다

    • 람다 표현식의 사용은 메서드 내부에서 주로 이루어지기 때문에 지역변수 사용시에 제약이 존재한다

      • 메서드 내부에서의 람다 표현식은 곧 익명 객체를 메서드 내부에서 생성하는 것과 같다

        public class MainActivity23 {
        
          public static void main(String[] args) {
              // TODO Auto-generated method stub
              System.out.println("[람다식 rambda 사용해 일회용 스레드 정의 및 for문 변수값 순차적 출력 실시]");
        
              /*[설 명]
               * 1. 람다식이란 간단히 말해 메소드를 하나의 식으로 표현한 것입니다
               * 2. 자바에서는 화살표(->) 기호를 사용하여 람다 표현식을 작성할 수 있습니다
               * 3. for문을 수행하면서 순차적으로 변수값을 출력합니다 
               * */            
        
              //스레드 람다식 정의 실시
              new Thread(() -> {
                  for (int i = 1; i <= 5; i++) {
                      System.out.println("값 : "+i);            
                  }
              }).start();
        
          }//메인 종료
        
        }//클래스 종료
        
      • 새로운 스레드에서 실행되기 때문에 thread safe하기 위해 final 변수만 외부 참조가 가능하다

      • 람다 표현식에서 해당 메서드의 지역 변수, 매개 변수를 참조하는 것을 람다 캡쳐링(lambda capturing)이라 합니다

        int number = 10;
        Runnable runnable = () -> System.out.println(number); // (O)
        
        int number = 10;
        Runnable runnable = () -> System.out.println(number); // (X)
        int number = 20;
        
    • java.util.function 패키지를 통해 다양한 함수형 인터페이스를 제공하고 있다

    • 기존 클래스의 메서드를 람다 표현식을 통해 호출하는 것을 메서드 레퍼런스라고 한다

      • 메서드 레퍼런스는 특정 메서드만을 호출하는 람다 표현식의 축약형이다

      • 람다의 메서드 레퍼런스는 :: 구분자를 통해 활용한다

          Consumer<String> con = s -> System.out.println(s);
          con.accept(str);
        
          con = System.out::println; // 메서드 레퍼런스
          con.accept(str);
        
      • 정적 메서드 레퍼런스는 [클래스::메서드] 형태로 사용한다

          IntBinaryOperator operator;
        
          operator = MethodReferenceSample::staticAdd;
          System.out.println(operator.applyAsInt(10, 20));
        
      • 인스턴스 메서드 레퍼런스는 [생성객체::메서드] 형태로 사용한다

          IntBinaryOperator operator;
          MethodRdferenceSample mf = new MethodReferenceSample();
        
          operator = mf::instanceAdd;
          System.out.println(operator.applyAsInt(10, 20));
        
  • Stream

    • 선언형으로 컬렉션 데이터를 처리할 수 있는 API

    • 멀티 스레드 구현을 하지 않아도 병렬 데이터 처리가 가능하다

      • stream().parallel()
    • 스트림은 한번 사용하면 소모되기 때문에 다시 사용하기 위해서는 새로 만들어서 사용한다

    • 스트림 연산은 원본을 변경하지 않는다

        List<String> list = Arrays.asList("Lee", "Park", "Kim");
      
        //기존
        Iterator<String> it = list.iterator();
        while(it.hasNext()){
            System.out.println(it.next());
        }
      
        //Stream 활용
        list.stream().forEach(name -> System.out.println(name));
      
    • 컬렉션은 데이터에 대한 저장 및 접근 연산이 주가 되는 반면 스트림은 계산식이 주를 이룬다

      • 스트림 연산은 순차적 연산, 혹은 병렬 연산이 가능하다

      • 스트림 연산은 연산끼리 연결하여 파이프라인을 구성할 수 있도록 각 연산은 스트림은 반환한다

      • 스트림 반복의 경우 내부 반복을 지원한다

      • 컬렉션을 통한 요소의 반복은 사용자가 직접 요소에 대한 반복을 정의해야 하며 이를 외부 반복이라고 한다

        List<String> customerNames = customers.stream()
          .filter(customer -> customer.getAge() < 30)
          .sorted(Comparator.comparing(Customer::getAge))
          .map(Customer::getName)
          .collect(Collectors.toList());
        
    • 스트림은 Iterator와 마찬가지로 한번만 탐색이 가능하다

      • 한 번 탐색한 요소를 다시 탐색하기 위해서는 초기 데이터 소스에서 새로운 스트림을 만들어야 한다

      • I/O를 통해 데이터 소스일 경우에는 스트림 생성을 다시 해야 한다

          List<String> list = Arrays.asList("Lee", "Park", "Kim");
        
          Stream<String> stream = list.stream();
          stream.forEach(System.out::println);
          stream.forEach(System.out::println); // (X)
        
    • 스트림의 연산은 중간 연산과 최종 연산이 있다

      • 중간 연산은 filter, map과 같은 연산으로 Stream을 반환한다

      • 최종 연산은 forEach, collect와 같은 연산으로 void를 반환하거나 컬렉션 타입을 반환한다

      • 스트림 요소를 특정 함수로 처리하여 결과 값으로 추출(요소의 누적 연산)하는 것을 리듀스(reduce) 연산이라고 한다

          //1.Stream.reduce()
          Stream<Integer> numbers1 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
          Optional<Integer> sum = numbers.reduce((x, y) -> x + y);
        
          //2.메서드 레퍼런스
          Stream<Integer> numbers2 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
          Optional<Integer> sum = numbers.reduce(Integer::sum);
        
          //3.초기값이 있는 Stream.reduce()
          Stream<Integer> numbers3 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
          Integer sum = numbers.reduce(0, (total, n) -> total + n); // 스트림의 초기값이 0 (0 + 1 + 2 + ...)
        
          //4.reduce()의 병렬 처리
          Stream<Integer> numbers4 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
          Integer sum = numbers.parallel().reduce(0, (total, n) -> total + n);
        
      • 병렬 처리의 주의점

          Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
          Integer sum = numbers.parallel().reduce(0, (total, n) -> total - n); // -55가 아니라 -5가 리턴
        
        • 빼기 연산의 경우 병렬처리는 순차적인 처리(병렬이 아닌)와 결과가 다릅니다. 아래 코드를 실행해보면 -55가 아니라 -5가 리턴됩니다. 결과가 다른 이유는 (1 - 2) - (3 - 4) - ... - (9 - 10) 처럼 연산이 수행되면서 순차적으로 연산하는 것과 결과가 달라지기 때문입니다.
      • 병럴 처리에서 순차적 처리 규칙 추가

          Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
          Integer sum = numbers.reduce(0,
                  (total, n) -> total - n,
                  (total1, total2) -> total1 + total2);
          System.out.println("sum: " + sum);
        
        • 병렬처리에서 연산 순서에 따라 발생하는 문제를 해결하기 위해, 아래 예제와 같이 다른 규칙을 추가할 수 있습니다.

        • 위의 예제와 비교해보면 (total1, total2) -> total1 + total2가 추가되었는데, 병렬로 처리된 결과들의 관계를 나타냅니다. 다시 설명하면, 첫번째 연산과 두번째 연산은 합해야 한다는 규칙을 추가한 것인데요. 이렇게 규칙을 추가하면, 첫번째 연산의 결과가 다음 연산에 영향을 주기 때문에 reduce()는 작업을 나눠서 처리할 수 없게 됩니다.

  • 참고 자료