Java 입문 | Lamda 함수

Java 8에서부터 도입된 Lambda에 대해서 알아보자.

Lambda 함수란?

Lambda 함수는 프로그래밍 언어에서 사용되는 개념으로 익명 함수(Anonymous functions)를 지칭하는 용어이다. 람다는 일시적으로 즉시 사용할 수 있기에 버릴수 있는 함수이다.

기본 작성법

(인수) -> {처리}
()-> {처리}
(인수) -> {처리}
(인수) -> {return 0;}

인수와 처리 ->으로 연결하는 것으로 쉽게 Lambda를 사용할 수 있다. 아래 다양한 예제를 통해서 구체적으로 알아보도록 하자.

기본형

Java7 이하 버전에서는 쓰레드 코드를 작성시 아래와 같이 작성을 하였다.

new Thread(new Runnable() {
   @Override
   public void run() { 
      System.out.println("Hello world lambda"); 
   }
}).start();

람다를 사용하게 되면 아래과 같이 코드가 간결해져서 가독성도 훨씬 좋아진 것을 볼 수 있다.

new Thread(()->{
      System.out.println("Hello world lambda"); 
}).start();

List에서의 Lambda

일반 for 문은 forEach함수에 Lambda로 대체할 수 있다.

아래와 같이 List 목록이 있다고 하자.

List<String> stringList = new ArrayList<String>();
stringList.add("apple");
stringList.add("orange");
stringList.add("strawberry");

Java7 이하 버전에서는 아래와 같이 for문을 사용했을 것이다.

for (String string : stringList) {
   System.out.println(string);
}

Java8이상부터는 Lambda를 사용하여 아래와 같이 작성할 수 있다.

stringList.forEach(string -> System.out.println(string));

Lambda는 인수와 처리의 생략이 가능하기에, 가능한 한 생략하는 형태로 작성할 수 있다.

생략 규칙은 다음과 같다.

  • 반환 값이 void이라면, return을 생략할 수 있다.
  • 처리 코드가 한 줄이라면, 중괄호 { }가 없이고 가능하고, 마지막에 세미콜론 ; 도 생략 가능하다.
  • 인수가 1개라면 괄호 ()을 생략할 수 있다.
  • 인수의 데이터 타입은 생략 가능하다. (모두 생략하거나 모든 작성하거나 하는 2가지 방법이 있다.)

이 밖에도 생략 가능한 규칙이 더 존재하지만, 여기서는 모두 언급하지 않겠다.

아래와 같이 메소드 참조를 사용하여 작성할 수도 있다.

stringList.forEach(System.out::println);

메소드 참조를 Lambda와 같이 사용하면, 간결하여 알기 쉬운 작성할 수 있게 된다. 다만 너무 많이 사용하게 되면 오히려 알아 볼수 없게 될수도 있으니 Ojbects::nonnull 라든지 XXXX::getId 등과 같이 명시적인 처리에서만 사용하길 바란다.

배열에서의 Lambda

배열에서도 위에 같이 작성하고 싶은 경우도 있겠지만, 배열에서는 foreach를 사용할 수 없기 때문에 List로 변환한 후 수행해야 한다.

String[] stringArray = { "apple", "orange", "strawberry" };
Arrays.asList(stringArray).forEach(string -> System.out.println(string));
  • 배열에서 Lambda를하는 경우에 Arrays.stream() 메소드를 사용하는 것도 가능하다.

Map에서의 Lambda

Map에서는 그대로 Lambda를 적응하는 것이 가능하다.

아래와 같이 Map 목록이 있다고 하자.

Map<String, String> stringMap = new HashMap<String, String>();
stringMap.put("apple", "red");
stringMap.put("orange", "orange");
stringMap.put("strawberry", "red");

Java7 이하 버전에서는 아래와 같이 for문을 사용했을 것이다.

for (Entry<String, String> entry : stringMap.entrySet()) {
    System.out.println(entry.getKey());
    System.out.println(entry.getValue());
}

Java8이상부터는 Lambda를 사용하여 아래와 같이 작성할 수 있다.

stringMap.forEach((key, value) -> {
    System.out.println(key);
    System.out.println(value);
});

함수형 인터페이스

