[java][lambda] 람다식(Lambda Expression)

2022. 8. 23. 13:22JAVA/Language

1. 람다식이란 무엇인가?

  • 람다식(Lambda Expression)이란 메서드를 하나의 '식(expression)'으로 표현한 것입니다.
  • 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 '익명 함수(anonymous function)'이라고도 합니다.

예를 들어 1~5사이의 랜덤한 수를 반환하는 메서드를 정의한다면 다음과 같이 정의할 수 있을 것입니다.

int method(){
	return (in)(Math.random()*5)+1;
}

위와 같은 메서드 정의를 다음과 같이 람다식으로 표현할 수 있습니다.

() -> (int)(Math.random() * 5) + 1)

 

람다식의 특징

  • 클래스의 생성 없이 람다식 자체만으로 메서드의 역할을 수행할 수 있음
  • 람다식은 메서드의 매개변수로 전달되어 지는 것이 가능하고, 메서드의 결과로 반환될 수 있음
  • 람다식으로 인해 메서드를 변수처럼 다루는 것이 가능함

 

2. 람다식 작성하기

람다식은 익명 함수답게 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통{} 사이에 '->'를 추가하여 작성할 수 잇습니다.

반환타입 메서드이름(매개변수 선언){
	문장들
}

=>

(매개변수 선언) ->{
	문장들
}

 

예를 들어 2개의 매개변수 값중 더 큰 값을 반환하는 max 메서드를 다음과 같이 표현이 가능합니다.

int max(int a, int b){
	return a > b ? a : b;
}

=>

(int a, int b) ->{
	return a > b ? a : b;
}

=>

(int a, int b) ->{
	a > b ? a : b
}

=>

(a, b) -> a > b ? a : b

 

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

람다식은 익명 클래스의 객체와 동등합니다.

(int a, int b) -> a > b ? a : b

<->

new Object(){
	int max(int a, int b){
        return a > b ? a : b;    
    }
}

위와 같은 람다식이나 익명 클래스의 객체는 참조변수가 가리키지 않으면 바로 사라집니다. 따라서 람다식을 다음과 같이 어떤 참조변수가 가리키게 할 수 있습니다.

타입 f = (int a, int b) -> a > b ? a : b;

위와 같이 참조변수 f의 타입을 정의하기 위해서는 함수형 인터페이스를 사용할 수 있습니다.

 

함수형 인터페이스 정의하여 참조변수로 가리키기

@FunctionalInterface
interface MyFunction{
	public abstract int max(int a, int b);
}

MyFunctionf f = (int a, int b) -> a > b ? a : b;
int big = f.max(5,3);

위의 코드에서 람다식이 추상 메서드 max와 매개변수의 타입과 반환값이 일치하기 때문에 가리킬 수 있습니다.

 

함수형 인터페이스 타입의 매개변수와 반환타입

다음과 같이 함수형 인터페이스를 정의하고 다른 클래스의 메서드에서 매개변수로 선언하여 함수형 인터페이스의 메서드를 가리킬 수 있습니다.

@FunctionalInterface
interface MyFunction{
	void myMethod();
}

void aMethod(MyFunction f){
	f.myMethod();
}
...

MyFunction f = ()->System.out.println("hello!");
aMethod(f);

 

메서드의 반환타입이 함수형 인터페이스 타입이면 함수형 인터페이스 타입을 반환할 수 있습니다.

@FunctionalInterface
interface MyFunction{
    void myMethod();
}

MyFunction myMethod(){
    MyFunction f = ()->{};
    return f;
}

위 코드들을 통하여 람다식을 참조변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고 받을 수 있다는 것을 의미합니다. 즉, 변수처럼 메서드를 주고받는 것이 가능해집니다.

 

 

람다식의 타입과 형변환

  • 함수형 인터페이스로 람다식을 참조 할 수 있는 것일 뿐, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아님
  • 람다식은 익명 객체이고 익명 객체는 타입이 없음

 

함수형 인터페이스 타입의 참조변수가 람다식을 가리키기 위해서 형변환이 다음과 같이 발생합니다.

MyFunction f = (MyFunction) (() => {}); // 양변의 타입이 다르므로 형변환이 필요함

 

람다식을 Object 타입으로 형변환하기 위해서는 함수형 인터페이스 타입으로 형변환 한뒤에 Object 타입으로 형변환을 수행해야 합니다.

Object obj = (Object) (MyFunction) (() -> {});
String str = ((Object) (MyFunction) (() -> {})).toString();

 

4. java.util.function 패키지

java.util.function 패키지의 주요 함수형 인터페이스

java.util.function 패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았습니다.

        Supplier<Integer> supplier = ()->(int)((Math.random()*100)+1);
        Consumer<Integer> consumer = i->System.out.print(i + " ");
        Predicate<Integer> predicate = i->i%2==0;
        Function<Integer, Integer> function = i->i/10*10;

 

조건식의 표현에 사용되는 Predicate

