Java - 람다

<이것이 자바다>를 읽으며 정리

Posted by Yan on May 17, 2021

객체지향 + 함수적 프로그래밍

  • 자바는 객체 지향 흐로그래밍이 소프트웨어 개발의 주요 패러다임이었던 1990년대에 디자인되었다.
  • 최근들어 함수적 프로그래밍이 다시 부각되고 있는데, 병렬 처리와 이벤트 지향 프로그래밍에 적합하기 때문이다.
  • 객체 지향 프로그래밍과 함수적 프로그래밍을 혼합하여 더욱 효율적인 프로그래밍이 될 수 있도록 개발 언어가 변화하고 있다.

Lambda Expressions

  • 함수적 프로그래밍을 위해 자바 8부터 람다식을 지원한다.
  • 익명 함수를 생성하기 위한 식으로, 객체지향 언어보다는 함수 지향 언어에 가깝다.
  • 자바에서 람다식을 수용한 이유 : 자바 코드가 매우 간결해지고, 컬렉션 요소를 필터링하거나 매핑해서 원하는 결과를 쉽게 집계할 수 있기 때문
  • 람다식 -> 매개변수를 가진 코드 블록 -> 익명 구현 객체
  • 예시
1
2
3
Runnable runnable = new Runnable() { // 익명 구현 객체
    public void run() {...}
}

람다식으로 표현

1
Runnable runnable = () -> {...} // 람다식
  • (매개변수) -> {실행코드} 형태로 작성되는데, 함수 정의 형태를 띄고 있지만, 런타임시에는 인터페이스의 익명 구현 객체로 생성된다.
  • 어떤 인터페이스를 구현할 것인가는 대입되는 인터페이스가 무엇이냐에 달려있다.
  • 위의 코드는 Runnable 변수에 대입되기 때문에 람다식은 Runnable의 익명 구현 객체를 생성하게 된다.

람다식 기본 문법

  • (타입 매개변수, ...) -> { 실행문; ...}
  • () -> {실행문}
  • (x, y) -> { return x+y; }
  • (x, y) -> x + y

타겟 타입과 함수적 인터페이스

  • 람다식의 형태는 마치 자바의 메소드를 선언하는 것 처럼 보인다.
  • 자바는 메소드를 단독 선언이 불가능하고, 항상 클래스의 구성 멤버로 선언한다.
  • 따라서 람다식은 단순히 메소드를 선언하는 것이 아니라 이 메소드를 가지고 있는 객체를 생성해낸다.
  • 인터페이스 변수 = 람다식;
  • 람다식은 인터페이스 변수에 대입된다.
  • 람다식은 인터페이스의 익명 구현 객체를 생성한다는 뜻이 된다.
  • 인터페이스는 직접 객체화할 수 없기 때문에 구현 클래스가 필요한데, 람다식은 익명 구현 클래스를 생성하고 객체화한다.
  • 람다식은 대입될 인터페이스의 종류에 따라 작성 방법이 달라지기 때문에, 람다식이 대입될 인터페이스를 람다식의 target type이라 한다.

함수적 인터페이스

  • 람다식은 하나의 메소드를 정의하기 때문에 두 개 이상의 추상 메소드가 선언된 인터페이스는 람다식을 통해서 구현 객체를 생성할 수 없다.
  • 함수적 인터페이스, functional interface : 하나의 추상 메소드가 선언된 인터페이스만이 람다식의 타겟 타입이 될 수 있고, 이를 부르는 말.
  • @FunctionalInterface어노테이션을 붙여서 함수적 인터페이스를 작성할 때 두 개 이상의 추상 메소드가 선언되지는 않았는지 컴파일러가 체크해준다.
  • 어노테이션은 없어도 되지만 실수로 2개 이상의 추상 메소드를 선언하면 어노테이션이 컴파일 오류를 발생시키기 때문에 붙여주는 것이 좋다.

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

1
2
3
4
@FunctionalInterface
public interface myFunctionalInterface {
    public void method();
}
이 인터페이스를 타겟 타입으로 갖는 람다식을 작성하는 방법
1
myFunctionalInterface fi = () -> {...}

method()가 매개변수를 가지지 않기 때문에 매개변수가 없다.

1
fi.method();

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class myFunctionalInterfaceExample {
    public static void main(String[] args) {
        myFunctionalInterface fi;

        fi = () -> {
            String str = "method call1";
            System.out.println(str);
        };
        fi.method();

        fi = () -> {
            System.out.println("method call2");
        };
        fi.method();

        fi = () -> { System.out.println("method call3"); };
        fi.method();
    }
}

매개 변수가 있는 람다식

1
2
3
4
@FunctionalInterface
public interface myFunctionalInterface {
    public void method(int x);
}
이 인터페이스를 타겟 타입으로 갖는 람다식을 작성하는 방법
1
2
3
myFunctionalInterface fi = (x) -> {...}
// 또는
myFunctionalInterface fi = x -> {...}

리턴값이 있는 람다식

1
2
3
4
@FunctionalInterface
public interface myFunctionalInterface {
    public void method(int x, int y);
}
이 인터페이스를 타겟 타입으로 갖는 람다식을 작성하는 방법
1
2
myFunctionalInterface fi = (x, y) -> {...; return ;}
// return문만 있을 경우 중괄호와 return 생략 가능

클래스 멤버와 로컬 변수 사용

  • 람다식 실행 블록에는 클래스의 멤버(필드 및 메소드)와 로컬 변수를 사용할 수 있다.
  • 클래스의 멤버는 제약 없이 사용이 가능하다.
  • 로컬 변수는 제약 사항이 따른다.

