동글이기가 레포

람다식 사용법

 

■ 람다식이란?

 

람다식은 메소드를 간결하게 표현하는 것으로 Java8부터 지원한다.

 

람다식을 사용하게 되면 코드가 간결해지는 것을 볼 수 있는데, 메서드를 람다식으로 표현하면 메서드의 이름과 반환타입이 없어지기 때문에 익명 함수라고도 한다.

 

모든 메소드는 클래스에 포함 되어야 하고 객체를 생성한 뒤 호출하지만, 람다식은 그러한 과정 없이 람다식 자체만으로 메소드의 역할을 대신할 수 있다는 것이 큰 장점이다.

 

 

■ 작성법

 

// 기존 방식
반환타입 메소드 이름(매개변수) {
    처리 할 문장
}

// 람다식
(매개변수) -> {처리 할 문장}

 

위의 작성법으로 간단한 예제를 만들어보자

int sum(int i, int j) {
    return i + j;
}

 

이 메소드를 아래와 같이 변경할 수 있다.

(int i, int j) -> i + j

 

코드도 간결해지고 알아보기 쉽게 바뀌었다.

 

 

람다식의 특징을 한번 더 정리해보자

 

1) 메서드 이름과 반환 타입 제거

2) 매개변수 타입은 추론이 가능한 경우 생략 가능

  > 이것은 람다식에 반환 타입을 써도 되지 않는 이유와 같은데, 항상 추론이 가능하기 때문

(int i, int j) -> i + j

(i, j) -> i + j

3) 하나의 매개변수일 경우에 () 생략 가능

i -> i * 100

4) 실행문이 하나일 경우에 {} 생략 가능

  > 이미 위의 예제에서 적용한 것을 확인할 수 있다.

 

 

■ 람다식의 장점

 

- 코드를 간결하게 작성할 수 있기 때문에 가독성이 향상

- 멀티 쓰레드 환경에서 적절하게 사용하면 효율성 증가

- 함수를 만들지 않기 때문에 생산성이 향상

 

■ 람다식의 단점

 

- 람다식을 사용한 경우 재사용이 불가능

- 디버깅이 까다로움

- 무분별하게 사용시 효율성이 오히려 낮아짐

- 자기 자신을 호출하는 재귀에는 부적합

 

 

 

함수형 인터페이스

 

함수형 인터페이스는 예전에 한 적이 있는데, 1개의 추상 메소드를 가지고 있는 인터페이스를 말한다.

 

람다식에서 함수형 인터페이스라는 주제가 나온 이유는...

람다식 자체가 함수형 인터페이스로만 접근이 가능하기 때문이다.

 

또한, 람다식은 메소드와 동등하게 보이지만 사실 익명 클래스의 객체와 동등하다고 보면 된다.

// 익명 클래스의 객체
new Object() {
    int sum(int i, int j) {
        return i + j;
    }
}

// 위의 익명 클래스 객체를 람다식으로 표현
(int i, int j) -> i + j

 

 

만약 아래와 같은 인터페이스가 있을 때 사용법을 알아보자

@FunctionalInterface // 오직 하나의 메소드 선언을 갖는 인터페이스라는 애노테이션
interface Test {
    public abstract int sum(int i, int j);
}

 

이것을 동작시키고 싶을 때 람다식을 사용하지 않으면 아래와 같이 한다.

Test test = new Test() {
    public int sum(int i, int j) {
        return i + j;
    }
};

test.sum(3, 4);

 

하지만, 람다식을 사용하면 어떻게 될까?

Test test = (i, j) -> i + j;
test.sum(3,4)

 

간단한 메소드를 람다식으로 사용했지만, 대충봐도 확연한 차이가 보인다.

 

전체 코드로 살펴보자

public class Main {
    @FunctionalInterface
    interface Test {
        public abstract int sum(int i, int j);
    }

    static Test test = (i, j) -> i + j;
    public static void main(String[] args){
        System.out.println(test.sum(3,4));
    }
}

 

 

