Java8 In Action

[Part1] Java8 In action - Chapter3 - 2

신나게개발썰 2022. 7. 27. 11:17
반응형

해당 내용은 Java8 In Action 책을 요약 및 정리한 내용입니다.

좀 더 자세히 알고 싶으신 분들은 책을 사서 읽어보심을 추천드립니다.!

3.5 형식 검사,형식추론,제약

Entity

AppleEntity.java

package Part1.Chapter3.Chapter3_3_5.entity;

public class AppleEntity {

    private String color;
    private Integer weight;

    public AppleEntity() {}

    public AppleEntity(String color, Integer weight) {
        this.color = color;
        this.weight = weight;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public Integer getWeight() {
        return weight;
    }

    public void setWeight(Integer weight) {
        this.weight = weight;
    }
}

Main

package Part1.Chapter3.Chapter3_3_5;

import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.function.ToIntBiFunction;

import Part1.Chapter3.Chapter3_3_5.entity.AppleEntity;

/*
 * 3.5 형식 검사, 형식 추론, 제약
 * 
 * 람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않다.
 * 따라서 람다 표현식을 더 제대로 이해하려면 람다의 실제 형식을 파악해야 한다. 
 */
public class Main_3_5 {

    public static List<AppleEntity> filter(List<AppleEntity> inventory, Predicate<AppleEntity> p) {
        List<AppleEntity> result = new ArrayList<>();

        for(AppleEntity apple : inventory) {
            if(p.test(apple)) {
                result.add(apple);
            }            
        }

        return result;
    }

