자바(Java)

[자바] 람다식(lamda)

Ash_jisu 2023. 11. 9. 11:59

람다식

설명

  • 자바 1.8부터 추가되었다
  • 함수랑 메서드는 약간은 다르다(메서드는 객체지향의 의미를 가진 함수, 클래스가 필요함)

 

 사용이유

  • 불필요한 코드를 줄이고, 가독성을 높이기 위함, 컴파일러가 문맥을 살펴 타입을 추론하고 람다식으로 선언된 함수는 1급 객체이기 때문에 Stream API의 매개변수로 전달가능
  • 람다식 작성법
//기존
반환 타입 메서드이름(매개변수 선언){
	문장들
}
//람다식
(매개변수 선언) -> { 
	문장들
}


//return 식 대체
//기존
(매개변수 선언) ->{
	return a>b ? a:b;
}
//람다식
(매개변수 선언) -> a > b ? a:b


//심지어 타입까지 생략가능(int형인 a,b가 있다고 가정)
(a, b) -> a > b ? a : b


//파라미터하나일경우
a -> a * a

//괄호안에 문장 하나인경우 {}생략가능, 
//return은 불가능 그러나 람다식으로 써서 return생략시 가능

 

 

람다식의 장단점

  • 장점
    • 코드를 간결하게 만들 수 있다
    • 식에 개발자의 의도가 명확히 드러나 가독성이 높아진다
    • 함수를 만드는 과정없이 한번에 처리할 수 있어 생산성이 높아진다
      • 간단한 작업에도 함수를 만드는 기존 방법과 다르게 함수 만들필요없이 람다식으로 바로 작업 가능하다
    • 병렬프로그래밍이 용이하다 → 병렬 실행이나 이벤트 기반 프로그래밍 쉽게 적용가능(ex. 안드로이드 스튜디오생각하면 될듯)
    • myButton.setOnClickListener(view -> { // 버튼 클릭 시 실행될 코드 작성 Log.d("ButtonClick", "Button clicked!"); });
  • 단점
    • 람다를 사용하면서 만든 무명함수는 재사용이 불가능하다(한번 정의된 후에 다른곳 재사용 어렵다)
    • 디버깅이 어렵다: 코드가 간결해서 람다식 자체에 오류가 뜨는게 아니라 람다식을 사용된 부분에서 오류가 뜬다.
    • 람다를 남발하면 비슷한 함수가 중복 생성되어 코드가 지저분해질 수 있다
    • 재귀로 만들경우에 부적합하다(람다식은 자기 참조가 불가능하므로, 적합하지 않다), 일반메서드 사용해야함)

 

람다식 바이트코드로 분석

  • 람다 바이트코드 분석 블로그 참고
  • 익명클래스가 새로운 클래스를 생성하여 초기화를 해주고 인터페이스를 실행한다.
  • 궁금증으로 자바에서는왜 람다를 내부적으로 익명클래스로 컴파일하지 않냐? 문제 2가지 발생
    1. 항상 새 인스턴스 할당
    2. 람다식 마다 클래스가 하나씩 생긴다
  • 자바 8이전 버전에서 람다를 쓰기위한 retrolambda 라이브러리나 kotlin같은 언어에서는 컴파일 시점에 람다를 단순히 익명클래스로 치환한다.
  • 자바 8이전과 이후를 놓고보면 한마디로 이전에는 새로운 익명 클래스를 만들어 사용한것과 달리 자바 8부터는 컴파일러 내부적으로 람다식을 처리한다
  • 결론은 구조적으로는 유사하지만 내부적으로는 다르다, 함수형 인터페이스는 간결한 표현 제공, 익명 클래스는 더 많은 유연성 제공

 

 

 


함수형 인터페이스

  • 결론만 말하면 자바의 람다식은 함수형 인터페이스로만 접근 가능하다
  • 자바에서 모든 메서드는 클래스 내에 포함되어야한다(애초에 메서드 의미가 클래스 안에 있는 함수의미 이므로), 그리고 람다식은 익명 클래스의 객체와 동등하다.
  • 람다식은 익명의 객체와 동등하다 따라서 참조변수도 타입이 클래스 또는 인터페이스 가능하다. 그리고 람다식과 동등한 메서드가 정의되어야지 호출이 가능하다.
  • 결국 람다식을 인터페이스를 통해서 다루기로 했고 그러한 인터페이스를 함수형 인터페이스라고 부른다
    • 추가로 FunctionalInterface를 붙이면 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인하므로 꼭 붙이자.
  • 메서드의 매개변수가 인터페이스형인 MyFunction타입이라면, 람다식을 참조하는 참조변수를 매개변수로 해야한다→ 이게 결국은 메서드를 변수처럼 주고 받는것이 가능하다.(객체를 주고 받는 것이므로)

 

기본 함수형 인터페이스

  • 종류
  • Runnable
    • 인자를 받지 않고 리턴값도 없는 인터페이스. 앞전 쓰레드를 공부할 때 Runnable 인터페이스로 실행 했던 것
    • 일전에 쓰레드의 실행시 Runnable run()호출 했던것이 Runnable 함수 인터페이스다

               어노테이션을 통해 함수형 인터페이스인것을 알려줌

 

  • Supplier
    • 인자를 받지 않고 T타입의 객체를 리턴한다
public interface Supplier<T> {
    T get();
}

 

 

  •  Consumer
    • T타입의 객체를 인자로 받고 리턴 값은 없다
public interface Consumer<T> {
        void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

 

  • 인터페이스 배울때 메서드 안에 내용을 추가 할 수 있게 해주던 default 사용해주고 super로 하한제한 걸어준 예시
  • andThen()를 사용하면 2개 이상의 Consumer를 사용할 수 있다

 

  •  Function
    • Function<T,R>은 T타입의 인자를 받아, R타입의 객체로 리턴한다
public interface Function<T, R> {
    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

 

 

  • Predicate
    • Predicate<T>는 T타입 인자를 받고 결과로 boolean으로 리턴한다
public interface Function<T, R> {
    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

 

예제 참고: https://sujl95.tistory.com/76

 

 

 


변수 캡처(Variable Capture)

스콥

  • 개념: 변수를 선언했을 때, 그 변수가 접근이 가능한 범위. 예를 들어, for문 내에서 선언한 변수는 for문 밖에서 접근할 수 없다.
  • 익명 클래스는 새로 *스콥을 만들지만, 람다는 람다를 감싸고 있는 범위와 같은 스콥을 가진다

 

변수 캡처

  • 람다는 final이거나 effectivy final인 경우에만 변수 참조가능
  • 아래의 경우 람다가 참조하는 변수 baseball은 final인 것이다.
package lamdaEx;

import java.util.function.Consumer;
import java.util.function.IntConsumer;

public class Foo {
    public static void main(String[] args) {
        Foo foo = new Foo();
        foo.run();

    }

    private void run() {
        int baseNumber = 10;

        // 로컬 클래스
        class LocalClass {
            int baseNumber = 11;

            void printBaseNumber() {
                System.out.printf("로컬 클래스: %d \\n", baseNumber); // 11
            }
        }
        //baseNumber변수의 스코프는 여기서 끝

        LocalClass localClass = new LocalClass();
        localClass.printBaseNumber();

        // 익명 클래스
        Consumer<Integer> integerConsumer = new Consumer<Integer>() {
            @Override
            public void accept(Integer baseNumber) {
                System.out.printf("익명 클래스: %d \\n", baseNumber); // 12
            }
        };
        integerConsumer.accept(12);

        // 람다, 익명클래스에서 사용된 변수의 스코프는 지속된다
        IntConsumer printInt = (i) -> {
            System.out.printf("람다: %d \\n", i + baseNumber); // 10+3=13
        };
        printInt.accept(3);

        //외부의 변수와는 별개의 취급을 받기에 영향을 주지않는다, 그대로 10이 출력
        System.out.println(baseNumber);
    }
}

참고: https://velog.io/@wijoonwu/람다-표현식-변수-캡처-Variable-Capture

 

 

 

 


메서드, 생성자 레퍼런스

  • Method Reference
    • 메소드를 간결하게 지칭할 수 있는 방법으로 람다가 쓰이는 곳에 사용 가능하다
    • 일반 함수를 람다 형태로 사용할 수 있도록 해준다
    • 두 메서드의 시그니처를 맞춰야한다(시그니처: 메서드의 이름, 매개변수의 개수, 매개변수의 타입)
package lamdaEx;

interface MethodReferenceInterface{
    void printMethod(int value);
}

public class MethodReferenceEx {
    public static void main(String[] args){
        MethodReferenceInterface methodReferenceInterface = MethodReferenceEx::printMethodDefine;
        methodReferenceInterface.printMethod(7);
    }

    //printMethod와 같은 시그니처인 메서드 생성
    public static void printMethodDefine(int value){
        System.out.println("value값은 : "+value);
    }
}

 

  • 타입 다를시 오류가 뜨는 모습
  • 메소드 참조 방법
    • Default Use
      • 특정 클래스의 인스턴스 메서드를 참조한다
      • ClassName::methodName
    • Constructor Reference
      • 클래스의 생성자를 참조한다
      • ClassName::new
    • Static Method Reference
      • 클래스의 정적 메서드를 참조한다
      • ClassName::staticMethodName
    • Instance Method Reference
      • 특정 객체의 인스턴스 메서드를 참조하는 것이다
      • objectName::instanceMethodName람다식

 


출처

- 자바의 정석(저자: 남궁성)

-https://sujl95.tistory.com/76

- https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

-https://velog.io/@wijoonwu/%EB%9E%8C%EB%8B%A4-%ED%91%9C%ED%98%84%EC%8B%9D-%EB%B3%80%EC%88%98-%EC%BA%A1%EC%B2%98-Variable-Capture