Lee's Grow up

[자바/Java] Lambda 람다 표현식, 함수형 인터페이스 본문

PROGRAMMING/JAVA

[자바/Java] Lambda 람다 표현식, 함수형 인터페이스

효기로그 2020. 5. 22. 10:08
반응형

해당 내용은 모던 자바 인 액션을 참고해서 작성한 내용입니다.

람다란 무엇인가

람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다. 람다 표현식에는 이름은 없지만, 파라미터나 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트는 가질 수 있다. 이러한 람다는 아래 4가지의 특징을 가진다.

  • 익명 : 메서드와 다르게 이름이 없으므로 익명이라고 표현한다.
  • 함수 : 람다는 메서드처럼 클래스에 종속되지 않아 함수라고 부른다.
  • 전달 : 람다 표현식을 메서드의 인수로 전달하거나 변수로 저장 가능
  • 간결성 : 많은 코드를 줄일 수 있다.

아래는 람다의 기본 구조이다
(Member m1, Member m2) -> m1.getName().compareTo(m2.getName());

위 예제처럼 람다는 { }return 문을 명시적으로 사용하지 않아도 된다. 하지만 람다 표현식 안에서 여러줄을 사용할 때는 { }를 사용해야 하며, 리턴이 있는 경우 return문을 명시적으로 선언해줘야 한다.

(int x, int y) -> { 
    System.out.println("x + y ");  
    System.out.println(" = " + (x + y)); 
    return x + y ; 
  } 

람다의 사용

람다는 그렇다면 어디에 사용할 수 있는 건가? 모든 코드를 람다로 변화 할 수 있는건가? 답은 아니다. 바로 함수형 인터페이스라는 문맥에서만 람다 표현식을 사용할 수 있다.
함수형 인터페이스하나의 추상 메서드를 지정하는 인터페이스이다. 더 많거나, 적어도 안된다. 여기서 자바 언어 설계자들은 시대의 변화에 따라 함수라는 타입을 새로 만드는 방법 대신, 자바 개발자들에게 익숙한 인터페이스를 활용해 다른 언어의 함수처럼 사용할 수 있게 만드는 방법을 제공했다.

추가로 @FunctionalInterface 어노테이션을 제공, 선언 된 인터페이스가 함수형 인터페이스가 아니면 컴파일 오류가 발생하게 기능을 추가했다.

함수형 인터페이스 사용

자바 8부터는 java.util.function 패키지로 여러 가지 함수형 인터페이스를 제공해주며. 그 중에 Predicate 함수형 인터페이스를 통해 람다의 사용법 예제를 본다.

@FunctionalInterface
public interface MemberPredicate {
     boolean test(Member member);
}

public static List<Member> filter(List<Member> members, MemberPredicate mp) {
        List<Member> result = new ArrayList<>();
        for(Member member : members) {
            if(mp.test(member)) { 
                result.add(member);
            } 
        }
        return result;
    }

위와 같은 예제가 존재한다고 가정하고, 물론 제네릭을 통해 더 재사용성이 높은 코드를 만들 수 있지만, 이전의 다른 포스팅의 예제를 사용하기 때문에 위에처럼 선언했다. 아래는 사용 방법이다.

MemberPredicate memberPredicate = (Member m) -> LEE.equals(member1.getLastName());
List<Member> result = filter(members, memberPredicate);

위에 처럼 사용 가능하며 더 코드를 줄인다면 List<Member> result = filter(members, (Member m) -> LEE.equals(m.getLastname())); 와 같이 선언해 사용할 수 있게 된다.

기본형 특화

자바는 모두 알다시피 참조형 또는 기본형으로 구성되어 있다. 그런데 public interface Predicate<T> { boolean test(T T); } 와 같이 선언된 함수형 인터페이스의 경우 기본형으로 사용하고자 할 때 불필요하게 오토박싱이 일어나 메모리를 낭비하거나, 메모리 탐색등의 과정이 추가로 필요하게 된다.