    public static void main(String[] args) throws Exception {
        List<AppleEntity> inventory = Arrays.asList(new AppleEntity("red", 100)
                , new AppleEntity("green", 200));

        /*
         * 3.5.1 형식 검사
         * 
         * 람다가 사용되는 콘텍스트(context)를 이용해서 람다의 형식(type)을 추론할 수 있다.
         * 어떤 콘텍스트(예를 들면 람다가 전달될 메서드 파라미터나 람다가 할당되는 변수 등)
         * 에서 기대되는 람다 표현식의 형식을 "대상 형식(target type)"이라고 부른다.
         * 
         * 위 람다 표현식을 사용할 때 다음과 같은 순서로 형식 확인 과정이 진행된다.
         * 1. filter 메서드의 선언을 확인.
         * 2. filter 메서드는 두 번째 파라미터로 Predicate<AppleEntity> 형식(대상 형식)을 기대한다.
         * 3. Predicate<AppleEntity>은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스이다.
         * 4. test 메서드는 AppleEntity을 받아 boolean을 반환하는 함수 디스크립터를 묘사한다.
         * 5. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.
         * 
         * 위 예제에서 람다 표현식은 AppleEntity을 인수로 받아 boolean을 반환하므로 유효한 코드이다.
         * 람다 표현식이 예외를 던질 수 있다면 추상 메서드도 같은 예외를 던질 수 있도록 throws로 선언해야 한다.
         * 
         * 람다 표현식의 형식 검사 과정의 재구성
         * filter(inventory, (AppleEntity a) -> a.getWeight() > 150)    
         *         1. 람다가 사용된 콘텍스트는 무엇인가? 우선 filter의 정의를 확인한다.    
         * filter(inventory, Predicate<AppleEntity> p)    
         *         2. 대상 형식은 Predicate<AppleEntity>이다.    
         * 대상 형식
         *         3. Predicate<AppleEntity>인터페이스의 추상 메서드는 무엇인가?    
         * boolean test(AppleEntity appleEntity)    
         *         4. AppleEntity을 인수로 받아 boolean을 반환하는 test 메서드다.    
         * AppleEntity -> boolean    
         *         5. 함수 디스크립터는 AppleEntity -> boolean이므로 람다의 시그너처와 일치한다.
         *         람다도 AppleEntity을 인수로 받아 boolean을 반환하므로 코드 형식 검사가 완료된다.
         */
        filter(inventory
                , (AppleEntity a) -> a.getWeight() > 150)
            .stream()
            .forEach((AppleEntity a) -> System.out.println("3.5.1 형식 검사 : " 
                + a.getColor() +" apple - " + a.getWeight() + " weight." ));
        /*
         * 3.5.2 같은 람다, 다른 함수형 인터페이스
         * 
         * 대상 형식이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다.
         * 예를 들어 Callable과 PrivilegedAction 인터페이스는 () -> T와 같은 메서드 시그너처를 가지고 있다.
         * 따라서 아래 할당문은 모두 유효한 코드이다.
         */
        Callable<Integer> c = () -> 42;
        System.out.println("Callable : " + c.call());
        PrivilegedAction<Integer> p = () -> 52;
        System.out.println("PrivilegedAction : " + p.run());

        /* 아래 코드와 같이 하나의 람다 표현식을 다양한 함수형 인터페이스에 사용할 수 있다.*/
        Comparator<AppleEntity> c1 = (AppleEntity a1, AppleEntity a2) -> a1.getWeight().compareTo(a2.getWeight());
        System.out.println("Comparator : " + c1.compare(new AppleEntity("green", 200), new AppleEntity("red", 100)));

        ToIntBiFunction<AppleEntity, AppleEntity> c2 = (AppleEntity a1, AppleEntity a2) -> a1.getWeight().compareTo(a2.getWeight());
        System.out.println("ToIntBiFunction : " + c2.applyAsInt(new AppleEntity("green", 100), new AppleEntity("red", 200)));

        BiFunction<AppleEntity, AppleEntity, Integer> c3 = (AppleEntity a1, AppleEntity a2) -> a1.getWeight().compareTo(a2.getWeight());
        System.out.println("BiFunction : " + c3.apply(new AppleEntity("green", 100), new AppleEntity("red", 200)));;

        /*
         * 다이아몬드 연산자
         * 주어진 클래스 인스턴스 표현식을 두 개 이상의 다양한 콘텍스트에 사용할 수 있다.
         * 이때 인스턴스 표현식의 형식 인수는 콘텍스트에 의해 추론된다.
         *         List<String> listOfStrings = new ArrayList<>();
         *         List<Integer> listOfIntegers = new ArrayList<>();
         * 
         * 특별한 void 호환 규칙
         * 람다의 바디에 일반 표현식이 있으면 void를 반환하는 함수 디스크립터와 호환된다.(물론 파라미터 리스트도 호환되어야 함.)
         * 예를 들어 다음 두 행의 예제에서 List의 add 메서드는 Consumer 콘텍스트(T -> void)가 기대하는 void 대신 boolean을 반환하지만
         * 유효한 코드이다.
         *         // Predicate는 불린 반환값을 갖는다.
         *         Predicate<String> p = s -> list.add(s);
         *         // Consumer는 void 반환값을 갖는다.
         *         Consumer<String> b = s -> list.add(s);
         */

        /*
         * 3.5.3 형식 추론
         * 
         * 자바 컴파일러는 람다 표현식이 사용된 콘텍스트(대상 형식)을 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다.
         * 즉, 대상 형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그너처도 추론할 수 있다.
         * 결과적으로 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서는 이를 생략할 수 있다.
         */
        filter(inventory, (a) -> "green".equals(a.getColor()) )
            .stream()
            .forEach((a) -> System.out.println("3.5.2 형식 추론 : " 
                    + a.getColor() +" apple - " + a.getWeight() + " weight." ));

        /*
         * 여러 파라미터를 포함하는 람다 표현식에서는 코드 가독성 향상이 두드러진다. 
         * 다음은 Comparator 객체를 만드는 코드다.
         */
        // 형식 추론을 하지 않음.
        Comparator<AppleEntity> notTypeResaoning = (AppleEntity a1, AppleEntity a2) -> a1.getWeight().compareTo(a2.getWeight());
        System.out.println("notTypeResaoning : " + notTypeResaoning.compare(new AppleEntity("red", 300), new AppleEntity("blue", 400)));

        // 형식 추론 함.
        Comparator<AppleEntity> typeResaoning = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
        System.out.println("typeResaoning : " + typeResaoning.compare(new AppleEntity("red", 500), new AppleEntity("blue", 400)));

        /*
         * 3.5.4 지역 변수 사용
         * 
         * 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수(free variable - 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를
         * 활용할 수 있다. 이와 같은 동작을 람다 캡쳐링(capturing lambda)이라고 한다.
         * 아래 예는 portNumber 변수를 캡처하는 람다이다.
         */
        int portNumber = 80;
        Runnable r = () -> System.out.println("capturing lambda : " + portNumber);
        r.run();

        /*
         * 하지만 자유 변수에도 약간의 제약이 있다. 인스턴스 변수와 정적 변수를 캡처(자신의 바디에서 참조할 수 있도록)할 수 있다.
         * 하지만 그러려면 지역 변수는 명시적으로 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야 한다.
         * 즉, 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡처할 수 있다.(참고 : 인스턴스 변수 캡처는 final 지역 변수 this를 캡처하는 것과 마찬가지이다.)
         */
        int errorPortNumber = 81;
        Runnable r1 = () -> System.out.println("errorPortNumber lambda : " + errorPortNumber);
        /* 아래 변수의 재할당 하는 부분을 주석 해제 하면 위 람다 표현식에서 에러가 발생한다. */
        //errorPortNumber = 82;
        r1.run();

        /*
         * 지역 변수의 제약
         * 지역 변수의 이런 제약은 인스턴스 변수와 지역 변수는 태생이 다른데 부터 발생한다.
         * 인스턴스 변수는 힙에 저장되는 반면 지역 변수는 스택에 위치한다. 람다에서 지역 변수에 바로 접근할 수 있다는
         * 가정 하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었음에도 람다를 실행하는
         * 스레드에서는 해당 변수에 접근하려 할 수 있다.
         * 따라서 원래 변수에 접근을 허용하는 것이 아니라 자유 변수의 복사본을 제공한다.
         * 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것이다.
         * 
         * 또한 지역 변수의 제약 때문에 외부 변수를 변화시키는 일반적인 명령형 프로그래밍 패턴(병렬화를 방해는 요소)에 제동을 걸 수 있다.
         */

        /*
         * 클로저
         * 원칙적으로 클로저란 함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스를 가리킨다.
         * 예를 들어 클로저를 다른 함수의 인수로 전달할 수 있고 클로저는 클로저 외부에 정의된 변수의 값에 접근하고 값을 바꿀 수 있다.
         * 자바 8의 람다와 익명 클래스는 클로저와 비슷한 동작을 수행한다.
         * 람다와 익명 클래스 모두 메서드의 인수로 전달될 수 있으며 자신의 외부 영역의 변수에 접근할 수 있다.
         * 다만 람다와 익명 클래스는 람다가 정의된 메서드의 지역 변수 값은 바꿀 수 없다.
         * 람다가 정의된 메서드의 지역 변수값은 final 변수여야 한다. 덕분에 람다는 변수가 아닌 값에 국한되어 어떤 동작을 수행한다는 사실이
         * 명확해진다.
         * 이전에도 설명한 것 처럼 지역 변수값은 스택에 존재하므로 자신을 정의한 스레드와 생존을 같이 해야 하며 따라서 지역 변수는 final이어야 한다.
         * 가변 지역 변수를 새로운 스레드에서 캡처할 수 있다면 안전하지 않는 동작을 수행할 가능성이 생긴다.(인스턴스 변수는 스레드가 공유하는 힙에
         * 존재하므로 특별한 제약이 없다.)
         */
    }

}

3.6 메서드 레퍼런스

Entity

Fruit.java

package Part1.Chapter3.Chapter3_3_6.entity;

public class Fruit {
    protected String color;
    protected Integer weight;

