FeGwan-Training / FeGwan

0 stars 0 forks source link

Chapter3 람다 표현식 #11

Closed MyeoungDev closed 1 year ago

MyeoungDev commented 1 year ago

Discussed in https://github.com/FeGwan-Training/FeGwan/discussions/10

Originally posted by **MyeoungDev** May 30, 2023 # 📕 Modern Java In Action Chapter_3 # Chapter3 람다 표현식 # 3.1 람다란 무엇인가? **람다 표현식이란?** → 메서드로 전달할 수 있는 익명함수를 단순화 한 것 람다 표현식의 특징 - 익명 - 보통의 메서드와 달리 **이름이 없으므로** **익명**이라 표현한다. - 함수 - 람다는 메서들처럼 **특정 클래스에 종속되지 않으므로 함수**라고 부른다. - 전달 - 람다 표현식을 **메서드 인수로 전달하거나 변수로 저장할 수 있다.** - 간결성 - 익명 클래스처럼 많은 자질구레한 코들르 구현할 필요가 없다. 즉, 람다를 사용하게 되면, 코드가 간결하고 유연해진다. ```java /* 익명 클래스를 이용한 Comparaotr 구현 */ Comparator byWeight = new Comparator() { public int compare(Apple a1, Apple a2) { return a1.getWeight().compareTo(a2.getWeight()); } } ``` ```java /* 람다 표현식을 이용한 코드 */ Comparator byWeight = (Apple a1, Apple a2) -> a1.getWeight.compareTo(a2.getWeight()); ``` 람다 표현식은 파라미터, 화살표, 바디로 이루어진다. ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/9c6368c9-7eac-46de-9c26-df52cb735384) - 파라미터 리스트 - Comparator의 compare **메서드 파라미터**(Apple a1, Applea2) - 화살표 - 화살표(→)는 **람다의 파라미터 리스트와 바디를 구분**한다. - 람다 바디 - 두 사과의 무게를 비교한다. **람다의 반환값**에 해당하는 표현식이다. ```java /* Java 8의 유요한 람다 표현식 */ // String 형식의 파라미터 하나를 가지며 int를 반환, // 람다 표현식에는 return이 함축되어 있으므로 명시적으로 사용하지 않아도 된다. (String s) -> s.length(); // Apple 형식의 파라미터 하나를 가지며 boolean을 반환한다. (Apple a) -> a.getWeight() > 150 // int 형식의 파라미터 두 개를 가지며 리턴값이 없다. return type void // 람다 표현식은 여러 행의 문장을 포함할 수 있다. (int x, int y) -> { System.out.println("Result: "); System.out.println(x + y); } // Apple 형식의 파라미터 두개를 가지며 int를 반환한다. (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); ``` 그렇다면, 어디에서 람다를 사용할 수 있는건가? 정답은 `함수형 인터페에스(Functional Interface)` 라는 문맥에서 람다 표현식을 사용할 수 있다. 그럼 `Functinal Interface` 가 뭔지 알아보자. # 3.2.1 함수형 인터페이스(FunctionalInterface) `함수형 인터페이스(FunctionalInterface)`란? → **정확히 하나의 추상 메서드를 지정하는 인터페이스** ```java /* 자바의 함수형 인터페이스인 것들 예시 */ // java.util.Comparator public interface Comparator { ... int compare(T o1, T o2); ... } // java.lang.Runnable public interface Runnable { ... void run(); ... } // java.awt.event.ActionListener public interface ActionListener extends EventListener { ... void actionPerformed(ActionEvent e): ... } // java.util.concurrent.Callable public interface Callable { ... V call() throws Exception; ... } ``` Java 8 부터 `default method` 를 통해 하나의 인터페이스에 많은 디폴트 메서드가 존재할 수 있다. 그러나, **많은 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스 이다.** 그렇다면, 함수형 인터페이스로 뭘 할 수 있을까? → 람다 표현식으로 **함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있다.** → **전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다.** 이를통해, 익명 클래스를 사용하는 방식보다 더 깔끔하고 간결한 코드를 작성할 수 있다. ```java public static void process(Runnable r) { r.run(); } // 람다 표현식 사용 Runnable r1 = () -> System.out.println("Hello World Lamda"); // 익명 클래스 사용 Runnable r2 = new Runnable() { public void run() { System.out.println("Hello World Anonymous Class"); } } process(r1); process(r2); // 직접 전달된 람다표현식으로 출력 process(() -> System.out.println("Hello World Direct Transfer Lamda Expression"); ``` 그렇다면 우리가 한번씩 봤던 `@FunctinalInterface` 얘는 뭐하는 걸까? → `@FunctionalInterface` 는 함수형 인터페이스임을 가르키는 어노테이션이다. → 만약 해당 어노테이션을 선언 하였지만, 함수형 인터페이스가 아닌 경우에는 에러를 발생시킨다. → 예를 들어, 추상 메서드가 한 개 이상이라면 `Multiple nonoverriding abstract mehods found is interface ` ## 함수 디스크립터(function descriptor) 함수형 인터페이스의 추상 메서드 시그니처(signature: 메서드 이름과 메서드 매게변수 리스트를 나타내는 것) 는 람다 표현식의 시그니처를 서술하는 메서드를 **함수 디스크립터(function descriptor) 라고 부른다.** `Predicate` , `Consumer`, `Function` , `Supplier` 등이 자바에 존재하는 함수 디스크립터이다. 이에 대해서는 뒤에 더 자세히 설명하겠다. 여기까지는, 람다 표현식은 **변수에 할당하거나 함수형 인터페이스를 인수로 받는 메서드로 전달할 수 있으며,** **함수형 인터페이스의 추상 메서드와 같은 시그니처를 갖는다**는 사실을 기억하고 있으면 된다. # 3.3 람다 활용 : 실행 어라운드 패턴 **실행 어라운드 패턴(Execute Around Pattern)** 이란? → 설정(SetUp) 과 정리(CleanUp) 두 과정이 둘러싸는 형태를 갖는 것을 실행 어라운드 패턴이라고 한다. ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/2e9fb93e-d267-4dc7-bc20-02c4db96f686) 그렇다면 위와 같이 람다를 활용하여 실행 어라운드 패턴으로 아래 코드를 재구성 해보자! > try-with-resources 구문을 사용하여 데이터 파일 읽어들이는 코드 > ```java public String processFile() throws IOException { try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { return br.readLine(); } } /* 만약 여기서 한줄이 아닌 두줄을 읽고 싶다면? 두줄로 수정 한 후 두줄이 아닌 세줄이라면?? 계속해서 읽어들이는 요구사항이 변한다면??? */ ``` > 함수형 인터페이스를 이용해서 동작 파라미터화 하기 위한 인터페이스 선언 > ```java public interface BufferedReaderProcessor { String process(BufferedReader b) throws IOException; } public String processFile(BufferedReaderProcessor p) throws IOException { ... } ``` > 실행을 담당할 메서드 정의 > ```java public String processFile(BufferedReaderProcessor p) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { return p.process(br); } } ``` > 람다를 이용한 다양한 동작 전달해서 사용 > ```java String oneLine = processFile((BufferedReader br) -> br.readLine()); String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine()); ``` 위의 4가지 단계를 통해 실행 어라운드 패턴을 적용할 수 있다. 이는 람다와 동적 파라미터화로 유연하고 간결한 코드를 구현하는 데 도움을 주는 예제이다. # 3.4 함수형 인터페이스 사용 Java 8 에서는 `java.util.function` 이라는 패키지로 여러 가지 새로운 함수형 인터페이스를 제공한다. 이는 - **Predicate** - 제네릭 형식의 T의 객체를 인수로 받아 `test()` 라는 메서드를 통해 boolean을 반환한다. ```java Predicate startsWithSeoul = (str) -> str.startsWith("Seoul"); System.out.println(startsWithSeoul.test("SeoulCity")); // true 출력 ``` ```java /* Predicate를 이용한 필터링 하는 예제 */ public List filter(List list, Predicate p) { List results = new ArrayList<>(); for(T t : list) { if(p.test(t)) { results.add(t); } } return results; } Predicate nonEmptyStringPredicate = (String s) -> !s.isEmpty(); List nonEmpty = filter(listOfStrings, nonEmptyStringPredicate); ``` ```java Predicate redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150) .or(apple -> GREEN.equals(a.getColor())); ``` - **Consumer** - 제네릭 형식 T의 객체를 인수로 받아 void를 반환하는 `accept()` 라는 추상 메서드를 정의한다. ```java Consumer printCity = (city) -> System.out.println(city); printCity.accept("Seoul"); ``` ```java public void forEach(List list, Consumer c) { for(T t : list) { c.accept(t); } } forEach( Arrays.asList(1, 2, 3, 4, 5), (Integer i) -> System.out.println(i) } ``` - ****************************Function**************************** - 제네릭 형식 T의 객체를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 `apply()`를 정의한다. ```java Function plus10 = (number) -> number + 10; System.out.println(plus10.apply(10)); // 20 출력 ``` ```java public List map(List list, Function f) { List result = new ArrayList<>(); for(T t : list) { result.add(f.apply(t)); } } List l = map ( Arrays.asList("lambdas", "in", "action"), (String s) -> s.length() ); ``` - **Supplier** - 매게변수를 받지 않고, 단순히 무언가를 반환하는 추상 메서드 `get()`이 존재한다. ```java Supplier helloSupplier = () -> "Hello "; System.out.println(helloSupplier.get() + "World"); // Hello World ``` --- 아래의 코드는 해당 함수형 인터페이스를 이용하여 구현된 코드이다. ```java /* java.util.Stream 라이브러리에 IntPipeline 에 구현되어있는 filter() 메서드 */ public final IntStream filter(final IntPredicate predicate) { Objects.requireNonNull(predicate); return new StatelessOp(this, StreamShape.INT_VALUE, StreamOpFlag.NOT_SIZED) { Sink opWrapSink(int flags, Sink sink) { return new Sink.ChainedInt(sink) { public void begin(long size) { this.downstream.begin(-1L); } public void accept(int t) { if (predicate.test(t)) { this.downstream.accept(t); } } }; } }; } ``` 위의 코드를 보면 `IntPredicate` 형의 `Predicate` 타입의 매개변수를 받고 `test()` 메서드를 수행한 것을 볼 수있다. 또한, `downstream`변수의 경우 따라 올라가서 보게되면 ```java protected final Sink downstream; ``` 위와 같이 선언되어 있고 `Sink` 인터페이스의 경우 ```java interface Sink extends Consumer ``` 위와 같이 선언되어 있다. ### 그런데 람다에서 예외처리는 어떻게? **함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않는다.** 즉, 예외를 던지는 람다 표현식을 만들려면 확인된 **예외를 선언하는 함수형 인터페이스를 직접 정의**하거나, 람다를 `try/catch` 블럭으로 감싸야 한다. ```java Function f = (BufferedReader b) -> { try { return b.readLine(); } catch(IOException e) { throw new RuntimeException(e); } }; ``` # 3.5 형식 검사, 형식 추론, 제약 **람다 표현식 자체에는** 람다가 **어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않다.** 그렇다면, 컴파일러는 어떻게 람다의 형식을 확인할까? ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/43707444-4520-4eec-a5b0-7115d260fd6f) 위의 사진은 람다 표현식의 형식 검사 과정을 나타내는 것이다. 1. `filter` 메서드의 선언을 확인한다. 2. `filter` 메서드는 두 번째로 파라미터로 `Predicate` 형식을 기대한다. 3. `Predicate`은 `test`라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스이다. 4. `test` 메서드는 Appl을 바아 boolean을 반환하는 함수 디스크립터를 묘사한다. 5. `filter` 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다. 이와 같이 **대상 형식(Traget Typing)**이라는 특징 때문에 **같은 람다 표현식이라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다.** ```java /* Callable 과 PrivilegedAction 모두 인수를 받지않고 제네릭 형식 T를 반환하는 함수형 인터페이스*/ Callable c = () -> 42; PrivilegedAction p = () -> 42; ``` 그렇다면 아래의 경우는 어떻게 될까? ```java Object o = () -> {System.out.println("Tricky example"); ``` 이 경우는 **컴파일 에러**가 나게 된다. 그 이유는 해당 람다식의 경우 **어떠한 함수형 인터페이스를 참조해야 할지 명확하지 않기 때문**이다. 이와 같은 경우는 아래와 같이 **어떤 메서드의 시그니처가 사용되어야 하는지를 명시적으로 구분하도록 람다를 캐스트 하여 사용**하면 된다. ```java Object o = (Runnable) () -> {System.out.println("Tricky example")}; ``` 이렇듯 자바 컴파일러는 람다 표현식과 관련된 함수형 인터페이스를 추론한다. 함수형 인터페이스의 추론이 올바르게 되었다면, **파라미터 형식도 추론이 가능하게 된다.** 즉, 파라미터의 형식을 명시적으로 작성할 수도 있고, 생략할 수도 있다. ```java /* 파라미터 형식을 추론하지 않음*/ Comparator c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); /* 파라미터 형식을 추론*/ Comparator c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); ``` 어느 코드가 더 좋은가? 그건 잘 모르겠다. 정해진 규칙은 없고, 개발자 스스로 어떤 코드가 가독성을 향상시킬 수 있는지 경정해야 한다. ### 지역 변수의 제약 람다 표현식에서는 익명 함수가 하는 것처럼 **자유 변수(free variable: 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)**를 사용할 수 있다. 이와 같은 동작을 **람다 캡처링(capturing lambda)** 라고 부른다. ```java int portNumber = 1234; Runnable r = () -> System.out.println(portNumber); ``` 하지만, 자유 변수에도 제약이 존재한다. 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡처할 수 있다. ```java /* 할당 되었던 portNumber가 재할당 되면서 컴파일 에러가 발생한다. */ int portNumber = 1234; Runnable r = () -> System.out.println(portNumber); int portNumber = 1111; ``` 이를 제약사항을 걸어둔 이유는, 다른 스레드에서 변수 할당이 해제되었는데, 해당 변수를 다른 스레드에서 접근하려 할 수 있다. 따라서, 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것이다. # 3.6 메서드 참조 메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다. ```java /* 기존 코드 */ inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())); /* 메서드 참조 코드*/ inventory.sort(comparing(Apple::getWeight)); ``` 그렇다면 이 메서드 참조가 왜 중요한가? - 메서드 호출에서 어떻게 메서드를 호출해야하는지 설명보다 **메서드명을 직접 참조하는 것이 편리하다.** - **기존 메서드 구현으로 람다 표현식을 만들 수 있다.** - **명시적으로 메서드명을 참조함으로써 가독성을 높일 수 있다.** 컴파일러는 람다 표현식의 형식을 검사하던 방식과 비슷한 과정으로 메서드 참조가 주어진 함수형 인터페이스와 호환하는지 확인한다. 즉, 메서드 참조는 콘텍스트의 형식과 일치해야 한다. ### 생성자 참조 `ClassName::new` 처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다. ```java /* Supplier를 이용한 방식*/ Supplier c1 = Apple::new; Apple a1 = c1.get(); /* Function을 이용한 방식*/ Function c2 = Apple::new; Apple a2 = c2.apply(110); /* 위를 응용하여 다양한 무게를 갖는 사과 리스트를 만드는 방식 */ List weights = Arrays.asList(100, 120, 140, 150); List apples = map(weights, Apple::new); public List map(List list, Function f) { List result = new ArrayList<>(); for(Integer i : list) { result.add(f.apply(i)); } return result; } ``` # 3.10 요약, 마침 - **람다 표현식은 익명 함수의 일종이다**. 이름은 없지만, 파라미터 리스트, 바디, 반환 형식을 가지며 예외를 던질 수 있다. - 람다 표현식으로 간결한 코드를 구현할 수 있다. - **함수형 인터페이스는 하나의 추상 메서드만을 정의하는 인터페이스이다.** - 함수형 인터페이스를 기대하는 곳에서만 람다 표현식을 사용할 수 있다. - **람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급된다.** - java.util.function 패키지에는 자주 사용하는 다양한 함수형 인터페이스를 제공한다. - 실행 어라운드 패턴을 람다와 활용하면 유연성과 재사용성을 추가로 얻을 수 있다. - 메서드 참조를 이용하면 기존의 메서드 구현을 재사용하고 직접 전달할 수 있다. - `Comparator`, `Predicate` , `Function` 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있는 다양한 디폴트 메서드를 제공한다.
HoFe-U commented 1 year ago

수고하셨습니다.