[STUDY HALLE] 15주차 과제: 람다식

4 minute read

학습 목표

자바의 람다식에 대해 학습하세요.

  • 람다식 사용법
  • 함수형 인터페이스
  • Variable Capture
  • 메소드, 생성자 레퍼런스

Deadline: ~2021/03/06 13:00

람다식

람다식이란 자바 8 부터 지원된 기능으로 메소드를 하나의 식으로 표현한 것인데, ‘익명 함수’라고도 부른다. ‘익명’인 이유는 보통의 메소드와 달리 이름이 없기 때문이고, ‘함수’인 이유는 메소드처럼 특정 클래스에 종속되지 않기 때문이다. 이러한 람다 표현식을 잘 사용하면 기존의 불필요한 코드를 줄여주고, 작성된 코드의 가독성을 높여준다고 한다.

@FunctionalInterface
interface MyFunction {
    int min(int a, int b);
}

public class Week15 {
    public static void main(String[] args) {
        MyFunction myFunction = (x, y) -> x < y ? x : y;
        System.out.println(myFunction.min(99,100));
    }
}
99

우선, 람다 표현식을 작성할 때 규칙은 다음과 같다.

  1. 매개변수의 타입을 추론할 수 있는 경우에는 타입을 생략할 수 있다.
  2. 매개변수가 하나인 경우에는 괄호를 생략할 수 있다.
  3. 함수의 몸체가 하나의 명령문만으로 이루어진 경우에는 중괄호를 생략할 수 있다. (이때 세미콜론(;)은 붙이지 않음)
  4. 함수의 몸체가 하나의 return 문으로만 이루어진 경우에는 중괄호를 생략할 수 없다.
  5. return 문 대신 표현식을 사용할 수 있으며, 이때 반환값은 표현식의 결괏값이 된다. (이때 세미콜론(;)은 붙이지 않음)
// example
(int x, int y) -> { return x < y ? x: y; }
(int a) -> { return a * a; }
(int i) -> { System.out.println(i); }

1. (x, y) -> {return x < y ? x: y;}
2. a -> {return a * a;}
3. i -> System.out.println(i)
4. a -> return a * a; => 불가능
5. a -> a * a

함수형 인터페이스

위의 예시에서 MyFunction 인터페이스가 함수형 인터페이스이다.

@FunctionalInterface
interface MyFunction {
    int min(int a, int b);
}

람다 표현식을 사용하기 위해서 선언한 람다 표현식을 저장하는 참조 변수가 필요하고, 이 참조 변수의 타입으로 사용하게된다.

// 세미콜론은 람다 표현식에 붙은 것이 아니라 전체 statement에 붙은 것
MyFunction myFunction = (x, y) -> x < y ? x : y;

함수형 인터페이스는 추상 클래스와는 달리 단 하나의 추상 메소드만을 가져야 한다. 그래야만 이름이 없는 람다식이 단 하나의 메소드와 연결될 수 있기 때문이다. 그리고 ‘@FunctionalInterface’ 어노테이션을 사용하여 함수형 인터페이스임을 명시할 수 있는데, 컴파일러는 이 어노테이션이 아니라 인터페이스의 구조(즉, 추상메소드가 하나만 존재하는지)에 따라 함수형 인터페이스 여부를 판단하기 때문에 필수적인 것은 아니다.

Object o = () -> System.out.println("안녕");

그렇다면 위의 예시는 가능할까? 람다식은 사실상 익명 클래스의 객체와 동일하므로 Object에 담을 수 있지 않을까?

위의 코드를 보고 컴파일러는 ‘Target type of a lambda conversion must be an interface’라는 대답을 내놓지만, 사실 진짜 문제는 컴파일러가 람다식이 어떤 함수형 인터페이스를 구현하고 있는지 알 수 없는 것이다. 따라서 위의 코드를

Object o = (Runnable)() -> System.out.println("안녕");

로 고친다면 정상적으로 컴파일된다.

람다 표현식을 ‘익명 객체를 짧게 대체할 수 있는 방법’으로 보는 것도 이해하는데 도움이 될 것 같다.

MyFunction myFunction1 = new MyFunction() {
        public int min(int x, int y) {
            return x < y ? x : y;
        }
};

실제로 위와 같이 코드를 작성하면 IDE에서

Anonymous new MyFunction() can be replaced with lambda