    public Fruit() {}

    public Fruit(Integer weight) {
        this.weight = weight;
    }

    public Fruit(String color, Integer weight) {
        this.color = color;
        this.weight = weight;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public Integer getWeight() {
        return weight;
    }

    public void setWeight(Integer weight) {
        this.weight = weight;
    }
}

AppleEntity.java

package Part1.Chapter3.Chapter3_3_6.entity;

public class AppleEntity extends Fruit {

    public AppleEntity() {}

    public AppleEntity(Integer weight) {
        this.weight = weight;
    }

    public AppleEntity(String color, Integer weight) {
        this.color = color;
        this.weight = weight;
    }
}

OrangeEntity.java

package Part1.Chapter3.Chapter3_3_6.entity;

public class OrangeEntity extends Fruit {
    public OrangeEntity() {}

    public OrangeEntity(Integer weight) {
        this.weight = weight;
    }

    public OrangeEntity(String color, Integer weight) {
        this.color = color;
        this.weight = weight;
    }
}

Main

package Part1.Chapter3.Chapter3_3_6;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

import Part1.Chapter3.Chapter3_3_6.entity.AppleEntity;
import Part1.Chapter3.Chapter3_3_6.entity.Fruit;
import Part1.Chapter3.Chapter3_3_6.entity.OrangeEntity;

/*
 * 3.6 메서드 레퍼런스
 * 
 * 메서드 레퍼런스를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다.
 * 때로는 람다 표현식보다 메서드 레퍼런스를 사용하는 것이 더 가독성이 좋으며 자연스러울수 있다.
 * 
 * 3.6.1 요약
 * 메서드 레퍼런스는 특정 메서드만을 호출하는 람다의 축약형이라고 생각할 수 있다.
 * 예를 들어 람다가 '이 메서드를 직접 호출해'라고 명령한다면 메서드를 어떻게 호출해야 하는지 설명을
 * 참조하기보다는 메서드명을 직접 참조하는 것이 편리하다.
 * 실제로 메서드 레퍼런스를 이용하면 기존에 구현된 메서드를 람다 표현식을 만들수 있다.
 * 이때 명시적으로 메서드명을 참조함으로써 가독성을 높일수 있다.
 * 메서드 레퍼런스는 메서드명 앞에 구분자(::)를 붙이는 방식으로 메서드 레퍼런스를 활용할 수 있다.
 *  
 * 람다와 메서드 레퍼런스 단축 표현 예제
 * 람다 : (AppleEntity a) -> a.getWeight()
 * 메서드 레퍼런스 단축 표현 : AppleEntity::getWeight
 * 
 * 람다 : () -> Thread.currentThread().dumpStack()
 * 메서드 레퍼런스 단축 표현 : Thread.currentThread()::dumpStack
 * 
 * 람다 : (str, i) -> str.substring(i)
 * 메서드 레퍼런스 단축 표현 : String::substring
 * 
 * 람다 : (String s) -> System.out.println(s)
 * 메서드 레퍼런스 단축 표현 : System.out::println
 * 
 * 메서드 레퍼런스를 새로운 기능이 아니라 하나의 메서드를 참조하는 람다를 편리하게 표현할 수 있는 문법으로 간주할 수 있다.
 */
public class Main_3_6 {

