ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java 람다, 람다식
    Java 2024. 2. 26. 18:39
    728x90
    반응형

    람다식

        자바가 1996년 등장한 이후로 두 번의 큰 변화가 있었는데, 
        1. jdk1.5부터 추가된 지네릭스의 등장
        2. jdk1.8부터 추가된 람다식(lambda expression)의 등장이다.

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

        

    1. 람다식이란?

    람다식은 간단히 말해서 메서드를 하나의 식으로 표현한 것이다.
    함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다.

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

    int[] arr = new int[5];
    Arrays.setAll(arr, (i) => (int)(Math.random() * 5) + 1);
    
    위 문장에서 '() => (int)(Math.random() * 5) + 1'이 람다식이다



    게다가 모든 메서드는 클래스에 포함되어야 하므로 클래스도 만들어야 하고,

    객체도 생성해야만 비로소 이 메서드를 호출할 수 있다.


    그러나 람다식은 이 모든 과정 없이 오직 람다식 자체만으로도 이 메서드의 역할을 대신할 수 있다.

    게다가 람다식은 메서드의 매개변수로 전달되어지는 것이 가능하고, 메서드의 결과로 반환될 수도 있다.
    람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해진 것이다.

    메서드와 함수의 차이
    기본적으로 비슷한 건데, 메서드는 클래스에 속해야 한다는 제약이 있어서,
    기존의 함수와 같은 의미를 가진 다른 단어를 선택해서 사용해왔다.
    그런데 람다식을 통해 메서드가 하나의 독립적인 기능을 하기 때문에 다시 함수라는 용어를 사용하게 되었다.

        

    2. 람다식 작성

    메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통{} 사이에 =>를 추가한다

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



    예를 들어 두 값 중에서 큰 값을 반환하는 메서드 max를 람다식으로 변환하면

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



    반환값이 있는 메서드의 경우, return문 대신 식으로 대신 할 수 있다.
    식의 연산결과가 자동으로 반환값이 된다.
    이 때는 문장이 아닌 식이므로 끝에 ;을 붙이지 않는다.

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



    람다식에 선언된 매개변수의 타입은 추론이 가능한 경우, 생략할 수 있다.
    대부분의 경우에 추론할 수 있기 때문에, 대부분 생략한다.

    (a, b) =>  a > b ? a : b


    주의            (int a, b) =>  a > b ? a : b 처럼 어느 하나의 타입만 생략하는 건 안된다

     

    선언된 매개변수가 하나인 경우 괄호() 생략 가능

    단, 매개변수의 타입이 있으면 생략 불가

    (a) => a * a
            >>
            a => a * a
    
    int a => a * a          // 에러



    마찬가지로 괄호{}안의 문장이 하나면 괄호{} 생략 가능

    (String name, int i) =>{
        sysout(name + "=" + i);
    }
    >>
    (String name, int i) => sysout(name + "=" + i);


    이 때 문장 끝에 ;을 붙이면 안된다.

    이 때 return문이면 괄호{} 생략 불가

    (int a, int b) => return a > b ? a : b          // 에러


        
        

    3. 함수형 인터페이스(Functional Interface)

    자바에서 모든 메서드는 클래스 내에 포함되어야 하는데, 람다식은 어떤 클래스에 포함될까?
    람다식이 메서드와 동등해보이지만, 사실 익명 클래스의 객체와 동일하다

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


    여기서 max는 그냥 아무 이름이므로 별 의미는 없다
            

    람다식으로 정의된 익명 객체의 메서드를 어떻게 호출할 수 있을까?
            
    이미 알고 있는 것처럼 참조변수가 있어야 객체의 메서드를 호출할 수 있으니까,
    일단 이 객체의 주소를 f라는 참조변수에 저장해보자

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

           
    그러면, 참조변수의 타입을 뭘로 해야 할까?

    참조형이니까 클래스 또는 인터페이스가 가능하다.
    그리고 람다식과 동등한 메서드가 정의되어 있는 것이어야 한다.
    그래야만 참조변수로 익명 객체(람다식)의 메서드를 호출할 수 있기 때문이다.

    예를 들어 아래와 같이 max()라는 메서드가 정의된 Fun인터페이스가 정의되어 있다고 가정하자

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

     


    그러면 이 인터페이스를 구현한 익명 클래스의 객체는 다음과 같다

    Fun f = new Fun(){
        public int max(int a, int b){
            return a > b ? a : b;
        }
    }



    Fun 인터페이스에 정의된 max는 람다식 (int a, int b) => a > b ? a : b과 메서드의 선언부가 일치한다.
    그래서 위 코드의 익명 객체를 람다식으로 아래와 같이 대체할 수 있다.

    Fun f = (int a, int b) => a > b ? a : b


    이처럼 인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유는,
    람다식도 실제로는 익명 객체이고, 
    인터페이스를 구현한 익명 객체의 메서드와 람다식의 매개변수의 타입과 개수, 반환타입이 일치하기 때문이다.

    이렇게 하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다루면,
    기존의 자바 규칙들을 어기지 않으면서도 자유롭다.

    그래서 인터페이스를 통해 람다식을 다루기로 했으며,
    람다식을 다루기 위한 인터페이스를 함수형 인터페이스라고 부르기로 했다.

    @FunctionalInterface
    interface Fun{
        public abstract int max(int a, int b);
    }


    단, 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다.
    static메서드와 default메서드의 개수는 제한이 없다.
    @FunctionalInterface를 붙이면 컴파일러가 함수형 인터페이스를 올바르게 정의했는지 확인해주니까 꼭 붙이자
            
             

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

    함수형 인터페이스 Fun

    @FunctionalInterface
    interface Fun{
        void method();      // 추상메서드
    }


    메서드의 매개변수가 Fun 타입이면, 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야 한다

    void aMethod(Fun f){            // 매개변수 타입이 함수형 인터페이스
        f.method();                 // Fun에 정의된 메서드 호출
    }
    
    Fun f = () => sysout("method");
    aMethod(f);



    또는 참조변수 없이 아래와 같이 직접 람다식을 매개변수로 지정할 수도 있다.

    aMethod(() => sysout("method()"));



    그리고 메서드의 반환타입이 함수형 인터페이스라면, 
    이 함수형 인터페이스의 추상 메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나, 람다식을 직접 반환할 수 있다.

    Fun method(){
        Fun f = () => {};
        return f;
        // 한 줄로 줄이면 return () => {};
    }


            

    5. 람다식의 타입과 형변환

    함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐,
    람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다

    람다식은 익명 객체이고, 익명 객체는 타입이 없다.
    정확히는 타입은 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수가 없다

    그래서 대입 연산자의 양 변의 타입을 일치시키기 위해 형변환이 필요하다    

    Fun f = (Fun) (() => {});       // 양변의 타입이 다르므로 형변환 필요

     


    람다식은 Fun 인터페이스를 직접 구현하지 않았지만, 
    이 인터페이스를 구현한 클래스의 객체와 완전히 같기 때문에 위와 같은 형변환을 허용한다
    이 형변환은 생략 가능

    람다식은 이름만 없을 뿐, 분명히 객체인데 Object타입으로 형변환 할 수 없다
    람다식은 오직 함수형 인터페이스로만 형변환 가능

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


    굳이 타입을 바꾸려면 먼저 함수형 인터페이스로 변환해야 한다.

    Object obj = (Object)(Fun)(() => {});
    String str = ((Object)(Fun)(() => {})).toString;


    람다식 내에서 참조하는 지역변수는 final이 붙지 않았어도 상수로 간주된다.
        
        

    6. java.util.function 패키지

            대부분의 메서드는 타입이 비슷하다.
            매개변수가 없거나 한개 또는 두 개,
            반환값은 없거나 한 개,
            게다가 지네릭 메서드로 정의하면 매개변수나 반환 타입이 달라도 문제가 되지 않는다.

            그래서 java.util.function 패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해놓았다.
            매번 새로운 함수형 인터페이스를 정의하지 말고, 가능하면 만들어진 것을 사용하자.
            그래야 함수형 인터페이스에 정의된 메서드 이름도 통일되고, 재사용성이나 유지보수 측면에서도 좋다.

            

    자주 쓰이는 가장 기본적인 함수형 인터페이스는 다음과 같다.

    함수형 인터페이스          메서드             설명
     java.lang.Runnable        void run()          매개변수도 없고, 반환값도 없음
    Supplier<T>                   T get()                매개변수는 없고, 반환값만 있음
    Consumer<T>                void accept(T t)   Supplier와 반대로 매개변수만 있고, 반환값이 없음
    Function<T, R>              R apply(T t)         일반적인 함수. 하나의 매개변수를 받아서 결과를 반환
    Predicate<T>                boolean test(T t)   조건식을 표현하는데 사용됨. 매개변수는 하나, 반환타입은 boolean, Function의 변형으로 타입이 boolean인거만 다르다.
        
            

    기본형을 사용하는 함수형 인터페이스

    지금까지 소개한 함수형 인터페이스는 매개변수와 반환값의 타입이 모두 지네릭 타입이었는데,
    기본형 타입의 값을 처리할 때도 래퍼클래스를 사용했다.
    그러면 당연히 비효율적이다.
    그래서 기본형을 사용하는 함수형 인터페이스들이 제공된다.

    함수형 인터페이스           메서드                               설명
    DoubleToIntFunction       int applyAsInt(double d)      AtoBFunction은 입력이 A타입, 출력이 B타입
    ToIntFunction<T>            int applyAsInt(T value)        ToBFunction은 입력은 지네릭 타입, 출력이 B타입 
    IntFunction<R>                R apply(T t, U u)                 AFunction은 입력이 A타입, 출력은 지네릭 타입
    ObjIntConsumer<T>        void accept(T t, U u)           ObjAFunction은 입력이 T, A타입, 출력은 없다.
            
        

    Function의 합성과 Predicate의 결합

    java.util.function 패키지의 함수형 인터페이스에는 추상메서드 외에도 디폴트메서드와 static메서드가 정의되어 있다.

    참고            원래 Function 인터페이스는 반드시 두 개의 타입을 지정해줘야하기 때문에,
                        두 타입이 같아도 Function<T>라고 쓸 수 없다. Function<T, T>라고 써야 한다.

             
            

    Function의 합성

    두 람다식을 합성해서 새로운 람다식을 만들 수 있다.

    f.andThen(g)는 함수 f를 먼저 적용하고, 그 다음 함수 g를 적용한다.
    f.compose(g)는 함수 g를 먼저 적용하고, f를 적용한다.

    identity()는 함수를 적용하기 이전과 이후가 동일한 항등함수가 필요할 때 사용한다.
    람다식으로 표현하면 x -> x이다.

    항등함수는 잘 사용되지 않는 편이며, 나중에 배울 map()으로 변환작업할 때, 변환없이 그대로 처리하고자할 때 사용된다.

        

    7. 메서드 참조

    람다식으로 메서드를 간결하게 표현할 수 있다.
    하지만 람다식조차 더 간결하게 표현할 수 있다.

    람다식이 하나의 메서드만 호출하는 경우, 
    메서드 참조라는 방법으로 람다식을 간략히 할 수 있다.

    문자열을 정수로 변환하는 람다식은 아래와 같다

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


    참고            람다식은 엄밀히 말하면 익명클래스의 객체지만 간단히 메서드만 적었다

    보통은 이렇게 람다식을 작성하는데, 메서드로 표현하면 아래와 같다

    Integer wrapper(String s){      // 메서드 이름은 별 의미 없다
        return Integer.parseInt(s);
    }



    이 wrapper메서드는 별로 하는 일이 없다.
    그저 값을 받아 Integer.parseInt에 넘겨주는 일만 할 뿐이다.
    차라리 거추장스러운 메서드를 벗겨내고 Integer.parseInt를 직접 호출하는 것이 낫지 않을까?

    Function<String, Integer> f = (String s) => Integer.parseInt(s);
        >>
    Function<String, Integer> f = Integer::parseInt;            // 메서드 참조


    위 메서드 참조에서 람다식의 일부가 생략되었지만, 
    컴파일러는 생략된 부분을 우변의 parseInt메서드 선언부로부터, 
    또는 좌변의 지네릭타입으로부터 쉽게 알아낼 수 있다.

            
    다음 예시

    BiFunction<String, String, Boolean> f = (s1, s2) => s1.equals(s2);
        >>
    BiFunction<String, String, Boolean> f = String::equals;     // 메서드 참조


    매개변수 s1, s2를 생략하면 equals만 남는데,
    두 개의 String을 받아 Boolean을 반환하는 equals라는 이름의 메서드는 다른 클래스에도 존재할 수 있기 때문에,
    equals 앞에 클래스 이름은 반드시 필요하다.

     


    이미 생성된 객체의 메서드를 람다식에서 사용한 경우,
    클래스 이름 대신 그 객체의 참조변수를 적어줘야 한다.

    MyClass obj = new MyClass();
    Function<String, Boolean> f = (x) => obj.equals(x);         // 람다식
    Function<String, Boolean> f2 = obj::equals;                 // 메서드 참조


            
            

    생성자의 메서드 참조

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

    Supplier<MyClass> s = () => new MyClass();      // 람다식
            >>
    Supplier<MyClass> s = MyClass::new;



    매개변수가 있는 생성자라면, 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하면 된다.
    필요하다면 함수형 인터페이스를 새로 정의해야 한다.

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


    그리고 배열을 생성할 때는 아래와 같이 하면 된다

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


    메서드 참조는 람다식을 마치 static 변수처럼 다룰 수 있게 해준다.

    728x90
    반응형

    'Java' 카테고리의 다른 글

    Java 입출력 - 1, 스트림(I/O Stream)  (1) 2024.02.27
    Java 스트림(Stream)  (1) 2024.02.27
    Java 쓰레드(Thread)  (1) 2024.02.26
    Java 애너테이션  (0) 2024.02.26
    Java 열거형(Enum)  (0) 2024.02.26
Designed by Tistory.