티스토리 뷰

728x90
반응형

Goals

  • 람다식(Lambda expression)이란?
  • 함수형 인터페이스(Functional Interface)
  • java.util.function 패키지

 

람다식(Lambda expression)이란?


JDK1.8부터 추가된 람다식(Lambda expression)은 메서드를 하나의 식(expression)으로 표현한 것이다.

메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 ‘익명 함수(anonymous function)’이라고도 한다.

 

람다식 작성 방법

람다식은 메서드에서 이름과 반환 타입을 제거하고, 매개변수 선언부와 몸통 {} 사이에 ->를 추가한다.

예를 들어 두 값 중 큰 값을 반환하는 메서드 max가 있다고 하자.

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

위 메서드를 람다식으로 변환하면 아래와 같다.

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

만약 몸통 내에 문장이 1개 밖에 없다면 {}를 생략할 수 있으며, return 문 역시 생략할 수 있다.

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

또한, 매개변수 타입은 추론이 가능할 경우 생략 가능하다.(반환 타입도 항상 추론 가능하기 때문에 없는 것)

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

매개변수가 하나 뿐인 경우에는 ()까지 생략할 수 있다.

a -> a * a // 매개변수 a를 제곱한 결과를 반환

람다식을 사용하면 메서드보다 간결하면서도 빠르게 이해하기도 쉬워 진다.

게다가 클래스를 작성한 뒤 메서드를 작성해야 비로소 메서드를 사용할 수 있는 반면, 람다식은 람다식 자체만으로도 메서드의 역할을 할 수 있다.

뒤에서 다시 다루겠지만, 람다식은 메서드의 매개변수로 전달되어지는 것이 가능하고, 반환도 가능하다. 즉, 람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해 졌다.

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


람다식이 ‘자바에서 모든 메서드는 클래스 내에 포함되어야 한다’는 기존의 자바 규칙을 어긴 것처럼 보인다. 하지만 실상은 그렇지 않다. 람다식은 익명 클래스의 객체와 동등하다.

