Post

[자바8] 자바8의 핵심 기능, 스트림 API와 method와 lambda를 일급값으로 사용함에 따른 변화에 유연한 동작 파라미터화

스트림 API

🐀 스트림은 데이터에 어떤 연산을 할 것인지 묘사하는 것에 중점을 둔다
📕 스트림이란
실제 입력이나 출력이 묘사된 데이터의 흐름을 말한다.
입력 스트림에서 데이터를 한개씩 읽어들이며,
출력 스트림에서 데이터를 한개씩 기록한다.
즉, OS에 의해 생성되는 '가상의 연결고리'*스트림 파이프라인을 이용하여 입력을 여러 CPU 코어에 쉽게 할당할 수 있다.

스트림 API를 이용하면 라이브러리 내부에서 모든 데이터가 처리되므로 루프를 신경쓸 필요가 없다.

🎯 조건에 따른 필터링
🎯 특정 필드를 추출
🎯 데이터 그룹화
🎯 쉬운 병렬화
⚠️ 컬렉션을 필터링할 수 있는 가장 빠른 방법은 컬렉션을 스트림으로 바꾸고 병렬로 처리한 다음 다시 컬렉션으로 복원하는 것이다.
(컬렉션은 데이터를 어떻게 저장하고 접근할 건지에 중점을 둔다.)

스트림 API를 사용하면 컬렉션을 처리하면서 발생하는 반복적인 코드 문제와 멀티 코어 활용의 어려움의 문제를 해결할 수 있다.

1
List<Apple> heavyApples = inventory.parallelStream().filter((Apple a) -> a.getWeight() > 150).collect(toList());

함수형 프로그래밍

🐀 거의 모든 것을 순수함수로 나누어 문제를 해결하는 기법
📕 순수함수란
다른 코드와 동시에 실행하더라도 부수효과(Side effect)가 없는 함수
⚠️ 상태없는 함수라고도 한다.

🚀 함수형 프로그래밍의 핵심적인 2가지 아이디어

  • methodlambda*일급값으로 사용
  • 가변 공유 상태가 없는 병렬 실행을 이용

스트림 API는 이 두가지 아이디어를 모두 활용한다.

변화하는 요구사항과 동작 파라미터화

🐁 원하는 동작을 캡슐화해서 호출할 메서드의 인수로 전달할 수 있는 것을 말한다

method에 파라미터를 추가하면, 변화하는 요구사항에 더 유연하게 대응하는 코드를 짤 수 있다.

만일, 특정 동작 이외에 나머지가 똑같은 코드가 반복적으로 존재한다면 특정 동작을 추상화하고 파라미터화한다.

📕 전략디자인 패턴
각 알고리즘(동작)을 캡슐화하는 알고리즘 패밀리를 정의해 둔 다음,
런타임에 알고리즘을 선택하는 기법을 말한다.
즉, 특정 조건에 따라 동일한 method가 다르게 동작하도록 설계한 패턴을 말한다.

즉, 동작 파라미터화라는 것은, method가 다양한 동작(전략)을 받아서 내부적으로 다양한 동작을 수행할 수 있게
추상화(인터페이스화)하여 파라미터화 하는 것을 말한다.
(※ 각각의 동작(전략)은 구현객체를 말한다.)

ApplePredicate.java
동작 추상화
※ 사실, java.util.function 패키지에 Preidcate라는 인터페이스가 구현되어 있다.
1
2
3
public interface ApplePredicate {
    boolean test(Apple apple);
}
AppleHeavyPredicate.java
각각의 동작(전략)
1
2
3
4
5
6
public class AppleHeavyPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}
1
2
3
4
5
6
7
8
9
10
11
public static List<Apple> filterHeavyApples(List<Apple> inventory, ApplePredicate p) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple : inventory) {
        if(p.test(apple)) {
            result.add(apple);
        }
    }
    return result;
}

filterHeavyApples(inventory, new AppleHeavyPredicate()); //⚠️