Predicate는 Function의 변형으로 반환타입이 boolean이라는 것만 다릅니다.

        Predicate<Integer> predicate = i->i%2==0;

 

매개변수가 두 개인 함수형 인터페이스

매개변수의 개수가 2개인 함수형 인터페이스는 이름 앞에 접두사 'Bi'가 붙습니다.

만약 3개 이상의 매개변수를 갖는 함수형 인터페이스를 선언한다면 다음과 같이 정의할 수 있습니다.

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

 

UnaryOperator와 BinaryOperator

UnaryOperator와 BinaryOperator 함수형 인터페이스는 Function의 변형으로 대표적인 특징은 입력 매개변수의 타입과 반환 매개변수의 타입이 동일하다는 점입니다. 그리고 매개변수의 개수가 한개이면 UnaryOperator, 두개이면 BinaryOperator라고 표현합니다.

 

컬렉션 프레임웤과 함수형 인터페이스

컬렉션 프레임웤의 인터페이스에 다수의 디폴트 메서드가 추가되었는데, 그중의 일부는 함수형 인터페이스를 사용합니다.

    /**
     * 컬렉션 프레임웤과 함수형 인터페이스 사용
     */
    @Test
    public void test4() throws Exception{
        //given
        ArrayList<Integer> list = new ArrayList<>(Arrays.asList(0,1,2,3,4,5,6,7,8,9));
        Map<String, String> map = new HashMap<>();
        StringBuilder actual1 = new StringBuilder();
        StringBuilder actual2 = new StringBuilder();
        StringBuilder actual3 = new StringBuilder();
        StringBuilder actual4 = new StringBuilder();
        map.put("1", "1");
        map.put("2", "2");
        map.put("3", "3");
        map.put("4", "4");

        //when
        list.forEach(i->actual1.append(i + ", "));

        list.removeIf(x-> x % 2 == 0 || x % 3 == 0);
        list.forEach(i->actual2.append(i + ", "));

        list.replaceAll(i->i*10);
        list.forEach(i->actual3.append(i + ", "));

        map.forEach((k, v)->actual4.append("{" + k + "," + v + "},"));

        //then
        assertThat(actual1.toString()).isEqualTo("0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ");
        assertThat(actual2.toString()).isEqualTo("1, 5, 7, ");
        assertThat(actual3.toString()).isEqualTo("10, 50, 70, ");
        assertThat(actual4.toString()).isEqualTo("{1,1},{2,2},{3,3},{4,4},");
    }

 

기본형을 사용하는 함수형 인터페이스

일반적인 함수형 인터페이스의 매개변수와 반환값의 타입은 모두 지네릭 타입이었습니다. 이 지네릭 타입들은 래퍼(wrapper) 클래스를 사용했습니다. 그러나 기본형 대신 래퍼클래스를 사용하는 것은 비효율적입니다. 그래서 기본형을 사용하는 함수형 인터페이스들이 제공됩니다.

        IntSupplier supplier = ()->(int)((Math.random()*100)+1);
        IntConsumer consumer = i->System.out.print(i + " ");
        IntPredicate predicate = i->i%2==0;
        IntUnaryOperator operator = i->i/10*10;

 

5. Function의 합성과 Predicate의 결합

java.util.function 패키지의 함수형 인터페이스에는 추상 메서드 외에도 디폴트 메서드와 static 메서드가 정의되어 있습니다. 그중 대표적인 메서드가 Function 함수형 인터페이스에 정의된 andThen, compose, identity() 메서드가 있고 Predicate  함수형 인터페이스에 정의된 and, or, negate, isEqual 메서드가 있습니다.

Function
default <V> Function<T,V> andThen(Function<? super R,? extends V> after)
default <V> Function<V,R> compose(Function<? super V, ? extends T> before)
static <T> Function<T,T> identity()

