티스토리 뷰

언어/Java

[Java]람다식(Lambda Expression)에 대하여

꼬마우뇽이(원종운) 2022. 1. 1. 19:18

1편 - 람다식(Lambda Expression)에 대하여 - 현재 글 

2편 - 메서드 참조(Method Reference)에 대하여


함수 타입(Function Type)

Java에서는 함수 타입을 표현하기 위해서 추상 메서드 하나만 정의된 인터페이스(혹은 추상 클래스)를 사용해왔습니다. 해당 인터페이스의 인스턴스를 함수 객체(function object)라고 합니다.

 

JDK 1.1 에서는 이러한 함수 객체를 생성하기 위하여 익명 클래스(Anonymous Class)를 주로 사용해왔지만, 익명 클래스를 이용할 경우 아래와 같이 작성해야 할 코드가 너무 길어진다는 단점이 있었습니다.

 

Collections.sort(words, new Comparator<String>() {
	public int compare(String s1, String s2) {
		return Integer.compare(s1.length(), s2.length());	
	}
});

위 코드는 문자열 리스트를 길이 순으로 정렬하기 위하여, Comparator 함수 객체를 익명 클래스를 이용하여 만든 것입니다.

 

함수형 인터페이스

Java 8부터 함수 객체를 생성하기 위하여 사용한 추상 메서드 하나만 정의된 인터페이스를 함수형 인터페이스라고 부르기로 하였습니다.

@FunctionalInterface // 함수형 인터페이스를 의미하는 애너테이션
public interface Runnable {
    public abstract void run(); // 단 하나만 존재하는 추상 메서드
}

Runnable 인터페이스는 대표적인 함수형 인터페이스 중 하나입니다. 처음 보는 애너테이션이 붙어있는 것을 볼 수 있습니다.

 

@FunctionalInterface을 함수형 인터페이스에 붙여줄 경우 컴파일러가 해당 인터페이스가 함수형 인터페이스에 부합하는지를 컴파일 타임에 확인해줄 뿐만 아니라 프로그래머가 해당 인터페이스가 함수형 인터페이스임을 명확히 알 수 있으므로 꼭 붙여주는 것이 좋습니다.

 

람다식

람다식은 람다 대수라는 학문의 람다에서 비롯되었고, 간단히 말하면 메서드를 하나의 식으로 표현한 것을 의미합니다. Java 8부터 도입되었으며 람다식을 사용하여 함수 객체를 익명 클래스를 사용했을 때보다 간결한 코드로 생성할 수 있게 되었습니다.

 

Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

위 코드는 앞서 익명 클래스로 작성한 함수 객체와 동일한 역할을 하는 함수 객체를 람다식으로 작성한 코드입니다. 확실히 익명 클래스를 사용했을 때보다 간결한 것을 알 수 있습니다.

 

다른 점은 익명 클래스에서 구현하는 추상 메서드의 이름과 반환형 등의 정보가 생략되었다는 점입니다. 이러한 부분은 타입 추론을 통하여 해결이 가능하므로 생략을 통하여 간결함을 우리에게 제공하여 준 것입니다.

 

람다식 사용 팁

첫째, 람다식에서 타입을 명시해야 코드가 더 명확해지는 경우를 제외하고는, 모든 매개변수 타입은 생략하는 것이 좋습니다. 그렇지 않으면 코드가 길어지고 장황해져 람다식의 간결함을 잃어버릴 수 있습니다.

 

둘째, 앞서 설명했듯이 람다식은 이름이 없으므로 문서화를 하지 못합니다. 따라 람다식을 설명할 수 있는 것은 람다식 그 자체밖에 없으므로 람다식은 최대한 짧고 그 자체로 의미가 명확하고 간결함을 유지하여야 합니다.

 

대표적인 함수형 인터페이스

람다식은 함수형 인터페이스의 인스턴스인 함수 객체를 익명 클래스보다 간결하게 작성하기 위하여 도입되었습니다. 그러면 Java에서 제공되는 표준 함수형 인터페이스는 java.util.functuion 패키지에 총 43개가 있으며, 그 이외도 Runnable 등과 같은 함수형 인터페이스들이 존재합니다. 그중 대표적인 함수형 인터페이스들 5가지와 Runnable 인터페이스를 살펴보겠습니다.

 

표준 함수형 인터페이스를 사용하면 좋은 점

표준 함수형 인터페이스에는 추상 메서드뿐만 아니라 Java 8부터 도입된 default 메서드들도 함께 정의가 되어있으므로 다른 코드들과 상호 운용성에 있어서 좋으므로 직접 구현하지 않아도 이미 제공되는 표준 함수형 인터페이스가 있다면 활용하는 것이 좋습니다.

 

