ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스터디 15주차] 람다식
    프로그래밍 언어/JAVA 2021. 7. 28. 16:14
    더보기

    목표: 자바의 람다식에 대해 학습

     

     

     

     

    1. 람다식 사용법

    람다식은 메소드를 하나의 '식(expression)' 으로 표현한 것 익명 함수(anonymous function)를 생성하기 위한 식이다.

    람다식을 사용하면 코드가 간결해지고 컬렉션의 요소를 필터링하거나 매핑해서 원하는 결과를 쉽게 집계할 수 있는 장점을 가진다.

    더보기

    익명함수란 말그대로 함수의 이름이 없는 함수다. 명함수들은 공통으로 일급객체(First Class citizen)라는 특징을 가지고 있다.

    일급객체란 다음과 같은 것들이 가능한 객체를 의미한다.

    - 변수나 데이터 구조 안에 담을 수 있다.

    - 파라미터로 전달 할 수 있다.

    - 반환값으로 사용할 수 있다.

    - 할당에 사용된 이름과 무관하게 고유한 구별이 가능하다.

     

    람다식을 사용하는 법은 다음과 같다.

    (타입 매개변수, ...) -> {실행문;...}

    (타입 매개변수, ...)는 ->의 오른쪽에 있는 중괄호 { } 블록을 실행하기 위해 필요한 값을 제공하는 역할을 한다.

    여기서 매개변수의 이름은 자신이 원하는 것으로 줄 수 있다.

     

    직접 람다식을 적용한 예를 보면서 이해해보자.

    int타입의 매개 변수 num의 값을 출력하는 코드를 짠다고 생각해보자.

    람다식으로 코드를 짜면 아래와 같다.

    (int num) -> {System.out.println(num); }

     

    여기서, 자바의 람다식이 제공하는 기능을 더 적용해보자.

    1) 매개 변수 타입은 런타임 시에 대입되는 값에 따라 자동으로 인식될 수 있기 때문에 매개 변수의 타입을 일반적으로 언급하지않는다.

    (num) -> {System.out.println(num); }

    2) 하나의 매개 변수만 사용한다면 소괄호( )를 생략할 수 있고, 하나의 실행문만 사용한다면 중괄호 { }를 생략가능하다.

    ※만약 매개 변수를 사용하지 않는다면, 반드시 소괄호( )를 써야한다.

    num -> System.out.println(num)

    3) 중괄호{ }를 실행하고 결과값을 리턴해야 한다면 return문으로 결과값을 지정할 수 있다.

    (num1, num2) -> {return num1-num2; }

    4) 만약에 중괄호 { }안에 return문만 있을경우, 아래와 같이 쓸 수있다.

    (num1, num2) -> num1 - num2

     

     

     

    2. 함수형 인터페이스

    함수형 인터페이스는 인터페이스 내에 추상 메소드가 한개만 있는 인터페이스를 말한다.

    함수형 인터페이스는 여러개의 디폴트 메서드를 포함할 수 있다.

    하지만 여러개의 디폴트 메서드(인터페이스에서 바디를 가질 수 있는 메서드)를 가지고 있더라도 추상메서드는 오직 하나여야지만 함수형 인터페이스이다.

     

    이러한 함수형 인터페이스를 선언할 때 같이 쓰는 @FunctionalInterface라는 애노테이션이 있다.

    @FunctionalInterface은 함수형 인터페이스를 작성할 때 두 개 이상의 추상 메소드가 선언되지 않도록 컴파일러가 채킹을 해주어 컴파일 에러를 발생시켜준다.

     

    자바 8부터는 빈번하게 사용되는 함수형 인터페이스를 java.util.function 표준 API 패키지로 제공한다.

    이 패키지의 제공 목적은 메소드 또는 생성자의 매개 타입으로 사용되어 람다식을 대입할 수 있도록 하기 위해서이다.

    java.util.function 패키지의 함수형 인터페이스는 크게 Consumer, Supplier, Function, Operator, Predicate로 구분된다.

    구분 기준은 인터페이스에 선언된 추상 메소드의 매개값과 리턴값의 유무이다.

    종류 추상 메소드 특징
    Consumer 매개값은 있고, 리턴값은 없음
    Supplier 매개값은 없고, 리턴값은 있음
    Function 매개값, 리턴값 둘 다 있음
    주로 매개값은 리턴값으로 매핑(타입 변환)
    Operator 매개값, 리턴값 둘 다 있음
    주로 매개값을 연산하고 결과를 리턴
    Predicate 매개앖은 있고, 리턴 타입은 boolean
    매개값을 조사해서 true/false를 리턴

     

     

     

    3. Variable Capture

    자바에서의 람다 표현식은 특정 상황에서 람다 표현식 외부에서 선언된 변수에 접근 할 수 있다.

    외부에서 선언된 변수를 사용하는 동작을 람다 캡쳐링(capturing lambda)라고 한다.

     

    람다는 지역 변수, 인스턴스 변수, 정적 변수를 캡쳐할 수 있다.

    1) 지역 변수 캡쳐(Local Variable Capture)

    @FunctionalInterface
    interface MyInterface {
        void print(String str);
    }
    
    public class LocalVariableCapture {
        public static void main(String[] args) {
            String name = "Ratel";
    
            MyInterface myInterface = (text) -> System.out.println(name + text);
    
            myInterface.print("반가워요.");
        }
    }

     

     

    2) 인스턴스 변수 캡쳐 (Instance Variable Capture)

    interface MyInterface {
        void print(String str);
    }
    
    public class IntanceVariableCapture {
        String name = "Ratel";
    
        public static void main(String[] args) {
    
            MyInterface myInterface = text -> System.out.println(new IntanceVariableCapture().name + text);
    
            myInterface.print("반가워요.");
        }
    }

     

    3) 스태틱 변수 캡쳐(Static Variable Capture)

    interface MyInterface {
        void print(String str);
    }
    
    public class StaticVariableCapture {
        static String name = "Ratel";
    
        public static void main(String[] args) {
    
            MyInterface myInterface = text -> System.out.println(StaticVariableCapture.name + text);
    
            myInterface.print("반가워요.");
        }
    }

     

    변수캡쳐에는 제약사항이 존재한다.

    람다는 인스턴스 변수(instance variable)와 정적 변수(static variable)는 자유롭게 캡쳐할 수 있다.

    하지만, 지역변수는 명시적으로 final이거나 effective final인 경우에만 캡쳐할 수 있다.

    더보기

    effectively final이란 Java 8에 추가된 syntactic sugar 일종으로, 초기화 된 이후 값이 한번도 변경되지 않은 것을 의미한다.

    effectively final변수는 final 키워드가 붙어있지 않지만 값이 변형되지 않았기에 final 키워드를 붙힌 것과 동일하게 컴파일러에서 처리한다.

    즉, 외부의 지역 변수를 람다 표현식 내부에서 사용하려면 지역변수를 final로 선언해서 상수로 만들거나 지역변수의 값을 재할당하지 않아야 한다.

     

    이러한 제약사항이 존재하는 이유는 인스턴스 변수는 힙(heap)에 저장되고 지역 변수는 스택(stack)에 저장되기 때문이다.

    자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공한다. 이러한 구현에 의해 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 최초 한 번만 값을 할당해야 한다는 제약이 생긴 것이다.

     

     

     

    4. 메소드, 생성자 레퍼런스

    먼저, 메소드 참조(method reference)에 대해 알아보자.

    메소드 참조는 람다식이 하나의 메서드만 호출하는 경우에 사용하는 방법으로 람다식을 더 간략하게 해준다.

    메소드 참조를 정리하면 다음과 같다.

    종류 람다 메소드 참조
    static 메소드 참조 x -> ClassName.method(x) ClassName::method
    인스턴스 메소드 참조 (obj, x) -> obj.method(x) ClassName::method
    특정 객체 인스턴스 메조드 참조 x -> obj.method(x) obj::method

     

    정적(static) 메소드를 참조할 경우에는 클래스 이름 뒤에 :: 기호를 붙이고 정적 메소드 이름을 쓰면 된다.

    인스턴스 메소드일 경우에는 먼저 객체를 생성한 다음 참조 변수 뒤에 ::를 붙이고 인스턴스 메소드 이름을 쓰면 된다.

     

    아래의 코드를 보며 메소드 참조를 자세히 이해해 보자.

    먼저, static 메소드와 인스턴스 메소드를 가진 클래스를 하나 만든다.

    public class Operator {
    	// static method
        public static int staticMethod(int x, int y) {
        	return x+y;
        }
        
        // instance method
        public int instanceMethod(int x, int y) {
        	return x+y;
        }
    }

    위 클래스의 정적 메소드와 인스턴스 메소드를 사용한 람다식이 메소드 참조를 사용하면 어떻게 변하는지 봐보자.

    import java.util.function.IntBinaryOperator;
    
    public class MethodReference {
    	public static void main(String[] args) {
        	IntBinaryOperator ibo;
            
            // 기본 lambda식 사용
            ibo = (x,y) -> Operator.staticMethod(x, y);
            
            // 메소드 참조 사용
            operator - Operator :: staticMethod;
            
            Operator obj = new Operator();
            // 기본 lambda식 사용
            ibo = (x,y) -> obj.instanceMethod(x,y);
            
            // 메소드 참조 사용
            ibo = obj :: instanceMethod;
        }
    }

     

    다음으로 생성자 참조 (Constructor Reference)에 대해 알아보자.

    생성자를 참조한다는 것은 객체 생성을 의미한다. 단순히 객체를 생성하고 리턴하도록 구성된 람다식을 생성자 참조로 대치할 수 있다.

    생성자 참조는 다음과 같다.

    // 기존 lambda식
    (a, b) -> {return new 클래스(a,b) }
    
    // 생성자 참조 사용 lambda식
    클래스 :: new

     

    생성자 참조를 사용한 코드를 보며 이해해보자.

    package Lambda;
    
    import java.util.function.Function;
    
    public class ConstructorReference {
        public static void main(String[] args) {
        
        	// 기존의 lambda식
        	// 문자로 이루어진 리스트를 입력받아 문자열로 출력.
            Function<char[], String> f1 = charList -> new String(charList);
    
            char[] src1 = {'R', 'a', 't', 'e', 'l'};
            String str = f1.apply(src1);
    
            System.out.println(str);
            
            // 생성자 참조 이용한 lambda식
            Function<char[], String> f2 = String::new;
    
            char[] src2 = {'R', 'a', 't', 'e', 'l'};
            str = f2.apply(src2);
    
            System.out.println(str);
        }
    }

    위와 같이 단순히 객체를 생성하여 리턴하도록 구성된 람다식을 생성자 참조를 사용하여 코드를 간략하게 만들 수 있다.

     

     

     

     

     


    <참고 자료>

    https://www.notion.so/758e363f9fb04872a604999f8af6a1ae

    https://yadon079.github.io/2021/java%20study%20halle/week-15

    https://watrv41.gitbook.io/devbook/java/java-live-study/15_week

    https://velog.io/@kwj1270/Lambda

    https://parkadd.tistory.com/60#recentComments

     

    댓글

Designed by Tistory.