포스트

02.람다 표현식

람다 표현식

  • 람다 표현식이란 함수형 프로그래밍을 구성하기 위한 함수식이다.
  • 간단히 말해 자바의 메소드를 간결한 함수 식으로 표현한 것이다
  • 메소드 표현식을 메소드타입, 메소드 이름, 매개변수 타입, 중괄호, return 문을 생략하고, 화살 기호를 넣는다
  • 이러한 특징으로 람다식을 이름이 없는 함수 익명 함수라고도 한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
  • 타입을 생략해도 컴파일러가 에러를 띄우지 않는 이유는
  • 컴파일러 나름대로 생략된 타입 위치를 추론하여 동작하게 해주기 때문이다.

람다식과 자바스크립트 익명 화살표 함수

  • JS의 익명 화살표 함수도 람다 함수의 일종이다.
  • 단지 화살표 모양이 자바(->), JS(=>) 이다
1
2
3
4
5
6
7
const Myfunction = {
  print: function(){}
};

Myfunction.print = (str) => console.log(str);
let myfunc = Myfunction;
myfunc.print("Hello");
1
2
3
4
5
6
7
8
9
10
interface Myfunction{
  void print(String str);
}

public class Main{
  public static void main(String[] args){
    Myfunction myfunc = (str) -> System.out.println(str);
    myfunc.print("Hello");
  }
}
  • 변수에 함수를 담을 때,
  • 자바스크립트는 약타입 언어라 타입에 관계없이 자유롭게 받을 수 있다
  • 하지만 자바에는 8가지 타입밖에 없기 때문에 함수 데이터 자체를 담을 수 있을만한 자료형이 딱히 없다
  • 그래서 인터페이스를 익명 구현 객체 타입으로써, 해당 인터페이스 타입으로 함수를 받을 수 있게 하였다.

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

  • 람다식 형태를 보면 마치 자바의 메소드를 변수로 선언하는 것 처럼 보이지만, 사실 자바는 메소드를 단독으로 선언할 수 없다
  • 형태만 그렇게 보일 뿐이고, 코드를 보면 람다 함수식을 변수에 대입하고 변수에서 메소드를 호출하는 것이다. 마치 객체와 같다.
  • 사실 람다식도 결국은 객체이다. 인터페이스를 익명 클래스로 구현한 익명 구현 객체를 짧게 표현한 것 뿐이다.
1
2
Myfunction myfunc = (str) -> System.out.println(str);
myfunc.print("Hello");

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface IAdd{
  int add(int x, int y);  
}

class Add implements IAdd{
  public int add(int x, int y){
    return x + y;
  }
}

public class Main{
  public static void main(String[] args){
    Add a = new Add();

    int res = a.add(1, 2);
    System.out.println(res);
  }
}

  • 한 번만 사용ㄷ하고 버려질 클래스라면, 번거롭게 클래스를 선언하지 않고, 익명 클래스로 일회용 오버라이딩하여 사용하기도 한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface IAdd{
  int add(int x, int y);  
}

public class Main{
  public static void main(String[] args){
    IAdd a = new IAdd() {
      public int add(int x, int y){
        return x + y;
      }  
    };

    int res = a.add(1, 2);
    System.out.println(res);
  }
}

  • 람다는 이 익명 클래스 코드를 짧게 표현한 것이다
1
2
3
4
5
6
7
8
9
10
11
12
interface IAdd{
  int add(int x, int y);  
}

public class Main{
  public static void main(String[] args){
    IAdd a = (x, y) -> {return x + y}; // 람다식의 끝에 세미콜론을 잊지 말자!!

    int res = a.add(1, 2);
    System.out.println(res);
  }
}

  • 아무 클래스나 추상 클래스의 메소드를 람다식으로 줄이거나 하는 행위는 못한다
  • 오로지 인터페이스로 선언한 익명 구현 객체만이 람다식으로 표현이 가능하다.
  • 그리고 람다식이 가능한 인터페이스를 가리켜 함수형 인터페이스라고 한다
함수형 인터페이스
  • 함수형 인터페이스란 딱 하나의 추상 메소드가 선언된 인터페이스이다.
  • 람다식 자체가 하나의 메소드를 한줄로 정의하는 표현식이기 때문에, 두 개 이상의 추상 메소드가 있으면 표현할 방법이 없다.
  • 단, 인터페이스의 final 상수나, default, static, private 메소드는 추상 메소드가 아니기 때문에 고려하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 함수형 인터페이스 가능
interface IAdd{
  int add(int x, int y);
}

// 불가능
interface Calculator{
  int add(int x, int y);
  int min(int x, int y);
}

// 구성요소가 많아도 추상 메소드는 하나라 가능
interface IAdd {
  int add(int x, int y);

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

@FunctionalInterface

  • 나만의 함수형 인터페이스를 만들 때 두 개 이상의 추상 메소드가 선언되지 않도록 컴파일러가 checking 해주는 기능
  • 두 개 이상의 추상화 메소드 선언 시 컴파일 에러 발생.
1
2
3
4
5
@FunctionalInterface
public interface MyFunctional{
  public void method();
  public void otherMethod(); // 컴파일 오류 발생
}

람다식의 타입 추론

  • 컴파일러가 람다 함수식을 보고 추론하여 타입을 유추한다
  • 추론의 근거로 사람이 미리 정의해 놓은 정의문을 보고 추론을 하는 것이다
1
2
3
4
5
6
7
8
9
10
11
12
13
interface IAdd{
  int add(int x, int y);
}

public class Main{
  public static int result(IAdd lambda){
    return lambda.add(1, 2);
  }

  public static void main(String[] args){
    int n = result((x,y) -> x + y);
  }
}
  • 1.. 람다식을 받는 메소드의 매개변수 타입을 본다 -> result 메소드의 매개변수 타입
  • 2.. 함수형 인터페이스 정의문을 찾아 추상 메소드 형태를 본다 -> IAdd 인터페이스로 가서 추상화 메소드 확인
  • 3.. 추상 메소드에 정의된 타입에 따라 람다 함수식의 타입을 자동으로 판별한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.*;

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

class Collections{
  public static <T> void sort(List<T> list, java.util.Comparator<? super T> c){
    list.sort(c);
  }
}

public class Main{
  public static void main(String[] args){
    List<String> words = Arrays.asList("aaa", "bbb", "ccc", "ddd");

    Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
  }
}
  • 위의 sort를 분석해보면
  • Collections 클래스의 sort 메소드 정의문을 보면, 람다 함수의 함수형 인터페이스 타입은 java.util.Comparator 이다.
  • 람다의 매개변수 타입은 Comparator 인터페이스의 제네릭 T 타입으로 지정되어 있다
  • 컴파일러가 타입을 유추하는 순서는 다음과 같다
    • 1.. sort 메소드의 첫번째 매개변수 List 형태의 객체가 들어온다
    • 2.. 첫번째 매개변수의 타입 지정에 의해 sort 메소드의 제네릭 타입 매개변수는 모두 String이 된다
    • 3.. Comparator 인터페이스 제네릭 타입도 String이 된다. 추상 메소드의 매개변수 타입도 String이 된다
    • 4.. 최종적으로 람다 함수의 타입 구성은 int형 메소드 반환 타입과 String 형 매개변수 타입 두개로 추론된다.
  • 람다식의 타입을 개발자가 유추하기 쉽게 도와주기 위해 상황에 따라 람다식 파라미터에 타입을 쓰기도 한다.
참고 사이트

인파-람다 표현식(Lambda Expression) 완벽 정리