    public static Fruit giveMeFruit(Map<String, Function<Integer, Fruit>> map, String fruit, Integer weight) {
        return map.get(fruit.toLowerCase()).apply(weight);        
    }

    public static List<AppleEntity> map(List<Integer> list , Function<Integer, AppleEntity> f) {
        List<AppleEntity> result = new ArrayList<>();

        for(Integer e : list) {
            result.add(f.apply(e));
        }

        return result;
    }

    public static void main(String[] args) {
        List<AppleEntity> inventory = Arrays.asList(new AppleEntity("green", 100)
                , new AppleEntity("red", 200)
                , new AppleEntity("blue", 300));

        /*
         * 람다 표현식으로 정렬.
         */
        inventory.sort((a1, a2) -> a2.getWeight().compareTo(a1.getWeight()));
        inventory.stream().forEach((appleEntity) -> System.out.println("[Lambda - Descending] color : " 
                + appleEntity.getColor() + ", weight : " + appleEntity.getWeight() ));

        /*
         * java.util.Comparator.comparing와 메서드 레퍼런스를 이용한 정렬.
         */
        inventory.sort(Comparator.comparing(AppleEntity::getWeight));
        inventory.stream().forEach((appleEntity) -> System.out.println("[Comparator.comparing - Ascending] color : " 
                + appleEntity.getColor() + ", weight : " + appleEntity.getWeight() ));

        /*
         * 정적 메서드 레퍼런스
         */
        Function<String, Integer> f = (str) -> Integer.parseInt(str);
        System.out.println("정적 메서드 람다 표현식 : " + f.apply("1234"));

        f = Integer::parseInt;
        System.out.println("정적 메서드 레퍼런스 표현식: " + f.apply("7894"));

        /*
         * 다양한 형식의 인스턴스 메서드 레퍼런스
         */
        f = (str) -> str.length();
        System.out.println("인스턴스 메서드 람다 표현식 : " + f.apply("kim sung"));

        f = String::length;
        System.out.println("인스턴스 메서드 레퍼런스 표현식 : " + f.apply("kim sung wook"));

        /*
         * 기존 객체의 인스턴스 메서드 레퍼런스
         */    
        List<AppleEntity> appleInventory = new ArrayList<AppleEntity>() {
            private static final long serialVersionUID = 1L;
            {
                add(new AppleEntity("blue1", 100));
                add(new AppleEntity("blue2", 200));
                add(new AppleEntity("blue3", 300));
            }
        };

        Function<AppleEntity, Boolean> appleEntityFunction = appleInventory::add;
        appleEntityFunction.apply(new AppleEntity("white2", 500));

        appleInventory.stream().forEach((appleEntity) -> System.out.println("기존 객체의 인스턴스 메서드 레퍼런스 : color : " 
                + appleEntity.getColor() + ", weight : " + appleEntity.getWeight() ));

        /*
         * 3.6.2 생성자 레퍼런스
         * 
         * ClassName::new처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 레퍼런스를 만들수 있다.
         * 이것은 정적 메서드의 레퍼런스를 만드는 방법과 비슷하다.
         */

        Supplier<AppleEntity> c1 = AppleEntity::new;
        AppleEntity a1 = c1.get();
        System.out.println("3.6.2 생성자 레퍼런스 Supplier(메서드 레퍼런스) : " + a1);

        /*
         * 위 예제는 아래 코드와 같다. 
         */
        c1 = () -> new AppleEntity();
        a1 = c1.get();
        System.out.println("3.6.2 생성자 레퍼런스 Supplier(람다 표현식) : " + a1);

        /*
         * AppleEntity(Integer weight)라는 시그너처를 갖는 생성자는 Function 인터페이스의 시그너처와 같다.
         * 따라서 다음과 같이 구현이 가능하다.
         */
        Function<Integer, AppleEntity> c2 = AppleEntity::new;
        AppleEntity a2 = c2.apply(110);
        System.out.println("3.6.2 생성자 레퍼런스 Function(메서드 레퍼런스) : " + a2);

        c2 = (Integer weight) -> new AppleEntity(weight);
        a2 = c2.apply(220);
        System.out.println("3.6.2 생성자 레퍼런스 Function(람다 표현식) : " + a2);

        /*
         * 다음 코드에서 Integer를 포함하는 리스트의 각 요소를 map 메서드를 이용해서 AppleEntity 생성자로 전달한다.
         * 결과적으로 다양한 무게를 포함하는 사과 리스트가 만들어진다.
         */
        List<Integer> weights = Arrays.asList(7, 3, 4, 10);
        List<AppleEntity> apples = map(weights, AppleEntity::new);

        apples.stream().forEach((AppleEntity a) -> System.out.println("3.6.2 생성자 레퍼런스 map 메서드 : weight = " 
                + a.getWeight() ));

        /*
         * AppleEntity(String, Integer)처럼 두 인수를 갖는 생성자는 BiFunction인터페이스와 같은 시그너처를 가지므로
         * 다음처럼 할 수 있다.
         */
        BiFunction<String, Integer, AppleEntity> c3 = AppleEntity::new;
        AppleEntity a3 = c3.apply("red", 100);
        System.out.println("3.6.2 생성자 레퍼런스 BiFunction(메서드 레퍼런스) : " + a3);

        /*
         * 인스턴스화하지 않고도 생성자에 접근할 수 있는 기능을 다양한 상황에 응용할 수 있다.
         * 예를 들어 Map으로 생성자와 문자열값을 관련시킬 수 있다.
         * 그리고 String과 Integer가 주어졌을 때 다양한 무게를 갖는 여러 종류의 과일을 만드는 메서드를
         * 만들수 있다.
         */
        Map<String, Function<Integer, Fruit>> map = new HashMap<String, Function<Integer,Fruit>>(){
            private static final long serialVersionUID = 1L;

            {
                put("apple", AppleEntity::new);
                put("orange", OrangeEntity::new);
            }
        };

        System.out.println("giveMeFruit 메서드(AppleEntity 객체) : " + giveMeFruit(map, "apple", 100).getWeight());
        System.out.println("giveMeFruit 메서드(OrangeEntity 객체) : " + giveMeFruit(map, "orange", 200).getWeight());

    }

}

3.7 람다,메서드 레퍼런스 활용하기

Entity

AppleEntity.java

package Part1.Chapter3.Chapter3_3_7.entity;

public class AppleEntity {

