[자바 인 액션] Ch 3

Ericamoyed·2021년 4월 26일
0

자바인액션

목록 보기
3/6

람다표현식

람다

  • 익명 클래스처럼 이름이 없는 함수면서 메서드를 인수로 전달할 수 있는, 익명클래스와 비슷한 아이
  • 메서드로 전달할 수 있는 익명 함수를 단순화한 것
  • 람다는 메서드처럼 특정 클래스에 종속되지 않고, 오히려 functional interface 구현에 가까우므로 메서드가 아니라 함수라고 부른다
  • 효과: 익명 클래스 등 판에 박힌 코드를 구현할 필요가 없음
  • ex) Comparator: 람다표현식을 이용하면 compare aㅔ서드의 바디를 직접 전달하는 것처럼 코드 전달 가능
  • 람다 표현식의 구성
    • 람다 파라미터
    • 화살표
    • 람다 바디
      • 여러 행의 문장을 포함할 수 있다.
  • 람다의 기본 문법
    • (parameters) -> expression;
    • (parameters) -> {statements;}

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

  • ex) Predicate<T>
    • 오직 하나의 추상 메서드 boolean text (T t); 만 지정되어 있다.
  • 정의: 정확히 하나의 추상 메서드를 지정하는 인터페이스
    • 추상 메서드가 하나라는 거지, default method는 여러개여도 상관 없다.
    • 즉 많은 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스이다.
  • 다른 예시로는 Comparator, Runnable 등이 있다.
  • 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있다.
    • 즉, 함수형 인터페이스를 구현한 클래스의 인스턴스로서 동작하게 할 수 있다.
  • 함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있는 것
    • 지금까지 난 lambda 넘길 때마다 method가 parameter로 넘어가는 건줄 알았는데, 그게 아니라 FI를 parameter로 가지고 있는 메소드에 람다를 넘기고 있는 거였음.. 단지 람다가 익명클래스를 만드는 부분까지도 모두 간략하게 생략되어 있어서 그렇게 느껴지지 않았을 뿐..
    • filter에 넘기던 lambda method도 Predicate FI를 param으로 받는거고, 요거를 람다가 익명클래스 전달하는 느낌으로 구성되어 있던 것이었다
  • @Functional Interface
    • 함수형 인터페이스임을 가리키는 어노테이션
    • 요것이 어노테이션으로 붇어있는데, 해당 interace에 두개 이상의 abstract method가 있다면 컴파일 에러 발생

함수 디스크립터

  • 람다 표현식의 시그니처를 서술하는 메서드
    • () -> void
    • (Apple, Apple) -> int
  • 람다 표현식은 변수에 할당하거나, 함수형 인터페이스를 인수로 받는 메서드로 전달할 수 있으며, FI의 추상 메서드와 동일한 시그니처를 갖는다.
  • 자바 언어 명세에서는 void를 반환하는 경우는 특별한 규칙을 정하고 있어, 한 개의 void 메소드 호출은 중괄호로 감싸지 않아도 된다.

람다 활용: 실행 어라운드 패턴

  • 초기화/준비 코드 -> 작업 A -> 정리/마무리 코드
  • 초기화/준비 코드 -> 작업 B -> 정리/마무리 코드
  • 요런식으로 반복되는 코드를 한번 짜고 재사용하기 위해서는 동작 파라미터화를 기억!
  • 동작 파라미터로 람다를 넘기기 위해서는, 일단 함수형 인터페이스의 정의 필요, 그리고 해당 FI를 param으로 받도록 메소드 구성

여러가지 FI

  • Predicate(boolean test)
    • T 형식의 객체를 사용하는 boolean 표현식이 필요한 상황에서 이용
  • Consumer(void accept)
    • T 형식의 객체를 받아서 void를 반환하는 표현식이 필요한 상황에서 이용
  • Function(R apply)
    • T 형식의 객체를 받아서 R 객체를 반환하는, 즉, 입력을 출력으로 매핑해야하는 상황에서 이용
  • 위의 아이들은 모두 제네릭 파라미터로 구성돼있는데, 제네릭의 내부구현은 참조형(Reference Type)만 사용 가능하도록 되어 있어, 기본형(Primitive Type)은 해당 FI를 모두 사용할 수 없다.
    • 따라서 IntPredicate, DoublePredicate 등 기본형에 대한 Predicate 등은 공통화를 못시키고 직접 제공한다.
    • primitive(int) -> reference(Integer): boxing
    • reference(Integer) -> primitive(int): unboxing

람다의 실제 형식

  • 람다는 FI의 인스턴스를 만드는 역할을 하지만, 람다 자체에 어떤 FI를 구현하는지에 대한 정보는 담겨있지 않다. (익명 클래스 등으로 모두 생략되어 있음)
    그러면 람다가 어떤 FI를 구현하는지 어떻게 알까?
    -> 람다가 사용되는 context를 통해 람다의 type을 추론한다! (형식 추론)
    -> 따라서 같은 람다 표현식이라도 호환되는 다른 abstract method를 가진 FI로 대응 및 사용될 수 있다.
    -> context를 통해 lambda의 시그니처도 추론 가능하므로, 람다 문법에서 시그니처도 생략이 가능하다.
    • ex) (Apple a1, Apple a2) -> a1
    • ex) (a1, a2) -> a1
  • 람다 표현식 내부에서 예외가 발생 가능한 상황이라면 abstract method도 같은 예외를 던질 수 있도록 throws로 선언해야한다

