Web/Java

[Java] 람다식

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

 

 

람다식 (Lambda expression)

람다식은 메서드를 하나의 '식(expression)'으로 표현한 것입니다.

즉, 람다식은 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해줍니다.

 

메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로 '익명 함수(anonymous function)'이라고도 부릅니다.

 

 

 

메서드와 함수의 차이

위의 설명에서 '함수'라는 용어를 사용했습니다.

원래 자바의 객체지향개념에서는 '함수' 대신 '메서드'라는 용어를 사용합니다.

 

메서드는 기본적으로 함수와 비슷한 의미이지만, 특정 클래스에 반드시 속해야 한다는 제약을 가지고 있습니다.

그래서 자바에선 함수와는 다른 '메서드'라는 용어를 사용한 것입니다.

 

그러나 이제 람다식을 통해 메서드가 하나의 독립적인 기능을 하기 때문에 함수라는 용어를 사용하게 되었습니다.

 

 

 

 

람다식 작성법

람다식은 '익명 함수'답게 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통 {} 사이에 '->'를 추가합니다.

 

 

기본 작성 방법


반환타입 메서드이름 (매개변수 선언) {

    문장들

}

 

 

 

반환타입 메서드이름 (매개변수 선언) -> {

    문장들

}

 


 

 

 

 

기호 생략조건

 

1. 반환값이 있는 메서드의 경우, return문 대신 '식'으로 대신 할 수 있습니다.

식의 연산결과가 자동으로 반환값이 됩니다.

문장이 아닌 '식'이므로 끝에 ';'를 붙이지 않습니다.

 

before

(int a, int b) -> {return a > b ? a : b;}

after

(int a, int b) -> a > b ? a : b

 

 

2. 매개변수의 타입은 추론이 가능한 경우 생략할 수 있습니다.

 

단, (int a, b) -> a > b ? a : b 와 같이 두 매개변수 중 어느 하나의 타입만 생략하는 것은 허용되지 않습니다.

 

before

(int a, int b) -> a > b ? a : b;

after

(a, b) -> a > b ? a : b;

 

 

3. 선언된 매개변수가 하나뿐인 경우에는 괄호()를 생략할 수 있습니다.

 

단, 매개변수의 타입이 있으면 괄호()를 생략할 수 없습니다.

 

before

(a) -> a * a

after

a -> a * a

 

 

4. 괄호{} 안의 문장이 하나일 때는 괄호{}를 생략할 수 있습니다.

 

이 때, 문장의 끝에 ';'를 붙이지 않습니다.

 

만약 괄호 {}안의 문장이 return 경우에는 괄호{}를 생략할 수 없습니다.

 

before

(String name, int i) -> {
    System.out.println(name+"="+i);
}

after

(String name, int i) -> 
    System.out.println(name+"="+i)

 

 

 


 

 

 

함수형 인터페이스

 

람다식을 의미상 함수라고 표현하지만 자바에서 모든 메서드는 항상 어떤 클래스 내에 포함되어야 합니다.

 

그렇다면, 람다식은 어떤 클래스에 포함되어있을까요?

 

사실 람다식은 익명 클래스의 "객체"입니다.

 

 

이해하기 쉽게 코드로 설명하겠습니다.

 

아래와 같은 람다식은,

(int a, int b) -> a > b ? a : b

 

아래와 같은 익명 클래스 객체입니다.

new Object() {
    int max(int a, int b) {
        return a > b ? a : b;
    }
}

 

???

 

여기서 의문.

 

람다식은 "익명 객체"라고 하는데 이 객체의 메서드는 어떻게 호출한 것인가?

 

일단은 익명 객체라고 하니까 람다식을 어떠한 참조변수에 담아보겠습니다.

타입 f = (int a, int b) -> a > b ? a : b;

 

또 한가지 더 의문.

그러면 참조변수 f의 타입은 어떤 것이어야 할까?

 

 

 

자바는 이것을 인터페이스로 설명합니다.

 

  1. 인터페이스 하나를 만들어 참조변수를 받고
  2. 이 참조변수에 익명 클래스의 객체를 생성하며 넣습니다.
  3. 익명 클래스의 객체는 람다식과 같으므로 여기에 람다식 또한 넣을 수 있습니다.

 