라며 람다 표현식으로 바꾸기를 권유해준다.

메소드 참조

메소드 참조(method reference)는 람다 표현식이 단 하나의 메소드만을 호출하는 경우에 해당 람다 표현식에서 불필요한 매개변수를 제거하고 사용할 수 있도록 해준다.

클래스::메소드이름
참조변수::메소드이름
//DoubleUnaryOperator 인터페이스는 한 개의 double 형 매개변수를 전달받아 한 개의 double 형 값을 반환
// java.util.function 패키지에서 제공하는 함수형 인터페이스
DoubleUnaryOperator oper;

oper = (n) -> Math.abs(n); // 람다 표현식
System.out.println(oper.applyAsDouble(-5));

oper = Math::abs; // 메소드 참조
System.out.println(oper.applyAsDouble(-5));

람다 표현식이 Math 클래스의 abs()메소드로 단순히 인자를 전달하는 역할만 하므로, 메소드 참조를 이용하여 더 간단히 표현할 수 있다.

생성자 참조

생성자를 호출하는 람다 표현식도 메소드 참조를 이용할 수 있다.

Supplier<Myclass> myclassSupplier1 = () -> {return new Myclass();};
Supplier<Myclass> myclassSupplier2 = Myclass::new;

Variable Capture

자바의 람다 표현식은 특정 상황에서 람다 표현식 외부에서 선언된 변수에 접근 할 수 있는데, 이와 같은 동작을 람다 캡쳐링(capturing lambda)이라고 한다. 지역 변수, static 변수, 인스턴스 변수에 접근할 수 있는데, 지역변수만 변경이 불가능하고 나머지 변수들은 읽기 및 쓰기가 가능하다.

람다는 지역 변수가 존재하는 스택에 직접 접근하지 않고, 지역 변수를 자신(람다가 동작하는 쓰레드)의 스택에 복사한다. 왜냐하면 지역 변수에 바로 접근하게 되면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하는 상황이 발생할 수 있기 때문이다. 그리고 멀티 쓰레드 환경에서 람다 표현식에 사용되는 지역 변수를 변경하게 되면 동기화 문제가 발생할 수 있으므로 지역변수는 final, Effectively Final 제약조건을 갖게된다.

Effectively Final : 변수가 변경되지 않으면 컴파일러가 final이라 판단

static 변수, 인스턴스 변수는 힙 영역에 생성되고, 힙 영역은 쓰레드끼리 공유하기 때문에 변경하는 제약에서 자유로울 수 있는 것이다.
이해하기 아주 좋은 예제를 해당 블로그에서 참고하였다.

@FunctionalInterface
interface MyFunction3 {
    void myMethod();
}

class Outer {
    int val = 10;

    class Inner {
        int val = 20;

        void method(int i) {    // void method(final int i)
            int val = 30;   // final int val = 30;
//          i = 10;       // ERROR. 상수의 값은 변경할 수 없다.

            MyFunction3 f = () -> {
                System.out.println("             i : " + i);
                System.out.println("           val : " + val);
                System.out.println("      this.val : " + ++this.val);
                System.out.println("Outer.this.val : " + ++Outer.this.val);
            };

            f.myMethod();
        }
    } // End Of Inner
}   // End Of Outer

public class VariableCapture {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.method(100);
    }
}
             i : 100
           val : 30
      this.val : 21
Outer.this.val : 11
  • 람다식 내에서 참조하는 지역변수는 final이 붙지 않아도 상수로 간주된다.
  • 람다식 내에서 지역변수 i와 val을 참조하고 있으므로 람다식 내에서나 다른 곳에서도 이 변수들의 값을 변경할 수 없다.
  • 반면, Inner 클래스와 Outer 클래스의 인스턴스 변수인 this.val과 Outer.this.val은 상수로 간주되지 않아서 값을 변경해도 된다.
  • 람다식에서 this는 내부적으로 생성되는 익명 객체의 참조가 아니라 람다식을 실행한 객체의 참조다. 따라서 위의 코드에서 this는 중첩 객체인 Inner이다.

참고문헌

https://www.oracle.com/technical-resources/articles/java/architect-lambdas-part1.html http://www.tcpschool.com/java/java_lambda_concept
https://sujl95.tistory.com/76
https://watrv41.gitbook.io/

Leave a comment