    private String color;
    private Integer weight;

    public AppleEntity() {}

    public AppleEntity(String color, Integer weight) {
        this.color = color;
        this.weight = weight;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public Integer getWeight() {
        return weight;
    }

    public void setWeight(Integer weight) {
        this.weight = weight;
    }
}

Implements

AppleEntityComparator.java

package Part1.Chapter3.Chapter3_3_7.impl;

import java.util.Comparator;

import Part1.Chapter3.Chapter3_3_7.entity.AppleEntity;

public class AppleEntityComparator implements Comparator<AppleEntity>{

    @Override
    public int compare(AppleEntity o1, AppleEntity o2) {
        return o1.getWeight().compareTo(o2.getWeight());
    }

}

Main

package Part1.Chapter3.Chapter3_3_7;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import Part1.Chapter3.Chapter3_3_7.entity.AppleEntity;
import Part1.Chapter3.Chapter3_3_7.impl.AppleEntityComparator;

/*
 * 3.7 람다, 메서드, 레퍼런스 활용하기!
 */
public class Main_3_7 {

    public static void main(String[] args) {
        List<AppleEntity> inventory = Arrays.asList(new AppleEntity("red", 300)
                , new AppleEntity("green", 120)
                , new AppleEntity("blue", 100));

        /*
         * 3.7.1 1단계 : 코드 전달
         * 자바 8의 List API에서 제공하는 sort메서드에 정렬 전략을 전달. 
         */
        inventory.sort(new AppleEntityComparator());
        inventory.stream().forEach((AppleEntity a) -> System.out.println("3.7.1 코드 전달 : 색깔 = " 
                + a.getColor() + ", 무게 = " + a.getWeight() ));

        /*
         * 3.7.2 2단계 : 익명 클래스 사용
         */
        inventory.sort(new Comparator<AppleEntity>() {
            @Override
            public int compare(AppleEntity o1, AppleEntity o2) {            
                return o2.getWeight().compareTo(o1.getWeight());
            }
        });
        inventory.stream().forEach((AppleEntity a) -> System.out.println("3.7.2 익명 클래스 사용 : 색깔 = " 
                + a.getColor() + ", 무게 = " + a.getWeight() ));

        /*
         * 3.7.3 3단계 : 람다 표현식 사용
         * 
         * 람다 표현식으로 경량화된 문법을 이용해서 코드를 전달.
         * 함수형 인터페이스를 기대하는 곳 어디에서나 람다 표현식을 이용할 수 있다.
         * 함수형 인터페이스란 오직 하나의 추상 메서드를 정의하는 인터페이스이다.
         * Comparator의 함수 디스크립터는 (T, T) -> int다.
         * 우리 같은 경우에는(AppleEntity, AppleEntity) -> int로 표현할 수 있다. 
         */
        /*
         *  람다 표현식이 사용된 콘텍스트를 활용해서 람다의 파라미터 형식을 추론할 수 있다.
         */
        inventory.sort((o1, o2) -> o1.getWeight().compareTo(o2.getWeight() ));
        inventory.stream().forEach((AppleEntity a) -> System.out.println("3.7.3 람다 표현식 사용 : 색깔 = " 
                + a.getColor() + ", 무게 = " + a.getWeight() ));

        inventory.sort(Comparator.comparing((AppleEntity a) -> a.getWeight() ));
        inventory.stream().forEach((AppleEntity a) -> System.out.println("3.7.3 람다 표현식 사용(Comparator.comparing) : 색깔 = " 
                + a.getColor() + ", 무게 = " + a.getWeight() ));

        /*
         * 3.7.4 4단계 : 메서드 레퍼런스 사용 
         */
        inventory.sort(Comparator.comparing(AppleEntity::getWeight));
        inventory.stream().forEach((AppleEntity a) -> System.out.println("3.7.4 메서드 레퍼런스 사용 : 색깔 = " 
                + a.getColor() + ", 무게 = " + a.getWeight() ));
    }

}

3.8 람다 표현식을 조합할 수 있는 유용한 메서드

Entity

AppleEntity.java

package Part1.Chapter3.Chapter3_3_8.entity;

public class AppleEntity {

