춤추는 개발자

[Java] 람다식과 함수형 인터페이스 본문

Developer's_til/그외 개발 공부

[Java] 람다식과 함수형 인터페이스

Heon_9u 2021. 7. 29. 19:37
728x90
반응형

 

 람다식은 Java를 객체지향 언어인 동시에 함수형 프로그래밍이 가능하게 만들었습니다. 지난 포스팅에서 Stream API를 통해 함수형 프로그래밍의 장점들을 살펴보았습니다.

 이번 포스팅에서는 Stream 연산들에 매개변수로 활용되는 람다식과 함수형 인터페이스에 대해 알아보겠습니다.

 

✅ 람다식(Lambda Expression)이란?

 간단히 말하자면 메서드를 하나의 식(Expression)으로 표현한 것입니다. 람다식은 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해줍니다. 람다식은 함수형 인터페이스를 반환하기 때문에 Streamp API를 정확히 이해하기 위해서는 람다식과 함수형 인터페이스에 대해 알고 있어야 합니다.

 

 메서드를 람다식으로 표현하기 때문에 메서드의 이름과 반환값이 없어지므로, 익명 함수(Anonymous Function)라고도 합니다. 예를 들어, 람다식을 표현하지 않은 코드와 표현한 코드를 비교해보겠습니다.

 

// 기존의 방식
public int method(int i) {
    return (int) (Math.random()*5) + 1;
}


// 람다식
int[] arr = new int[5];
Arrays.setAll(arr, i -> (int) (Math.random()*5) + 1);

 기존 방식은 메서드를 통해 구현했습니다. 모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 생성해야만 비로소 해당 메서드를 호출할 수 있습니다. 하지만, 람다식은 이 모든 과정없이 오직 람다식 자체만으로 메서드의 역할을 대신할 수 있습니다.

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

 결과적으로 이러한 람다식은 불필요한 코드를 줄이고, 가독성을 높여준다.

 

[ 람다식의 특징 ]

람다식 내에서 사용되는 지역변수는 final이 없어도 상수로 간주된다.

람다식으로 선언된 변수명은 다른 변수명과 중복될 수 없다.

람다식은 메서드의 매겨변수로 전달되거나, 결과값으로 반환될 수 있다.

[ 람다식의 단점 ]

람다를 사용하면서 만든 익명함수는 재사용이 불가능하다.

재귀식에서는 부적합하다.

디버깅이 어렵다.

[ 람다식의 작성 요령 ]

// 기존방식
반환타입 메서드이름(매개변수 선언) {
	...
}

// 람다식
(매개변수 선언) -> {
	...
}

[ 람다식의 예시 ]

메서드 람다식
int max(int a, int b) {
    return a > b ? a : b;
}
(int a, int b) -> { return a > b ? a : b; }
(int a, int b) -> a > b ? a : b
(a, b) -> a > b ? a : b
void printVar(String name, int i) {
    System.out.println(name+"="+i);
}
(name, i) -> System.out.println(name+"="+i)
int square(int x) {
    return x * x;
}
(int x) -> x * x
(x) -> x * x
x -> x * x
int roll() {
    return (int) (Math.random() * 6);
}
() -> { return (int) (Math.random() * 6); }
() -> (int) (Math.random() * 6)
int sumArr(int[] arr) {
    int sum = 0;
    for(int i: arr)
        sum += i;

    return sum;
}
(int[] arr) -> {
    int sum = 0;
    for(int i: arr)
        sum += i;
    return sum;
}

 

✅ 함수형 인터페이스(Functional Interface)란?

 Java에서 모든 메서드는 클래스 내에 포함되어야 합니다. 이러한 규칙을 어기지 않으면서 람다식을 호출하기 위해 인터페이스를 사용하게 됩니다.

 이렇게 람다식을 다루기 위한 인터페이스를 바로 '함수형 인터페이스'라고 부르게 되었습니다. 단, 함수형 인터페이스는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있습니다. 그래야 람다식과 인터페이스의 메서드가 1:1로 연결되기 때문입니다.

 (@FunctionalInterface를 붙이면 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인하기 때문에 꼭 붙이는 것을 권장합니다.)

 

 예를 들어 max()라는 메서드가 정의된 MyFuncation 인터페이스를 기반으로 이를 구현한 익명 클래스의 객체와 람다식을 살펴보겠습니다. 

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

// 기존 방식
MyFuncation f = new MyFunction() {
    public int max(int a, int b) {
        return a>b ? a:b;
    }
}

int big = f.max(5, 3);

// 람다식
MyFuncation f = (a, b) -> a>b ? a:b;
int big = f.max(5, 3);

 기존 방식과 다르게 간결해진 코드와 가독성이 높아진 것을 확인할 수 있습니다. 람다식의 반환값이 함수형 인터페이스이기 때문에 위와 같은 코드를 작성할 수 있는 것입니다.

 

✅ Java에서 제공하는 함수형 인터페이스

 Java.util.function패키지에 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았습니다. 이를 활용하여 함수형 인터페이스에 정의된 메서드 이름도 통일되고, 재사용성이나 유지보수성을 높일 수 있을 것입니다. 자주 쓰이는 함수형 인터페이스는 다음과 같습니다.

 

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

 

1. Supplier<T>

 Supplier는 매개변수 없이 반환값만을 갖는 함수형 인터페이스입니다.

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

Supplier<String> supplier = () -> "Hello World!";
System.out.println(supplier.get());

 

