공부/전자컴퓨터공학

Java 람다식(Lambda Expression)이란? 자바 기초 배우기

AhJustC 2024. 5. 30. 12:58
반응형
람다식(Lambda Expression)

람다식은 함수형 프로그래밍 기법을 지원하는 자바의 문법 요소이다. 람다식은 간단히 말해서 메서드를 하나의 식(expression)으로 표현한 것으로 코드를 매우 간결하면서 명확하게 표현할 수 있다는 큰 장점을 가지고 있다.

다음의 예를 살펴보자.

//기존 메서드 표현 방식
void sayhello() {
	System.out.println("HELLO!")
}

//위의 코드를 람다식으로 표현한 식
() -> System.out.println("HELLO!")

 

람다식에서는 기본적으로 반환 타입과 이름을 생략할 수 있다. 그러한 이유때문에 종종 이름없는 함수, 익명함수(anonymous) 라 부르기도 한다.

아래의 예시를 통해 메서드를 람다식으로 만드는 방법에 대해 살펴보자.

 

int sum(int num1, int num2) {
	return num1 + num2;
}

sum 메서드를 람다식으로 바꿔보자

(int num1, int num2) -> { // 반환타입과 메서드명 제거 + 화살표 추가
	return num1 + num2;
}

반환타입과 메서드명을 제거하고 코드블록 사이에 화살표를 추가해주었다.

 

// 기존 방식
void example1() {
	System.out.println(5);
}

// 람다식
() -> {System.out.println(5);}
// 기존 방식
int example2() {
	return 10;
}

// 람다식
() -> {return 10;}
// 기존 방식
void example3(String str) {
	System.out.println(str);
}

// 람다식
(String str) -> {	System.out.println(str);}

 

이렇게 람다식을 사용하면 기존 방식을 더 간편하고 명확하게 표현할 수 있다. 이외에도 특정 조건이 충족되면 람다식을 더욱 축약하여 표현할 수 있다.

 

// 기존 방식
int sum(int num1, int num2) {
	return num1 + num2;
}

// 람다식
(int num1, int num2) -> {
	num1 + num2
}

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

(int num1, int num2) -> num1 + num2

 

거기에 매개변수 타입을 함수형 인터페이스를 통해 유추할 수 있는 경우에는 매개변수 타입도 생략할 수 있다.

(num1, num2) -> num1 + num2
반응형

 

함수형 인터페이스

람다식 또한 사실은 객체이다. 이름이 없는 익명 객체라 할 수 있다. 아래의 예제를 봐보자.

// sum 메서드 람다식
(num1, num2) -> num1 + num2

// 람다식을 객체로 표현
new Object() {
	int sum(int num1, int num2) {
		return num1 + num1;
	}
}

위의 람다식으로 표현한 sum 메서드는 사실 아래와 같은 이름이 없는 익명 객체이다.

익명 객체는 익명 클래스를 통해 만들 수 있는데, 익명 클래스란 객체의 선언과 생성을 동시에 하여 오직 하나의 객체를 생성하고, 단 한번만 사용되는 일회용 클래스이다. 아래와 같이 생성과 선언을 한번에 할 수 있다.

new Object() {
	int sum(int num1, int num2) {
		return num1 + num1;
	}
}

 

만약에 람다식이 객체라 한다면 객체에 접근하고 사용하기 위한 참조변수가 필요할 것이다. 그런데 기존에 객체를 생성할 때 만들었던 Object 클래스에는 sum 이라는 메서드가 없으므로 Object 타입의 참조변수에 담는다고 하더라도 sum 메서드를 사용할 수 없다.

public class LamdaExample1 {
    public static void main(String[] args) {

        // 람다식 Object obj = (num1, num2) -> num1 + num2; 로 대체 가능
        Object obj = new Object() {
            int sum(int num1, int num2) {
                return num1 + num1;
            }
        };

        obj.sum(1, 2);
    }
}

출력 결과
java: cannot find symbol
  symbol:   method sum(int,int)
  location: variable obj of type java.lang.Object

 

이 같은 문제를 해결하기 위해 사용하는 자바의 문법요소가 바로 함수형 인터페이스(Functional Interface)라고 할 수 있다. 즉 자바에서 함수형 프로그래밍을 하기 위한 새로운 문법 요소를 도입하는 대신, 기존의 인터페이스 문법을 활용하여 람다식을 다루는 것이라고 할 수 있다. 함수형 인터페이스는 단 하나의 추상 메서드만 선언될 수 있는데 이는 람다식과 인터페이스의 메서드가 1:1로 매칭되어야 하기 때문이다.

