Java

Java 8 함수형 인터페이스, 람다, 스트림

rohyun 2021. 11. 4. 16:06

Java 8


함수형 인터페이스

함수형 인터페이스 구현

@Functionalnterface
public interface RunSomething {

    void doIt(); // 추상 메소드가 하나만 있다면 함수형 인터페이스이다. 앞에 abstract 생략

    // void doItAgain(); // 추상 메소드가 하나를 초과하면 함수형 인터페이스가 아니다.

}

public class Foo {

    public static void main(String[] args) {
        // Java 8 이전 방식
        // 익명 클래스
        RunSomething runSomething = new RunSomething() {
            @Override
            public void doIt() {
                System.out.println("dohyun");
            }
        };

        // Java 8 람다 표현식
        RunSomething runSomething = () -> System.out.println("dohyun");
    }
}

자바에서 제공하는 함수형 인터페이스

Function<T, R>

// Function<> 구현방법 1.
public class Plus10 implements Function<Integer, Integer> {
    @Override
    public Integer apply(Integer Integer) {
        return integer + 10;
    }
}

// Function<> 구현방법 2.
public class Foo {

    public static void main(String[] args) {
        // apply
        Function<Integer, Integer> plus10 = (i) -> i + 10;
        plus10.appy(1);

        // compose
        Function<Integer, Integer> multi2 = (i) -> i * 2;
        plus10.compose(multi2).apply(2); // 먼저 2를 곱하고 10을 더한다.  

        //andThen
        plus10.andThen(multi2).apply(2); // 먼저 10을 더하고 2를 곱한다.
    }
}

BiFunction<T, U, R>

  • 두 개의 값(T, U)를 받아서 R 타입을 리턴하는 함수 인터페이스 : R apply(T t, U u)

Consumer

  • T 타입을 받아서 아무값도 리턴하지 않는 함수 인터페이스 : void Accept(T t)
  • 함수 조합용 메소드 : andThen

Supplier

public class Foo {

    // get
    public static void main(String[] args) {
        Supplier<Integer> get10 = () -> 10;
        get10.get();
    }
}

Predicate

public class Foo {

    // test
    public static void main(String[] args) {
        Predicate<String> str = (s) -> s.startsWith("dohyun");
        str.test("dohyun world") // return true
    }
}

람다 표현식

public class Foo {

    public static void main(String[] args) {
       Foo foo = new Foo();
       foo.run();
    }

    private void run() {
        int baseNumber = 10;

        //로컬 클래스
        class LocalClass {
            void printBaseNumber() {
                int baseNumber = 11; // 쉐도윙 !
                System.out.println(baseNumber); // 11
            }
        }

        //익명 클래스
        Consumer<integer> integerConsumer = new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                System.out.println(baseNumber);
            }
        };

        //람다
        IntConsumer printInt = (i) -> { // 만약 i -> baseNumber 라면 같은 스콥이기 때문에 컴파일 에러 ! 
            System.out.println( i + baseNumber);
        };


        /**
        셋의 공통점 : 로컬 변수를 참조할 수 있다. 
        로컬, 익명 클래스 vs 람다 : 로컬, 익명 클래스는 쉐도윙하고 ( run() 안에 새로운 스콥 생성 ), 람다는 쉐도잉하지 않는다 ( run() 과 같은 스콥 ). 
        **/
    }
}

메소드 레퍼런스

  • 스태틱 메소드 참조 : 타입::스태틱 메소드
  • 특정 객체의 인스턴스 : 메소드 참조 객체 레퍼런스::인스턴스 메소드
  • 임의 객체의 인스턴스 : 메소드 참조 타입::인스턴스 메소드
  • 생성자 참조 타입 : ::new

Java 8 API의 기본 메소드와 스태틱 메소드

public class App {
    public static void main(String[] args) {
        List<String> name = new ArrayList<>();
        name.add("dohyun");
        name.add("github");

        //forEach
        name.forEach( s -> {
            System.out.println(s);
        });

        //stream
        name.stream()
            .map(String::toUpperCase)
            .filter( s -> s.startsWith("K"))
            .count();

        //removeIf
        name.removeIf( s -> s.startsWith("K"));
    }
}

Iterable의 기본 메소드

  • forEach()
  • spliterator()

Collection의 기본 메소드

  • stream() / parallelStream()
  • removeIf(Predicate)
  • spliterator()

Comparator의 기본 메소드 및 스태틱 메소드

  • reversed()
  • thenComparing()
  • static reverseOrder() / naturalOrder()
  • static nullsFirst() / nullsLast()
  • static comparing()

Stream

  • 간단하게 말해, 연속된 데이터들을 처리하는 오퍼레이션들의 집합이다.
  • 비유하자면 컨베이어 벨트에 데이터를 하나씩 흘려보내면서 특정 오페레이션을 수행하는 것이다.
  • 스트림 자체가 데이터를 담고 있는 저장소 ( 컬렉션 ) 이 아니다.
  • 손쉽게 병렬 처리가 가능하다.
public class App {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        name.add("dohyun");
        name.add("github");

        Stream<String> stringStream = names.stream()
                                            .map(s -> s.toUpperCase());

        names.forEach(System.out::printLn); // 데이터가 그대로 소문자로 남아있다. 
    }
}

