Java Lamda 함수 Function, Consumer, Supplier, Predicate
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