본문 바로가기

JAVA

[JAVA] 람다 (Lambda)


 

람다란?


람다식(Lambda Expression)은 함수형 프로그래밍 기법을 지원하는 자바의 문법입니다.

람다식은 코드를 매우 간결하면서도 명확하게 표현할 수 있다는 장점이 있습니다.

 

 

람다식의 기본 문법


람다는 간단히 말해 메서드를 하나의 식으로 표현한 것입니다.

아래의 예제 코드를 보도록 하겠습니다

// 메서드
int main(int x, int y) {
	return x < y ? x : y;
}

// 람다 표현식
(x, y) -> x < y ? x : y;

위의 예제를 보면 메서드를 람다로 표현한 모습을 볼 수 있습니다.

보시는 바와 같이 클래스를 작성하고 객체를 생성하지 않아도 메서드를 사용할 수 있는 모습을 볼 수 있습니다.

 

그런데 자바에서는 클래스의 선언과 동시에 객체를 생성하므로, 단 하나의 객체만을 생성할 수 있는 클래스를 익명 클래스라고 합니다.

따라서 자바에서 람다 표현식은 익명 클래스와 같다고 할 수 있습니다.

//람다 표현식
(x, y) -> x < y ? x : y;

//익명 클래스
new Object() {
	int main(int x, int y) {
    	return x < y ? x : y;
    }
}

위와 같이 람다 표현식을 통해, 기존의 불필요한 코드를 줄여주고, 작성된 코드의 가독성이 높아진 모습을 볼 수 있습니다.

 

람다식 작성 방법


아래의 예제 코드를 통하여 메서드를 람다식으로 만드는 방법에 대해 살펴보도록 하겠습니다.

int sum(int x, int y){
	return x < y ? x : y;
}

위의 일반적인 메서드의 반환타입과 메서드명을 제거하고 코드 블록사이에 화살표를 추가해 줍니다

(int x, int y) -> {
	return x > y ? x : y;
}

위의 메서드는 조금 더 축약할 수 있습니다.

메서드 바디에 문장이 실문 하나만 존재할 때 중괄호와 return 문을 생략할 수 있습니다. 

이 경우에는 세미클론까지 생략해야 합니다.

(int x, int y) -> x > y ? x : y

매개 변수 타입을 함수형 인터페이스를 통해 유추할 수 있는 경우에는 매개변수의 타입 또한 생략할 수 있습니다.

(x, y) -> x > y ? x : y

 

함수형 인터페이스


자바의 함수는 반드시 클래스 안에서 정의되어야 하기 때문에 메서드가 독립적으로 있을 수는 없고 반드시 클래스 객체를 먼저 생성한 후 생성한 객체로 메서드를 호출하여야 합니다.

 

람다식 또한 객체입니다. 또 다르게는 이름이 없기 때문에 익명 객체라 할 수 있습니다.

 

앞서 보았던 예제 코드를 다시 한번 살펴보겠습니다.

// 람다
(x, y) -> x > y ? x : y

// 람다를 객체로 표현

new Object() {
	int ex(int x, int y) {
    	return x > y ? x : y;
    }
}

익명 객체는 익명 클래스를 통해 만들 수 있는데, 익명 클래스란 객체의 선언과 생성을 동시에 하여 오직 하나의 객체를 생성하고, 단 한 번만 사용되는 일회용 클래스입니다. 또한 위의 예제처럼, 생성과 선언을 한 번에 할 수 있습니다.

 

하지만 기본의 객체를 생성할 때 만들었던 Object 클래스에는 ex 메서드가 없기 때문에, Object 타입의 참조변수에 담는다고 하더라도 ex 메서드를 사용할 수없습니다.

public class ex {
	public static void main(Stringp[] args) {
    
    Object obj = new Object() {
    	int ex(int x, int y) {
        	return x > y ? x : y;
        }
    }
    
    obj.ex(1, 2);
    }
}

//출력 결과
java: cannot find symbol
...

위의 코드 예제를 보면 익명 객체를 생성하여 참조변수 obj에 담아준다 하더라도 ex 메서드를 사용할 수 있는 방법이 없습니다.

 

이 같은 문제를 해결하기 위해 사용되는 것이 함수형 인터페이스(Functional Interface)입니다.

 

함수형 인터페이스에는 단 하나의 추상 메서드만 선언할 수 있는데, 이는 람다식과 인터페이스가 1:1로 매칭되어야 하기 때문입니다.

 

위의 예제를 함수형 인터페이스를 적용하여 보겠습니다.

