개요.

Java8 이 2014년 3월 18일에 나왔다고 한다. 올해(2020년) 기준 벌써 6년이란 시간이 흘렀고 자바버전도 계속해서 올라가고 있다. 하지만 여전히 나는 Java8 에 익숙치 않다. 그래서 공부가 필요하다. 회사에 굴러다니는 InAction 시리즈 중에 Java8 이 있길래 읽고 여기다가 주요 키워드들을 기록하고자 한다. 

 

관련소스코드

 

Chapter01 : 자바8을 눈여겨봐야 하는 이유

- 컬렉션은 데이터를 어떻게 저장하고 접근할 것인지 중점
스트림은 데이터에 어떤 계산을 할 것인지 묘사에 중점

 

컬렉션을 필터링할 수 있는 가장 빠른 방법은

  1. 컬렉션을 스트림으로 변경한다.
  2. 병렬로 처리한다.
  3. 리스트로 다시 복원한다.

☞ "스트림과 람다를 이용하면 병렬성을 공짜로 얻을 수 있다."

/** 순차 **/
final List<Apple> applesList1 = apples.stream()
        .filter(Apple::isGreenApple)
        .collect(Collectors.toList());

/** 병렬 **/
final List<Apple> applesList2 = apples.parallelStream()
        .filter(Apple::isGreenApple)
        .collect(Collectors.toList());

 

☞ 프레디케이트와 메소드 참조, 람다표현

// 프레디케이트를 사용하였다,
final List<Apple> filterApples = filterApplesOpt(apples, Apple::isGreenApple);

// 람다를 이용하여 프레디케이트를 활용하였다.
final List<Apple> filterApples = filterApplesOpt(apples, (Apple a) -> "green".equals(a.color));

// 프레디케이트가 있고 메소드를 전달한다. (프레디케이트는 argu 에 대한 boolean 을 나타낸다.)
private static List<Apple> filterApplesOpt(List<Apple> inventory, Predicate<Apple> predicate) {
    List<Apple> results = new ArrayList<>();
    for (Apple apple : inventory) {
        if (predicate.test(apple)) {
            results.add(apple);
        }
    }
    return results;
}

class Apple {
    String color;
    int weight;

    private Apple(String color, int weight) {
        this.color = color;
        this.weight = weight;
    }
    
    // 메소드 참조에 이용될 메소드.
    public boolean isGreenApple(Apple apple) {
        return "green".equalsIgnoreCase(apple.color);
    }
}

기억하려고 하는 문장.

  • 람다 표현식을 사용하면 프레디케이트도 활용할 수 있다.
  • 람다 표현식을 통한 조건문이 장황해지면 메소드 참조로 변경하는 것이 바람직하다.
  • 메소드 참조란, 메소드를 전달할 수 있음을 의미하는데 인수 값을 받아서 true/false 를 리턴하는 수학에서의 표현식이다.

 

Chapter02 : 동작 파라미터화 코드 전달하기

- 동작 파리미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드블럭을 의미한다. 해당 코드블럭은 나중에 프로그램에서 호출한다. 

- 즉, 코드블럭의 실행은 나중으로 미뤄진다. 

 

컬렉션을 처리할 때 아래와 같은 항목들을 할 수 있다.

  1. 리스트의 모든 요소에 `어떤 동작` 을 수행할 수 있음
  2. 리스트 관련 작업을 끝낸 다음에 `어떤 다른 동작` 을 수행할 수 있음
  3. 에러가 발생하면 `정해진 어떤 다른 동작` 을 수행할 수 있음

동작 파라미터가 필요한 이유는 많은 요구사항에 대해서 유연하게 대처하기 위함이다. 주먹구구식으로 요구사항이 들어올때 마다 그때그때 처리하게 된다면 중복코드 발생은 물론이고, 혹여나 모를 부작용이 잠재하고 있을 수 있고 이후에 유지보수 할 때도 리소스 낭비가 심하게 될 것이다.

농부가 있다.
농부는 올해 100개의 사과를 수확했다.
100개의 사과 중에는 녹색사과, 빨간사과가 있고 무게도 100g, 120g, 150g, 200g 제각기 다르다. 이에 따라서 농부가 100개의 사과를 하나의 메소드를 통해서 여러가지 분류로 하고싶다.

(1) 녹색사과만 분류하기
(2) 빨간사과만 분류하기
(3) 녹색사과이면서 150g 미만인 사과 분류하기
(4) 빨간사과이면서 150g 이상인 사과 분류하기

 