    private String color;
    private Integer weight;

    public AppleEntity() {}

    public AppleEntity(String color, Integer weight) {
        this.color = color;
        this.weight = weight;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public Integer getWeight() {
        return weight;
    }

    public void setWeight(Integer weight) {
        this.weight = weight;
    }
}

Process

Letter.java

package Part1.Chapter3.Chapter3_3_8.letter;

public class Letter {
    public static String addHeader(String text) {
        return "From Raoul, Mario and Alan : " + text;
    }

    public static String addFooter(String text) {
        return text + " Kind regards";
    }

    public static String checkSpelling(String text) {
        return text.replaceAll("labda", "lambda");
    }
}

Main

package Part1.Chapter3.Chapter3_3_8;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;

import Part1.Chapter3.Chapter3_3_8.entity.AppleEntity;
import Part1.Chapter3.Chapter3_3_8.letter.Letter;

/*
 * 3.8 람다 표현식을 조합할 수 있는 유용한 메서드
 * 
 * 자바 8 API의 몇몇 함수형 인터페이스는 다양한 유틸리티 메서드를 포함한다.
 * 간단한 여러 개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있다.
 * 예를 들어 두 프레디케이트를 조합해서 두 프레디케이트의 or 연산을 수행하는 커다란 프레디케이트를 만들수 있다.
 * 또한 한 함수의 결과가 다른 함수의 입력이 되도록 두 함수를 조합할 수도 있다.
 * 이와 같이 가능한 이유는 함수형 인터페이스의 디폴트 메서드(default method) 때문이다.
 * (디폴트 메서드는 추상 메서드가 아니므로 함수형 인터페이스 정의에 어긋나지 않는다.) 
 */
public class Main_3_8 {

