[Java] 람다식
본문 바로가기
Java

[Java] 람다식

by IYK2h 2023. 9. 25.
728x90

람다식

목차

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

람다식이란?

식별자 없이 실행가능한 함수

간단히 말해 메서드를 하나의 식으로 표현한 것이다.

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

익명 구현 객체는 인터페이스나 클래스의 객체를 생성해서 사용할 때, 재사용하지 않는 경우 보통 사용한다.

람다식의 도입으로 인해 자바는 객체지향언어인 동시에 함수형 언어가 되었다.

람다식 사용법

int[] arr = new int[5];
Arrays.setAll(arr, (i) -> i + 1);
// [1, 2, 3, 4, 5]

위 코드에서(i) -> i + 1 구문이 람다식이다.

메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 생성해야 이 메소드를 호출할 수 있지만, 람다식은 이 과정 없이 오직 람다식 자체만으로 이 메서드의 역할을 수행할 수 있는 것이 큰 장점이다.

함수형 인터페이스

추상 메소드가 하나뿐인 인터페이스를 함수형 인터페이스라고 한다.

자바의 람다식은 함수형 인터페이스로만 접근이 가능하다.

기본 함수형 인터페이스

  • Runnable
  • Supplier
  • Consumer
  • Function<T, R>
  • Predicate
  • 등등 docs

Runnable

인자를 받지 않고 리턴값도 없는 인터페이스

예제

Runnable runnable = () -> System.out.println("runnable run");
runnable.run();
//결과
// runnable run
  • Runnable은 run()을 호출해야한다. 함수형 인터페이스마다 run() 과 같은 실행 메소드 이름이 다르다. 인터페이스 종류마다 만들어진 목적이 다르고, 인터페이스 별 목적에 맞는 실행 메소드 이름을 정하기 때문이다.
  • 람다식으로는 () -> void 로 표현한다.
  • Runnable 이라는 이름에 맞게 "실행 가능한" 이라는 뜻을 나타내며 이름 그대로 실행만 할 수 있다고 생각하면 된다.

Supplier

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
  • Supplier 는 아무런 인자를 받지 않고 T 타입의 객체를 리턴한다.
  • 람다식으로는 () -> T 로 표현한다.
  • 공급자라는 이름처럼 아무것도 받지 않고 특정 객체를 리턴한다.

Consumer

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
  • Consumer 는 인자 하나를 받고 아무것도 리턴하지 않는다.
  • 람다식으로는 T -> void 로 표현한다.

Function

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
  • Function 은 T 타입 인자를 받아서 R 타입을 리턴한다.
  • 람다식으로는 T -> R 로 표현한다.
  • 수학식에서의 함수처럼 특정 값을 받아서 다른 값으로 반환한다.
  • T 와 R 은 같은 타입을 사용할 수도 있다.

Predicate

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
  • Predicate 는 인자 하나를 받아서 boolean 타입을 리턴한다.
  • 람다식으로는 T -> boolean 로 표현한다.

Variable Capture (lambda capturing)

람다(Lambda)의 바디에서는 파라미터가 아닌 바디 외부에 있는 변수를 참조할 수 있다.

유사하게 로컬 클래스, 익명 클래스에서도 참조가 가능하다.

  • 람다, 로컬 클래스 또는 익명 클래스가 자유 변수를 참조할 때 직접 그 변수를 참조하는 것이 아니라 자유 변수를 자신의 stack에 복사하여 참조하기 때문이다.
public class VariableCapture {
  private void run() {
      // 로컬 클래스, 익명 클래스, 람다에서 이 변수를 참조하면 effective final로 변경
        int baseNumber = 10;
        
        // 람다
        IntConsumer lambda = (i) -> System.out.println(i + baseNumber); // i + 10
        
        // 로컬 클래스
        class LocalClass {
            void printBaseNumber() {
                System.out.println(baseNumber); // 10
            }
        }
        
        // 익명 클래스
        IntConsumer intConsumer = new IntConsumer() {
            @Override
            public void accept(int i) {
                System.out.println(i + baseNumber); // i + 10 
            }
        };
    }
}

람다 시그니처의 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수를 자유 변수라고 한다. 또 람다 바디에서 자유 변수를 참조하는 것을 람다 캡쳐링(Lambda Capturing)이라고 한다.

Java8 버전 이전에서는 익명의 내부 클래스가 이를 둘러싼 메서드에 대한 로컬 변수를 캡처할 때 이 문제가 발생했다. 컴파일러가 만족할 수 있도록 로컬 변수 앞에 final 키워드를 추가해야만 했었다.

우리가 변수를 final 로 선언하면 컴파일러가 변수가 사실상 final로 인식할 수 있다.

때문에 variable capture가 될 자유 변수는 수정이 불가하도록 final이거나 final처럼 동작해야 한다. 자바 8 이전에는 이런 이유로 final이 아닌 변수는 익명/로컬 클래스, 람다에서 참조를 하지 못했는데 자바 8 이후로 final을 붙이지 않아도 effectively final로 선언이 된다.