✨ 자원 처리 예제

📕 실행 어라운드 패턴
실제 자원을 처리하는 코드가 setupclean과정으로 둘러싸인 형태를 말한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@FunctionalInterface
public interface BufferedReaderProcessor {
    String process(BufferedReader br) throws IOException;
}

public String processFile(BufferedReaderProcessor p) throws IOException {
    try (BufferedReader br =  // try-with-resources 구문
                new BufferedReader(new FileReader("data.txt"))) {
        return p.process(br);
    }
}

// 한 행을 처리하는 코드
processFile((BufferedReader br) -> br.readLine());

// 두 행을 처리하는 코드
processFile((BufferedReader br) -> br.readLine() + br.readLine());

다음과 같이 동작을 추상화하여 파라미터화하면
탐색로직과 각 요소에 적용할 동작을 분리할 수 있다.

⚠️ 단, 각 동작마다 클래스를 구현해서 인스턴스화하는 과정이 거추장스럽다.

클래스의 선언과 인스턴스화를 동시에, 익명 클래스

🐙 익명 클래스

ApplePredicate 구현한 클래스를 정의하지 않고도 표현식을 메서드의 인자로 전달할 수 있다.

1
2
3
4
5
filterHeavyApples(inventory, new ApplePredicate() {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
});

더 간결하게, 람다 표현식

🌋 이름이 없는 함수면서, 메서드 인수로 전달할 수 있고, 익명함수를 단순화한 것을 말한다
1
2
// (람다 파라미터) -> 람다 바디(반환값에 해당하는 표현식)
filterHeavyApples(inventory, (Apple apple) -> apple.getWeight() > 150);

⚠️ 람다는 method처럼 특정 클래스에 종속되지 않음으로 함수라고 부른다.

함수형 인터페이스

🐙 함수형 인터페이스

오직 하나의 추상 method만 지정하는 인터페이스를 말한다.
자바8은 java.util.function패키지에 새로운 함수형 인터페이스를 제공한다.

java.util.function.Predicate.java
test 라는 추상 메서드를 정의하며,
제네릭 형식 T객체를 인수로 받아 booelan을 반환한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

// 예제
public <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> results = new ArrayList<>();
    for(T t : list) {
        if(p.test(t)) {
            results.add(t);
        }
    }
    return results;
}

Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
java.util.function.Consumer.java
accept라는 추상 메서드를 정의하며,
제네릭 형식 T객체를 인수로 받아 어떤 동작을 수행하고 싶을 때. void
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t)
}

// 예제
public <T> void forEach(List<T> list, Consumer<T> c) {
    for(T t : list) {
        c.accept(t);
    }
}

forEach(
    Arrays.asList(1, 2, 3, 4, 5),
    (Integer i) -> System.out.println(i) // Consumer의 accept method를 구현하는 람다
)
java.util.function.Function.java
apply 라는 추상 메서드를 정의하며,
제네릭 형식 T객체를 인수로 받아 R객체를 반환한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

// 예제
public <T, R> List<R> map(List<T> list, Function<T, R> f) {
    List<R> result = new ArrayList<>();
    for(T t : list) {
        result.add(f.apply(t));
    }
    return result
}

List<Integer> l = map(
    Arrays.asList("lambdas", "in", "action"),
    (String s) -> s.length() // Function의 apply method를 구현하는 람다
)
📕 제네릭 파라미터에는 참조형만 사용할 수 있다.
기본형을 참조형으로 변환하는 기능을 박싱이라 하는데, 자바에는 오토박싱 기능도
존재한다. 하지만 모든 변환에는 비용이 소모되고, 이 변환을 피하기 위해서
자바8에서는 오토박싱을 피할 수 있는 기본형 특화 함수형 인터페이스를 제공한다.
ex. IntPredicate, IntConsumer, IntFunction
1
2
3
4
5
6
7
@FunctionalInterface
public interface IntPredicate {
    boolean test(int t);
}

IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000); //true

이미 자바 API는 다양한 함수형 인터페이스를 포함하고 있다.

java.util.Comparator.java
정렬
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

// 예제 (익명클래스)
inventory.sort(new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
});

// 예제 (람다표현식)
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
java.util.Runnable.java
병렬로 코드 블록 지정 및 실행
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@FunctionalInterface
public interface Runnable {
    void run();
}

// 예제 (익명클래스)
Thread t = new Thread(new Runnable() {
    public void run() {
        System.out.println("Hello World");
    }
});

// 예제 (람다표현식)
Thread t = new Thread(() -> System.out.println("Hello World"));
java.util.concurrent.Callable.java
task 제출과 실행 과정의 연관성을 단절
⚠️ task를 스레드풀로 보내고 결과를 Future로 저장할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

ExecutorService executorService = Executors.newCachedThreadPool();

// 예제 (익명클래스)
Future<String> threadName = executorService.submit(new Callable<String>() {
    public String call() throws Exception {
        return Thread.currentThread().getName();
    }
});

// 예제 (람다표현식)
Future<String> threadName = executorService.submit(() -> Thread.currentThread().getName());
⚠️ @FunctionalInterface
함수형 인터페이스임을 가리키는 어노테이션이다.
해당 어노테이션을 선언했지만 실제로 함수형 인터페이스가 아니라면 컴파일러가 에러를 발생시킨다.

※ 인터페이스 안에 다수의 디폴트 메서드가 있다하더라도 추상메서드가 오직 하나면 함수형 인터페이스다.

📕 디폴트 메서드, 키워드: default
: 자바 8에서는 기존에 정의된 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다.
⚠️ 인터페이스 안에 디폴트 메서드를 구현해도, 해당 인터페이스를 구현한 구현 클래스에서 해당 메서드를 구현하지 않아도 된다.
(즉, 구현 객체를 바꾸지 않고도 기존 인터페이스에 메서드를 추가할 수 있다.)

🔮 함수형 인터페이스라는 문맥에서만 람다 표현식을 사용할 수 있다
람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있다.
즉, 람다 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다.
📕 함수 디스크립터
함수형 인터페이스의 추상 메서드 시그니처를 말한다.
⚠️ 람다표현식이 함수형 인터페이스의 추상 메서드와 같은 시그니처를 가져야
  람다 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다.
1
2
3
4
5
6
public interface Predicate<T> { // 함수형 인터페이스
    boolean test(T t); // 함수 디스크립터(추상 메서드 시그니처): (T) -> boolean
}

Predicate<Apple> p1 = (Apple a) -> a.getWeight(); //(x) 시그니처 (Apple) -> Integer
Predicate<Apple> p2 = (Apple a) -> a.getWeight() > 150; //(0) 시그니처 (Apple) -> boolean

☄️ 람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지에 대한 정보가 포함되어 있지 않다
람다가 사용되는 Context(코드가 실행되는 위치나 상황)를 통해 람다의 형식을 추론할 수 있다.
Context에서 기대되는 람다 표현식의 형식대상 형식이라 한다.
1
2
3
4
filter(inventory, (Apple apple) -> apple.getWeight() > 150);
// (Apple) -> boolean
// 대상 형식은 Predicate<Apple>이다.
// 해당 인터페이스의 추상 메서드는 test(Apple): boolean이다.
📕 특별한 void 호환 규칙
람다의 바디에 일반 표현식이 있으면 void를 반환하는 함수 디스크립터와 호환된다.
ex. Consumer<String> b = s -> list.add(s);
함수 디스크립터는 사실은 (String) -> boolean이지만
(String) -> void로도 볼 수 있다.

하지만, 같은 람다 표현식이더라도 호환되는 추상메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다.
이럴때는 어떻게 해야할까?

🦕 캐스팅하면 된다.

1
Object o = (Runnable) () -> { System.out.println("example."); }
This post is licensed under CC BY 4.0 by the author.