    public static void main(String[] args) {
        List<AppleEntity> inventory = Arrays.asList(new AppleEntity("green", 140)
                , new AppleEntity("red", 200)
                , new AppleEntity("green", 200)
                , new AppleEntity("blue", 300)
                , new AppleEntity("red", 100));

        /*
         * 3.8.1 Comparator 조합
         * 
         * 정적 메서드 Comparator.comparing을 이용해서 비교에 사용할 키를 추출하는 Function 기반의 Comparator를
         * 반환할 수 있다. 
         *         Comparator<AppleEntity> c = Comparator.comparing(AppleEntity::getWeight);
         */
        /* 
         * 역정렬
         * 사과의 무게를 내림차순으로 정렬하고 싶다면? 다른 Comparator 인스턴스를 만들 필요가 없다.
         * 인터페이스 자체에 주어진 비교자의 순서를 뒤바꾸는 reverse라는 디폴트 메서드를 제공하기 떄문이다.
         * 아래 코드와 같이 처음 비교자 구현을 그대로 재사용해서 사과의 무게를 기준으로 역정렬할 수 있다.
         */
        inventory.sort(Comparator.comparing(AppleEntity::getWeight).reversed());
        inventory.stream().forEach((appleEntity) -> System.out.println("3.8.1 Comparator 조합(reversed 메서드 사용) color : " 
                + appleEntity.getColor() + ", weight : " + appleEntity.getWeight() ));

        /*
         * Comparator 연결
         * 무게가 같다면 색깔로 사과를 정렬할 수 있다.
         * thenComparing 메서드로 두 번재 비교자를 만들수 있다.
         * thenComparing은 함수를 인수로 받아 첫 번째 비교자를 이용해서 두 객체가 같다고 판단되면
         * 두 번째 비교자에 객체를 전달한다.
         */
        inventory.sort(Comparator.comparing(AppleEntity::getWeight).reversed()
            .thenComparing(Comparator.comparing(AppleEntity::getColor)) );
        inventory.stream().forEach((appleEntity) -> System.out.println("3.8.1 Comparator 조합(thenComparing 메서드 사용) color : " 
                + appleEntity.getColor() + ", weight : " + appleEntity.getWeight() ));

        /*
         * 3.8.2 Predicate 조합
         * 
         * Predicate 인터페이스는 복잡한 프레디케이트를 만들수 있도록 negate, and, or 세 가지 메서드를 제공한다. 
         */
        inventory.stream().forEach((AppleEntity a) -> {
            Predicate<AppleEntity> redApple = (AppleEntity a1) -> "red".equals(a1.getColor());
            Predicate<AppleEntity> gram150Up = (AppleEntity a1) -> a1.getWeight() > 150;

            /*
             * 예를 들어 "빨간색이 아닌 사과"처럼 특정 프레디케이트를 반전시킬 때 negate메서드를 사용할 수 있다.
             */
            Predicate<AppleEntity> notRedApple = redApple.negate();

            if(notRedApple.test(a)) {
                System.out.println("3.8.2 Predicate 조합(빨간색이 아닌 사과 - negate) color :" + a.getColor() + ", weight : " + a.getWeight());
            }

            /*
             * and 메서드를 이용해서 "빨간색이 아니면서 무거운 사과"를 선택하도록 두 람다를 조합할 수 있다.
             */
            Predicate<AppleEntity> notRedAndHeavyApple = notRedApple.and(gram150Up);

            if(notRedAndHeavyApple.test(a)) {
                System.out.println("3.8.2 Predicate 조합(빨간색이 아니면서 무거운 사과 - and) color :" + a.getColor() + ", weight : " + a.getWeight());                
            }            

            /*
             * or을 이용해서 "빨간색이면서 무거운(150그램이상) 사과 또는 그냥 녹색 사과"인 경우의 조합
             * 왼쪽에서 오른쪽으로 연결된다.
             */
            Predicate<AppleEntity> redAndHeavyAppleOrGreen = redApple
                    .and(gram150Up)
                    .or((AppleEntity a1) -> "green".equals(a1.getColor()));

            if(redAndHeavyAppleOrGreen.test(a)) {
                System.out.println("3.8.2 Predicate 조합(빨간색이면서 무거운 사과 또는 녹색 사과 - or) color :" + a.getColor() + ", weight : " + a.getWeight());                
            }            
        } );

        /*
         * 3.8.3 Function 조합
         * 
         * Function 인터페이스는 Function 인스턴스를 반환하는 andThen, compose 두 가지 디폴트 메서드를 제공한다.
         * addThen 메서드는 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환한다.
         * 예를 들어 숫자를 증가(x -> x + 1)시키는 f라는 함수가 있고, 숫자에 2를 곱하는 g라는 함수가 있다고 가정하면
         * f와 g를 조립해서 숫자를 증가시킨 뒤 결과에 2를 곱하는 h라는 함수를 만들 수 있다.
         */
        Function<Integer, Integer> f = (x) -> x + 1;
        Function<Integer, Integer> g = (x) -> x * 2;
        // 수학적으로 write g(f(x)) 또는 (g o f)(x)라고 표현.        
        System.out.println("3.8.3 Function 조합(andThen 메서드): " + f.andThen(g).apply(1));

        /*
         * compose 메서드는 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공한다.
         * 즉, f.andThen(g)에서 andThen 대신에 compose를 사용하면 g(f(x))가 아니라 f(g(x))라는 수식이 된다.
         */
        // 수학적으로 write f(g(x)) 또는 (f o g)(x)라고 표현.
        System.out.println("3.8.3 Function 조합(andThen 메서드): " + f.compose(g).apply(1));

        /*
         * 아래 코드는 문자열로 구성된 편지 내용을 변환하는 다양한 유틸 메서드이다.
         * 여러 유틸 메서드를 조합해서 다양한 변환 파이프라인을 만들 수 있다.
         */
        Function<String, String> addHeader = Letter::addHeader;

        System.out.println("3.8.3 Function 조합 : " + addHeader
                .andThen(Letter::checkSpelling)
                .andThen(Letter::addFooter)
                .apply("kim sungw wook - labda") );

        /*
         * 철자 검사는 빼고 헤더와 푸터만 추가하는 파이프라인도 만들 수 있다.
         */
        System.out.println("3.8.3 Function 조합 : " + addHeader
            .andThen(Letter::addFooter)
            .apply("kim sungw wook - labda") );
    }

}

람다 표현식