클래스의 멤버 사용

  • 일반적으로 익명 객체 내부에서 this는 익명 객체의 참조지만, 람다식에서 this는 내부적으로 생성되는 익명 객체의 참조가 아니라 람다식을 실행한 객체의 참조다.
  • 아래 예시에서는 중첩 객체 Inner에서 람다식을 실행했기 때문에, 람다식 내부에서 this는 중첩 객체 Inner다.
1
2
3
public interface MyFunctionalInterface {
    public void method();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class UsingThis {
    public int outterField = 10;

    class Inner {
        int innerField = 20;

        void method() {
            // 람다식
            MyFunctionalInterface fi = () -> {
                System.out.println(outterField);
                System.out.println(ThisExample.this.outterField);
                // 바깥 객체의 참조를 얻기 위해서는 클래스명.this를 사용

                System.out.println(innerField);
                System.out.println(this.innerField);
                // 람다 내부에서 this는 Inner객체를 참조
            };
            fi.method();
        }
    }
}
1
2
3
4
5
6
7
public class UsingThisExample {
    public static void main(String[] args) {
        UsingThis usingThis = new UsingThis();
        UsingThis.Inner inner = usingThis.new Inner();
        inner.method();
    }
}

로컬 변수 사용

  • 람다식은 메소드 내부에서 주로 작성되기 때문에 로컬 익명 구현 객체를 생성시킨다고 봐야 한다.
  • 람다식에서 메소드의 매개변수 또는 로컬 변수를 사용하면 이 두 변수는 final특성을 가져야 한다.
  • 따라서 매개변수 또는 로컬 변수를 람다식에서 읽는 것은 허용되지만, 람다식 내부 또는 외부에서 변경될 수는 없다.

표준 API의 함수적 인터페이스

  • 자바에서 제공되는 표준 API에서 한 개의 추상 메소드를 가지는 인터페이스들은 모두 람다식을 이용해서 익명 구현 객체로 표현이 가능하다.
  • 예) 스레드의 작업을 정의하는 Runnable interface
1
2
3
4
5
6
7
8
9
10
11
12
public class RunnableExample {
    public static void main(String[] args) {
        Runnable runnable = () -> {
            for (int i = 0; i < 10; i++) {
                System.out.prinln(i);
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
    }
}

Thread 생성자를 호출할 때 람다식을 매개값으로 대입할 수도 있다.

1
2
3
4
5
Thread thread = new Thread() -> {
    for (int i = 0; i < 10; i++) {
        System.out.prinln(i);
    }
}
  • 자바 8부터 빈번하게 사용되는 함수적 인터페이스functional interfacejava.util.function 표준 API패키지로 제공한다.
  • 이 패키지가 함수적 인터페이스를 제공하는 목적은 메소드 똔느 생성자의 매개타입으로 사용되어서 람다식을 대입할 수 있도록 하기 위해서다.

java.util.function 패키지의 함수적 인터페이스 구분

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

종류 추상메소드 특징
Consumer 매개값 O 리턴값 X
Supplier 매개값 X 리턴값 O
Function 매개값 O 리턴값 O (주로 매개값을 리턴값으로 매핑하여 타입변환)
Operator 매개값 O 리턴값 O (주로 매개값을 연산하고 결과를 리턴)
Predicate 매개값 O 리턴값 boolean (매개값을 조사해서 true/false를 리턴)
Consumer 함수적 인터페이스
  • 리턴값이 없는 accept()메소드를 가지고 있다.
  • accept() : 단지 매개값을 소비하는 역할만 한다. (사용만 하고 리턴이 없다는 뜻)
  1. Consumer<T> 인터페이스
  • 추상메소드 void accept(T t)
  • 객체 T를 받아 소비
1
Consumer<String> consumer = t => { t를 소비하는 실행문; };
  • 타입 파라미터 T에 String이 대입되었기 때문에 람다식의 t 매개 변수타입은 String이 된다.
  1. BiConsumer<T,U> 인터페이스
  • 추상메소드 void accept(T t, U u)
  • 객체 T와 U를 받아 소비
1
BiConumer<String, String> conumer = (t, u) -> { t와 u를 소비하는 실행문; }
  1. DoubleConsumer 인터페이스
  • 추상메소드 void accept(double value)
  • double 값을 받아 소비
1
DoubleConsumer consumer = d -> { d를 소비하는 실행문; }
  • 매개값으로 double 하나를 가지므로 람다식도 한 개의 매개변수 사용. d는 고정적으로 double타입이 된다.
  1. ObjIntConsumer<T> 인터페이스
  • 추상메소드 void accept(int value)
  • int값을 받아 소비
1
ObjIntConsumer<String> consumer = (t, i) -> { t와 i를 소비하는 실행문; }
Consumer 사용 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.function BiConsumer;
import java.util.function Consumer;
import java.util.function DoubleConsumer;
import java.util.function ObjIntConsumer;

public class ConsumerExample {
    public static void main(String[] args) {
        Consumer<String> consumer = t -> System.out.println(t + "16");
        Consumer.accept("java");
        // Java16

        BiConsumer<String, String> bigConsumer = t -> System.out.println(t + u);
        BiConsumer.accept("java", "15");
        // Java16

        DoubleConsumer doubleConsumer = d -> System.out.println("Java" + d);
        Consumer.accept(16.0);
        // Java16.0

        ObjIntConsumer<String> objIntConsumer = (t, i) -> System.out.println(t + i);
        ObjIntConsumer.accept("Java"+16);
        // Java16
    }
}
Supplier 함수적 인터페이스
  • 매개변수가 없고 리턴값이 있는 getXXX() 메소드를 가지고 있다.
  1. Supplier<T> 인터페이스
  • 추상메소드 T get()
  • T 객체를 리턴
1
Supplier<String> supplier = () -> { ...; return "문자열"; }