Skills/Java

Java - Lambda

aoaa 2022. 11. 6. 22:10

이 글은 프리코스 진행 중 람다식의 사용법을 정리하고 작성한 글입니다. 

 

람다 함수는 프로그래밍 언어에서 사용되는 개념으로 익명 함수를 지칭하는 용어입니다.

익명 함수라고 하는 이유는 글짜 그대로 함수의 이름이 없는 함수라는 것입니다.

 

이 익명함수들은 일급객체(First Class Citizen)라는 특성을 지닙니다. 일급 객체는 일반적으로 다룰 객체들에 적용 가능한 연산을 모두 지원하는 개체를 말합니다. 메서드를 값으로 사용할 수도 있으며, 패러미터로 전달하거나 변수에 대입하는 연산들이 가능합니다.


1. 표현식

반환Type 메서드명 = (Parameter)->{구현코드};

 Java에서 기본 람다식은 '->'를 기준으로 그 왼쪽에는 람다식을 실행하기 위한 패러미터가 그 오른쪽에는 패러미터를 이용한 구현할 코드가 옵니다. 먼저 간단한 예시를 통해 살펴보겠습니다.

 

 

// method
public int sum(int a, int b){
	return a+b;
}

// lambda
(a, b)->a + b;

 sum()메서드를 실행하는 식을 작성했습니다. int형인 a와 b를 입력받아 두 값을 더하는 메서드인데, 아래의 람다식으로 변환하게 되면 클래스의 정의도 필요 없이 한 줄로 간결하게 작성할 수 있습니다. 다른 예시도 한번 보겠습니다.

 

Thread thread = new Thread(new Runnable() {
	
    @Override
    public void run() {
    	System.out.println("Start Thread");
        Thread.sleep(1000);
        System.out.println("End Thread");
        }
 });

 Java에서 멀티쓰레드 프로그램을 작성할 때, Runnable 인터페이스를 구현한 클래스가 필요합니다. (멀티쓰레드에 관한 글은 나중에 한번 작성해보겠습니다.)  위의 코드를 람다식으로 구현해보겠습니다. 

 

Thread thread = new Thread(() -> {
	System.out.println("Start Thread");
    Thread.sleep(1000);
    System.out.println("End Thread");
});

 

 이처럼 람다식으로 호출하면, 불필요한 반복문을 삭제가 가능하여 단순하게 표현할 수 있습니다. 또한, 지연연상을 수행함으로써 불필요한 연산을 최소화할 수 있습니다. 하지만 불필요하게 많이 사용된다면 오히려 가독성을 떨어뜨릴 수 있어 주의해서 사용해야 합니다.

 


2. 함수형 인터페이스

 람다식을 통해 Java에서 순수 함수를 선언할 수 있게 되었습니다. 

But, Java는 기본적으로 객체지향 언어이기 때문에 순수 함수와 일반 함수를 다르게 취급하고 있으며, 이를 구분하기 위해 함수형 인터페이스가 등장하게 되었습니다.

 이 함수형 인터페이스는 메서드를 1급 객체처럼 다룰 수 있게 해주는 어노테이션(@)으로, 인터페이스에 선언하여 단 하나의 추상 메서드만을 갖도록 제한하는 역할을 합니다. 예시를 한번 보겠습니다.

 

public class Lambda {

    public static void main(String[] args) {
    
        // 기존의 익명함수
        System.out.println(new MyLambdaFunction() {
            public int max(int a, int b) {
                return a > b ? a : b;
            }
        }.max(3, 5));

    }

}

 위는 3과 5 중 큰 값을 구하는 익명 함수입니다. 이를 함수형 인터페이스로 구현해보겠습니다. 

 

@FunctionalInterface
interface MyLambdaFunction {
    int max(int a, int b);
}

public class Lambda {

    public static void main(String[] args) {

        // 람다식을 이용한 익명함수
        MyLambdaFunction lambdaFunction = (int a, int b) -> a > b ? a : b;
        System.out.println(lambdaFunction.max(3, 5));
    }

}

  먼저 @FunctionalInterface라는 어노테이션을 붙여주고, MyLambdaFunction이라는 인터페이스를 개발하여 내부에는 함수를 선언했습니다. 선언함으로써 함수를 변수처럼 선언할 수 있게 되고, 이를 람다식을 이용해 구현이 가능합니다.

 

 이 때 주의해야 할 점은, 람다식으로 생성된 순수 함수는 함수형 인터페이스로만 선언이 가능하고, @FunctionalInterface는 해당 인터페이스가 1개의 함수만을 가지도록 제한을 하여 여러 개의 함수를 선언하면 컴파일 에러가 발생할 것이라는 점입니다. 

 

 

2.1 Java에서 제공하는 함수형 인터페이스

2.1.1 Supplier <T>

Supplier는 매개변수 없이 반환값 만을 갖는 함수형 인터페이스입니다. 이는 T get()을 추상 메소드로 갖고 있습니다.

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

// 사용 예시
Supplier<String> supplier = () -> "Hello World!";
System.out.println(supplier.get());

2.1.2 Consumer <T>

 Consumer는 객체 T를 매개변수로 받아서 사용하며, 반환값은 없는 함수형 인터페이스입니다. void accept(T t)를 추상메서드로 가지며, andThen이라는 함수를 제공하고 있는데, 하나의 함수가 끝난 후 다음 Consumer를 연쇄적으로 이용할 수 있습니다.

 아래의 예제에서는 먼저 accept로 받아들인 Consumer를 먼저 처리하고, andThen으로 받은 두 번째 Consumer를 처리하고 있습니다. 함수형에서 함수는 값의 대입 또는 변경 등이 없기 때문에 첫 번째 Consumer가 split으로 데이터를 변경하였다 하더라도 원본의 데이터는 유지됩니다.

// 정의
@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

// 예시
Consumer<String> consumer = (str) -> System.out.println(str.split(" ")[0]);
consumer.andThen(System.out::println).accept("Hello World");

// 출력
Hello
Hello World

 

2.1.3 Function <T, R>

 Function은 객체 T를 매개변수로 받아서 처리한 후 R로 반환하는 함수형 인터페이스로 R apply(T t)를 추상메서드로 갖습니다. Consumer와 마찬가지로 andThen을 제공하고 있으며, 추가적으로 compose를 제공합니다.

 앞에서 andThen은 첫 번째 함수가 실행된 이후에 다음 함수를 연쇄적으로 실행하도록 연결해준다고 하였습니다.

하지만 compose는 첫 번째 함수 실행 이전에 먼저 함수를 실행하여 연쇄적으로 연결해준다는 점에서 차이가 있습니다.

또한 identity 함수가 존재하는데, 이는 자기 자신을 반환하는 static 함수입니다.

// 정의
@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

// 예시, 메소드 참조로 간소화 가능(String::length;)
Function<String, Integer> function = str -> str.length();
function.apply("Hello World");

 

2.1.4 Predicate <T>

 Predicate는 객체 T를 매개 변수로 받아 처리한 후 Boolean을 반환하며, Boolean test(T t)을 추상 메소드로 갖고 있습니다.

// 정의
@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
    
}

// 예시
Predicate<String> predicate = (str) -> str.equals("Hello World");
predicate.test("Hello World");