Justin의 개발 로그
article thumbnail

스트림은 왜 람다를 사용할 수 있나?

(스트림은 어떻게 람다 표현식을 지원하나?)

 

public interface Stream<T> extends BaseStream<T, Stream<T>> {

    Stream<T> filter(Predicate<? super T> predicate);
    
    IntStream mapToInt(ToIntFunction<? super T> mapper);
    
    Stream<T> sorted(Comparator<? super T> comparator);
    
    void forEach(Consumer<? super T> action);
    
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    ...

 

스트림 인터페이스에는 스트림에서 지원하는 여러 메서드가 추상 메서드로 선언되어 있다.

스트림을 구현하는 클래스는 이러한 스트림 메서드가 구현되어 있다는 것을 유추할 수 있다.

 

누가 이런 메서드를 구현하고 있는지 역으로 추적을 해 보면...

 

1. 스트림을 생성하는 Stream.of Static 메서드

2. of를 따라가 보면 Arrays.stream을 호출하고 있고

 

3. Arrays.stream은 다시 stream 메서드 -> StreamSupport.stream 으로 연결되어 있다.

4. StreamSupport.stream 메소드는 ReferencePipeLine.Head 메서드로 연결되고...

5. ReferencePipeLine의 헤더는 ReferencePipeLine 내부의 Static class 이고, 생성자를 호출하는 것을 알 수 있다.
즉, Steam.of 또는 Arrays.stream은 여러 중간 클래스를 거쳐서 결국 ReferencePipeLine 객체를 생성한다.

 

6. ReferencePipeLine 클래스

  • 정의를 보면 추상 클래스로 이를 상속받은 클래스가 바로 5번에서 언급한 static class인 Head 임을 알 수 있다.
  • Implements Stream<P_OUT>으로 Stream Interface에 선언된 메서드를 모두 구현했음을 알 수 있다.

ReferencePipeLine에서 제공되는 메서드 목록을 보면, Steam Interface에 정의된 익숙한 메서드 들이 나열되어 있다.

이렇게 해서 우리는 Arrays로 구성된 배열이 어떻게 스트림으로 만들어지고,
스트림 인터페이스에 정의된  메서드가 어떻게 호출이 될 수 있는지 Java의 Core 코드를 통해서 확인했다.
* 실제 세부 구현까지 모두 확인하는 것은 너무 방대한 작업이라 여기까지만...

 

 

스트림 메서드는 대부분 함수형 인터페이스를 인자로 받는다.

public interface Stream<T> extends BaseStream<T, Stream<T>> {

    Stream<T> filter(Predicate<? super T> predicate);
    
    IntStream mapToInt(ToIntFunction<? super T> mapper);
    
    Stream<T> sorted(Comparator<? super T> comparator);
    
    void forEach(Consumer<? super T> action);
    
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    ...

즉, 스트림 메서드를 이용한 방법 = 함수형 인터페이스를 구현한 메서드를 제공하는 것이며, 

다음 4가지 방식을 사용할 수 있다.

 

스트림에 함수형 인터페이스의 인자(파라미터)를 주입하는 방법

직접 익명 클래스를 만들어 인터페이스를 구현한 메서드를 제공한다.

    private <T> void printStream(Stream<T> t) {
        t.forEach(System.out::println);
    }

    @Test
    void inheritFilterTest() {

        Stream<Student> students = StreamTestGenerator.getPersonStream();

        Stream<Student> youngStudents = students.filter(new Predicate<Student>() {
            @Override
            public boolean test(Student student) {
                return student.getAge() < 20;
            }
        });

        printStream(youngStudents);
    }

 

익명 클래스를 생성하는 팩토리 메서드를 통해 제공한다. 1번의 확장 개념

    private static Predicate<Student> YoungFilter() {
        return new Predicate<Student>() {
            @Override
            public boolean test(Student t) {
                return t.getAge() < 20;
            }
        };
    }

    @Test
    void StaticMethodOverride() {
        Stream<Student> students = StreamTestGenerator.getPersonStream();
        Stream<Student> youngStudents = students.filter(YoungFilter());

        printStream(youngStudents);
    }

 

함수형 인터페이스를 구현한 클래스의 객체(인스턴스)로 넘긴다.

    class YoungFilterClass<T extends Student> implements Predicate<T> {

        @Override
        public boolean test(T t) {
            return t.getAge() < 20;
        }
    }

    @Test
    void implementsClassTest() {
        Stream<Student> students = StreamTestGenerator.getPersonStream();
        Stream<Student> youngStudents = students.filter(new YoungFilterClass<>());
        printStream(youngStudents);

        students = StreamTestGenerator.getPersonStream();
        YoungFilterClass<Student> yf = new YoungFilterClass<>();
        youngStudents = students.filter(yf);
        printStream(youngStudents);

    }

 

Lambda 표현식 또는 Lambda 표현식을 변수에 할당해서 넘긴다.

    @Test
    void lambdaTest() {
        Stream<Student> students = StreamTestGenerator.getPersonStream();
        Stream<Student> youngStudents = students.filter((t)-> t.getAge() < 20 );
        printStream(youngStudents);

        Predicate<Student> youngFilter = (t) -> t.getAge() < 20;
        students = StreamTestGenerator.getPersonStream();
        youngStudents = students.filter(youngFilter);
        printStream(youngStudents);

    }

 

반복을 줄이기 위해 만든 printStream도 Lambda로 변경한 코드는 아래와 같다. 

    @Test
    void allLambdaTest() {
        Stream<Student> students = StreamTestGenerator.getPersonStream();
        students.filter((t)-> t.getAge() < 20 )
                .forEach(System.out::println);

        Consumer printT = (t) -> System.out.println(t);
        students = StreamTestGenerator.getPersonStream();
        students.filter((t)-> t.getAge() < 20 )
                .forEach(printT);

    }

 

profile

Justin의 개발 로그

@라이프노트

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!