  • 동작 파라미터화를 이용해서 변화하는 요구사항에 효과적으로 대응하는 코드를 구현할 수 있음을 확인 했다.
    • 또한 정의한 코드 블록을 다른 메서드로 전달할 수 있고,
    • 정의한 코드 블록을 특정 이벤트(예를 들면 마우스 클릭)가 발생할 때 실행 되도록 설정 하거나
    • 알고리즘의 일부(150그램 이상의 사과와 같은 프레디게이트)로 실행되도록 설정할 수 있다. 따라서 동작 파라미터화를 이용하면 더 유연하고 재사용할 수 있는 코드를 만들 수 있다.
  • 익명 클래스로 다양한 동작을 구현할 수 있지만 만족할 만큼 코드가 깔끔하지 않다. 깔끔하지 않는 코드는 동작 파라미터를 실전에 적용하는 것을 막는 요소다.
  • 람다 표현식은 익명 클래스처럼 이름이 없는 함수면서 메서드를 인수로 전달할 수 있으므로 일단은 람다 표현식이 익명 클래스와 비슷한 것이라고 생각하자.

요약

  • 람다 표현식은 익명 함수의 일종이다. 이름은 없지만, 파라미터 리스트, 바디, 반환 형식을 가지며 예외를 던질 수 있다.

  • 람다 표현식은 간결한 코드를 구현할 수 있다.

  • 함수형 인터페이스는 하나의 추상 메서드만을 정의하는 인터페이스다.

  • 함수형 인터페이스를 기대하는 곳에서만 람다 표현식을 사용할 수 있다.

  • 람다 표현식을 이용해서 함수형 인터페이스의 추상 메서드를 즉석으로 제공할 수 있으며 "람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급된다."

  • java.util.function 패키지는

    • Predicate
    • Function<T, R>
    • Supplier
    • Consumer
    • BinaryOperator

    등을 포함해서 자주 사용하는 다양한 함수형 인터페이스를 제공한다.

  • 자바 8은 Predicate와 Function<T, R>같은 제네릭 함수형 인터페이스와 관련한 박싱 동작을피할 수 있도록 IntPredicate, IntToLongFunction 등과 같은 기본형 특화 인터페이스도 제공한다.

  • 실행 어라운드 패턴(예를 들면 자원 할당, 자원 정리 등 코드 중간에 실행해야 하는 메서드에 꼭 필요한 코드)을 람다와 활용하면 유연성과 재사용성을 추가로 얻을 수 있다.

  • 람다 표현식의 기대 형식을 "대상 형식"이라고 한다.

  • 메서드 레퍼런스를 이용하면 기존의 메서드 구현을 재사용하고 직접 전달할 수 있다.

  • Comparator, Predicate, Function 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있는 다양한 디폴트 메서드를 제공한다.

반응형