람다(Lambda)

김수민·2023년 3월 13일
0

백엔드 부트캠프

목록 보기
22/52

개요

  • 람다식(Lambda Expression): 함수형 프로그래밍 기법을 지원하는 자바의 문법요소
  • 장점: 메서드를 하나의 '식(expression)'으로 표현한 것으로, 코드를 매우 간결하면서 명확하게 표현할 수 있음

람다식의 기본 문법

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

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

람다식에서는 기본적으로 반환타입과 이름을 생략할 수 있음. -> 익명 함수(anonymous function)

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

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

특정 조건이 충족되면 람다식을 더욱 축약하여 표현 가능

(num1, num2) -> num1 + num2

함수형 인터페이스

람다식 또한 객체. 이름이 없기 때문에 ㅇ기명 개체.

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

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

람다식으로 표현한 sum 메서드는 이름이 없응 익명 객체
익명 클래스: 객체의 선언과 생성을 동시에 하여 오직 하나의 객체를 생성하고, 단 한번만 사용되는 일회용 클래스

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

생선과 선언을 한번에 할 수 있음

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

기존에 객체를 생성할 때 만들었던 Object 클래스에는 sum라는 메서드가 없기 때문에 Object 타입의 참조변수에 담는다고 하더라도 sum 메서드를 사용할 수 없음.
이 같은 문제를 해결하기 위해 사용하는 자바의 문법요소가 바로 자바의 함수영 인터페이스(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()의 호출은 람다식의 중괄호{}를 실행시킴.

@FunctioanlInterface
interface MyFunctionalInterface {
  void accept();
}

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

// 출력값
accept() 호출

매개변수가 있는 람다식

매개 변수가 있고 리턴값이 없는 추상 메서드를 가진 함수형 인터페이스가 있다고 가정.

@FunctionalInteface
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.accpet(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);
    Sysetm.out.println(result2);
    
    example = (x, y) -> x + y;
    //return문만 있을 경우, 중괄호 {}와 reutrn문 생략가능
    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는 중괄호에서 사용됨.

메서드 레퍼런스

  • 메서드 참조: 람다식에서 불필요한 매개변수를 제거할 때 주로 사용
  • 람다식으로 더욱 간단해진 익명 개체를 더욱더 간단하게 사용하고 싶을 때 사용

예시코드: 두 개의 값을 받아 큰 수를 리턴하는 Math 클래스의 max() 정적 메서드를 호출하는 람다식

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

람다식은 단순히 두 개의 값을 Math.max() 메서드의 매개값으로 전달하는 역할만 하기 때문에 다소 불편해보임. 또한 입력값과 출력값의 반환타입을 쉽게 유추할 수 있기 때문에 입력값과 출력값을 일일이 적어주는 것이 크게 중요하지 않음.

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

Math :: max // 메서드 참조

메서드 참조도 람다식과 마찬가지로 인터페이스의 익명 구현 객체로 생성되므로 인터페이스의 추상 메서드가 어떤 매개 변수를 가지고, 리턴 타입이 무언가에 따라 달라짐.
IntBinaryOperator 인터페이스는 두 개의 int 매개값을 받아 int값을 리턴하므로, Math::max 메서드에 참조를 대입할 수 있음

IntBinaryOperator oprato = 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
*/

생성자 참조

메서드 참조는 생성자 참조도 포함함.
생성자를 참조한다는 것은 객체 생성을 의미함. 단순히 메서드 호출로 구성된 람다식을 메서드 참조로 대치할 수 있듯이, 단순히 객체를 생성하고 리턴하도록 구성된 람다식은 생성자 참조로 대치 가능.

(a,b) -> new 클래스(a,b)

// 람다식 표현
클래스 :: 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 객체를 생성함. 이 때 생성자 참조는 두 가지 방법 모두 동일하지만, 실행되는 Member 생성자가 다름.

0개의 댓글