왜 final 또는 effectively final이어야 할까?

지역 변수는 JVM 영역 중 stack 영역에 생성된다. 그리고 쓰레드별로 이 stack영역이 별도로 생성된다. 즉, 지역 변수는 쓰레드끼리 공유가 안된다. 반면 인스턴스 변수는 힙 영역에 생성된다. 따라서 인스턴스 변수는 쓰레드끼리 공유가 가능하다.

람다는 별도의 쓰레드에서 실행이 가능하다. 따라서 지역 변수(자유 변수)가 있는 쓰레드가 사라졌을 때, 람다가 이 변수를 참조하고 있다면 오류가 날 것이다. 하지만 위에서 본 코드처럼 람다에서 자유 변수 참조가 가능하다.

메소드, 생성자 레퍼런스

메소드, 생성자 레퍼런스는 람다식을 더 간략하게 표현할 수 있게 해준다.

콜론 두 개 :: 를 사용하며, 크게 다음과 같이 구분할 수 있다.

메소드 참조하는 방법

  1. 스태틱 메소드 참조 → 타입::스태틱 메소드
  2. 특정 객체의 인스턴스 메소드 참조 → 객체 래퍼런스::인스턴스 메소드
  3. 임의 객체의 인스턴스 메소드 참조 → 타입::인스턴스 메소드
  4. 생성자 참조 → 타입::new
  • 메소드 또는 생성자의 매개변수로 람다의 입력값을 받는다.
  • 리턴값 또는 생성한 객체는 람다의 리턴 값이다.
// 생성자 참조
String::new // ClassName::new
() -> new String()
​
// static 메소드 참조
String::valueOf // ClassName::staticMethodName
(str) -> String.valueOf(str)
​
// Instance 메소드 참조 클로저
x::toString // instanceName::instanceMethodName
() -> "TheWing".toString()
​
// Instance 메소드 참조 람다
String::toString // ClassName::instanceMethodName
(str) -> str.toString()

Greeting.java

public class Greeting {
    private String name;
    public Greeting(){
​
    }
    public Greeting(String name){
        this.name = name;
    }
    public String hello(String name){
        return "hello " + name;
    }
    public static String hi(String name){
        return "hi " + name;
    }
}

Use Greeting.java

  • Function<T, R> 을 이용해 구현 가능하지만, 동일한 작업을 하는 Greeting 객체의 메소드를 활용하여 아래와 같이 작업해볼 수 있다.
  • 메소드 레퍼런스 → Greeting::hi
package me.ssonsh.java8to11;
​
import java.util.function.UnaryOperator;
​
public class App {
​
    public static void main(String[] args){
        UnaryOperator<String> hiUseFunction = (s) -> "hi " + s;
        UnaryOperator<String> hiUseGreetingObj = Greeting::hi;
        System.out.println(hiUseGreetingObj.apply("sson"));
    }
}

인스턴스 메소드 사용

package me.ssonsh.java8to11;
​
import java.util.function.UnaryOperator;
​
public class App {
​
    public static void main(String[] args){
        Greeting greeting = new Greeting();
        UnaryOperator<String> hello = greeting::hello;
        System.out.println(hello.apply("sson"));
    }
}

생성자 사용

  • Supplier를 이용한 것과 Function을 이용한 생성자 호출은 엄연히 다르다.
  • Supplier는 인자가 없고 Function은 인자가 있다.
  • 사용하는 부분인 메소드 레퍼런스만 보면 "Greeting::new" 와 동일하지만 다르다
package me.ssonsh.java8to11;
​
import java.util.function.Supplier;
​
public class App {
​
    public static void main(String[] args){
        // 입력값은 없는데 반환값은 있는 함수형 인터페이스 > Supplier
        Supplier<Greeting> newGreeting = Greeting::new;
        Greeting greeting = newGreeting.get();
    }
}
package me.ssonsh.java8to11;
​
import java.util.function.Function;
import java.util.function.Supplier;
​
public class App {
​
    public static void main(String[] args){
        // 입력값 T 를 받아 R 반환 함수형 인터페이스 > Function
        Function<String, Greeting> ssonGreeting = Greeting::new;
        Greeting greeting = ssonGreeting.apply("sson");
    }
}

임의의 객체를 참조하는 메소드 레퍼런스

  • 정렬 예
  • package me.ssonsh.java8to11;

    import java.util.Arrays;

    public class App {

       public static void main(String[] args){
           String[] names = {"A", "B", "C", "D"};
           Arrays.sort(names, String::compareToIgnoreCase);
           System.out.println(Arrays.toString(names));
      }
    }
728x90

'Java' 카테고리의 다른 글

[Java] 제네릭(Generic)  (0) 2023.08.07
[Java] I/O  (0) 2023.07.22
[Java] 어노테이션(Annotation)  (0) 2023.07.04
[Java] 리플렉션(Reflection)  (0) 2023.06.30
[Java] 바이트코드 조작  (0) 2023.06.23

댓글