티스토리 뷰

JAVA

[JAVA] 람다식(Lambda)의 개념과 사용법

까리한 새우 2024. 3. 20. 23:42

람다식(Lambda)이란?

람다식이란 함수형 프로그래밍을 구성하기 위한 함수식이며, 쉽게 말해 자바의 메서드를 "하나의 식" 으로 표현한 것입니다.

 

지금까지 자바에서는 메서드를 하나 표현하려면 클래스를 정의해야 했습니다.

하지만 람다식으로 표현하면 메서드의 이름과 반환값을 생략할 수 있고 이를 변수에 넣어 자바 코드가 매우 간결해지는 장점이 있습니다.

 

아래 그림에서 보듯이 메서드 표현식을 메서드 타입, 메서드 이름, 매개변수 타입, 중괄호, return 문을 생략하고, 화살표 기호를 넣음으로써 코드를 간략하게 함축했음을 볼 수 있습니다.

이러한 특징으로 람다식을 이름이 없는 함수 익명 함수(anonymous function) 라고도 합니다.

int add(int x, int y) {
    return x + y;
}

// 위의 메서드를 람다 표현식을 이용해 아래와 같이 단축 시킬수 있다. (메서드 반환 타입, 메서드 이름 생략)
(int x, int y) -> {
	return x + y;
};

// 매개변수 타입도 생략 할 수 있다.
(x, y) -> {
	return x + y;
};

// 함수에 리턴문 한줄만 있을 경우 더욱 더 단축 시킬 수 있다. (중괄호, return 생략)
(x, y) -> x + y;

 

위 코드를 보면 코드가 훨씬 간략해진것을 볼 수 있습니다.

 

이렇게 람다식은 오직 람다식 자체만으로도 메서드의 역할을 대신할 수 있으며, 메서드의 매개변수로 전달되어지는 것도 가능하고, 메서드의 결과로 반환 될 수도 있습니다.

따라서 람다식을 사용하면, 기존의 불필요한 코드를 줄여주고, 작성된 코드의 가독성을 높여줍니다.

 

이러한 람다식은 Java 8버전부터 적용이 가능합니다.

 


 

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

람다식의 형태를 보면 마치 자바의 메서드를 변수로 선언하는 것 처럼 보이지만, 사실 자바는 메서드를 단독으로 선언할 수는 없습니다.

형태만 그렇게 보일 뿐이지 코드를 보면 람다 함수식을 변수에 대입하고 변수에서 메서드를 호출해서 사용하는 것이 마치 객체와 다름이 없어보입니다.

MyFunction myFunc = (str) -> System.out.println(str);
myFunc.print("Hello World");

 

사실 람다식도 결국은 객체입니다.

정확히 말하면 인터페이스를 익명 클래스로 구현한 익명 구현 객체를 짧게 표현한 것이죠.

 

 

객체 지향 방식 vs 람다 표현 방식

좀더 이해를 돕고자 기존 자바7에서 표현 했던 객체 지향 방식과 람다 표현 방식을 비교해보겠습니다.

 

다음 코드처럼 MyAdd 인터페이스가 있고 add() 추상 메서드가 있습니다.

이 인터페이스를 구현하여 메서드를 정의해서 덧셈 기능을 이용할 예정입니다.

interface MyAdd {
    int add(int x, int y);
}

class Add implements MyAdd {
    public int add(int x, int y) {
        return x + y;
    }
}
        
public class Main {
    public static void main(String[] args) {
        Add a = new Add();
        
        int result = a.add(1, 2);
        System.out.println(result);
    }
}

 

여기서 조금 더 간략하게 수정해서, 한 번만 사용하고 버려질 클래스라면, 굳이 번거롭게 클래스를 선언하지 않고 익명 클래스로 일회용 오버라이딩을 하여 사용해보겠습니다.

interface MyAdd {
    int add(int x, int y);
}

public class Main {
    public static void main(String[] args) {
        //익명 클래스로 정의해 사용하기 (일회용)
        MyAdd a = new MyAdd() {
            public int add(int x, int y) {
                return x + y;
            }
        };
        
        int result = a.add(1, 2);
        System.out.println(result);
    }
}

 

그리고 람다식은 이 익명클래스 코드 부분을 짧게 표현한 것입니다.

interface MyAdd {
    int add(int x, int y);
}

public class Main {
    public static void main(String[] args) {
        //람다식으로 함축 하기
        MyAdd b = (x, y) -> { return x + y; }; // 람다식 끝에 세미콜론 붙여주기
        
        int result = b.add(1, 2);
        System.out.println(result);
    }
}

 

