Java/Java Effective

3. 람다(Lambda) - Java 8

mozzi329 2022. 7. 18. 19:03
728x90
 

 

     

    람다의 출현배경(?)

    📌 Goal

    • 람다식이 무엇이고, 어떻게 사용할 수 있는 지 이해할 수 있다.
    • 함수형 인터페이스를 통해 람다를 다루는 방법을 이해하고 설명할 수 있다.
    • 람다식을 메서드 참조 방식으로 변환할 수 있다.

     

    📌 람다(Lambda)

    함수(메서드)를 좀 더 간단하고 편리하게 표현하기 위해 고안된 문법 요소이다.

    기본적으로 람다식에서는 반환타입과 이름을 생략할 수 있다.

    따라서 람다 함수를 종종 이름이 없는 함수, 즉 익명 함수(annonymous function)라고 부르기도 한다.

    //기존 메서드 표현 방식
    void sayhello() {
    	System.out.println("HELLO!")
    }
    
    //위의 코드를 람다식으로 표현한 식
    () -> System.out.println("HELLO!")
    

     

    아래의 코드 예제로 람다식에 대해 살펴보자.

    int sum(int num1, int num2) {
    	return num1 + num2;
    }
    

     

    위의 코드를 람다식으로 변경해보자.

    (int num1, int num2) -> { // 반환타입과 메서드명 제거 + 화살표 추가
    	return num1 + num2;
    }

     

    또한 반환 값이 있는 메서드의 경우에는 return문과 문장 뒤에 오는 세미클론(;)을 생략할 수 있다.

    (int num1, int num2) -> {
    	num1 + num2
    }

     

    여기서 실행문이 하나만 존재한다면 중괄호도 생략할 수 있다.

    (int num1, int num2) -> num1 + num2

     

    마지막으로, 매개변수 타입을 쉽게 유추할 수 있는 경우에는 매개변수의 타입을 생략할 수 있다.

    사실 많은 경우 유추가 쉽기 때문에 대부분 생략할 수 있다.

    (num1, num2) -> num1 + num2

     

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

    추상 메서드가 오직 하나인 인터페이스를 의미한다.

    자바에서는 함수를 반드시 클래스 안에서 정의되어야 하기 때문에 메서드가 독립적으로 있을 수 없고 반드시 클래스 객체를 먼저 생성한 후 생성한 객체로 메서드를 호출해야 한다. 이런 맥락에서 람다식 또한 객체이다. 더 정확히는 이름이 없기 때문에 익명 클래스라고 부른다.

    ※ 익명 클래스란?

    객체의 선언과 생성을 동시에 하여 오직 하나의 객체를 생성하고, 단 한번만 사용되는 일회용 클래스이다.

     

    앞서 sum 메서드 예제를 다시 살펴보자.

    // sum 메서드 람다식
    (num1, num2) -> num1 + num2
    
    // 람다식을 객체로 표현
    new Object() {
    	int sum(int num1, int num2) {
    		return num1 + num1;
    	}
    }
    

     

    위의 람다식으로 표현한 sum 메서드는 사실 아래와 같은 익명 클래스와 같다.

    그리고 위에서 봤던 것처럼 아래와 같이 생성과 선언을 한번에 할 수 있다.

    new Object() {
    	int sum(int num1, int num2) {
    		return num1 + num1;
    	}
    }
    

     

    다시 돌아가서,

    만약에 람다식이 객체라 한다면 이 객체에 접근하고 사용하기 위한 참조변수가 필요하다. 그런데 기존 객체를 생성할 때 만들었던 Object 클래스에는 sum이라는 메서드가 없기 때문에, Object 타입의 참조변수에 담는다고 하더라도 sum 메서드를 사용할 수 없다.

    public class LamdaExample1 {
        public static void main(String[] args) {
    		   Object obj = new Object() {
                int sum(int num1, int num2) {
                    return num1 + num1;
                }
            };
    			// 람다식 Object obj = (num1, num2) -> num1 + num2 로 대체 가능
    }
    

    그러므로 위의 코드 예제에서 익명 객체를 생성하여 참조변수 obj에 담아준다 하더라도 sum메서드를 사용할 수 있는 방법이 없다. 이와 같은 문제를 해결하기 위해 사용하는 자바의 문법 요소가 바로 자바의 함수형 인터페이스(Functional Interface)이다.

     

    자바에서 함수형 프로그래밍을 하기 위해서 새로운 문법 요소를 도입하는 대신, 기존의 인터페이스 문법을 활용하여 람다식을 다루는 것이라 할 수 있다. 이것이 가능한 이유는 람다식도 결국 하나의 객체이기 때문에 인터페이스에 정의된 추상메서드를 구현할 수 있기 때문이다.

     

    함수형 인터페이스에는 단 하나의 추상 메서드만 선언될 수 있는 데, 이것은 어렵지 않게 유추할 수 있는 것처럼 람다식과 인터페이스의 메서드가 1:1로 매칭되어야 하기 때문이다.

     

    다시 돌아가서 위의 예제에서 직면했던 문제를 함수형 인터페이스를 적용해서 풀어보도록 하자.

    public class LamdaExample1 {
        public static void main(String[] args) {
    		   /* Object obj = new Object() {
                int sum(int num1, int num2) {
                    return num1 + num1;
                }
            };
    			*/
    		ExampleFunction exampleFunction = (num1, num2) -> num1 + num2
    		System.out.println(exampleFunction.sum(10,15))
    }
    
    @FunctionalInterface // 컴파일러가 인터페이스가 바르게 정의되었는지 확인할 수 있도록
    interface ExampleFunction {
    		public abstract int sum(int num1, int num2);
    }
    
    // 출력값
    25
    

    위의 예제를 확인해보면, 함수형 인터페이스를 활용하여 자바의 문법 요소를 해치지 않으면서도 우리가 원하는 결과를 얻어낼 수 있는 것을 볼 수 있다.

     

    이렇게 함수형 인터페이스를 사용하면 참조변수의 타입으로 함수형 인터페이스를 사용하여 우리가 원하는 메서드에 접근이 가능하다.

     

    ✔️ 매개변수와 리턴 값이 없는 람다식

    다음과 같이 매개변수와 리턴 값이 없는 추상 메서드를 가진 함수형 인터페이스가 있다고 가정해보자.

    @FunctionalInterface
    public interface MyFunctionalInterface {
        public void accept();
    }
    

     

    이 인터페이스를 타겟 타입으로 갖는 람다식은 다음과 같은 형태로 작성해야 한다.

    람다식에서 매개변수가 없는 이유는 accept()가 매개변수를 가지지 않기 때문이다.

    MyFunctionalInterface example = () -> { ... };
    
    // example.accept();
    

    람다식이 대입된 인터페이스의 참조 변수는 위의 주석과 같이 accept()를 호출할 수 있다.

     

    ✔️ 매개변수가 있는 람다식

    다음과 같이 매개 변수가 있고 리턴 값이 없는 추상 메서드를 가진 함수형 인터페이스가 있다고 가정해보자.

    @FunctionalInterface
    public interface MyFunctionalInterface {
        public void accept(int x);
    }
    

     

    이 인터페이스를 타겟 타입으로 갖는 람다식은 다음과 같은 형태로 작성해야한다.

    람다식에서 매개변수가 한 개인 이유는 메서드 accept()가 매개변수를 하나만 가지기 때문이다.

    public class MyFunctionalInterfaceExample {
        public static void main(String[] args) throws Exception {
            MyFunctionalInterface example;
            example = (x) -> {
                int result = x * 5;
                System.out.println(result);
            };
            example.accept(2);
    
            example = (x) -> System.out.println(x * 5);
            example.accept(2);
        }
    }
    
    // 출력값
    10
    10
    

     

    ✔️ 리턴 값이 있는 람다식

    다음과 같이 매개 변수와 리턴 값을 가지는 추상 메서드를 포함하는 함수형 인터페이스가 있다고 가정해보자.

    (추상 메서드가 return 타입이 있을 경우 return은 생략될 수 있다)

    @FunctionalInterface
    public interface MyFunctionalInterface {
        public int accept(int x, int y);
    }
    

     

    이 인터페이스를 타겟 타입으로 갖는 람다식은 다음과 같은 형태로 작성해야 한다.

    람다식에서 매개 변수가 두 개인 이유는 accept()가 매개변수를 두 개 가지기 때문이고,

    accept()가 return타입이 있기 때문에 중괄호 { }에는 return문이 있어야 한다.

    public class MyFunctionalInterfaceExample {
        public static void main(String[] args) throws Exception {
            MyFunctionalInterface example;
            example = (x, y) -> {
                int result = x + y;
                return result;
            };
            int result1 = example.accept(2, 5);
            System.out.println(result1);
    
            example = (x, y) -> { return x + y; };
            int result2 = example.accept(2, 5);
            System.out.println(result2);
    
    	      example = (x, y) ->  x + y;
    				// 실행문이 한 줄인 경우, 중괄호 {}와 return문 생략가능
            int result3 = example.accept(2, 5);
            System.out.println(result3);
    
            example = (x, y) -> sum(x, y);
    				// 실행문이 한 줄인 경우, 중괄호 {}와 return문 생략가능
            int result4 = example.accept(2, 5);
            System.out.println(result4);
    
        }
    
        public static int sum(int x, int y){
            return x + y;
        }
    }
    
    //출력값
    7
    7
    7
    7
    

     

    ✔️ 자바에서 기본적으로 제공하는 함수형 메서드

    마지막으로, 자바에서는 빈번하게 사용되는 함수형 인터페이스를 기본적으로 제공하고 있다.

    즉, 기본적으로 내장된 함수형 인터페이스를 사용하여 매번 같은 기능을 수행하는 함수형 인터페이스를 직접 만드는 번거로움을 줄여줄 수 있다.

     

    이와 관련한 자세한 내용은 자바 함수형 인터페이스 API DOC을 통해 직접 확인해보도록 하자.

     

    📌 메서드 레퍼런스(Method Reference)

    메서드 참조는 람다식에서 불필요한 매개변수를 제거할 때 주로 사용된다.

    즉, 람다식으로 더욱 간단해진 익명 객체를 더욱 간단하게 사용하고 싶은 개발자의 니즈가 반영된 산물이라 할 수 있다.

     

    람다식은 종종 기존 메서드를 단순히 호출만 하는 경우가 많다.

    다음은 두 개의 값을 받아 큰 수를 리턴하는 Math 클래스의 max() 정적 메서드를 호출하는 람다식이다.

    (left, right) -> Math.max(left, right);
    

    람다식은 단순히 두 개의 값을 Math.max() 메서드의 매개 값으로 전달하는 역할만 하기 때문에 다소 불편해 보인다.

     

    이 경우, 입력 값과 출력 값의 반환타입을 쉽게 유추할 수 있기 때문에 입력 값과 출력 값을 일일이 적어주는 게 크게 중요하지 않다.

    다음과 같이 메서드 참조를 이용하면 매우 깔끔하게 처리할 수 있다.

    // 클래스이름::메서드이름
    
    Math :: max; // 메서드 참조
    

    메서드 참조도 람다식과 마찬가지로 인터페이스의 익명 구현 객체로 생성되므로 인터페이스의 추상 메서드가 어떤 매개 변수를 가지고, 리턴 타입이 무엇인가에 따라 달라진다.

     

    IntBinaryOperator(자바 기본 추상 인터페이스) 인터페이스는 두 개의 int 매개 값을 받아 int 값을 리턴하므로, Math::max 메서드 참조를 대입할 수 있다.

    IntBinaryOperator operato = Math :: max; //메서드 참조
    

    메서드 참조는 정적 혹은 인스턴스 메서드를 참조할 수 있고 생성자 참조도 가능하다.

     

    ✔️ 정적 메서드와 인스턴스 메서드 참조

    정적 메서드를 참조할 경우에는 클래스 이름 뒤에 :: 기호를 붙이고 정적 메서드 이름을 기술하면 된다.

    클래스 :: 메서드
    

     

    인스턴스 메서드의 경우에는 먼저 객체를 생성한 다음 참조 변수 뒤에 :: 기호를 붙이고 인스턴스 메서드 이름을 기술하면 된다.

    참조 변수 :: 메서드
    

     

    다음 예제는 Calculater의 정적 및 인스턴스 메서드를 참조한다.

    //Calculator.java
    public class Calculator {
      public static int staticMethod(int x, int y) {
           return x + y;
      }
    
      public int instanceMethod(int x, int y) {
    		   return x * y;
      }
    }
    
    import java.util.function.IntBinaryOperator;
    
    public class MethodReferences {
      public static void main(String[] args) throws Exception {
        IntBinaryOperator operator;
    
        /*정적 메서드
    		클래스이름::메서드이름
    		*/
        operator = Calculator::staticMethod;
        System.out.println("정적메서드 결과 : " + operator.applyAsInt(3, 5));
    
        /*인스턴스 메서드
    		인스턴스명::메서드명
    		*/
    
        Calculator calculator = new Calculator();
        operator = calculator::instanceMethod;
        System.out.println("인스턴스 메서드 결과 : "+ operator.applyAsInt(3, 5));
      }
    }
    /*
    정적메서드 결과 : 8
    인스턴스 메서드 결과 : 15
    */
    

     

    ✔️ 생성자 참조

    메서드 참조는 생성자 참조도 포함된다. 생성자를 참조한다는 것은 객체의 생성을 의미한다.

     

    단순히 메서드 호출로 구성된 람다식을 메서드 참조로 대치할 수 있듯이, 단순히 객체를 생성하고 리턴하도록 구성된 람다식은 생성자 참조로 대치가 가능하다.

    (a,b) -> {return new 클래스(a,b);};
    

     

    위 경우 생성자 참조로 표현하면 다음과 같다. 클래스 이름 뒤에 :: 기호를 붙이고 new 연산자를 기술하면 된다.

    //생성자 참조 문법
    
    클래스 :: new
    

    생성자가 오버로딩 되어 여러 개가 있을 경우, 컴파일러는 함수형 인터페이스의 추상 메서드와 동일한 매개 변수 타입과 개수를 가지고 있는 생성자를 찾아 실행한다. 만약 해당 생성자가 존재하지 않으면 컴파일 오류가 발생한다.

     

    다음 예제는 Member 클래스의 오버로딩된 생성자를 참조한다.

    //Member.java
    public class Member {
      private String name;
      private String id;
    
      public Member() {
        System.out.println("Member() 실행");
      }
    
      public Member(String id) {
        System.out.println("Member(String id) 실행");
        this.id = id;
      }
    
      public Member(String name, String id) {
        System.out.println("Member(String name, String id) 실행");
        this.id = id;
        this.name = name;
      }
    
      public String getName() {
        return name;
      }
    
    public String getId() {
        return id;
      }
    }
    
    import java.util.function.BiFunction;
    import java.util.function.Function;
    
    public class ConstructorRef {
      public static void main(String[] args) throws Exception {
        Function<String, Member> function1 = Member::new;
        Member member1 = function1.apply("kimcoding");
    
        BiFunction<String, String, Member> function2 = Member::new;
        Member member2 = function2.apply("kimcoding", "김코딩");
      }
    }
    
    /*
    Member(String id) 실행
    Member(String name, String id) 실행
    */
    

    이때 생성자 참조는 member1, member2 두 가지 모두 동일하지만 매개변수와 타입에 따라 실행되는 Member 생성자가 다름을 볼 수 있다.