함수형 인터페이스(Functional Interface)는 추상 메서드가 딱 하나만 존재하는 인터페이스를 말한다. 즉, 함수를 1급 객체처럼 다룰 수 있게 해주는 어노테이션으로 인터페이스에 선언하여 단 하나의 추상 메소드만을 갖도록 제한하는 역할을 한다.

먼저, 함수형 인터페이스를 생성한다.

@FunctionalInterface
public interface Math {
    int calc(int first, int second);
}

인터페이스 메소드를 구현하여 실행해 보도록 하자.

public class MathCalc {

    public static void main(String[] args) {
        Math plus = (first, second) -> first + second;
        System.out.println(plus.calc(3, 2));

        Math minus = (first, second) -> first - second;
        System.out.println(plus.calc(3, 2));
    }
}

실핼 결과:

5
5

함수형 인터페이스 4가지

Java에는 자주 사용하게 될 함수형 인터페이스가 이미 정의되어 제공하고 있으며, 총 4가지 함수형 인터페이스가 존재한다.

함수형 인터페이스 매개 변수 반환값
Supplier<T> X O
Consumer<T> O X
Function<T, R> O O
Predicate<T> O Boolean

Supplier <T>

  • Supplier는 영어로 “공급자"를 의미하며, 매개변수 없이 반환값 만을 갖는 함수형 인터페이스이다.
  • T get()을 추상 메소드로 갖는다.

Supplier 함수형 인터페이스

package java.util.function;

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

사용 예제

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

실행 결과:

Hello World!

Consumer<T>

  • Consumer는 영어로 “소비자"를 의미하며, 객체 T를 매개변수로 받아서 사용하며, 반환값은 없는 함수형 인터페이스이다.
  • void accept(T t)를 추상메소드로 갖는다.
  • 그리고 andThen이라는 함수를 제공하고 있는데, 이를 통해 하나의 함수가 끝난 후 다음 Consumer를 연쇄적으로 이용할 수 있다.

Consumer 함수형 인터페이스

package java.util.function;

import java.util.Objects;

@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.accept(t); };
    }
}

사용 예제

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

실행 결과:

Hello
Hello World!

위에 예제에서는 먼저 accept로 받아들인 Consumer를 먼저 처리하여 “Hello"가 표시되고, andThen으로 받은 두 번째 Consumer를 처리하여 “Hello World!“가 표시되었다.
여기서 함수형에서 함수는 값의 대입 또는 변경 등이 없기 때문에 첫 번째 Consumer가 split으로 데이터를 변경했더라도 원본의 데이터는 유지된 것을 볼 수 있다.

Function<T, R>

  • Function는 영어로 “함수"를 의미하며, 객체 T를 매개변수로 받아서 처리한 후 R로 반환하는 함수형 인터페이스다.
  • R apply(T t)를 추상메소드로 갖는다.
  • Function은 Consumer와 마찬가지로 andThen을 제공하고 있으며, 추가적으로 compose를 제공하고 있다.
  • 앞에서 andThen은 첫 번째 함수가 실행된 이후에 다음 함수를 연쇄적으로 실행하도록 연결해 주었다면, compose는 첫 번째 함수 실행 이전에 먼저 함수를 실행하여 연쇄적으로 연결해준다는 점에서 차이가 있다.
  • 그리고 identity 함수가 존재하는데, 이는 자기 자신을 반환하는 static 함수이다.

Function 함수형 인터페이스

package java.util.function;

import java.util.Objects;

@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();
int length = function.apply("Hello World!");
System.out.println(length);

실행 결과:

12

Predicate<T>

  • Predicate는 “(사실이라고) 단정하다"라는 의미이며, 객체 T를 매개 변수로 받아 처리한 후 Boolean을 반환하는 함수형 인터페이스다.
  • boolean test(T t)을 추상 메서드로 갖고 있다.

Predicate 함수형 인터페이스

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(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.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

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

    @SuppressWarnings("unchecked")
    static <T> Predicate<T> not(Predicate<? super T> target) {
        Objects.requireNonNull(target);
        return (Predicate<T>)target.negate();
    }
}

사용 예제

Predicate<String> predicate = (str) -> str.equals("Hello World!");
boolean test = predicate.test("Hello World!");
System.out.println(test);

실행 결과:

true