스트림 파이프라인

  • 0 또는 다수의 중개 오퍼레이션 (intermediate operation)과 한개의 종료 오퍼레이션 (terminal operation)으로 구성한다.
  • 스트림의 데이터 소스는 오직 터미널 오퍼네이션을 실행할 때에만 처리한다.

중개 오퍼레이션

  • Stream을 리턴한다.
  • Stateless / Stateful 오퍼레이션으로 더 상세하게 구분할 수도 있다. (대부분은 Stateless지만 distinct나 sorted 처럼 이전 이전 소스 데이터를 참조해야 하는 오퍼레이션은 Stateful 오퍼레이션이다.)
  • filter, map, limit, skip, sorted, ...

종료 오퍼레이션

  • Stream을 리턴하지 않는다.
  • collect, allMatch, count, forEach, min, max, ...

Stream API 예제

걸러내기 - Filter(Predicate)


List<OnlineClass> springClasses = new ArrayList<>();
List<OnlineClass> javaClasses = new ArrayList<>();

// spring 으로 시작하는 수업
springClasses .stream()
            .filter(oc -> oc.getTitle().startsWith("spring")) // OnlineClass 타입 그대로
            .forEach(oc -> System.out.println(oc.getId()));

// close 되지 않은 수업
springClasses.stream()
            .filter(oc -> !oc.isClosed())
            .forEach(oc -> System.out.println(oc.getId()));

// 수업 이름만 모아서 스트림 만들기
springClasses.stream()
            .map(ec -> oc.getTitle()) // String 타입으로 변경
            .forEach(oc -> System.out.println(oc.getId()));

List<List<OnlineClass>> dohyunEvents = new ArrayList<>();
dohyunEvents.add(springClasses);
dohyunEvents.add(javaClasses);

// 두 수업 목록에 들어있는 모든 수업 아이디 출력
dohyunEvents.stream()
            .flatMap(list -> list.stream())
            .forEach(oc -> System.out.println(oc.getId()));

// 10부터 1씩 증가하는 무제한 스트림 중에서 앞에 10개 빼고 최대 10개 까지만
Stream.iterate(10, i -> i + 1)
    .skip(10)
    .limit(10)
    .forEach(System.out::println);

// 자바 수업 중에 Test 가 들어있는 수업이 있는 지 확인
javaClasses.stream()
        .anyMatch(oc -> oc.getTitle().contains("Test"));

// 스프링 수업 중에 제목이 spring 이 들어간 것만 모아서 List 로 만들기
springClasses.stream()
            .filter(oc -> oc.getTitle().contains("spring"))
            ,map(oc -> oc.getTitle())
            .collect(Collectors.toList());

Optional

메소드에서 작업 중 특별한 상황에서 값을 제대로 리턴할 수 없는 경우 선택할 수 있는 방법

  • 예외를 던진다. (비싸다, 스택트레이스를 찍어두니까.)
  • null을 리턴한다. (비용 문제가 없지만 그 코드를 사용하는 클리어인트 코드가 주의해야 한다.)
  • (자바 8부터) Optional을 리턴한다. (클라이언트에 코드에게 명시적으로 빈 값일 수도 있다는 걸 알려주고, 빈 값인 경우에 대한 처리를 강제한다.)

Optional

  • 오직 값 한 개가 들어있을 수도 없을 수도 있는 컨네이너.
public Optional<Progress> getProgress() {
    return Optional.ofNullable(progress);
}

주의할 것

  • 리턴값으로만 쓰기를 권장한다. (메소드 매개변수 타입, 맵의 키 타입, 인스턴스 필드 타입으로 쓰지 말자.)
  • Optional을 리턴하는 메소드에서 null을 리턴하지 말자.
  • 프리미티브 타입용 Optional을 따로 있다. OptionalInt, OptionalLong,...
  • Collection, Map, Stream Array, Optional은 Opiontal로 감싸지 말 것.

Optional API 예제

public class App {
    public static void main(String[] args) {
        List<OnlineClass> springClasses = new ArrayList<>();

        Optional<OnlineClass> spring = springClasses.stream()
                                                    .filter(oc -> oc.getTitle().startsWith("spring"))
                                                    .findFirst();

        spring.isPresent(); // 있는 지 없는 지 
        spring.get(); // Optional 안의 값을 꺼낸다. 만약 Optional 안에 값이 없다면? NosuchElementException !

        spring.ifPresent(oc -> {
            System.out.println(oc.getTitle());
        });

        OnlineClass onlineClass = spring.orElse(createNewClasses()); // spring 이 있던 없던 createNewClasses() 를 실행한다.
        OnlineClass onlineClass = spring.orElseGet(() -> createNewClasses()); // spring 이 있는 경우 createNewClasses() 를 실행하지 않는다.
        OnlineClass onlineClass = spring.orElseThrow(() -> {
            return new IllegalStateException();
        });
        Optional<OnlineClass> onlineClass = spring.filter(oc -> oc.getId() > 10); // 빈 Optional 
        Optional<Integer> integer =  spring.map(oc -> oc.getId());

    }

    private static OnlineClass createNewClasses() {
        return new OnlineClass(10, "New class", false);
    }
}

REFERENCES

백기선 님 더 자바, Java 8