JAVA 8 : 람다 표현식

600g (Kim Dong Geun)·2021년 4월 2일
1

오랜만에 포스팅이다. 👨‍💻

Lambda Expression

  • 함수 타입을 표현할 때 추상 메소드를 하나만 담은 인터페이스(함수형 인터페이스)를 람다식을 사용해 코드를 재정의 하는 기술
  • JAVA 8 API 에서 추가된 기술
  • 익명 클래스를 상속하는 방식은 코드가 길어지나, 람다식을 이용하여 재정의하면 유지보수에 도움이 됨.
  • 익명클래스를 사용한 코드를 다음과 같이 줄일 수 있다.
//익명클래스
Arrays.sort(words, new Comparator<String(){
  public int compare(String s1,String s2){
    return Integer.compare(s1.length(),s2.length());
  }
});

//Lambda식
Arrays.sort(words, (s1,s2)-> Integer.compare(s1.length(),s2.length()));
  

Lambda는 Syntatic Sugar가 아니다.

  • 익명 클래스는 인스턴스를 생성해야 하지만, 함수는 평가될 때마다 새로 생성되지 않습니다. 함수를 위한 메모리 할당은 자바 힙의 Permanent 영역에 한 번 저장됩니다.
  • 객체는 데이터와 밀접하게 연관해서 동작하지만, 함수는 데이터와 분리되어 있습니다. 상태를 보존하지 않기 때문에 연산을 여러 번 적용해도 결과가 달라지지 않습니다(멱등성).
  • 클래스의 스태틱 메소드가 함수의 개념과 가장 유사합니다.
  • 람다에서의 this는 람다 블록의 범위입니다. (익명 클래스와 차이)

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

  • 함수형 인터페이스는 하나의 추상 메소드로 이루어진 메소드

  • @FunctionalInterface 를 선언하여 사용가능

  • 컴파일러는 함수형 인터페이스에 정의된 정보를 참조하여 타입을 추론.

    • Arrays.sort(words, (s1,s2)-> Integer.compare(s1.length(),s2.length()));
    • 위 코드에서 s1과 s2의 타입을 명시하지 않아도 String Type인 것을 알 수 있음.
  • 선언방법

@FunctionalInteface
interface A {
  void apply();
}
  • Functinal Inteface를 상속한 인터페이스는 똑같이 Functional Interface로 사용 가능합니다.
interface B extends A{
  
}

//또한 속성또한 이어받으므로 하나의 메소드 이상 추가 할 수 없음
// 하나의 추상메소드 외에 메소드 추가 불가
interface B extends A {
  void illegal(); // error
}

Lambda 사용법

@FunctionalInterface
interface Calculation {
  Integer apply(Integer x, Integer y);
}

static Integer calculate(Calculation operation, Integer x, Integer y) {
  return operation.apply(x, y);
}

// 람다 생성
Calculation addition = (x, y) -> x + y;
Calculation subtraction = (x, y) -> x - y;

// 사용
calculate(addition, 2, 2);
calculate(substraction, 5, calculate(addition, 3, 2));

메소드 참조

  • 함수형 인터페이스를 매핑하는 기술
  • 잘 사용하면 Lambda 코드를 좀 더 간결하게 사용할 수 있음
//Lambda식
Arrays.sort(words, (s1,s2)-> Integer.compare(s1.length(),s2.length()));

//메소드 참조
Arrays.sort(words, Comparator.comparingInt(String::length));
  • 메소드 참조의 유형은 총 5가지로 존재.

    • 정적 : 정적 메소드를 가리키는 메소드 참조
  //정적 메소드 참조
  Integer::parseInt
  
  //같은 기능의 람다
  str -> Integer.parseInt(str)
  • 한정적 : 수신 객체를 특정하는 메소드 참조
  Instant.now()::isAfter
  
  //같은 기능의 람다
  Instance then = Instant.now();
  t -> then.isAfter(t);
  • 비한정적 : 수신 객체를 특정하지 않는 메소드 참조
  String::toLowCase
    
  //같은 기능의 람다
  str -> str.toLowerCase()
  • 클래스 생성자
  TreeMap<K,V>::new
    
  //같은 기능의 람다
  () -> new TreeMap<K,V>()
  • 배열 생성자
  int[]::new
    
  //같은 기능의 람다
  len -> new int[len]

Predicate<T>

  • java.util.function.Predicate
  • T 형식을 받아 boolean 객체 리턴
@FunctionalInterface
public interface Preidcate<T> { // T 형식을 받아 boolean 반환
  boolean test(T t);
}

public boolean isGood(Predicate<Integer> func){
  return func.test();
}

isGood((data)-> data%2 ==0);