그래서 자바는 기본적으로 기본형 특화 함수형 인터페이스를 제공해준다. public interface IntPredicate { boolean test(int t);}와 같이 해당 인터페이스들은 종류가 많기 때문에, 그냥 기본형만 사용할 경우 기본형 특화 인터페이스를 사용 할 수 있다는 것을 알고 필요하면 직접 구현 또는, Java API를 참조 하면 될 것 같다.

예외 사용법

함수형 인터페이스의 함수도 일반 메서드처럼 예외를 사용할 수 있다고 설명했다. 직접 구현한 경우 문제가 되지 않는다 가령, 아래처럼 말이다.

@FunctionalInterface
public interface MemberPredicate {
     boolean test(Member member) throws Exception;
}

그러나 좀 전에 설명한 Java에서 제공해주는 기본 함수형 인터페이스의 경우 내 마음대로 인터페이스를 수정할 수 없다는 문제점이 생긴다. 그래서 명시적으로 throw new Exception() 을 발생시켜 원하는 예외처리를 할 수 있다.

람다의 형식 추론, 지역 변수

제너릭의 다이아몬드 연산자가 가능한 것 처럼 List<Member> members = new ArrayList<>() 에서의 <> 람다도 형식을 추론해 코드를 더욱 간결하게 만들 수 있다.
List<Member> result = filter(members, (Member m) -> LEE.equals(m.getLastname())); 앞서 사용한 예제 코드중 한 부분이며, 이를 다음처럼 코드를 줄일 수 있다. List<Member> result = filter(members, (m) -> LEE.equals(m.getLastname()));

람다는 지역 변수를 사용할 수 있다. 다만 지역 변수의 경우 값을 변경할 수 없다는 제약이 존재한다. 이는 동작 방식의 메커니즘에 의해서 제약이 생기게 되는데 자세한 내용은 추가로 검색하길 바란다.

메서드 참조

