본 게시글은 백기선 님의 live-study 과제를 수행하면서 작성한 글입니다.
목표
자바의 람다식에 대해 학습하세요.
학습할 것 (필수)
- 람다식 사용법
- 함수형 인터페이스
- Variable Capture
- 메서드, 생성자 레퍼런스
람다식이란?
자바 8에서 추가된 람다식은 메서드를 하나의 '식(Expression)'으로 표현한 것이다. 람다식의 등장으로 메서드를 보다 간략하게 표현할 수 있게 되었으며, 메서드를 변수처럼 매개변수로 전달하거나 반환 값으로 받을 수도 있게 되었다.
예를 들어 두 정수 중에서 큰 값을 반환하는 메서드가 있다고 하자.
int max(int a, int b) {
return a > b ? a : b;
}
위 메서드를 람다식으로 바꾸면 다음과 같다.
(int a, int b) -> {
return a > b ? a : b;
}
기존 메서드에서 반환 타입과 메서드 이름을 생략하고, → 를 추가하면 그게 바로 람다식이다.
메서드 내의 코드가 한 줄이라면 다음과 같이 { }를 생략할 수 있고, 그게 return 문이라면 return 키워드도 생략할 수 있다.
(int a, int b) -> a > b ? a : b
람다식 매개변수의 타입이 추론이 가능한 경우 타입을 생략할 수 있다.(대부분의 경우에 가능)
(a, b) -> a > b ? a : b
매개변수가 1개이고, 타입이 생략된 경우 ( )를 생략할 수 있다.
a -> a*a //가능.
int a -> a*a //불가능. 매개변수에 타입이 있기 때문에
함수형 인터페이스(Functional Interface)
앞서 람다식을 통해 메서드를 변수처럼 다룰 수 있다고 하였다. 그렇다면 변수의 타입은 어떤 것을 지정해줘야 될까?
타입 max = (a, b) -> a > b ? a : b
사실 람다식은 익명 클래스의 객체와 동등하다.
public interface MyFunction {
int max(int a, int b);
}
예를 들어 위와 같은 인터페이스가 있고, 해당 인터페이스를 구현한 익명 클래스의 객체는 다음과 같이 구현할 수 있다.
MyFunction f = new MyFunction() {
int max(int a, int b) {
return a > b ? a : b;
}
};
그리고 이 익명 클래스 객체를 다음과 같이 람다식으로 대체할 수 있다.
MyFunction f = (a, b) -> a > b ? a : b;
람다식도 실제로는 익명 객체이기 때문에 대체가 가능한 것이고, MyFunction처럼 람다식을 다루기 위한 인터페이스를 '함수형 인터페이스'라고 한다.
함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어야 한다. 그래야 람다식과 추상 메서드를 매치시킬 수 있기 때문이다. (default, static 메서드의 개수에는 제약이 없다)
또한 메서드의 매개변수 타입 또는 리턴 타입이 함수형 인터페이스 일 때, 참조 변수가 아닌 람다식을 그대로 넘겨줄 수 있다.
//함수형 인터페이스 선언
@FuntionalInterface
public interface MyFunction {
void myMethod();
}
//메서드 선언
void method(MyFunction f) { }
//메서드 사용
method(() -> System.out.println("hello"));
//리턴 타입으로 함수형 인터페이스 사용
MyFunction retMethod() {
return () -> System.out.println("hello");
}
참고 : 함수형 인터페이스를 사용할 때 '@FunctionalInterface' 애노테이션을 사용하면 컴파일러가 함수형 인터페이스를 올바르게 정의했는지 체크해주기 때문에 꼭 사용하자.
java.util.function 패키지
java.util.function 패키지에는 일반적으로 자주 쓰이는 형식의 메서드들이 함수형 인터페이스로 정의되어있다.
새로운 함수형 인터페이스를 정의해도 되지만, 가능하면 해당 패키지를 활용하는 것이 재사용성이나 유지보수 측면에서 좋다.
리턴 값의 유무와 매개변수의 개수에 따라 분류할 수 있다.
매개변수가 1개인 함수형 인터페이스
Supplier : 매개변수는 없고, 리턴 값만 있는 경우
@FuntionalInterface
public interface Supplier<T> {
T get();
}
Consumer : 매개 변수만 있고, 리턴 값은 없는 경우
@FuntionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? Super T> after) { ... }
}
default 메서드인 andThen을 사용하면 매개변수를 다른 함수에게 넘겨 추가적인 처리를 할 수 있다.
Consumer<String> consumer1 = s -> System.out.println("consumer1 " + s);
Consumer<String> consumer2 = consumer1.andThen( s -> System.out.println("consumer2 " + s));
consumer2.accept("Hello!");
/*실행 결과
consumer1 Hello!
consumer2 Hello!
*/
실행 결과 consumer1이 실행되고, 다음으로 같은 매개변수로 consumer2가 실행된 것을 알 수 있다.
Function<T, R> : 일반적인 함수로 하나의 매개변수와 리턴 값을 가지는 경우
@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 함수형 인터페이스의 andThen 메서드는 리턴 값을 매개변수로 전달된 다음 함수의 매개변수로 넘긴다.
그리고 이밖에 추가적으로 compose, identity 메서드가 있다.
compose 메서드는 andThen메서드와 실행 방향이 반대 방향이고, identity메서드는 매개변수로 전달된 값을 그대로 다시 반환하는 함수를 리턴한다.
변환 예제
Function<Integer, String> function1 = i -> i.toString();
Function<String, Integer> function2 = s -> Integer.parseInt(s); //전달된 문자열을 정수로 변환하여 리턴
//값 변환 과정 : Integer -> String -> Integer
//함수 실행 흐름 : function1 -> function2
Integer andThenApply = function1.andThen(function2).apply(10); //10
//andThen과 실행 방향이 반대 방향이다.
//값 변환 과정 : String -> Integer -> String
//함수 실행 흐름 : function2 -> function1
String composeApply = function1.compose(function2).apply("100"); //"100"
//매개변수를 그대로 반환하는 함수를 반환한다.
Function<Integer, Integer> identity = Function.identity();
Integer identityApply = identity.apply(15); //15
Predicate : 조건식을 표현하는 데 사용. 매개변수는 하나, 리턴 타입은 Boolean
@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);
}
// Objects.equals (Object, Object)에 따라 두 인수가 같은지 테스트하는 Predicate를 리턴한다.
// Objects.equals (Object, Object) : 인수가 서로 같으면 true를 반환하고 그렇지 않으면 false를 반환합니다. 따라서 두 인수가 모두 null이면 true가 반환되고 정확히 하나의 인수가 null이면 false가 반환됩니다. 그렇지 않으면 첫 번째 인수의 equals 메소드를 사용하여 동등성을 판별합니다.
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<Integer> predicate = i -> i >100; //i가 100보다 크다면 true
//매개변수의 값이 100보다 크다면 true
boolean test = predicate.test(101);
//부정. 즉, 매개변수의 값이 100보다 크다면 false
boolean negateTest = predicate.negate().test(101);
//i > 100 && i < 150
boolean andTest = predicate.and(i -> i < 150).test(120);
//i > 100 || i < 50
boolean orTest = predicate.or(i -> i < 50).test(40);
//전달된 predicate의 부정인 predicate를 반환하다.
Predicate<Integer> not = Predicate.not(i -> i > 100);
boolean notTest = not.test(99);
boolean equalTest = Predicate.isEqual(100).test(100);
System.out.println("test = " + test);
System.out.println("negateTest = " + negateTest);
System.out.println("andTest = " + andTest);
System.out.println("orTest = " + orTest);
System.out.println("notTest = " + notTest);
System.out.println("equalTest = " + equalTest);
/* 실행 결과
test = true
negateTest = false
andTest = true
orTest = true
notTest = true
equalTest = true
*/
매개변수가 2개인 함수형 인터페이스
앞서 설명한 것들과 매개변수 개수의 차이가 있을 뿐, 크게 다르지 않기 때문에 간단하게 설명하도록 하겠다.
- BiConsumer<T, U> : 두 개의 매개 변수만 있고, 리턴 값은 없다.
- BiPredicate<T, U> : 조건식을 표현하는 데 사용. 두 개의 매개변수, 리턴 값은 boolean.
- BiFunction<T,U,R> : 두 개의 매개변수를 받아서 하나의 결과를 반환.
참고로 당연한 이야기지만 Supplier는 리턴 값만 가지기 때문에 매개변수가 2개인 함수형 인터페이스가 없다.
매개변수가 3개 이상인 함수형 인터페이스
만약 3개 이상의 매개변수를 가지는 함수형 인터페이스가 필요하다면 다음과 같이 직접 만들어서 사용해야 한다.
@FuntionalInterface
public interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
UnaryOperator, BinaryOperator
UnaryOperator, BinaryOperator는 Function의 변형으로, 매개변수의 타입과 리턴 타입이 같은 것을 제외하면 Function과 같으며 각각 Function과 BiFunction을 상속받는다.
- 인터페이스 : UnaryOperator, 메서드 : T apply(T t)
- 인터페이스 : BinaryOperator, 메서드 : T apply(T t, T t)
프리미티브 타입을 사용하는 함수형 인터페이스
지금까지 살펴본 함수형 인터페이스는 모두 제네릭 타입을 사용하기 때문에 프리미티브 타입을 사용할 때 래퍼 클래스를 사용해야 했다. 그래서 프리미티브 타입을 사용할 때 보다 효율적으로 사용할 수 있도록 기본형을 사용하는 함수형 인터페이스가 제공된다.
기본형을 사용하는 것을 제외하면 앞서 설명한 것들과 크게 다르지 않기 때문에 자세한 설명은 생략하고, 필요하다면 공식 문서에서 프리미티브 타입의 이름이 포함된 함수형 인터페이스들을 살펴보도록 하자.
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/function/package-summary.html
Variable Capture
람다식 내에서 필드, 메서드, 지역변수를 모두 사용할 수 있다. 필드와 메서드는 사용이 자유로운 반면 지역변수를 사용할 때는 주의해야 할 것이 있다.
앞서 람다식도 결국은 익명 객체와 같다고 설명했었다. new 키워드로 생성하는 익명 객체의 경우 JVM의 Heap영역에 저장되어 자신을 포함하고 있는 메서드의 실행이 끝나도 사용할 수 있다. 반면에 메서드 내에서 사용되는 지역변수 또는 매개변수는 Stack영역에 저장되며 해당 메서드의 실행이 끝나면 소멸되게 된다. 그렇기 때문에 익명 객체를 포함하고 있는 메서드의 지역변수 또는 매개변수를 사용할 때 문제가 생길 수 있다.
이를 해결하기 위해 자바에서는 컴파일 시점에 메서드 내의 익명 객체에서 지역변수 또는 매개변수를 사용할 경우 해당 변수들이 final 또는 effectvely fianl 변수라면 객체 내부로 값을 복사해서 사용할 수 있도록 하는데, 이것이 바로 Variable Capture이다.
앞선 설명에 final 또는 effectively final 변수일 경우에만 익명 객체의 내부에서 사용이 가능하다고 했는데, effetively final 변수에 대해 생소할 수 도 있을 것이다.
effectively final이란 자바 8에서 생긴 개념으로 final 변수는 아니지만, 값이 변하지 않는 변수를 가리키는 용어이다.
메서드 참조(Method Reference)
람다식이 하나의 메서드만 호출하는 경우, '메서드 참조'를 이용해서 람다식을 '클래스이름::메서드' 또는 '인스턴스::메서드'로 더 간략하게 만들 수 있다.
static 메서드 참조
- 람다식 : (x) → ClassName.method(x)
- 메서드 참조 : ClassName::method
//람다식
Function<String, Integer> f = (String s) -> Integer.parseInt(s);
//메서드 참조
Function<String, Integer> f = Integer::parseInt;
인스턴스 메서드 참조
- 람다식 : (obj, x) → obj.method(x)
- 메서드 참조 : ClassName::method
//람다식
BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equals(s2);
//메서드 참조
BiFunction<String, String, Boolean> f = String::equals;
특정 객체 인스턴스 메서드 참조
- 람다식 : (x) → obj.method(x)
- 메서드 참조 : obj::method
Hello hello = new Hello();
//람다식
Function<String, Boolean> f = (x) -> hello.equals(x);
//메서드 참조
Function<String, Boolean> f = (x) -> hello::equals;
생성자의 메서드 참조
생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있다.
매개변수가 없는 생성자
//람다식
Supplier<Hello> hello = () -> new Hello();
//메서드 참조
Supplier<Hello> hello = () -> Hello::new;
매개변수가 있는 생성자
- 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용.
class Hello {
public Hello(int i, String s) { ... }
}
//람다식
Function<Integer, String, Hello> hello = (i, s) -> new Hello(i, s);
//메서드 참조
Function<Integer, String, Hello> hello = (i, s) -> Hello::new;
배열을 생성할 때는 다음과 같다.
//람다식
Function<Integer, int[]> arr = x -> new int[x];
//메서드 참조
Function<Integer, int[]> arr = int[]::new;
댓글