2. Consumer<T>

 Consumer는 객체 T를 매개변수로 받아서 사용하며, 반환값이 없는 함수형 인터페이스입니다. Consumer는 andThen이라는 함수를 제공하는데, 하나의 함수가 끝난 후, 다음 Consumer를 연쇄적으로 사용할 수 있습니다.

 아래 예제에서는 먼저 accept로 받아들인 Consumer를 처리하고, andThen으로 받은 두번째 Consumer를 처리하고 있습니다. 함수형에서 함수는 값의 대입 또는 변경 등이 없기 때문에 첫번째 Consumer가 split으로 데이터를 변경하여도 원본 데이터는 유지됩니다.

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
    
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> {accept(t); after.accpet(t); };
    }
}

Consumer<String> consumer = (str) -> System.out.println(str.split(" ")[0]);
consumer.andThen(System.out::println).accept("Hello World");

 

3. Function<T, R>

 Function은 객체 T를 매개변수로 받아서 R로 반환하는 함수형 인터페이스입니다. Consumer와 마찬가지로 andThen을 제공하고 있으며, 추가적으로 composeidentity 함수가 존재합니다.

 compose는 첫번째 함수 실행 이전에 먼저 함수를 실행하여 연쇄적으로 연결해주는 역할을 하며, identity 함수는 자기 자신을 반환하는 static 함수입니다.

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);
    
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
    
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
    
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

Function<String, Integer> function = str -> str.length();
function.apply("Hello World");

 

4. Predicate<T>

 Predicate는 객체 T를 매개변수로 받고, Boolean을 반환하는 함수형 인터페이스입니다.

@FunctionalInterface
public interface Predicate<T> {
    
    boolean test(T t);
    
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requiredNonNull(other);
        return (t) -> test(t) && other.test(t);
    }
    
    default Predicate<T> negate() {
        return (t) -> !test(t);
    }
    
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requiredNonNull(other);
        return (t) -> test(t) || other.test(t);
    }
    
    static <T> Predicate<T> isEqual(Object targetRef) {
        return(null == targetRef)
            ? Objects::isNull
            : object -> targetRef.equals(object);
    }
}

Predicate<String> predicate = (str) -> str.equals("Hello World");
predicate.test("Hello World");

5. 컬렉션 프레임워크

 컬렉션 프레임워크의 인터페이스에 다수의 default 메서드가 추가되었습니다. 그 중의 일부는 함수형 인터페이스를 사용합니다.

인터페이스 메서드 설명
Collection boolean removelf(Predicate<E> filter) 조건에 맞는 요소를 삭제
List void replaceAll(UnaryOperater<E> operator) 모든 요소를 변환하여 대체
Iterable void forEach(Consumer<T> action) 모든 요소에 작업 action을 수행
Map V compute(K key, BiFunction<K,V,V> f) 지정된 키의 값에 작업 f를 수행
V computeIfAbsent(K key, Function<K,V> f) 키가 없으면, 작업 f 수행 후, 추가
V computeIfPresent(K key, BiFunction<K,V,V> f) 지정된 키가 없을 때, 작업 f 수행
V merge(K key, V value, BiFunction<V,V,V> f) 모든 요소에 병합작업 f를 수행
void forEach(BiConsumer<K,V> ation) 모든 요소에 작업 action을 수행
void replaceAll(BiFunction<K,V,V> f) 모든 요소에 치환작업 f를 수행

 

✅ 메서드 참조

 람다식으로 메서드를 간결하게 표현하는 방법들을 배웠습니다. 이번에는 람다식을 한번 더 간결하게 표현해줄 수 있는 메서드 참조에 대해 알아보겠습니다.

 메서드 참조란, 함수형 인터페이스를 람다식이 아닌 일반 메서드를 참조시켜 선언하는 방법입니다. 일반 메서드를 참조하기 위해서는 함수형 인터페이스와 3가지 조건이 같아야 합니다.

 

  • 매개변수 타입
  • 매개변수 개수
  • 반환형

 참조가능한 메서드는 일반 메서드, static 메서드, 생성자가 있으며 클래스이름::메서드이름으로 참조할 수 있습니다. 이렇게 참조하면 함수형 인터페이스로 반환됩니다.

 

1. 일반 메서드 참조

 기존에 선언한 function과 String의 length 함수는 모두 매개변수가 없으며, 반환형이 int로 동일하기 때문에 String::length로 다음과 같이 메서드 참조를 적용할 수 있습니다.

// 람다식
Function<String, Integer> function = (str) -> str.length();
function("Hello World");

// 메서드 참조
Function(String, Integer> function = String::length;
function.apply("Hello World");

 이외에도 파라미터로 String, 반환형이 void인 System.out.println() 메서드는 Consumer에서 참조할 수 있습니다.

Consumer<String> consumer = System.out::println;
consumer.accept("Hello World");

List<String> list = Arrays.asList("a", "b", "c");
list.forEach(System.out::println);

// interface Iterable<T>
default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for(T t: this) {
        action.accept(t);
    }
}

2. static 메서드 참조

 Objecs의 isNull은 반환값이 Boolean이며, 매개변수 값은 1개입니다. 매개변수가 Object이므로 Predicate로 다음과 같이 메서드 참조가 가능합니다.

Predicate<Boolean> predicate = Objects::isNull;

public static boolean isNull(Object obj) {
    return obj == null;
}

3. 생성자 참조

 생성자는 new로 생성하므로 클래스이름::new로 참조할 수 있습니다. Supplier는 매개변수 없이 반환값만을 갖는 인터페이스기 때문에, 매개변수없이 String 객체를 새롭게 생성하는 String의 생성자를 참조하여 Supplier로 선언할 수 있습니다.

Supplier<String> supplier = String::new;

 

 

✅ References

Java의 정석

https://mangkyu.tistory.com/113

 

 

728x90
반응형