메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다. 아래가 예시이다.
members.sort((Member m1, Member m2) -> m1.getAge().compareTo(a2.getAge())) 를 메서드 참조를 사용하면
members.sort(Comparator.compareTo(Member::getAge)로 줄일 수 있게 된다. 이러한 메소드 참조를 만드는 3가지 방법은 아래와 같다.

  • 정적 메서드 참조 : Integer의 parseInt는 Integer::parseInt

  • 인스턴스 메서드 참조 : String의 length는 String::length

  • 개존 객체의 인스턴스 메서드 참조 : Member객체의 인스턴스 member의 getAge는 member::getAge

람다와 메서드 참조 단축 표현 예제

( Member member ) -> member.getName()           //  Member::getName
( String str, Integer i ) -> str.substring(i)   // String::substring
( String str ) -> System.out.println(str)       // System.out::println
( String str ) -> this.isValidStr(str)          // this::isValid 

위 예제들과 같이 메서드 참조를 사용하게 되면 가독성을 높일 수 있게 된다는 큰 장점이 생긴다.

생성자 참조

ClassName::new 처럼 클래스명과 new 연산자를 통해 메서드 참조처럼 기존 생성자의 참조를 만들 수 있다. 마치 메서드 참조의 정적 메서드 참조를 만드는 방법과 비슷하다.

예를들어 매개변수가 없는 기본 생성자가 존재하는 Member 클래스가 있다고 가정했을 때, 아래와 같이 사용 가능
Supplier<Member> m1 = Member::new Supplier 함수형 인터페이스는 매개변수 없이 T 타입을 리턴하는 () -> T 형식의 시그니처를 가진기 때문에 이처럼 사용이 가능하다.

그렇다면 생성자에 매개변수를 가지는 경우는 어떻게 사용할까? 바로 Function 함수형 인터페이스를 사용하면 편하게 사용이 가능하다. Member 클래스는 생성자가 나이를 매개변수로 받는다고 가정한다.

Function<Integer, Member> f = Member::new;
Member member = f.apply(28); 

위처럼 선언하면 28이라는 나이를 가진 새로운 맴버 객체를 생성하게 된다. 그렇다면 매개변수가 2개인 경우는? 다행히 BiFunction 함수형 인터페이스의 경우 ( T, U ) -> R 형식의 시그니처를 가지기 때문에 활용이 가능하다. 더 나아가, 3개 이상이거나 가변적일 경우는? 새로운 함수형 인터페이스를 선언하면 문제는 해결이 된다.

public interface TriFunction<T, U, V, R> {
    R apply ( T t, U u, V v );
}

디폴트 메서드와 메서드의 조합

아래와 같은 이름을 가지는 리스트가 있을 존재한다고 가정한다.

List<Member> members = Arrays.asList(
                    new Member(23),
                    new Member(15),
                    new Member(41),
                    new Member(61),
                    new Member(7)
                );

이때, 나이순으로 정렬을 원하는 요구가 생길 경우 아래와 같이 정렬을 쉽게 할 수 있다.

members.sort(Comparator.comparing(Member::getAge))

그런데 여기에서 결과를 내림차순으로 보고 싶다고 하면 또 다른 Comparator 인터페이스를 생성할 필요 없이, 간단하게 반전된 결과를 얻을 수 있게 된다.

members.sort(Comparator.comparing(Member::getAge).reversed())

그런데 처음 위 예제를 봤을 때 뭔가 불편한 느낌을 받았었다. 람다를 사용할 수 있다는 것은, 함수형 인터페이스라는말이고, 함수형 인터페이스는 1개의 추상 메서드를 가지는 메서드라고 배웠는데, 여기서 comparing가 해당하는 추상 메서드인건 알겠다.
그럼 reversed() 메서드는 무엇인가? 바로 자바8에 새로 추가된 디폴트 메서드이다. 디폴트 메서드는 추상 메서드가 아니기 때문에, 위와 같이 함수형 인터페이스에 선언이 가능하고 사용이 가능하다.

추가로 Comperator에는 thenComparing를 제공한다. 해당 메소드는 기존의 정렬 기준이 동일 할때 즉, 예제에선 나이가 같은 사람들이 있을 때, 추가로 조건을 줄 수 있게 해주는 메소드이다.

Predicate, Function 의 디폴트 메소드

Predicate 인터페이스의 경우도 결과를 반전시켜주는 negate(), or과 and 연산을 위한 or, and도 제공해준다.
Function 인터페이스의 경우 andThen , compose를 제공하며, andThen의 경우 결과를 전달, compose의 경우 인수로 주어진 함수를 먼저 실행한 후에 실행 f.andThen(g) -> f 실행 후 g 실행, f.compose(g) g 실행 후 f 실행

함수형 인터페이스의 종류

기본형 특화를 제외한 큰 분류의 함수형 인터페이스만 소개하겠습니다.

함수형 인터페이스 함수 디스크립터 디폴트 메소드
Predicate<T> T -> boolean and(Predicate<? super T> other) - && 연산자와 대응
or(Predicate<? super T> other) - || 연산자와 대응
negate() - ! 연산자와 대응 ( 반전 )
Consumer<T> T -> void andThen(Consumer<? super T> after) : 후 처리 연결
Function<T, R> T -> R andThen(Function<? super R, ? extends V> after ) :  후 처리 연결
compose(Function<? super R, ? extends V> after ) : 전처리 ( 인자로 받은 function을 먼저 실행 )
Supplier<T> ( ) -> T  
UnaryOperator<T> T -> T  
BinaryOperator<T> ( T, T ) -> T  
BiPrdecate<T, U> ( T, U ) -> boolean and(BiPrdecate<? super T, ? super > other) - && 연산자와 대응
or(BiPrdecate<? super T, ? super> other) - || 연산자와 대응
negate() - ! 연산자와 대응 ( 반전 )
BiConsumer<T, U> ( T, U ) -> void andThen(BiConsumer<? super T, ? super U> after) : 후 처리 연결
BiFunction<T, U, R> ( T, U ) -> R andThen(BiFunction<? super R, ? extends V> after ) :  후 처리 연결

스태틱 메소드나, 기본형 특화에 대한 참고를 원하시는분은 링크를 클릭해서 확인하시길 바랍니다. ( java 8 기준 )

반응형
Comments