(익명 클래스에 대한 설명은 https://limkydev.tistory.com/226 참고)

위 예시에서 작성했던 max는 아래의 익명 클래스의 객체와 같다.(익명 클래스기 때문에 이름은 중요하지 않음)

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

그럼 이 익명 객체의 메서드를 어떻게 호출할 수 있을까?

이를 호출하기 위해서는 일단 참조변수가 있어야 한다. 하지만 참조 타입도 없다. 그래서 참조 타입으로 인터페이스를 정의해 보자.

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

MyFunction 인터페이스는 max 메서드 하나가 정의되어 있다. 이 메서드는 위 익명 객체의 max와 선언부가 일치하기 때문에 아래와 같이 MyFunction 타입의 참조변수로 참조할 수 있다. 즉, MyFunction 인터페이스를 구현한 익명 클래스의 객체를 아래와 같이 생성할 수 있다.

MyFunction f = new MyFunction() {
    public int max(int a, int b) {
    	return a > b ? a : b;
    }
};

익명 클래스의 객체를 이제 람다식으로 다시 바꿔보자.

MyFunction f = (a, b) -> a > b ? a : b;

이로써 람다식으로 MyFunction 인터페이스의 max 메서드가 구현된 것이고, 호출도 다음과 같이 할 수 있다.

System.out.println(max(5, 3));
// 5

위 내용을 정리해 보면,

  • 람다식 = 익명 객체
  • 람다식의 매개변수의 타입과 개수, 그리고 반환값이 일치(Override 조건과 동일)하다면 인터페이스를 람다식으로 구현 가능

 

위와 같이 람다식을 다루기 위한 인터페이스함수형 인터페이스(functional interface)라고 한다.

단, 함수형 인터페이스는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있다.

@FunctionalInterface  // 컴파일러가 함수형 인터페이스를 올바르게 정의했는지 확인해 줌
interface MyFunction {
	public abstract int max(int a, int b);
}

 

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

람다식을 참조변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 변수처럼 주고받을 수 있다는 것을 의미한다.

이는 람다식이 메서드가 아니라 (익명)객체이기 때문.

아래 예시는 매개변수 또는 반환타입이 함수형 인터페이스 타입일 때를 보여준다.

@FunctionalInterface
interface MyFunction {
    public abstract void run();
}

class LambdaEx {

		// 매개변수 타입이 함수형 인터페이스
    static void execute (MyFunction f) {
        f.run();
    }

		// 반환 타입이 함수형 인터페이스
    static MyFunction getMyFunction () {
        MyFunction f = () -> System.out.println("f3.run()");
        return f;
    }

    public static void main(String[] args) {

				// 람다식으로 함수형 인터페이스를 구현(run 메서드 구현)
        MyFunction f1 = () -> System.out.println("f1.run()");

				// 익명 클래스로 함수형 인터페이스를 구현(run 메서드 구현)
        MyFunction f2 = new MyFunction() {
            public void run () {
                System.out.println("f2.run()");
            }
        };

				// 람다식을 반환 받아 함수형 인터페이스를 구현(run 메서드 구현)
        MyFunction f3 = getMyFunction();

				// 각 구현된 run 메서드 호출
        f1.run();
        f2.run();
        f3.run();

				// 함수형 인터페이스 타입의 참조 변수를 매개변수 값으로 넣어줘 호출
        execute(f1);
				// 람다식을 매개변수 값으로 넣어줘 호출
        execute( () -> System.out.println("run()"));
    }
}

실행 결과

  • 매개변수 타입이 함수형 인터페이스
    • 매개변수는 구현된 메서드를 호출 가능
    • 람다식(익명 클래스)을 매개변수 값으로 넣어줘 호출 가능
  • 반환 타입이 함수형 인터페이스
    • 반환 값을 참조한 참조 변수는 구현된 메서드를 호출 가능
    • 람다식(익명 클래스)을 반환 가능

 

java.util.function 패키지


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

이 패키지의 함수형 인터페이스를 활용하면 이름도 통일되고, 재사용성이나 유지보수 측면에서 이점이 많기 때문에 자주 쓰이는 함수형 인터페이스들을 알아 두면 좋다.

  • Interface Consumer<T>
    • void accept(T t) : 매개변수 1개, 반환값 없음
  • Interface Supplier<T>
    • T get() : 매개변수 없음, 반환값 1개
  • Interface Function<T,R>
    • R apply(T t) : T 타입 매개변수, R 타입 반환값
  • Interface Predicate<T>
    • boolean test(T t) : T 타입 매개변수, boolean 타입 반환값 (조건식을 표현하는데 사용)

아래는 위 함수형 인터페이스들을 활용한 예시다.

public class LambdaEx2 {
    
    public static void main(String[] args) {

        Supplier<Integer> s = () -> (int)(Math.random() * 100) + 1;
        Consumer<Integer> c = i -> System.out.print(i + ", ");
        Predicate<Integer> p = i -> i % 2 == 0;
        Function<Integer, Integer> f = i -> i / 10 * 10;  // i의 일의 자리 없애기

        List<Integer> list = new ArrayList<>();
        makeRandomList(s, list);
        System.out.println(list);

        printEvenNum(p, c, list);

        List<Integer> newList = doSomething(f, list);
        System.out.println(newList);
    }

    static <T> void makeRandomList(Supplier<T> s, List<T> list) {
        for(int i = 0; i < 10; i++) {
            list.add(s.get());
        }
    }

    static <T> void printEvenNum(Predicate<T> p, Consumer<T> c, List<T> list) {
        System.out.print("[");

        for(T i : list) {
            if(p.test(i)) c.accept(i);
        }

        System.out.println("]");
    }

    static <T> List<T> doSomething(Function<T, T> f, List<T> list) {
        List<T> newList = new ArrayList<T> (list.size());

        for (T i : list) {
            newList.add(f.apply(i));
        }

        return newList;
    }

}

실행 결과

 

처음에는 함수형 인터페이스 사용이 어색할 수 있다. 이 때는 위에서 했던 것처럼 람다식을 그나마 더 익숙한 익명 클래스로 바꿔서 차근차근 이해해 보는 것을 추천한다.

Supplier<Integer> s = () -> (int)(Math.random() * 100) + 1;

위 문장은 아래와 동일하다.

Supplier<Integer> s = new Supplier<Integer>() {
    @Override
    public Integer get() {
    	return (int)(Math.random() * 100) + 1;
    }
};

즉, 위 람다식(익명 클래스)이 Supplier 인터페이스의 get 메서드를 구현했을 뿐이다.

함수형 인터페이스는 자신의 이름과 메서드의 이름만 봐도 어떤 역할인 지 예측 가능하다.

Supplier는 숫자 1~100 중 임의의 정수 1개를 얻는 역할을 하는데, 메서드 이름도 ‘얻다’의 get이고 인터페이스 역시 ‘공급자’의 Supplier인 것이다.

다른 함수형 인터페이스들도 위와 같이 분해해서 이해해 보고, 이름과 매칭시켜 보면 금방 이해가 갈 것이다.

추가적으로 아래는 Collection 프레임워크에서 함수형 인터페이스들을 사용하는 예시다.

public class LambdaEx3 {
    
    public static void main(String[] args) {

        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) list.add(i);

        System.out.println("---list의 모든 요소 출력---");
        list.forEach(i -> System.out.print(i + " "));
        System.out.println();

        System.out.println("---list에서 2 또는 3의 배수 제거---");
        list.removeIf(x -> x % 2 == 0 || x % 3 == 0);
        System.out.println(list);

        System.out.println("---list의 각 요소에 곱하기 10---");
        list.replaceAll(i -> i * 10);
        System.out.println(list);

        Map<String, String> map = new HashMap<>();
        map.put("1", "1");
        map.put("2", "2");
        map.put("3", "3");
        map.put("4", "4");

        System.out.println("---map의 모든 요소를 {k, v} 형식으로 출력---");
        map.forEach((k, v) -> System.out.print("{" + k + ", " + v + "} "));
        System.out.println();
    }
}

실행 결과

 

References

728x90
반응형
댓글