람다의 여러가지 제약사항

  • 람다 내부에서 바디 안에 있는 지역 변수를 참조하지 않아야 한다.
  • 람다는 instance 변수와 static 변수를 자유롭게 캡처해서 내부에서 사용할 수 있지만 한가지 제약 사항이 있다면
  • method 내에서 선언된 지역변수는 final로 선언되어 있어야 하거나, 실질적으로 final된 변수와 똑같이 사용되어야 한다!
  • 즉, 람다 내에서 지역변수를 바꾸거나, 람다가 끝난 이후에 지역변수가 바뀌는 상황이 있으면 안된다
    • 람다 표현식은 한번만 할당할 수 있는 지역변수만을 캡처할 수 있다!
  • 원인
    • 인스턴스 변수: 힙에 저장, 지역변수: 스택에 저장
    • 람다 function이 다른 스레드에서 수행되게 되면, 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다 실행 스레드에서 해당 변수에 접근하려는 상황이 발생 가능
    • 이러한 상황 때문에, 람다에서는 지역변수를 직접 이용하는 것이 아니라, 지역 변수의 복사본을 사용한다. 복사본을 사용하게 되면 스레드 때문에 접근 못하는 상황이 발생하지 않으니까!
      • 사실 요건 람다 뿐만 아니라 약간 자바 자체가 그런건데, 자바는 Call by Reference가 불가능하도록 설계되어 있다. cpp에서는 &등을 넘겨서 주소 값을 넘겨 받아 reference 값 자체 수정이 가능하지만, 자바에서는 다른 메소드 parameter로 넘기는 순간 해당 값을 복사해서 가져가기 때문에 Call by Value만이 가능하다.
    • 여튼 복사본을 사용하는데, 타 스레드로 수행되는 구조면 지역 변수 값이 언제 지정되느냐에 따라서 복사본의 값이 달라지는 비일관성 문제가 있을 수 있기 때문에 복사본의 값이 일관성을 갖도록 local variable을 사용할 경우에는 final 변수만 접근 가능하다는 제약이 생긴 것이다.
  • final 키워드가 붙어도 지역변수는 항상 스택에 저장되는가?
    • _final이던 아니던, 지역변수는 항상 스택 ㅇㅇ. 단지 final 키워드는 해당 변수 값 변경만 불가능 한 것. 추가적으로 GC 대상 범위는 heap인데, 스택에서 사용되는 변수를 보면서 heap을 트래킹해서 사용하는 객체들을 마킹하고, 마킹이 안된 객체들 대상으로 메모리 제거 처리를 수행 (MarkAndSweep)
  • 인스턴스 변수는 스레드가 공유하는 힙에 존재하므로 로컬변수와 같은 특별한 제약이 존재하지 않는다.

메서드 참조

  • (Apple a) -> a.getWeight() -> Apple::getWeight
  • 메서드 참조로 교환 가능한 방식 세가지
    • Function<String, Integer> stringToInteger
    • = (String s) -> Integer.parseInt(s)
    • = Integer::parseInt;
    • BiPredicate<List<String>, String>> contains
    • = (list, element) -> list.contains(element)
    • = List::contains;
    • Predicate<String> startsWithNumber
    • = (String string) -> this.startsWithNumber(string)
    • = this::startsWithNumber

생성자 참조

  • ClassName::new를 사용해서 기존 생성자의 참조 만들기
  • Apple(Integer weight) 시그니처를 갖는 생성자가 있다면
    Function<Integer, Apple> c2 = Apple::new;
    Apple a2 = c2.apply(110);
    Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
    Apple a2 = c2.apply(110);
    • 상기 두 코드는 동일한 코드로 동작한다.

람다 표현식 조합이 가능한 useful method

  • Compaerator, Function, Predicate 등의 FI는 람다 표현식 조합이 가능하도록 몇가지 유용한 utility method를 제공
  • FI의 default method는 static이어야만 하고 뭐 그런 제약이 있나?
    • static/dynamic 모두 가눙. 다만 static은 override를 못하고, dynamic은 override가 가능하다 정도의 차이만 있음
  • Comparator
    • comparing, reversed
      inventory.sort(Comparator.comparing(Apple::getWeight).reversed());
    • thenComparing: 두 객체가 같을 시 다음 비교 조건 제시
      inventory.sort(Comparator.comparing(Apple::getWeight)
        .reversed()
        .thenComparing(Apple::getCountry));
  • Predicate
    • negate, and, or
    Predicate<Apple> notRedApple = redApple.negate();
    Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);
    • 단순한 람다 표현식의 조합으러 더 복잡한 람다 표현식 생성이 가능하다!
  • Function
    • andThen, compose
      • andThen : g(f(x))
      • compose : f(g(x))

결론

  • FI를 기대하는 곳에서만 람다 표현식 사용 가능
  • 람다 표현식 전체가 FI의 인스턴스로 취급
profile
꿈많은 개발자, 일상 기록을 곁들인

0개의 댓글