Consumer<T>

  • java.util.function.Consumer
  • T형식을 받아서 void 객체 리턴
@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));

Function<T,R>

  • java.util.function.Function

  • T 인수를 받아서 R 객체를 리턴

@FunctionalInterface
public interface Function<T,R>{
  R apply(T t);
}

public String toString(Function<Integer,String> func){
  return func.apply();
}

String data = toString(it -> Integer::toString);
//딱히 생각나는 예제가 없어서...

이외에도...

  • Supplier\ : () -> T
  • UnaryOperator\ : T -> T
  • BinaryOperator\ : (T, T) -> T
  • BiPredicate<L,R> : (T, U) -> boolean
  • BiConsumer<T,U> : (T,U) -> void
  • BiFunction<T,U,R> : (T,U) -> R

등이 존재.

Unboxing 함수형 인터페이스

  • 기본적으로 객체들은 Boxing된 객체
  • Java에서 int, long, byte, float, double등 언박싱 객체도 존재
  • 자바에서는 Boxing <-> UnBoxing 하는 과정에서 비용 발생
  • 따라서 함수형 인터페이스에 int,long,byte를 파라미터로 대입시 Wrapping 하는 비용이 발생
  • 이런 AutoBoxing 비용을 해소해주는 함수형 인터페이스가 존재.

사용법

  • 특정 형식을 입력받는 함수형 인터페이스의 이름앞에 형식명을 붙이면된다
  • DoublePredicte
  • IntConsumer
  • LongBinaryOperator
  • IntFunction

형식추론

  • 람다식은 관련된 함수형 인터페이스를 통해 형식을 추론할 수 있다.
  @FunctionalInterface
  public interface Example {
    public void test(Integer t);
  }
  
  public void hello(Example ex){
    ex.test();
  }
  //Example.test가 Integer 를 Parameter로 받고 있기 때문에 a가 Integer형임을 알 수 있다.
  hello( a -> System.out.println(a+1)); 
  

지역변수 사용

  • 람다 캡쳐링 : 외부에서 정의된 함수를 람다함수 내부에서 사용하는 것
  • 다만, 람다 캡쳐링은 정적변수만을 캡쳐링 할 수 있다.
int portNumber = 8888;
Runnable r = () -> System.out.println(portNumber); //사용불가 -> 컴파일 단계에서 Error
portNumber =31337;

왜 이런 제약이 발생하냐면, 지역변수와 인스턴스 변수의 차이를 이해하면 알 수 있다.

지역변수는 스택에 위치하고, 인스턴스 변수는 힙에 위치한다. 람다가 지역변수에 바로 접근할 수 있는 가정하에 람다가 쓰레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수할당이 해제 되었는데도 람다를 실행하여, 스레드는 해당 변수에 접근할 수 있다.

따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 지역 변수의 복사본을 제공한다.

그렇기 때문에 지역변수에는 한번만 값을 할당해야 된다 라는 제약이 생김.

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

  • 자바 8 API의 몇몇 함수형 인터페이스는 다양한 유틸리티 메소드 포함.
  • 예를들어 Comparator, Function, Predicate와 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메소드 제공
  • 이를 디폴트 메소드 라고한다.

Comparator 조합

  • Comparator.comparing을 이용해서 비교에 사용할 키를 추출하는 Function 기반의 Comparator를 반환
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

역정렬

  • Comparator는 인터페이스 자체에서 주어진 비교자의 순서를 뒤바꾸는 디폴트 메소드
inventory.sort(comparing(Apple::getWeight).reversed());

Comparator 연결

  • 만약 비교하는 대상이 같을경우에는 어떻게 사과를 나열해야 할까?
  • thenComparing을 사용해서 다음 비교를 할 수 있다.
inventory.sort(comparing(Apple::getWeight)
               .reversed()
               .thenComparing(Apple.getCountry));

Predicate 조합

  • Predicate 인터페이스는 negate,and,or 세가지 디폴트 메소드를 제공한다.
  //기존 Predicate 객체의 결과를 반전
  Preicate<Apple> notRedApple = redApple.negate();
  
  //and 메소드를 이용해서, 빨간색이면서, 무거운 사과를 선택하도록 조합.
  Preicate<Apple> redApple = redApple.and(apple -> apple.getWeight()> 150);
  
  // 빨갛거나 초록색 사과 이면서 무거운 사과 조합
  Predicate<Apple> redAndGreenApple = redApple
    																	.and(apple -> apple.getWeight() > 150)
    																	.or(apple -> GREEN.equals(apple.getColor()));
    
profile
수동적인 과신과 행운이 아닌, 능동적인 노력과 치열함

0개의 댓글