☞ Code (인터페이스를 활용한 사례)

// 사과 프레디케이트라는 인터페이스를 작성.
public interface ApplePredicate {
    boolean test (Apple apple);
}

// (1) 녹색사과 분류기
public class AppleGreenColorPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return "green".equals(apple.getColor());
    }
}

// (2) 100g 초과되는 사과 분류기
public class AppleHeavyWeightPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() > 100;
    }
}

/**
 * (3) 추상적 조건으로 필터링 수행 : 다양한 ApplePredicate 를 만들 수 있다.
 * ==> 결국 메소드의 동작을 파라미터화 시켰다. : [동작 파리미터화]
 */
public static List<Apple> filterApplesStep01(List<Apple> inventory, ApplePredicate predicate){
    List<Apple> result = new ArrayList<>();
    for(Apple apple : inventory){
        if(predicate.test(apple)){
            result.add(apple);
        }
    }
    return result;
}

 

여기서 코드의 간결함을 더 추구하기위해 책에서는 아래의 단계별로 설명해준다.

  • 익명클래스 사용 : 코드는 여전히 많은 공간을 차지하고 있다. 익명클래스를 거듭사용하면 코드가 장황하다.
  • 람다표현식 사용 : filterApplesStep01(apples, new AppleGreenColorPredicate()) 가 들어가지만 람다를 통해 (apple) -> "green".equals(apple.getColor()) 가 가능하다.
  • 리스트 형식 추상화 : Stream() 의 filter() 메소드를 구현해서 유연성과 간결함을 잡는다.

비슷한 유형 : 아래의 내용들도 람다를 통해서 간결하게 만들 수 있다

  • public interface Comparator<T> {}
  • public interface Runnable {}

 

Chapter03 : 람다 표현식

람다의 특징 (예제를 보면서 특징을 이입하자.)

  • 익명 : 메소드에 이름이 없으므로 익명
  • 함수 : 메소드처럼 특정 클래스에 종속되지 않음
  • 전달 : 람다 표현식을 메소드 인수로 전달하거나 변수로 저장할 수 있음
  • 간결성 : 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없음

람다는 세부분으로 이루어져 있다.

  • 파라미터 리스트 : Comparator 의 compare 메소드의 파라미터
  • 화살표 : `->` 는 람다의 파라미터와 바디를 구분
  • 람다 바티 : 람다의 반환값에 해당하는 표현식
//  [람다 파라미터]  [화살표]  [람다 바디]
(1) (Apple a1, Apple a2) 
(2) -> 
(3) a1.getWeight().compareTo(a2.getWeight());

(결과) (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

람다를 알기 이전에, 람다를 쓸 수 있게 해주는 함수형 인터페이스를 알아야 한다.

 

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

함수형 인터페이스란 추상메소드가 딱 하나만 있는 인터페이스이다. 앞선 프레디케이트도 함수형 인터페이스의 일종인데, 자바 코드단을 살펴보면 아래와 같이 작성되어 있고 @FunctionalInterface 라는 주석을 통해서 함수형 인터페이스를 명시하고 있다. 만약 커스텀하게 함수형 인터페이스를 만들다고 하였을때 @FunctionalInterface 를 명시적으로 붙이면 추가적인 메소드를 붙였을 시 컴파일 에러가 나기 때문에 좀 더 안정적인 코드를 작성할 수 있다.

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

 

함수 디스크립터

함수형 인터페이스의 추상메소드 시그너처는 람다표현식의 시그너처를 가리킨다. 람다 표현식의 시그너처를 서술하는 메소드를 함수 디스크립터라고 한다. 예를 들어 Runnable 인터페이스는 인수와 반환값이 없는 시그너처라고 생각할 수 있다.

 

람다표현식의 시그너처와 일치하는 함수형 인터페이스를 만들면, 언제든 우리는 커스텀하게 람다표현식와 함수형 인터페이스를 같이 사용이 가능하다.

result = ProcessFile.processFile((BufferedReader br) -> br.readLine() + br.readLine());

@FunctionalInterface
public interface BufferedReaderProcessor {
    String process(BufferedReader br) throws IOException;
}

public class ProcessFile {
    public static String processFile(BufferedReaderProcessor processor) throws IOException{
        try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))){
            return processor.process(br);   // bufferedReader 객체를 처리한다.
        }
    }
}

 