public class LamdaExample1 {
    public static void main(String[] args) {
        /* Object obj = new Object() {
            int sum(int num1, int num2) {
                return num1 + num1;
            }
        };
        */
        ExampleFunction exampleFunction = (num1, num2) -> num1 + num2;
        System.out.println(exampleFunction.sum(10,15));
}

@FunctionalInterface // 컴파일러가 인터페이스가 바르게 정의되었는지 확인하도록 합니다.
interface ExampleFunction {
		int sum(int num1, int num2);
}

// 출력값
25

 

위의 예제에서 함수형 인터페이스인 ExampleFunction에 추상메서드 sum()이 정의되어 있다. 이 함수형 인터페이스는 람다식은 참조할 참조변수를 선언할 때 타입으로 사용하기 위해 필요하다.

함수형 인터페이스 타입으로 선언된 참조변수 exampleFunction에 람다식이 할당되었으며 이제 exampleFunction을 통해 sum()메서드를 호출할 수 있다.

 

매개변수와 리턴값이 없는 람다식
@FunctionalInterface
public interface MyFunctionalInterface {
    void accept();
}

 

이 인터페이스를 타깃 타입으로 갖는 람다식은 아래와 같은 형태로 작성되어야 한다. 람다식에서 매개변수가 없는 이유는 accept()가 매개변수를 가지고 있지 않기 때문이다.

MyFunctionalInterface example = () -> { ... };

// example.accept();

 

람다식이 대입된 인터페이스의 참조 변수는 위의 주석과 같이 accept)를 호출할 수 있다. accept()의 호출은 람다식의 중괄호{} 를 실행시킨다.

@FunctionalInterface
interface MyFunctionalInterface {
    void accept();
}

public class MyFunctionalInterfaceExample {
    public static void main(String[] args) throws Exception {
        MyFunctionalInterface example = () -> System.out.println("accept() 호출");
        example.accept();
    }
}

// 출력값
accept() 호출

매개변수가 있는 람다식
@FunctionalInterface
public interface MyFunctionalInterface {
    void accept(int x);
}

이 인터페이스를 타깃 타입으로 갖는 람다식은 다음과 같은 형태로 작성해야 한다. 람다식에서 매개변수가 한 개인 이유는 추상메서드 accept()가 매개변수를 하나만 가지기 때문이다.

public class MyFunctionalInterfaceExample {

    public static void main(String[] args) throws Exception {

        MyFunctionalInterface example;
        example = (x) -> {
            int result = x * 5;
            System.out.println(result);
        };
        example.accept(2);

        example = (x) -> System.out.println(x * 5);
        example.accept(2);
    }
}

// 출력값
10
10

 

람다식이 대입된 인터페이스의 참조변수는 다음과 같이 accept()를 호출할 수 있다. 위의 예시와 같이 매개값으로 2를 주면 람다식의 x 변수에 2가 대입되고, x는 중괄호 {}에서 사용된다.

 

리턴값이 있는 람다식
@FunctionalInterface
public interface MyFunctionalInterface {
    int accept(int x, int y);
}

 

이 인터페이스를 타깃 타입으로 갖는 람다식은 다음과 같은 형태로 작성해야 한다. 람다식에서 매개변수가 두 개인 이유는 accept()가 매개변수를 두 개 가지기 때문이다. 또한 accept()가 리턴 타입이 있기 때문에 중괄호 {}에는 return 문이 있어야 한다.

public class MyFunctionalInterfaceExample {

    public static void main(String[] args) throws Exception {

        MyFunctionalInterface example;

        example = (x, y) -> {
            int result = x + y;
            return result;
        };
        int result1 = example.accept(2, 5);
        System.out.println(result1);

        example = (x, y) -> { return x + y; };
        int result2 = example.accept(2, 5);
        System.out.println(result2);

        example = (x, y) ->  x + y;
        //return문만 있으면, 중괄호 {}와 return문 생략 가능
        int result3 = example.accept(2, 5);
        System.out.println(result3);

        example = (x, y) -> sum(x, y);
        //return문만 있으면, 중괄호 {}와 return문 생략 가능
        int result4 = example.accept(2, 5);
        System.out.println(result4);

    }

    public static int sum(int x, int y){
        return x + y;
    }
}

//출력값
7
7
7
7

 

람다식이 대입된 인터페이스 참조변수는 다음과 같이 accept()를 호출할 수 있다. 매개값으로 2와 5를 주면 람다식의 x 변수에 2, y 변수에 5가 대입되고 x와 y는 중괄호에서 사용된다.

 

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

