본문 바로가기

자바

자바 람다식

람다식

함수를 간략하게 표현할 수 있는 표현 방식이다. 함수의 이름, 반환타입, 파라미터 타입을 생략할 수 있어 익명 함수의 한 종류라고 볼 수 있다.

void print(int value) {
    System.out.println(value);
}

위의 함수를 람다식을 통해서 다음과 같이 표현할 수 있다.

value -> System.out.println(value);

반환값이 있는 함수들은 아래와 같이 변환할 수 있다.

double pow(double base, double exponent) {
		return Math.pow(base, exponent);
}

(base, exponent) -> Math.pow(base, exponent); // return 생략 가능
// or
(base, exponent) -> { // return 생략하지 않을 수도 있음
		return Math.pow(base, exponent);
}

사용해보면 알겠지만, 자바에서는 메소드를 단독으로 람다식으로 사용할 수는 없다. 그렇다면 어떤 경우에 람다식으로 쓸 수 있을까?

함수형 인터페이스

람다식을 어떤 경우에 사용할 수 있는 지는 차치하고 먼저 함수형 인터페이스가 어떤 것인지 알아보자.

함수형 인터페이스는 딱 단 하나의 추상 메소드가 선언된 인터페이스이다. 인터페이스 안에 추상 메소드 말고 다른 default, static, private 메소드가 있어도 추상 메소드가 단 한 개라면 함수형 인터페이스로 볼 수 있다.

@FunctionalInterface 를 붙여서 단 하나의 추상메소드를 갖도록 제한할 수 있다.

@FunctionalInterface
public interface Calculator {

    long add(long value1, long value2);

    default long plusOne(long value) { // default 메소드는 있어도 된다.
        return plus(value, 1L);
    }
    
    private long plus(long value1, long value2) { // private 메소드는 있어도 된다.
        return value1 + value2;
    }
    
    static long plusArray(long[] values) { // static 메소드는 있어도 된다.
        long result = 0;
        for (long value : values) {
            result += value;
        }
        return result;
    }

}

람다식과 함수형 인터페이스

자바에서는 함수형 인터페이스의 추상 메소드를 구현할 때에 람다식을 사용할 수 있다.

람다식 이전에 익명 함수로 선언하여 사용하던 것을 람다식을 사용하여 간단하게 사용할 수 있다.

// 익명함수 사용
public static void main(String[] args) {
    Calculator calculator = new Calculator() {
        @Override
        public long add(long value1, long value2) {
            return value1 + value2;
        }
    };
    System.out.println(calculator.add(1, 2));
}
// 람다식 사용
public static void main(String[] args) {
    Calculator calculator = (value1, value2) -> value1 + value2;
    System.out.println(calculator.add(1, 2));
}

자바에서 기본적으로 제공하는 함수형 인터페이스

자바에서는 많이 사용될만한 함수형 인터페이스를 기본적으로 제공하고 있다. 가장 많이 사용하는 4가지 함수형 인터페이스를 살펴보자.

Supplier<T>

  • 인자를 받지 않고 T 타입의 객체를 반환한다.
  • 아래의 단 하나의 추상 메소드를 가진다.
@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}
public static void main(String[] args) {
    Supplier<String> supplier = () -> {
        return "Supplier Test";
    };

    String result = supplier.get();
    System.out.println(result);
}

Consumer<T>

  • T 타입의 객체를 인자로 받고 반환값이 없다.
  • 아래의 단 하나의 추상 메소드를 가진다.
@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
    ...
}
public static void main(String[] args) {
    Consumer<String> consumer = (value) -> System.out.println(value);
    consumer.accept("Consumer Test");
}

Function<T, R>

  • T 타입 객체를 인자를 받고 R 타입의 객체를 반환한다.
  • 아래의 단 하나의 추상 메소드를 가진다.
@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
    ...
}
public static void main(String[] args) {
    Function<Long, Long> increasor = (value) -> value + 1;
    Long increased = increasor.apply(1L);
    System.out.println(increased);
}