람다 실제 형식을 파악

람다 콘텍스트를 이용하여 람다의 형식을 추론할 수 있다. 콘텍스트에서 기대되는 람다 표현식의 형식을 대상형식 (target type) 이라고 부른다.

List<Apple> heavierThan150g = fitler(inventory, (Apple a) -> a.getWeight() > 150);
  1. filter() 메소드의 선언을 확인한다.
  2. filter() 메소드는 두번째 파라미터로 Predicate<Apple> 형식(대상형식) 을 기대한다. 
  3. Predicate<Apple> 은 test 라는 한 개의 추상메소드를 저의하는 함수형 인터페이스이다.
  4. test 메소드는 Apple 을 받아 boolean 을 반환하는 함수 디스크립터를 묘사한다.
  5. filter 메소드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.

자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 활용해서 람다의 파라미터 형식을 추론한다.

 

// 1
apples.sort((Apple apple1, Apple apple2) -> apple1.getWeight() - apple2.getWeight());

// 2 : 타입추론
apples.sort((apple1, apple2) -> apple1.getWeight() - apple2.getWeight());

 

+) 다이아몬드 연산자

다이아몬드 연산자 (<>) 은 콘텍스트에 따른 제네릭 형식을 추론할 수 있다.

List<Integer> listOfIntegers = new ArrayList<>();
List<String> listOfStrings = new ArrayList<>();

 

람다 캡처링(Lambda Capturing)

람다 표현식에서는 스태틱 변수, 인스턴스 변수, 로컬 변수에 사용할 수 있다. 그리고 익명함수가 사용하는 자유변수, 즉 외부에서 정의된 변수를 사용할 수 있는데 이를 람다 캡처링이라고 부른다. 람다 캡처링에서 지역변수를 사용하기 위해선 지역변수는 명시적으로 final 로 선언되어 있어야 한다.

 

왜 final 로 선언되어야 하는가?

자바 메모리 관점 지역변수는 스택메모리에 저장되는 메소드가 완료되면 해당 스레드는 사라진다. 이때, 스택메모리 내의 할당된 공간 지역변수도 사라진다. 이 경우에서 사라진 지역변수의 값을 변경한다는 문제가 발생하기 때문에 애초에 그런 문제를 방지하고자 final 을 넣는것이다. (변경하려는 포인트를 아예 막아버린다.)

 

정상동작

public class LambdaCapturing {

    private int number = 5;

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

    private void foo(){
        // 인스턴스 변수를 increase 시킨다.
        Runnable r = () -> System.out.println(number++);
        r.run();
    }
}

 

IDE 에서 컴파일 에러발생

public class LambdaCapturing {
    public static void main(String[]args) {
        LambdaCapturing lambdaCapturing = new LambdaCapturing();
        lambdaCapturing.bar();
    }

	/**
     * bar() 메소드가 끝난 뒤, localNumber 를 변경하려고 한다.
  	**/
    private void bar(){
        int localNumber = 10;
        Runnable r = () -> {
            try {
                Thread.sleep(5000);
                localNumber++;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        r.run();
    }
}

 

+) Comparator 함수의 응용

// 무게 오름차순
apples.sort(comparing(Apple::getWeight));

// 무게 내림차순 (오름차순의 역정렬)
apples.sort(comparing(Apple::getWeight).reversed());

// 무게 역정렬하면서 동일 무게에 대해선 색깔 정렬을 수행
apples.sort(comparing(Apple::getWeight)
        .reversed()
        .thenComparing(Apple::getColor));

 

+) Function 조합

Function<Integer, Integer> f = (x) -> x + 1;
Function<Integer, Integer> g = (x) -> x * 2;
Function<Integer, Integer> h1 = f.compose(g);   // f(g(x))
Function<Integer, Integer> h2 = f.andThen(g);   // g(f(x))

System.out.println(h1.apply(1));    // 4
System.out.println(h2.apply(1));    // 3
  • 파이프라인을 만들어 메소드 체이닝처럼 응용해서 쓸 수 있을 듯하다.

기억하려고 하는 문장.

  • 람다표현식을 이용해서 함수형 인터페이스의 추상메소드를 즉석으로 제공할 수 있으며 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급된다.
  • 람다 표현식의 기대형식을 대상형식이라고 한다.
  • Comparator, Predicate, Function 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있는 다양할 디폴트 메소드를 제공한다.
Posted by doubler
,