사실 스트림 API를 정확히 이해하기 위해 스트림 정리보다 람다식을 먼저 정리할 필요가 있었다.
스트림의 filter, forEach 등의 연산을 사용하기 전에 람다식이란 무엇인지, 함수형 인터페이스 반환값인 Predicate, Consumer이 무엇인지 알고 사용하면 좋을 것 같다.
함수를 하나의 식으로 표현한 것
함수를 람다식으로 표현하면 메소드명이 필요 없기 때문에 익명 함수
의 한 종류라고 볼 수 있다.
익명 함수(Anonymous Function): 함수의 이름이 없는 함수
일반 함수
public int sum(int op1, int op2) {
return op1 + op2;
}
//반환 타입 - 메소드명 (매개변수, ...) {
// 실행문 ...
//}
람다식
(op1, op2) -> op1 + op2
// (매개변수, ...) -> {실행문}
단순한 함수를 기존 방식에서 람다 방식으로 바꾼 것만 봐도 코드가 단순해 진 것을 확인할 수 있다.!
상황에 맞게 사용하는 것이 중요해 보인다.
재사용 가능성이 높거나 람다식으로 표현하기에 다소 복잡한 함수는 일반 함수를 쓰는 것이 좋을 것 같다.
실행문이 단순하고 재사용 가능성이 낮은 일회성 함수를 간편하게 구현하고 싶을 때 람다식을 쓰는 것이 좋아보인다.
또한 컬렉션이나 스트림을 적재적소에 사용하기 위해 알고 있으면 좋은 기능같다.
1개의 추상메소드를 갖는 인터페이스
익명 함수들은 공통적으로 일급 객체
라는 특징을 가지고 있다.(함수를 변수처럼 사용 가능하다)
일급 객체(First-class Object): 변수나 데이터 구조 안에 담을 수 있으며 매개변수로 전달, 반환값으로 사용할 수 있는 객체
람다식을 하나의 변수에 대입할 때 사용하는 참조 변수의 타입을 함수형
함수형 인터페이스를 구현하기 위해서는 인터페이스에 @FunctionalInterface 어노테이션을 붙이고 abstract 함수를 하나만 선언하면 된다.
@FunctionalInterface
interface MyInterface<T> {
T myAbstractMethod();
...
}
매개변수 X 반환값 X
자바에서 쓰레드를 구현할 때 사용하는 방법 중 하나이다.
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
//사용 예
class MyThread implements Runnable {
String str;
public MyThread(String str) {
this.str = str;
}
@Override
public void run() {
for(int i = 1; i <= 10; i++) {
System.out.print(str + " ");
Thread.sleep(1000);
}
}
}
// Main 함수
MyThread r1 = new MyThread("hi");
MyThread r2 = new MyThread("yun");
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
thread1.
<
T>
() -> T (매개변수 X 반환값 O)
T get()을 추상메소드로 갖는다. get()으로 반환값을 말 그대로 supply 받을 수 있다.
@FunctionalInterface
public interface Supplier<T> {
T get();
}
//사용 예
Supplier<Integer> supplier = () -> 1;
System.out.println(supplier.get());
// 출력
1
<
T>
T -> void (매개변수 O, 반환값 X)
void accept(T t)를 추상메소드로 갖는다. 객체 T를 매개변수로 받아서 말 그대로 consume한다.
@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> consumer1 = (str) -> System.out.println("I am " + str);
Consumer<String> consumer2 = (str) -> System.out.println("Hi!! " + str + "!");
consumer1.andThen(consumer2).accept("yun");
// 출력
I am yun
Hi!! I am yun!
andThen()은 Consumer들을 연쇄적으로 사용할 수 있게 해주는(연결해주는) 메소드이다.
동일한 객체를 마지막의 accept로 전달해주면 제일 앞의 Consumer가 처리한 결과를 andThen의 Consumer가 사용한다. (첫번째 처리 결과를 두번째의 매개변수로 제공한다)
<
T>
T -> R (매개변수 T, 반환값 R)
R apply(T t)를 추상메소드로 갖는다. andThen, compose, indentity가 추가로 제공된다.
@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<Integer, Integer> function = (value) -> value * 2;
System.out.println(function.apply(3));
//출력
6
compose()는 두 Function을 조합하여 새로운 Function을 만들어주는 메소드이다.
// compose() 사용 예
Function<Integer, Integer> multiply = (value) -> value * 2;
Function<Integer, Integer> add = (value) -> value + 3;
System.out.println(multiply.compose(add).apply(3));
// 결과, compose(add)가 먼저 실행된다.
12
<
T>
T -> boolean (매개변수 T, 반환값 boolean)
boolean test(T t)를 추상메소드로 갖는다.
@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);
}
}
// 사용 예
Predicate<Integer> predicate = num -> num > 0;
predicate.test(10); // true
and()는 두 Predicate의 결과가 true면 true를 반환, or()은 둘 중 하나라도 true면 true를 반환한다.
//and(), or() 사용 예
Predicate<Integer> isBiggerThanFive = num -> num > 5;
Predicate<Integer> isLowerThanSix = num -> num < 6;
System.out.println(isBiggerThanFive.and(isLowerThanSix).test(10));
System.out.println(isBiggerThanFive.or(isLowerThanSix).test(10));
// 출력
false
true
isEqual()은 인자로 전달되는 객체와 같은지 확인하는 메소드이다.
// isEqual() 사용 예
Predicate<String> predicate = (str) -> str.equals("Hello World");
predicate.test("Hello World"); // true
predicate.test("Helloooo"); // false
매개변수의 정보와 반환 타입을 파악하여 람다식에서 불필요한 매개변수를 제거하는 것을 의미한다.
// 예시 데이터
String[] names = new String[] {"lee", "choi", "park"};
List<String> list = Arrays.asList(names);
// 일반 방법
for(String name : list)
System.out.println(name);
// forEach 적용
list.forEach(name -> System.out.println(name));
// forEach에 메소드참조 적용
list.forEach(System.out::println);
// 다른 예시
Function<String, Integer> function = (str) -> str.length();
//메소드참조 적용
Function<String, Integer> function = String::length;
::
연산자로 이름과 클래스를 분리하거나 메소드의 이름과 객체의 이름을 분리할 수 있다.
Java8 - 함수형 인터페이스(Functional Interface) 이해하기
[Java] 람다식(Lambda Expression)과 함수형 인터페이스(Functional Interface)