아래 콘솔 출력의 결과는 익명 클래스 표현 방식의 객체 a 와, 람다식 객체 b 의 객체를 출력한 것입니다.

 

위에 출력값이 객체 a의 값인데, 아래 출력값인 람다식 객체 b의 출력값은 "외부클래스명$$Lambda/번호" 와 같은 독자적인 표현 형식을 지니고 있음을 확인할 수 있습니다.

 

독자적인 표현 방식

람다식을 사용하여 생성된 객체는 JVM(Java Virtual Machine)에 의해 내부적으로 처리되며, 특정한 형식의 이름을 가지게 되는데, 예를 들어 위의 그림에서 보는 '외부클래스명$$Lambda/번호' 요런 형태입니다.

이러한 표현 방식은 람다식이 실제로 어떻게 JVM 내부에서 표현되고 관리되는지를 나타냅니다.

 

람다식의 사용 범위

람다식은 함수형 인터페이스의 추상 메서드를 구현하기 위한 수단으로 제공됩니다.

일반 클래스나 추상 클래스의 메서드를 람다식으로 "줄이는"것은 불가능 합니다.

왜냐하면, 람다식은 함수형 인터페이스의 구현체를 제공하는 구문이므로, 여러 개의 추상 메서드를 가지고 있거나, 인터페이스가 아닌 클래스의 메서드를 대상으로 사용할 수 없기 때문이다.

 

즉, 아무 클래스나 추상 클래스의 메서드를 람다식으로 줄이거나 하는 행위는 못한다는 뜻입니다.

오로지 인터페이스로 선언한 익명 구현 객체만이 람다식으로 표현이 가능하다는 것이죠.

그리고 람다 표현이 가능한 이러한 인터페이스를 가리켜 함수형 인터페이스라고 합니다.

 

 

함수형 인터페이스란?

함수형 인터페이스란 딱 하나의 추상 메서드가 선언된 인터페이스를 말합니다.

아까 위에서 작성한 코드 예제에서 MyAdd 인터페이스가 바로 함수형 인터페이스입니다.

람다식은 함수형 인터페이스에서만 사용이 가능하며, 람다식을 사용하면 인터페이스를 구현하는 객체가 생성됩니다.

람다식은 이름을 따로 지정하지 않으므로 익명 구현 객체가 생성됩니다.

 

인터페이스에는 추상메서드가 있고, 이를 구현해야 메서드를 사용할 수 있습니다.

메서드가 1개라면 당연히 람다식이 해당 메서드가 구현하려는 메서드인지 알것입니다.

하지만, 메서드가 여러개라면 람다식으로 표현하였을 때 어떤 메서드를 실행해야 하는지 몰라 에러가 발생할 것입니다.

 

단, Java 8버전 부터 이용이 가능한 인터페이스의 final 상수나 default, static, private 메서드는 추상메서드가 아니기 때문에, 이들 여러개가 인터페이스에 들어있어도 오로지 추상 메서드가 한개라면 함수형 인터페이스로 취급 됩니다.

// 함수형 인터페이스가 될 수 있다.
interface MyAdd {
    int add(int x, int y);
}

// 함수형 인터페이스가 될수 없다.
interface MyCalculate {
    int add(int x, int y);
    int min(int x, int y);
}

// 구성요소가 많아도 결국 추상 메서드는 한개이기 때문에 함수형 인터페이스이다.
interface MyAdd {
    int add(int x, int y);

    final boolean isNumber = true; // final 상수
    default void print() {}; // 디폴트 메서드
    static void print2() {}; // static 메서드
}

 

 

@FunctionalInterface

나만의 함수적 인터페이스를 만들 때 혹시나 두 개 이상의 추상 메서드가 선언되지 않도록 컴파일러가 체크해주는 기능이 있는데, 인터페이스 선언 시 @FunctionalInterface 어노테이션을 붙여주게 된다면 두 개 이상의 메서드 선언 시 컴파일 오류를 발생시켜줍니다.

이 어노테이션은 개발자의 실수를 줄여주는 역할을 하는 것이죠.

 

함수형 인터페이스에 추상 메서드가 두 개 이상 선언되어 있다면, 다음과 같은 에러가 발생합니다.

 


 

람다식 작성 및 활용

자바에서 람다식을 화살표(->) 기호를 사용하여 람다 표현식을 작성하는 것을 이제 이해하셨을 겁니다.

(매개변수 목록) -> { 함수 몸체 };

 