인터페이스 하나 만들어 참조변수를 받고

interface MyFunction {
    public abstract int max(int a, int b);
}

 

 

이 참조변수에 익명 클래스의 객체를 생성하며 넣습니다.

MyFunction f = new MyFunction() {
  @Override
    public int max(int a, int b) {
      return a > b ? a : b;
    }
  int big = f.max(5, 3); // 익명 객체의 메서드를 호출
};

 

 

익명 클래스의 객체는 람다식과 같으므로 여기에 람다식 또한 넣을 수 있습니다.

MyFunction f = (a, b) -> a > b ? a : b;
int big = f.max(5, 3); // 익명 객체의 메서드를 호출

 

 

람다식을 넣었는데 익명 객체의 메서드를 호출할 수 있는 이유는 3가지 입니다.

  1. 람다식도 실제로는 익명 객체
  2. MyFunction인터페이스를 구현한 '익명 객체의 메서드 max()'와 '람다식의 매개변수의 타입과 개수 그리고 반환값'이 일치하기 때문
  3. 이 인터페이스는 오직 하나의 추상메서드로만 정의됨

 

우리는 이 인터페이스를 '함수형 인터페이스' 라고 부릅니다.

 

 

함수형 인터페이스

  • 오직 하나의 추상메서드로만 정의되어있다.
  • static 메서드와 default메서드의 개수에는 제약이 없다.

 

+ 추가로

애노테이션에서 언급한 @FunctionalInterface를 붙이면 함수형 인터페이스를 올바르게 정의했는지 확인해주므로 꼭 붙이도록 합시다.

 

 

 

함수형 인터페이스가 활용된 곳 예시

그럼 이 함수형 인터페이스가 어떻게 쓰였는지 보겠습니다.

 

 

Collections

기존의 Collections.sort 코드는 아래와 같았습니다.

Collections.sort(list, new Comparator<String>() {
    public int campare(String s1, String s2) {
        return s2.compareTo(s1);
    }
});

 

여기서 람다식을 적용해서 아래와 같이 간단하게 처리할 수 있게 되었습니다.

(익명 객체 new Comparator<String>() {...} 를 람다식으로 교체)

Collections.sort(list, (s1, s2) -> s2.compareTo(s1));

 

 

 

 

함수형 인터페이스 타입의 매개변수와 반환타입

 

함수형 인터페이스 MyFunction이 정의되어 있을때

@FunctionalInterface
interface MyFunction {
    void myMethod();
}

 

어떤 메서드의 매개변수의 타입이 MyFunction이면

    static void execute(MyFunction f) {
        f.myMethod();
    }

 

이 메서드를 호출할땐 "람다식을 참조하는 참조변수" 또는 그냥 "람다식"을 매개변수로 지정해야 합니다.

        MyFunction f = () -> System.out.println("myMethod()");
        
        execute(f); // 람다식을 참조하는 매개변수
        execute(() -> System.out.println("myMethod()")); // 람다식

 

 

즉, "메서드"를 변수처럼 주고받는 것이 가능해진 것입니다.

 

 

 

 

 

 

람다식의 타입과 형변환

 

 

람다식은 익명 객체이지만 컴파일러가 임의의 타입으로 설정합니다.

 

그래서 객체인데도 Object타입으로 형변환 할 수 없고 오직 함수형 인터페이스로만 형변환이 가능하도록 되어있습니다.

Object obj = (Object) (() -> {}); // 에러발생

 

하지만 이 말이 "람다식의 타입이 함수형 인터페이스의 타입과 일치하다"는 말은 아닙니다.

함수형 인터페이스로 람다식을 참조만 할 수 있는 것일 뿐입니다.

 

그래서 양변의 타입을 일치시키기 위해 형변환이 필요하고 형변환이 생략되어 있는 것입니다.

MyFunction f = (MyFunction) (() -> {});

 

 

package javastudy.ch15.functionalinterface;

@FunctionalInterface
interface MyFunction {
    void myMethod();
}