1. Predicate<T> 

수학에서 결과를 논리형(true, false)로 반환하는 함수를 Predicate라고 부르는 것에서 따왔습니다. 인수로 객체(T 타입)를 전달받아 그 결과로 true 또는 false를 반환하여 주는 추상 메서드 test가 선언되어있습니다.

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

사용

public class App {

    public static void main(String[] args) {
        Predicate<String> isLengthCheck = s -> s.length() >= 4;
        System.out.println(isLengthCheck.test("안녕하세요")); // 결과 : true
        System.out.println(isLengthCheck.test("안녕")); // 결과 : false
    }
}

 

2. Function<T, R>

인수로 T 타입 객체를 전달받아 R 타입의 객체를 반환하여 주는 apply 메서드가 선언되어있습니다. 여기서 눈치가 빠르다면 Predicate<T>의 경우는 Function<T, Boolean>과 동일한 작업을 확인할 수 있으셨을 겁니다. 다만 제네릭의 특성상 기본 자료형은 지원하지 않으므로 약간의 차이가 존재하며 인터페이스 내부에 정의된 추상 메서드의 이름이 목적에 맞게 작성되어있다는 것이 다릅니다.

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

사용

public class App {

    public static void main(String[] args) {
        Function<String, String> reverse = s -> new StringBuffer(s).reverse().toString();
        System.out.println(reverse.apply("토마토")); // 결과 : 토마토
        System.out.println(reverse.apply("나무")); // 결과 : 무나
    }
}

 

3. BiFunction<T>

인수로 T 타입 객체와 U 타입 객체를 전달받아 R 타입의 객체를 반환하여주는 apply 메서드가 선언되어있습니다.

Function<T, R> 함수형 인터페이스와 다른 점은 "Bi"에서 알 수 있듯이 두 개의 인수를 전달받는다는 것뿐입니다.

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

사용

public class App {

    public static void main(String[] args) {
        BiFunction<String, String, Integer> concat = (s1, s2) -> s1.length() + s2.length();

        System.out.println(concat.apply("안녕", "하세요")); // 결과 : 5(2+3)
        System.out.println(concat.apply("저는 ", "바보입니다.")); // 결과 : 9(3+6)
    }
}

 

4. Supplier<T>

인수는 없으며 T 타입의 객체를 반환하여주는 get 메서드가 선언되어있습니다. 인터페이스 이름에서 알 수 있다시피 무엇을 제공하여주는 역할을 하는 함수형 인터페이스입니다.

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

사용

public class App {

    public static void main(String[] args) {
        Supplier<Integer> random = () -> new Random().nextInt();
        System.out.println(random.get()); // 결과 : int 범위의 랜덤한 수
        System.out.println(random.get()); // 결과 : int 범위의 랜덤한 수
    }
}

 

5. Consumer<T>

인수로 T 타입 객체를 전달받아 아무런 결과도 반환하여 주지 않는 accept 메서드가 선언되어있습니다. 인터페이스 이름에서 알 수 있다시피 하나의 객체를 소비하는 역할을 하는 함수형 인터페이스입니다.

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

사용

public class App {

    public static void main(String[] args) {
        Consumer<String> print = s -> System.out.println(s);
        print.accept("안녕하세요"); // 결과 : 안녕하세요
        print.accept("Consumer 사용 방법입니다."); // 결과 : Consumer 사용 방법입니다.
    }
}

6. Runnable

아무런 인수도 받지 않고 아무런 결과도 반환하여 주지 않는 run 메서드가 선언되어있습니다.

Runnable 함수형 인터페이스의 경우는 쓰레드를 선언할  사용되며, 쓰레드에서 동작할 작업을 정의하는 데 사용됩니다.

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

사용

public class App {

    public static void main(String[] args) {
        Runnable target = () -> {
            int sum = 0;
            for (int i = 1; i <= 10; i++) {
                sum += i;
            }
            System.out.println("1부터 10까지의 합 : " + sum);
        };

        Thread sumThread = new Thread(target);
        sumThread.start(); // 결과 : "1부터 10까지의 합 : 55"
    }
}

 

마무리

람다식이 왜 도입되었는지, 함수형 인터페이스가 무엇인지, 람다식을 사용할 때 주의하여하는 점이 무엇인지, 대표적인 함수형 인터페이스들이 어떤 것들이 있는지를 살펴보았습니다.

 

다음 편에서는 이러한 람다식을 더 간결하고 람다의 아쉬운 점을 보완하여 주는 메서드 참조에 대해서 알아보도록 하겠습니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함