Java Stream이란? 3탄 람다식과 함수형 인터페이스

Java Stream이란? 3탄 람다식과 함수형 인터페이스

2021, Mar 18    

정리를 하게 된 이유.

Stream API, 함수형 프로그래밍을 떠나서 람다에 대해서도 한번 정리를 해보고 싶었는데, 마침 같이 정리를 할 수 있게 되어서 정리를 해보려고 한다.

망나니개발자라는 블로그가 이번 Stream API관련해서 참고를 하니, 너무 정리가 잘되어있어서 훗날 기회가 되면 감사하다는 말 전하고 싶다 ㅋㅋ

람다식이란?

Stream 연산들은 매개변수로 함수형 인터페이스를 받도록 되어있다. 그리고 람다식은 반환값으로 함수형 인터페이스를 반환하고 있다. 그렇기 때문에 우리는 Stream API를 정확히 이해하기 위해 람다식과 함수형 인터페이스에 대해 할고 있어야 한다.

[람다식이란]

  • 함수를 하나의 식으로 표현한 것이다. 함수를 람다식으로 표현하면 메소드의 이름이 필요없기 때문에 , 람다식은 익명함수의 한 종류라고 볼 수 있다. 익명함수란 함수의 이름이 없는 함수로, 익명함수들은 모드 일급 객체이다. 일급 객체인 함수는 변수처럼 사용가능하며 매개변수로 전달이 가능하는 등의 특징을 가지고 있다.

기존의 방식에서는 함수를 선언할때

//기존의 방식
반환타입 메소드명(매개변수,...){
    실행문
}

public String hello(){
    return "Hello World!";
}

하지만 람다 방식으로는 위와 같이 메소드 명이 불필요하며, 다음과 같이 괄호와 화살표를 이용해 선언한다

 (매개변수,...)->(실행문...)
 
 ()->"Hello World!";

이렇게 람다식이 등장한건 불필요한 코드를 줄이고, 가독성을 높이기 위함이다. 그렇기 때문에 함수형 인터페이스의 인스턴스를 생성하여, 함수를 변수처럼 선언하는 람다식에서는 메소드의 이름이 불필요하다고 여겨져서 이를 사용하지 않는다. 대신 컴파일러가 문맥을 살펴 타입을 추론한다. 람다식으로 선언된 함수는 1급 객체이기 때문에 Stream API의 매개변수로 전달이 가능하다.

[람다식의 특징]

  • 람다식 내에서 사용되는 지역변수는 Final이 붙지 않아도 상수로 간주된다.
  • 람다식으로 선언된 변수명은 다른 변수명과 중복될 수 없다.

[람다식의 장점]

  • 코드를 간결하게 만들 수 있다.
  • 식에 개발자의 의도가 명확히 드러나 가독성이 높아진다.
  • 함수를 만드는 과정없이 한번에 처리할 수 있어 생산성이 높아진다.
  • 병렬프로그래밍이 용이하다.

[람다식의 단점]

  • 람다를 사용하면서 만든 무명함수는 재사용이 불가능하다.
  • 디버깅이 어렵다.
  • 람다를 남발하면 비슷한 함수가 중복 생성되어 코드가 지저분해질 수 있다.
  • 재귀로 만들경우에 부적합하다.

람다를 사용하기 전 코드

public class Lambda{
    public static void main(String[] args){
        System.out.println(new MyLambdaFuntion()){
            public int max(int a, int b){
                return a > b ? a : b;
            }
        }.max(3,5));
    }
}

람다를 사용한 후 코드

@FunctionalInterface
interface MyLambdaFunction{
    int max(int a, int b);
}
public class Lambda{
    public static void main(String[] args){
        MyLambdaFunction lambdaFuntion = (int a, int b) -> a > b ? a : b;
        System.out.println(lambdaFuntion.max(3,5));
    }
}

우리는 Java8 이전에 사용했던 익명함수들을 람다식으로 변경해 코드를 줄일 수 있게 되었고, 여기서 놓치지 말아야 하는 것은 람다식으로 생성된 순수 함수는 함수형 인터페이스로만 선언이 가능하다는 점이다. 또한 @FunctionalInterface는 해당 인터페이스가 1개의 함수만을 갖도록 제한하기 때문에, 여러 개의 함수를 선언하면 컴파일 에러가 발생할 것이라는 점이다.

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

  • Supplier
  • Consumer
  • Function<T,R>
  • Predicate
  1. Supplier 매개 변수 없이 반환값 만을 갖는 함수형 인터페이스이다. Supplier T get()을 추상 메소드로 갖고 있다. ```java @FunctionalInterface public interface Supplier{ T get(); }

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


2. Consumer<T>
Consumer는 객체 T를 매개변수로 받아서 사용하며, 반환값은 없는 함수형 인터페이스이다. 
Consumer는 void accept(T t)를 추상메소드로 갖는다.
또한 Consumer는 andThen이라는 함수를 제공하고 있는데, 이를 통해 하나의 함수가 끝난 후 
다음 Consumer를 연쇄적으로 이용할 수 있다. 
```java
@FunctionalInterface
public interface Consumer<T>{
    void accept(T t);
    
    default Consumer<T> andThen(Consumer<?superT>after){
        Objects.requireNonNull(after);
        return (T t) -> {accept(t); after.accept(t);}
    }
}

Consumer<String> consumer = (str) -> System.out.println(str.split(" ")[0]);
consumer.andThen(System.out.println).accept("Hello World");
  1. Function<T,R> Function은 객체 T를 매개변수로 받아서 처리한 후 R로 반환하는 함수형 인터페이스다. Function은 R apply(T t)를 추상메소드로 갖는다. 또한 Funtiondms Consumer와 마찬가지로 andThen을 제공하고 있으며, 추가적으로 compose를 제공하고 있다. 앞에서 andThen은 첫번째 함수가 실행된 이후에 다음 함수를 연쇄적으로 실행핟록 연결해준다고 하였다. 하지만 compose는 첫 번째 함수 실행 이전에 먼저 함수를 실행하여 연쇄적으로 연결해준다는 점에서 차이가 있다. 이는 identity 함수가 존재하는데, 이는 자기 자신을 반환하는 static 함수이다.
  • apply() 메소드는 ArrayList에 map형태의 특정 객체의 값을 String으로 변환해주는 역할을 합니다.
@FunctionalInterface
Public interface Function<T,R>{
 
    R apply(T t);
    
    default<V> Function<V,R> compose(Funtion<? super V, ? extends T> before){
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
    
    default<V> Function<T,V> andThen(Funtion<? 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> funtion = str -> str.lenght();
function.apply("Hello World");
  1. Predicate Predicate는 객체 T를 매개 변수로 받아 처리한 후 Boolean을 반환한다. Supplier는 Boolean test(T t)을 추상 메소드로 갖고 있다. ```java @FunctionalInterface public interface Predicate{ boolean test(T t);

    default Predicate and (Predicate<? super T> other){ Objects.requireNonNull(other); return(t) -> test(t) && other.test(t); }

    default Predicate negate(){ return(t) -> !test(t); }

    default Predicate or (Predicate<? super T> other){ Objects.requireNonNull(other); return (t) -> test(t) || other.test(t); }

    static Predicate inEqual(Object targetRef){ return (null == targetRef) ? Objects::isNull :object - > targetRef.equals(object); } }

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