public class LambdaEx1 {
    static void execute(MyFunction f) {
        f.myMethod();
    }
    public static void main(String[] args) {
        MyFunction f = () -> {};
        Object obj = (MyFunction) (() -> {});
        String str = ((Object) (MyFunction) (() -> {})).toString();

        System.out.println(f);
        System.out.println(obj);
        System.out.println(str);
        
        System.out.println((MyFunction) (() -> {}));
        System.out.println(((Object) (MyFunction) (() -> {})).toString());
    }
}

람다식의 타입이 ~$$Lambda&번호~ 로 되어있는 것을 볼 수 있다.

 

 

 

 

java.util.function 패키지

java.util.function패키지에는 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았습니다.

 

매번 새로운 함수형 인터페이스를 정의하지 않고, java.util.function패키지의 인터페이스를 사용하면 좋습니다.

(함수형 인터페이스에 정의된 메서드 이름이 통일되면 재사용성과 유지보수 측면해서 좋습니다.)

 

함수형 인터페이스 메서드 설명
java.lang.Runnable void run() 매개변수 x. 반환값 x
Supplier<T> T get() 매개변수 x. 반환값 o
Consumer<T> void accept(T t) 매개변수 o. 반환값 x
Function<T, R> R apply (T t) 매개변수 o. 반환값 o
Predicate<T> boolean test (T t) 매개변수 o. 반환값 o
* 조건식 표현용. 매개변수는 항상 하나

 

 

 

Predicate 사용 예시

 

조건식이 포함된 람다식을 함수형 인터페이스의 참조변수로 넣고 사용하는 모습입니다.

        Predicate<String> isEmptyStr = s -> s.length() == 0;
        String s = "";
        System.out.println(isEmptyStr.test(s));

 

 

 

 

 

 

Variable Capture (외부 변수 참조)

 

람다식에서는 전달된 파라미터 말고 바디 외부에 있는 변수를 참조할 수 있습니다.

 

 

아래의 식에서 람다식은 void method(int i){}의 바디 안에서 만들어집니다.

package javastudy.ch15.functionalinterface;

class Outer {
    int val = 10;

    class Inner {
        int val = 20;

        void method(int i) {
            int val = 30;

            MyFunction 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();
        }
    }
}

public class LambdaEx2 {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.method(100);
    }

}

 

 

파라미터로 전달되는 값은 없지만 자신의 바디 외에 있는 변수들에 접근이 가능한 것을 볼 수 있습니다.

 

이렇게 람다에 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수를 참조하는 행위를

람다캡처링(lambda capturing) (=variable capture) 이라고 부릅니다.

 

 

 

 

Variable Capture의 제약 조건

  1. 지역변수는 final로 선언되어 있어야 한다.
  2. final로 선언되지 않은 지역변수는 final처럼 동작해야 한다. (값을 바꾸면 안된다.)
  3. 단, 인스턴스 변수에는 위의 제약조건들이 해당되지 않는다.

 

 

제약 조건의 이유

JVM 메모리 구조를 통해 이유를 찾을 수 있습니다.

 

지역변수스택영역에 생성됩니다.

지역변수는 스택영역에 생성됨

 

그리고 JVM의 스택영역은 쓰레드마다 별도의 스택이 생성되기 때문에 스택은 쓰레드끼리 공유가 되지 않습니다.

새로운 쓰레드는 별도의 스택영역을 가짐

 

람다별도의 쓰레드에서 실행됩니다.

 

람다에서 다른 쓰레드의 지역변수를 참조하고 있다면 오류가 나겠지만

람다에서는 다른 쓰레드의 지역변수의 값을 복사해서 가지고 있습니다.

 

여기서 복사해 가지고 있는 다른 쓰레드의 지역변수를 수정하게되면 복사된 값은 다른 쓰레드의 지역변수의 값과 같다고 신뢰할 수 없기 때문에 값의 변경을 금지하는 것입니다.

 

그렇기 때문에 람다에선 다른 쓰레드의 지역변수의 값은 그대로 가지고 있지만 변경은 할 수 없는 상태.

즉, final 이거나 final과 같이 동작해야 합니다.

 

 

반면 인스턴스 변수heap 영역에 존재하기 때문에 쓰레드끼리 공유가 가능해 힙에 직접 접근해 사용해도 됩니다.

직접 접근해 사용하므로 값의 변경도 가능한 것입니다.

 

 

 

 

 

람다캡처링 도움받은 곳 : https://perfectacle.github.io/2019/06/30/java-8-lambda-capturing/

 

 

 

 

 

 

메서드 참조

 

지금까지 람다식을 이용해 메서드를 간결하게 표현했습니다.

 

그런데, 람다식을 더 간결하게 표현할 수 있는 "메서드 참조"라는 방법이 있습니다.

 

아래 3가지 경우에서 메서드 참조를 사용할 수 있습니다.

  1. static 메서드 참조
  2. 인스턴스메서드 참조
  3. 특정 객체 인스턴스메서드 참조

 

 

1. static 메서드 참조

아래는 람다식에서 매개변수 s를 받아 Integer 클래스의 static 메서드 parseInt()를 사용하는 코드입니다.

Function<String, Integer> f = (String s) -> Integer.parseInt(s);

 

단지 매개변수 s를 그대로 static 메서드에 넘기기만 하는 일을 하는 것 뿐이니까,

"유추할 수 있는 정보들을 람다식에서 뺄 수 있지 않을까?"

 

타입이 Function<String, Integer> 이므로 매개변수는 String, 반환타입은 Integer라는 것을 유추할 수 있습니다.

 

그래서 아래와 같이 메서드 참조를 이용해 간결하게 표현이 가능합니다.

Function<String, Integer> f = Integer::parseInt;

 

 

 

2. 인스턴스메서드 참조

 

함수형 인터페이스 BiFunction은 Function과 같지만 매개변수가 2개라는 것이 다릅니다.

 

그래서 아래 코드에서는 매개변수의 타입이 String이고 2개인 것과 반환타입이 Boolean이라는 것을 유추할 수 있습니다.

BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equals(s2);

 

다른 클래스에도 equals라는 이름의 메소드가 있을 수 있기 때문에 앞에 클래스이름을 적어주고 메서드 참조를 할 수 있습니다.

BiFunction<String, String, Boolean> f = String::equals;

 

 

 

 

3. 특정 객체 인스턴스 메서드 참조

인스턴스 메서드참조를 사용할때 만약 이미 생성된 객체의 메서드를 사용하는 경우에는 클래스 이름 대신 객체의 참조변수를 적어주어야 합니다.

MyClass obj = new MyClass();
Function<String, Boolean> f = obj::equals;

 

 

 

 

2번과 3번이 헷갈릴 수 있는데 기본적으로 "클래스이름::메서드이름"으로 사용하고

이미 객체가 생성되어 있다면 클래스 이름대신 "참조변수::메서드이름"으로 사용하면 된다고 기억합시다.

 

 

 

 

 

 

 

 

생성자의 메서드 참조

 

생성자를 호출하는 람다식도 메서드 참조로 반환할 수 있습니다.

 

 

 

생성자 호출하는 메서드 참조

클래스이름::new; 를 해주면 됩니다.

Supplier<MyClass> s = () -> new MyClass(); // 람다식
Supplier<MyClass> s = MyClass::new; // 메서드참조

 

 

 

매개변수가 있는 생성자 메서드 참조

매개변수가 있는 생성자라면 매개변수에 따라 알맞은 함수형 인터페이스를 사용해야합니다.

Function<Integer, MyClass> f = (i) -> new MyClass(i); // 람다식
Function<Integer, MyClass> f = MyClass::new; // 메서드 참조

 

상황에 따라 새로운 함수형 인터페이스를 새로 정의해서 사용할 수 도 있습니다.

BiFunction<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s); // 람다식
Function<Integer, MyClass> bf = MyClass::new; // 메서드 참조

 

 

 

 

배열 생성할때 메서드 참조

Function<Integer, int[]> f = x -> new int[x]; // 람다식
Function<Integer, int[]> f = int[]::new; // 메서드 참조

'Web > Java' 카테고리의 다른 글

[우테코 프리코스] 2주차: 자동차 경주 게임  (0) 2021.12.01
[우테코 프리코스] 1주차: 숫자 야구 게임  (2) 2021.11.25
[Java] 제네릭  (1) 2021.08.30
[Java] I/O  (2) 2021.08.24
[Java] Enum  (2) 2021.08.11