Test 인터페이스를 구현한 익명 객체를 람다식으로 표현할 수 있는 이유는

 

람다식도 어떻게 보면 익명 객체이고, 인터페이스를 구현한 익명 객체의 sum 메소드와 람다식의

1) 매개변수 타입

2) 매개변수 개수

3) return 값

이 일치하기 때문이다.

 

Java에서는 하나의 추상 메소드가 선언된 인터페이스를 정의해서 람다식을 이용하는 것이 기존 규칙을 어기지 않고 효율적으로 사용할 수 있기 때문에 함수형 인터페이스를 통해 람다식을 사용하기로 하였다.

 

 

람다식과 인터페이스의 메소드는 1:1로 매칭되어야 하기 때문에 함수형 인터페이스에는 단 하나의 추상 메소드만 정의해야 한다.

 

람다식을 가리키는 참조변수를 반환하거나 람다식 자체를 반환할 수 있으며, 변수처럼 메소드를 주고 받는 것이 가능한데, 이는 사실 메소드가 아니라 객체를 주고 받는다고 생각하면 된다.

 

이것이 무슨 말인지는 아래의 예제를 통해 살펴보자

 

public class Main {
    @FunctionalInterface
    interface Test {
        void SubTest();
    }

    static void bridge(Test test) {
        test.SubTest();
    }

    static Test direct() {
        Test t = () -> System.out.println("람다식 자체 반환");
        return t;
    }

    public static void main(String[] args){
        // 참조변수
        Test t = () -> System.out.println("참조변수 이용");
        bridge(t);

        // 람다식을 직접 매개변수로 삽입
        bridge(() -> System.out.println("람다식 이용"));

        // 람다식 자체를 반환
        Test t2 = direct();
        t2.SubTest();
    }
}

 

결과

참조변수 이용
람다식 이용
람다식 자체 반환

 

람다식을 참조하는 참조변수인 t를 넣어주면서 동작시킬 수 있고

 

bridge 메소드에 람다식 자체를 매개변수로 넣어서 동작시킬 수 있다.

 

람다식 자체를 direct 메소드를 통해 반환할 수 있다.

 

 

 

Variable Capture

 

람다식의 블록 내에서 람다식을 감싸고 있는 클래스의 instance 변수, static 변수, 지역 변수에 접근하는 것이 가능하다.

 

위의 세 가지의 변수 중에서 지역 변수에 접근할 때에는 Variable Capture라는 특별한 작업이 생기기 때문에 제약이 생기게 되었다.

 

1) 지역 변수는 final로 선언되어 있어야 함

2) final로 선언되지 않은 지역 변수는 final과 같이 동작해야 함

  > final이 아니지만 final과 같은 성격을 지녀야 함(값이 바뀌면 안됨)

 

 

이러한 내용을 바탕으로 에제를 만들어보자

 

public class Main {
    private static int a = 111;

    public static void test() {
        final int b = 222;
        int c = 333;
        int d = 444;

        final Runnable test1 = () -> {
            // a는 인스턴스 변수이므로 제약 X
            a = 123456;
            System.out.println(a);
        };

        // b는 지역 변수이지만 final 선언이 되어 있음
        final Runnable test2 = () -> System.out.println(b);

        // c는 지역 변수이고 final도 선언 안되어 있지만, final의 상태를 유지 중
        final Runnable test3 = () -> System.out.println(c);

        // d는 지역 변수이고 final도 선언 안되어 있는데, 값을 변경하려고 해서 에러 발생
        //d = 555;
        //final Runnable r4 = () -> System.out.println(d);

        test1.run();
        test2.run();
        test3.run();
        //r4.run();
    }
    public static void main(String[] args) {
        test();
    }
}

 

결과

123456
222
333

 

위의 결과를 통해

 

instance 변수, static 변수는 제약이 없고 지역 변수에만 제약이 적용된다.

 

 

■ 지역 변수에만 제약이 생기는 이유는 뭘까?

 

JVM에서 지역 변수는 스택 영역에 생성되는데, 스택 영역은 실제 메모리와 다르게 쓰레드마다 별도의 스택이 생성된다.

 