우선 자바에서 람다 표현식을 작성할 때 유의해야 할 사항은 다음과 같습니다.

 

1. 매개변수의 타입을 추론할 수 있는 경우에는 타입을 생략할 수 있습니다.

// 타입 생략 전
(int x, int y) -> { return x + y; };


// 타입 생략 후
(x, y) -> { return x + y; };

 

 

2. 매개변수가 하나인 경우에는 괄호() 를 생략할 수 있습니다. 

단, 타입이 있으면 괄호()를 생략할 수 없습니다.

// 괄호 생략 전        // 괄호 생략 후
(a) -> a + a         a -> a + a  

// 괄호 생략 전         // 괄호 생략 불가
(int a -> a + a)     int a -> a + a

 

 

3. 함수의 몸체가 하나의 명령문만으로 이루어진 경우에는 중괄호{} 를 생략할 수 있습니다.

 

매개변수가 없는 경우

Runnable run = () -> System.out.println("Hello, World!");
run.run();

 

매개변수가 있는 경우

Consumer<String> printer = message -> System.out.println(message);
printer.accept("Print this message.");

 

다음 예제도 매개변수가 있는 경우 예제입니다.

Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // true 출력

 

 

4. 함수의 몸체가 하나의 return 문으로만 이루어진 경우에는 중괄호{}를 생략할 수 없습니다.

 

중괄호와 return 키워드를 포함하는 경우

Function<Integer, Integer> square = (Integer x) -> { return x * x; };
System.out.println(square.apply(5)); // 출력: 25

 

중괄호와 return 키워드를 생략하는 경우

Function<Integer, Integer> square = (Integer x) -> x * x;
System.out.println(square.apply(5)); // 출력: 25

 

위의 경우에는 return 문 대신 표현식을 사용할 수 있으며, 이때 반환값은 표현식의 결과값이 됩니다.

 

 

다음 예제는 전통적인 방식의 스레드 생성과 람다 표현식을 사용한 스레드 생성을 비교하는 예제입니다.

new Thread(new Runnable() {
    public void run() {
        System.out.println("전통적인 방식의 일회용 스레드 생성");
    }
}).start();

new Thread( () -> {
    System.out.println("람다 표현식을 사용한 일회용 스레드 생성");
};).start();


Runnabel 인터페이스는 자바에서 함수형 인터페이스로 사용되며, 단 하나의 추상 메서드인 run() 메서드를 가지기 때문에 람다식으로 표현이 가능하다.

 

람다식은 스트림(Stream) API와도 밀접하게 사용됩니다.

컬렉션을 함수형 스타일로 처리할 수 있게 해주며, 데이터의 필터링, 변환, 집계 등을 간결하고 선언적으로 표현할 수 있다.

List<String> names = Arrays.asList("John", "Doe", "Jane", "Smith");
names.stream()
    .filter(name -> name.startsWith("J"))
    .forEach(System.out.println(name)); // "John", "Jane" 출력
    //System.out.println(name) 을 System.out::println 으로도 간략 가능

 


 

추가로 자바는 java.util.function 패키지를 통해 여러 상황에서 사용할 수 있는 다양한 함수형 인터페이스를 미리 정의하여 제공합니다. 매번 새로운 함수형 인터페이스를 정의하지 않고, 이 패키지의 인터페이스를 활용함으로써 재사용할 수 있고 유지보수 측면에서도 좋습니다.

 

이 글에서 java.util.function 패키지의 기본적인 주요 함수형 인터페이스까지 설명하기에는 집중력이 떨어질 수 있으며 글이 너무 길어질 거 같아 따로 포스팅을 올리도록 하겠습니다. 

 

따로 function 패키지의 자세한 사항을 알고싶으신 분들은 아래 링크를 참고해주세요~

Package java.util.function =>


 

참고(출처)

https://inpa.tistory.com/entry/%E2%98%95-Lambda-Expression

https://hstory0208.tistory.com/entry/Java%EC%9E%90%EB%B0%94-%EB%9E%8C%EB%8B%A4%EC%8B%9DLambda%EC%9D%B4%EB%9E%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%82%AC%EC%9A%A9%EB%B2%95

https://makecodework.tistory.com/entry/Java-%EB%9E%8C%EB%8B%A4%EC%8B%9DLambda-%EC%9D%B5%ED%9E%88%EA%B8%B0

https://www.tcpschool.com/java/java_lambda_concept

+ 귀찮을 법 하지만 친절하고 자세하게 알려주는 챗 GPT 4.0님