Predicate
default Predicate<T> and(Predicate<? super T> other)
default Predicate<T> or(Predicate<? super T> other)
default Predicate<T> negate()
static <T> Predicate<T> isEqual(Object targetRef)
  • Function 함수형 인터페이스의 default, static 메서드
    • andThen : 함수 f, g가 있을때 f를 먼저 적용하고나서 g를 적용하는 메서드입니다. 호출은 f.andThen(g)와 같이 호출합니다.
    • compose : andThen과는 반대로 함수 f,g가 있을때 g를 먼저 적용하고나서 f를 적용하는 메서드입니다. 호출은 f.compose(g)와 같이 호출합니다.
    • identity : 이 함수는 항등함수로써 함수를 이전과 이후가 동일해야할 때 호출합니다.
  • Predicate 함수형 인터페이스의 default, static 메서드
    • and : Predicate 함수 f,g가 있을때 f.and(g)와 같이 호출하여 하나의 새로운 Predicate로 결합할 수 있습니다. 함수를 호출할때 모두 참이여야지 true를 반환합니다.
    • or : Predicate 함수 f,g가 있을때 f.or(g)와 같이 호출하여 하나의 새로운 Predicate로 결합할 수 있습니다. 함수를 호출할때 둘중 하나가 참이면 true를 반환합니다.
    • negate : Predicate 함수 f가 negate 함수를 호출하면 조건이 반대로된 Predicate로 나옵니다. not의 의미와 동일합니다.
    • isEqual : 매개변수인 targetRef와 비교하기 위한 Predicate가 나옵니다.
    @Test
    public void Predicate결합_테스트() throws Exception{
        //given
        Function<String, Integer> f = (s) -> Integer.parseInt(s, 16);
        Function<Integer, String> g = (i) -> Integer.toBinaryString(i);
        Function<String, String> h = f.andThen(g);
        Function<Integer, Integer> h2 = f.compose(g);

        Function<String, String> f2 = x -> x; // 항등함수

        Predicate<Integer> p = i -> i < 100;
        Predicate<Integer> q = i -> i < 200;
        Predicate<Integer> r = i -> i % 2 == 0;
        Predicate<Integer> notP = p.negate(); // i >= 100
        Predicate<Integer> all = notP.and(q.or(r));

        String str1 = "abc";
        String str2 = "abc";
        Predicate<String> p2 = Predicate.isEqual(str1);

        //when
        String  actual1 = h.apply("FF");   // "FF" -> 255 -> "11111111"
        Integer actual2 = h2.apply(2);     // 2 -> "10" -> 16
        String  actual3 = f2.apply("AAA"); // "AAA" -> "AAA"
        boolean actual4 = all.test(150);   // true
        boolean actual5 = p2.test(str2);     // true

        //then
        assertThat(actual1).isEqualTo("11111111");
        assertThat(actual2).isEqualTo(16);
        assertThat(actual3).isEqualTo("AAA");
        assertThat(actual4).isEqualTo(true);
        assertThat(actual5).isEqualTo(true);

    }

 

6. 메서드 참조

람다식이 하나의 메서드만 호출하는 경우에는 '메서드 참조(method reference)'라는 방법으로 람다식을 한번더 간략히 표현할 수 있습니다.

 

람다식을 메서드 참조로 변환하는 방법

  • 하나의 메서드만 호출하는 람다식은 '클래스이름::메서드이름' 또는 '참조변수::메서드이름'으로 바꿀수 있습니다.
    @Test
    public void 메서드의참조_테스트() throws Exception{
        //given
        Function<String, Integer> f1 = (String s) -> Integer.parseInt(s);
        Function<String, Integer> f2 = Integer::parseInt;

        BiFunction<String, String, Boolean> f3 = (s1, s2) -> s1.equals(s2);
        BiFunction<String, String, Boolean> f4 = String::equals;

        //이미 생성된 객체의 메서드를 람다식에서 사용하는 경우
        Person person = new Person();
        Function<String, Boolean> f5 = (x)->person.equals(x); // 람다식
        Function<String, Boolean> f6 = person::equals;        // 메서드 참조

        //when
        int actual1 = f2.apply("10");
        boolean actual2 = f4.apply("a", "a");
        boolean actual3 = f6.apply("a");

        //then
        assertThat(actual1).isEqualTo(10);
        assertThat(actual2).isTrue();
        assertThat(actual3).isFalse();
    }

    @Test
    public void 생성자의메서드참조_테스트() throws Exception{
        //given
        // 공백 생성자 메서드 참조
        Supplier<Person> s1 = () -> new Person();   // 람다식
        Supplier<Person> s2 = Person::new;          // 메서드 참조

        // 매개변수 있는 생성자의 메서드 참조
        BiFunction<String, Integer, Person> bf1 = (name, age) -> new Person(name, age); // 람다식
        BiFunction<String, Integer, Person> bf2 = Person::new;                          // 메서드 참조

        // 배열 생성
        Function<Integer, int[]> f1 = x -> new int[x]; // 람다식
        Function<Integer, int[]> f2 = int[]::new;      // 메서드 참조

        //when
        Person p1 = s1.get();
        Person p2 = s2.get();
        Person p3 = bf1.apply("김용환", 25);
        Person p4 = bf2.apply("김용환", 28);
        int[] array1 = f1.apply(5);
        int[] array2 = f1.apply(3);

        //then
        assertThat(p1).isNotNull();
        assertThat(p2).isNotNull();
        assertThat(p3.getName()).isEqualTo("김용환");
        assertThat(p4.getAge()).isEqualTo(28);
        assertThat(array1).isEqualTo(new int[]{0,0,0,0,0});
        assertThat(array2).isEqualTo(new int[]{0,0,0});
    }

 

References

source code : https://github.com/yonghwankim-dev/java_study/tree/main/ch14/ex01_lambda
[도서] Java의 정석, 남궁 성 지음
Lambda
14-9~12 Predicate의 결합. CF와 함수형 인터페이스
14-7~8 java.util.function 패키지