따라서, 지역 변수는 쓰레드끼리 공유가 되지 않는다.

 

이와 다르게 인스턴스 변수는 힙 영역에 생성되는데, 힙 영역에 직접 접근해서 사용하면 되기 때문에 쓰레드끼리 공유가 된다.

 

이러한 기본 지식을 가지고...

 

 

람다는 별도의 쓰레드에서 생성이 가능한데, 지역 변수를 가지고 있는 쓰레드가 먼저 사라져서 해당 지역 변수를 사용하지 못하게 된다면 에러가 발생 할 것이라는게 보통의 생각이다.

 

그러나, 이러한 상황에 오류는 발생하지 않는다.

 

이는 람다에서 지역 변수(해당 쓰레드의 스택)에 직접적으로 접근하고 있는 것이 아니라

 

해당 지역 변수를 자신의 스택에 복사해놓기 때문이다.

 

이렇게 복사해서 쓰기 때문에 변수의 값이 바뀌게 된다면 바뀐 값이 맞는지 틀린지 확인할 수 없기 때문에(싱크) final이거나 final과 같은 성격을 가져야 한다는 것이다.

 

 

 

메소드, 생성자 레퍼런스

 

 

메소드 레퍼런스

 

메소드 참조(레퍼런스)는 Java 8에 추가된 기능으로 함수를 메소드의 인자로 전달하는 것을 말한다.

 

더욱 간단히 말하면 람다식을 더욱 간단하게 표현하는 것이다.

 

Test라는 함수형 인터페이스를 생성하여 예제를 만들어보자

public class Main {
    @FunctionalInterface
    interface Test{
        void printTest(String text);
    }

    public static void main(String[] args){
        Test test = text -> System.out.println(text);
        test.printTest("Hello");
    }
}

 

위의 람다식은 아래와 같이 표현할 수 있다.

public class Main {
    @FunctionalInterface
    interface Test{
        void printTest(String text);
    }

    public static void main(String[] args){
        Test test = System.out::println;
        test.printTest("Hello");
    }
}

 

람다식 구현 부분에서 출력 부분을 System.out:println라는 메소드 레퍼런스로 표현할 수 있다.

 

 

메소드 레퍼런스는

ClassName::MethodName

형식으로 입력한다.

 

이때, 메소드를 호출하는 것이지만 괄호는 생략한다.

 

위의 예제는 간단한 것이지만, 좀 더 복잡해진다면 많은 코드가 생략되어 있기 때문에 사용하려는 메소드의 인자와 리턴 타입을 알고 있어야 한다.

 

 

■ 메소드 레퍼런스의 분류

 

1) Static 메소드 레퍼런스

 

타입 :: (Static Method)

public class Main {
    @FunctionalInterface
    interface Test{
        void printTest(String text);
    }

    public static void main(String[] args){
        Test test = a -> System.out.println(a);
        
        // Static Method Reference
        Test test2 = System.out::println;
    }
}

 

 

 

2) Instance 메소드 레퍼런스

 

(Object Reference) :: (Instance Method)

public class Main {
    @FunctionalInterface
    interface Test{
        void printTest(String text);
    }

    public static void main(String[] args){
        Test test = a -> a.toLowerCase();

        // Instance Method Reference
        Test test2 = String::toLowerCase;
    }
}

 

 

생성자 레퍼런스

 

타입 :: new

public class Main {
    @FunctionalInterface
    interface Test{
        void printTest(String text);
    }

    public static void main(String[] args){
        Test test = a -> new String(a);

        // Constructor Method Reference
        Test test2 = String::new;
    }
}

 

'스터디 > 동기 Java 스터디' 카테고리의 다른 글

[QueryDSL] QueryDSL이란?  (0) 2022.04.17
9주차 : 제네릭  (0) 2021.05.23
8주차 : I/O  (0) 2021.05.19
8주차 : 애노테이션  (0) 2021.05.17
7주차 : Enum  (0) 2021.05.13

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading