본문 바로가기

Learning

JAVA 람다함수 총정리

유래

λ-calculus(람다대수)에서 유래했다. 어것은 1930년경 논리학등에 사용하기 위해 제안된 수학적 형식인데, 예를 들면 아래와 같다. 의미는 "매개변수를 받아 1을 더해서 반환한다" 이다.

λx.x+1


'λ'다음에 매개변수 'x'가 선언되고, '.'뒤에 함수의 내용이 정의된다. 이 형식은 2가지 중요한 의미를 가진다. 하나는 '함수를 정의' 한다는  것이고 다른 하나는 함수의 이름이 없다는 것이다. 따라서 λ는 Anonymous function을 의미한다.


왜 λ(람다)를 쓰게 되었을까?

현대의 프로그래밍 언어는 대부분 객체지향이거나 적어도 이 방식에 근거를 한다. 대표적인 언어가 JAVA인데, 객체지향적인 언어의 규약이 가끔은 비효율적인 경우가 있다.  λ(람다)는 이를 해결하기 위해 도입이 되었다. 단순히 코드의 길이를 단축하는 것 만은 아니다.


아래는 λ(람다)함수와 클래스의 주요한 차이점이다. 이 것은 좋다-나쁘다로 보기보다는 하나의 특성으로 이해하면 되겠다. JAVA에서 '~Helper' 혹은 '~Util' 클래스에 static 함수를 선언하고 사용하는 것과 유사하다.

λ(람다) 함수

클래스 

  •  인스턴스를 생성할 필요가 없다.
  • static 처럼 'permanent' 메모리에 적재된다.
  •  클래스를 쓰기 위해서는 'new'등을 통해 인스턴스를 만들어야 한다.
  • 필요할 때, heap 메모리에 적재된다.



JAVA에서 λ(람다)를 쓸 수 있는 경우

SAM(Single Abstract Method)인 경우  λ(람다)를 이용할 수 있다. 예를 들면 'Runnable'같은 인터페이스를 SAM이라 한다. 즉, 하나의 함수를 갖는 인터페이스를 말한다. 이 SAM은 상속받은 구현객체 혹은 Anonymous class를 통해 사용될 수 있고, λ(람다)를 이용한 함수로 사용할 수 있다.


interface Runnable 

{

     public void run();

}

...

// anonymous class 

Runnable r = new Runnable() {

   public void run() {

      ...

   }

};


// 람다.

Runnable r2 = () -> { ... };


'FunctionalInterface' 표식을 사용할 수도 있다. 이 것은 상속등의 수정작업이 있을 때, 컴파일 단계에서 실수를 막아준다. 아래는 앞으로 예로 사용할 인터페이스 선언이다.


@FunctionalInterface

interface Calculator

{

    public int calc(int p1, int p2);

}


@FunctionalInterface

interface Calculator2

{

    public int calc(int p1);

}


@FunctionalInterface

interface Calculator3

{

    public void calc();

}


JAVA,  λ(람다)함수의 기본형식 

맨 위에 예를 든 λ(람다)의 형식 λx.x+1은 JAVA에서 표현하면 (x)->x + 1 로 된다. 

  • λ  =>  ()
  • .   =>  ->

이 기본형식을 가지고 앞서 예를 든 SAM들에 대한 λ(람다)식을 나열해 본다. 나열된 표현은 모두 유효하다.


Calculator::calc

  • (int x, int y) -> { return x + y; }
  • (x, y) -> { return x + y; }            // 데이터형은 암묵적으로 생략 가능하다. 
  • (x, y) -> x + y;                            // 예약어 'return' 생략 가능하며, '{}'도 단문이면 생략 가능하다. 

    Calculator2::calc

    • x -> x * 2;                                 // 매개변수가 하나일 때, '()'를 생략할 수 있다.

    Calculator3::calc

    • () -> System.out.println("Hello");  // 매개변수가 없을 때


     λ(람다)함수의 고급 사용 - static method, 생성자 참조

    @FunctionalInterface

    interface Parser

    {

        public int parse(String s);

    }


    Parser p1 = (s) -> Integer.parseInt(s);

    Parser p2 = Integer::parseInt


    위의 코드에서 'p1'을 선언할 때 사용한 람다식에서 함수 'parseInt'는 'Integer'객체의 static method이다. 이 것은 'p2'를 선언하는 것처럼 "class::static_method" 형식을 따르는 식으로 대체 가능하다.


    @FunctionalInterface

    interface Factory

    {

        public String create();

    }


    Factory f1 = () -> new String();

    Factory f2 = String::new;


    static method와 마찬가지로 생성자에 대해서도 'f2'를 선언할 때와 같이 'class::new' 형식의 식으로 대체 가능하다. 여기서 하가지 의문이 드는데, 매개변수가 다르게 여러개의 생성자 혹은 같은 이름의 static method가 있을 경우에는 어떻게 될까?


    아래의 코드가 해답이 될 것이다. 매개변수의 데이터형, 갯수가 모두 일치하는 것이 짝지워진다.

    @FunctionalInterface

    interface Factory

    {

        public String create(String maker);

    }


    class Car

    {

        public Car() { }

        public Car(String maker) { ... }

        ...

    }


    Factory f1 = Car::new;            // Car(String maker)와 짝지워짐


    Car c1 = f1.create("bentz");     // OK

    Car c2 = f1.create();               // Error



    λ(람다)함수의 고급 사용 - 일반 method 참조

    일반 메소드도 참조가 가능한데 인스턴스된 것의 함수인지 아니면 객체에 선언된 것을 사용하는지에 따라 다르다. 그리고 후자는 아직 JAVA에서 정리되지 않은 느낌이다.



    String s = "Hello";

    Greeting g1 = () -> s.toString();

    Greeting g2 = s::toString;



    마치며

    Closure라는 개념도 있다. 람다가 더 상위 개념인데, 람다함수를 static영역이 아니라 어떤 컨텍스트에서 인스턴스로 생성한 개념인데, 다음 기회에 정리해보도록 한다.