자바에서는 빈번하게 사용되는 함수형 인터페이스를 기본적으로 제공하고 있다.

 

메서드 레퍼런스

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

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

람다식은 단순히 두 개의 값을 Math.max()메서드의 매개값으로 전달하는 역할만 하므로 다소 불편해 보인다. 또한 이 경우 입력값과 출력값의 반환타입을 쉽게 유추할 수 있으므로 입력값과 출력값을 일일이 적어주는게 크게 중요하지 않다. 이럴때는 다음과 같이 메서드 참조를 이용하면 깔끔하게 처리할 수 있다.

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

Math :: max // 메서드 참조

메서드 참조도 람다식과 마찬가지로 인터페이스의 익명 구현 객체로 생성되므로 인터페이스의 추상 메서드가 어떻게 매개변수를 가지고, 리턴타입이 무앗인가에 따라 달라진다.

IntBinaryOperator 인터페이스는 두 개의 int 매개값을 받아 int 값을 리턴하므로 Math::max 메서드 참조를 대입할 수 있다.

IntBinaryOperator operato = Math :: max; //메서드 참조

 

정적 메서드와 인스턴스 메서드 참조

정적 메서드를 참조할 때는 클래스 이름 뒤에 :: 기호를 붙이고 정적 메서드 이름을 기술하면 된다. 

인스턴스 메서드의 경우에는 먼저 객체를 생성한 다음 참조변수 뒤에 :: 기호를 붙이고 인스턴스 메서드 이름을 기술하면 된다.

아래 예제는 Calculator의 정적 및 인스턴스 메서드를 참조한다.

//Calculator.java
public class Calculator {
  public static int staticMethod(int x, int y) {
                        return x + y;
  }

  public int instanceMethod(int x, int y) {
   return x * y;
  }
}
import java.util.function.IntBinaryOperator;

public class MethodReferences {
  public static void main(String[] args) throws Exception {
    IntBinaryOperator operator;

    /*정적 메서드
		클래스이름::메서드이름
		*/
    operator = Calculator::staticMethod;
    System.out.println("정적메서드 결과 : " + operator.applyAsInt(3, 5));

    /*인스턴스 메서드
		인스턴스명::메서드명
		*/

    Calculator calculator = new Calculator();
    operator = calculator::instanceMethod;
    System.out.println("인스턴스 메서드 결과 : "+ operator.applyAsInt(3, 5));
  }
}
/*
정적메서드 결과 : 8
인스턴스 메서드 결과 : 15
*/

생성자 참조

생성자를 참조한다는 것은 객체 생성을 의미한다. 단순히 메서드 호출로 구성된 람다식을 메서드 참조로 대치할 수 있듯, 단순히 객체를 생성하고 리턴하도록 구성된 람다식은 생성자 참조로 대치할 수 있다. 이 경우 생성자 참조로 표현한다면 클래스 이름 뒤에 :: 기호를 붙이고 new 연산자를 기술하면 된다.

생성자가 오버로딩되어 여러개가 있으면 컴파일러는 함수형 인터페이스의 추상 메서드와 동일한 매개변수 타입과 개수가 있는 생성자를 찾아 실행한다. 다음 예제를 확인해보자.

//Member.java
public class Member {
  private String name;
  private String id;

  public Member() {
    System.out.println("Member() 실행");
  }

  public Member(String id) {
    System.out.println("Member(String id) 실행");
    this.id = id;
  }

  public Member(String name, String id) {
    System.out.println("Member(String name, String id) 실행");
    this.id = id;
    this.name = name;
  }

  public String getName() {
    return name;
  }

public String getId() {
    return id;
  }
}
import java.util.function.BiFunction;
import java.util.function.Function;

public class ConstructorRef {
  public static void main(String[] args) throws Exception {
    Function<String, Member> function1 = Member::new;
    Member member1 = function1.apply("kimcoding");

    BiFunction<String, String, Member> function2 = Member::new;
    Member member2 = function2.apply("kimcoding", "김코딩");
  }
}

/*
Member(String id) 실행
Member(String name, String id) 실행
*/

 

위의 코드는 생성자 참조를 이용하여 두 가지 방법으로 Member 객체를 생성하고 있다. 하나는 Function<String, Member> 함수형 인터페이스의 Member apply(String) 메서드를 이용해서 Member 객체를 생성하고 다른 하나는 BiFunction<String, String, Member> 함수형 인터페이스의 Member 객체를 생성한다.

반응형