public class ex{
	public static void main(String[] args) {
    	ExFunction exFunction = (x, y) -> x > y ? x : y;
        System.out.println(exFunction.ex(15,10);
    }
@FunctionalInterface
interface ExFunction {
	int ex(int x, int y);
}
}

//출력 값
15

위의 예제에서 함수형 인터페이스에 추상메서드 ex가 정의되어 있는 모습을 확인할 수 있습니다.

이 함수형 인터페이스는 람다식을 참조할 참조변수를 선언할 때, 타입으로 사용하기 위해 필요합니다.

 

이처럼, 함수형 인터페이스를 사용해 참조변수의 타입으로 함수형 인터페이스를 사용하여 우리가 원하는 메서드에 접근이 가능합니다.

 

JAVA에서 기본적으로 제공하는 Functional Interfaces


자바에서는 기본적으로 함수형 인터페이스를 제공합니다.

기본적으로 사용하는 함수형 인터페이스는 아래와 같습니다.

함수형 인터페이스 Descripter Method
Predicate T -> boolean boolean test(T t)
Consumer T -> void void accept(T t)
Supplier () -> T T get()
Function<T, R> T -> R R apply(T t)
Comparator (T, T) -> int int compare(T o1, T o2)
Runnable () -> void void run()
Callable () -> T V call()

 

Predicate


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

Predicate는 인자 하나를 받아 boolean 타입을 리턴합니다

람다식 : T -> boolean

 

Consumer


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

Consumer는 인자 하나를 받지만 아무것도 리턴하지 않습니다.

람다식 : T -> void

 

Supplier


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

Supplier는 인자를 받지 않고 T 타입의 객체를 리턴합니다.

람다식: () -> T

 

Function


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

Function은 T 타입 인자를 받아 R 타입을 리턴합니다.

람다식: T -> R

 

Comparator


@FunctionalInterface
public interface Comparator<T> {
	int compare (T o1, T o2);
}

Comparator는 T 타입 인자 두 개를 받아 int 타입으로 리턴합니다.

람다식: (T, T) -> int

 

Runnable


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

Runnable은 아무런 객체를 받지 않고 리턴도 하지 않습니다.

람다식: () -> void

 

Callable


@FunctionalInterface
public interface Callable<V> {
	V call() throws Exception;
}

Callable은 아무런 인자도 받지 않고 T타입 객체를 리턴합니다.

ㄹ마다식으로는 () -> T로 표현합니다.

 

메서드 참조


 

메서드 참조는 람다식에서 불필요한 매개 변수를 제거할 때 주로 사용합니다.

 

메서드 참조는 다음과 같이 표현할 수 있습니다.

클래스이름::메서드이름

or

참조변수이름::메서드이름

 

 

다음 예제코드는 두 개의 큰 값을 받아 큰 수를 리턴하는 Math 클래스의 max() 메서드를 호출하는 람다식입니다.

(left, right) -> Math.max(left, right)

위의 코드 예제를 메서드 참조를 이용하여 재 작성해 보도록 하겠습니다.

Math::max

또한, 특정 인스턴스의 메서드를 참조할 때에도 참조 변수의 이름을 통해 메서드 참조를 사용할 수 있습니다 

MyClass obj = new MyClass;
Function<String, Boolean> func = (a) -> obj.equals(a); //람다 표현식
Function<String, Boolean> func = boj::equals(a);

다음 예제는 람다 표현식과 메서드 참조를 비교하는 예제입니다.

 

import java.util.function.DoubleUnaryOperator;

public class Main {
    public static void main(String[] args) {
        DoubleUnaryOperator oper;
        oper = (n) -> Math.abs(n);
        System.out.println(oper.applyAsDouble(-5));
        oper = Math::abs;
        System.out.println(oper.applyAsDouble(-5));
    }
}

//출력 결과

5.0
5.0

 

생성자 참조


생성자를 호출하는 람다 표현식도 앞서 살펴본 메서드 참조를 이용할 수 있습니다.

즉, 단순히 객체를 생성하고 반환하는 람다 표현식은 생성자 참조로 변환할 수 있습니다.

 

다음 예제는 객체를 생성하고 반환하는 람다 표현식입니다.

(a) -> { return new Object(a); }

 

위의 예제는 단순히 Object 클래스의 인스턴스를 생성하고 반환하기만 하므로, 생성자 참조를 사용하여 다음과 같이 간단히 표현할 수 있습니다.

Object::new;

이때 해당 생성자가 존재하지 않으면 컴파일 시 오류가 발생합니다.

또한, 배열을 생성할 때에도 다음과 같이 생성자 참조를 할 수 있습니다.

Function<Integer, double[]> func1 = a -> new double[a]; // 람다 표현식
Function<Integer, double[]> func2 = double[]::new; //생성자 참조