Predicate<T>

  • T 타입의 객체를 인자로 받고 boolean 값을 반환한다.
  • 아래의 단 하나의 추상 메소드를 가진다.
@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
    ...
}
public static void main(String[] args) {
    Predicate<Long> predicate = (value) -> value > 0;
    boolean isPositive = predicate.test(1L);
    System.out.println(isPositive);
}

Variable Capture

람다는 파라미터로 넘겨온 변수 이외에 바깥에 선언된 변수에도 접근할 수 있다. 특히, 지역변수를 가져와 사용할 때에 variable capture 가 일어난다.

이 때 지역변수에 값이 재할당이 되면 안 된다는 제약사항이 있다.

long standard = 2; // final 키워드는 없지만 재할당이 되지 않으면 capture 가능하다.
final long standard = 2; // final 키워드가 붙은 변하지 않는 지역 변수 capture 가능하다.
Predicate<Long> biggerThanStandard = (value) -> value > standard;
boolean isBigger = biggerThanStandard.test(1L);
System.out.println(isBigger);
long standard = 2;
Predicate<Long> biggerThanStandard = (value) -> value > standard; // (1)

standard = 3; // 만약에 재할당이 일어나면 (1) 에서 컴파일 에러가 발생한다.
boolean isBigger = biggerThanStandard.test(1L);
System.out.println(isBigger);

람다식은 별도의 쓰레드로 실행시킬 수 있다. 지역변수는 쓰레드마다 가지고 있는 스택 영역에 저장된다.

람다를 호출한 쓰레드가 종료되고 스택 메모리에서 지역변수가 사라지고 난 후에 람다가 실행될 수도 있기 때문에 지역변수에 직접 접근할 수 없고 값을 캡쳐, 즉 복사해와서 사용한다.

그렇기 때문에 재할당, 즉 값의 변화가 없어야 한다는 제약사항이 생겼다.

그렇다면 인스턴스 변수, static 변수는 각각 힙 영역, data 영역에 저장된다. 이는 여러 쓰레드가 접근할 수 있다. 그래서 주소로 직접 접근해서 사용하고 람다 내에서 값도 변화시킬 수 있다.

public class LambdaDemoApplication {

    static Long instanceVariable = 0L;

    public static void main(String[] args) {
        Consumer<Long> modifyInstanceVariable = (value) -> {
            instanceVariable += value;
        };
        modifyInstanceVariable.accept(1L);
        System.out.println(instanceVariable);
    }

}
1

메소드, 생성자 레퍼런스

람다식을 사용할 때 메소드 및 생성자 레퍼런스로 표현하면 더 가독성이 좋을 때가 있다.

메소드 레퍼런스

// 람다식
Function<String, Long> convertStringToLong = (value) -> {
    return Long.parseLong(value);
};

// 메소드 레퍼런스
Function<String, Long> convertStringToLong = Long::parseLong;

:: 구분자를 사용하여 (클래스 타입)::(참조할 메소드 이름) 으로 메소드 레퍼런스를 사용할 수 있다.

생성자 레퍼런스

public record Car(String name) {}

// 람다식
Function<String, Car> createCar = (name) -> {
    return new Car(name);
};

// 생성자 레퍼런스
Function<String, Car> createCar = Car::new;

크게 보면 메소드 레퍼런스와 비슷하다. :: 와 더불어 new 라는 키워드로 (클래스 타입)::new 로 생성자 레퍼런스를 사용할 수 있다.

그래서 클래스의 생성자를 호출해 객체를 생성할 수 있다.

'자바' 카테고리의 다른 글

java.util.ConcurrentModificationException 원인과 해결  (1) 2025.01.28
자바 제네릭  (0) 2024.08.21
자바 I/O  (0) 2024.07.15
자바 애노테이션 프로세서  (1) 2024.04.12
자바 애노테이션  (0) 2024.04.03