람다식
람다는 JDK 1.8버전부터 추가된 기능입니다.
람다가 추가되면서 자바는 객체지향언어인 동시에 함수형 언어가 되었습니다.
람다식은 메서드를 하나의 식으로 표현한 것입니다. 메서드에서 생략할 수 있는 부분을 최대한 생략하여
간략하고 명확한 식으로 표현한 것이죠. 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지면서,
람다식을 익명 함수라고도 합니다.
모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체 또한 생성해야지 메서드를
호출할 수 있습니다. 하지만 람다식은 이런 과정들이 필요하지 않고 오직 람다식 자체만으로도 이 메서드의 역할을
할 수 있습니다. 이 뿐만 아니라 람다식은 메서드의 파라미터로 전달되어지는 것이 가능하고, 메서드의 결과로
반환될 수 있습니다. 람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해진 것입니다.
람다식 작성 방법
먼저 메서드에서 이름과 반환 타입을 제거합니다.
그리고 파라미터 선언부와 메서드 몸체 { } 사이에 -> 를 추가합니다
ex)
int max (int a, int b) {
return a > b ? a : b;
}
(int a, int b) -> {
return a > b ? a : b;
}
반환값이 있는 메서드일 때는 return 문 대신 식으로 대신할 수 있으며 식의 연산결과가 자동으로 반환값이 됩니다.
이 때는 문장이 아닌 식이므로 끝에 ;(세미콜론) 을 붙이지 않습니다.
ex)
(int a, int b) -> { return a > b ? a : b; }
(int a, int b) -> a > b ? a : b
람다식에 선언된 파라미터 타입이 어떤 타입인지 유추할 수 있으면 타입도 생략할 수 있습니다.
대부분의 경우 생략이 가능합니다.
ex)
(int a, int b) -> a > b ? a : b
(a, b) -> a > b ? a : b
선언된 파라미터가 하나뿐이면 ( )(소괄호)도 생략할 수 있습니다. 단, 파라미터 타입이 있으면 생략할 수 없습니다.
ex)
(a) -> a * a
a -> a * a
{ }(중괄호)안에 메서드 내용이 한 줄일 때는 괄호를 생략할 수 있습니다.
이 때 문장의 끝에 ;(세미콜론) 을 붙이지 않습니다.
그런데 { }(중괄호)안에 문장이 return 문일 경우 괄호를 생략할 수 없습니다.
ex)
(String name, int age) -> { System.out.println("이름 = " + name + " 나이 = " + age); }
(String name, int age) -> System.out.println("이름 = " + name + " 나이 = " + age)
함수형 인터페이스
자바에서 모든 메서드는 클래스에 포함되어야 합니다. 그럼 람다식은 어떤 클래스에 포함되는 것일까요?
사실 람다식은 메서드보다 익명 클래스 객체와 비슷한데요. 익명 클래스는 클래스의 선언과 객체의 생성을
동시에 하기 때문에 단 한 번만 사용될 수 있고, 오직 하나의 객체만을 생성할 수 있는 일회용 클래스입니다.
이름이 없는 클래스이기 때문에 생성자도 가질 수 없으며, 부모 클래스의 이름이나 구현하고자 하는 인터페이스의
이름을 사용해서 정의하기 때문에 하나의 클래스를 상속받는 동시에 인터페이스를 구현하거나 둘 이상의
인터페이스를 구현할 수 없습니다.
(int a, int b) -> a < b ? a : b
new Object() {
int max(int a, int b) {
return a < b ? a : b;
}
}
참조변수가 있어야 객체의 메서드를 호출할 수 있듯이 익명 클래스 객체인 람다식도 참조변수에 저장해야합니다.
타입 f = (int a, int b) -> a < b ? a : b;
그렇다면 참조변수 타입은 어떤 것이여야 할까요?
참조형이므로 클래스 또는 인터페이스가 가능합니다. 그리고 람다식과 동일한 메서드가 정의되어 있는 것이어야 합니다.
그래야만 참조변수로 익명 객체(람다식)의 메서드를 호출할 수 있기 때문입니다.
public interface MyFunction {
public abstract int max(int a, int b);
}
MyFunction f = new MyFunction() {
@Override
public int max(int a, int b) {
return a > b ? a : b;
}
};
int result = f.max(10, 20);
max() 라는 추상 메서드를 가진 인터페이스인 MyFunction 을 생성하고 MyFunction 를 구현한 익명 클래스 객체를
생성했습니다. MyFunction 에서 정의한 max() 메서드와 (int a, int b) -> a > b ? a : b 와 메서드 선언부가 일치하기
때문에 위 코드의 익명 객체를 람다식으로 변경할 수 있습니다.
MyFunction f = (a, b) -> a > b ? a : b;
int result = f.max(10, 20);
이렇게 익명 객체를 람다식으로 대체가 가능한 이유는, 람다식도 실제로는 익명 객체이고, MyFunction 를 구현한
익명 객체의 메서드 max() 와 람다식의 매개변수의 타입과 개수, 반환값이 일차하기 때문입니다. 방금 예제처럼
하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다루는 것은 기존의 자바의 규칙들을 어기지 않으면서도
자연스럽습니다. 그래서 인터페이스를 통해 람다식을 다루기로 결정했고, 람다식을 다루기 위한 인터페이스를
함수형 인터페이스라고 합니다.
단 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있는데요. 그래야지
람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문입니다.
반면에 static 메서드와 default 메서드의 개수는 제약이 없습니다.
Comparator<String> com = new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
};
String[] nameList = new String[] {"Iron Man", "Captima America", "Thor"};
Arrays.sort(nameList, com);
for (String s : nameList) {
System.out.println(s);
}
이번에는 함수형 인터페이스인 Comparator 를 이용한 예제입니다. 배열 안에 있는 문자열의 길이를 비교해서
정렬했는데요.
Comparator<String> com = (o1, o2) -> o1.length() - o2.length();
String[] nameList = new String[] {"Iron Man", "Captima America", "Thor"};
Arrays.sort(nameList, com);
for (String s : nameList) {
System.out.println(s);
}
위 코드의 익명 객체를 람다식으로 바꾸면 이렇게 코드가 간결해지는걸 확인할 수 있습니다.
java.util.function 패키지
java.util.function 패키지에는 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았습니다.
매번 새로운 함수형 인터페이스를 정의할 필요 없이 이 패키지의 인터페이스를 활용할 수 있습니다.
자주 쓰이는 기본적인 함수형 인터페이스
항수형 인터페이스 | 추상 메서드 | 설명 |
java.lang.Runnable | void run () | 파라미터도 없고, 반환값도 없음. |
Supplier<T> | T get () | 파라미터도 없고, 반환값만 있음. |
Consumer<T> | void accept (T t) | Supplier 와 반대로 매개변수만 있고 반환값이 없음 |
Function<T, R> | R apply (T t) | 일반적인 함수. 하나의 매개변수를 받아서 결과를 반환. |
Predicate<T> | boolean test (T t) | 조건식을 표현하는데 사용. 매개변수는 하나. 반환 타입은 boolean. |
매개변수와 반환값의 유무에 따라 4개의 함수형 인터페이스가 정의되어 있습니다
Predicate 는 Function 의 변형으로 반환값이 boolen 이라는 것만 제외하면 Function 과 동일합니다.
Predicate 는 조건식을 메서드로 표현하는데 사용합니다. Runnable 인터페이스의 run() 메서드는파라미터와
반환값이 없고 메서드의 내용만 실행, Supplier 인터페이스의 get() 메서드 는 공급자로 파라미터가 없고
반환만 수행, Consumer 인터페이스의 accept() 메서드는 파라미터만 받아 메서드를 실행하고 반환이 없습니다.
Predicate<String> isEmptyStr = s -> s.length() == 0;
String s ="";
if(isEmptyStr.test(s))
System.out.println("빈 문자열");
Predicate 를 이용해 파라미터로 들어온 문자열의 길이가 0인지 판단해 결과를 반환하는 예제입니다.
파라미터가 두 개인 함수형 인터페이스
파라미터의 개수가 2개인 함수형 인터페이스는 이름 앞에 접두사 'Bi' 가 붙습니다.
.Runnable 와 Supplier 인터페이스는 파라미터를 받지 않으므로 추가되지 않았습니다.
함수형 인터페이스 | 추상 메서드 | 설명 |
BiConsumer | void accept (T t, U u) | 두 개의 파라미터만 있고, 반환값이 없음. |
BiPredicate | boolean test (T t, U u) | 조건식을 표현하는데 사용. 매개변수는 둘, 반환값은 boolean. |
BiFunction | R apply (T t, U u) | 두 개의 파라미터를 받아서 하나의 결과 로 반환. |
만약 3개 이상의 파라미터를 갖는 함수형 인터페이스가 필요하다면 직접 만들어서 사용해야 합니다.
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v)
}
3개의 파라미터를 갖는 함수형 인터페이스를 선언한다면 위 코드와 같습니다. 함수형 인터페이스를 선언할 때는
@FunctionalInterface 어노테이션을 인터페이스 위에 붙여줍니다. @FunctionalInterface 을 안 붙여줘도
함수형 인터페이스를 구현할 수 있고 람다식으로 변환이 되지만 붙여주는 이유는 컴파일러가 올바르게
함수형 인터페이스를 작성했는지 확인해주기 때문입니다. 만약 위 인터페이스에 추상 메서드를 추가한다면
컴파일 에러가 발생합니다.
UnaryOperator, BinaryOperator
Function 의 또 다른 변형으로는 UnaryOperator 과 BinaryOperator 가 있는데, 파라미터의 타입과 반환타입의 타입이
모두 일치한다는 점을 제외하고는 Function 과 같습니다.
함수형 인터페이스 | 메서드 | 설명 |
UnaryOperator | T apply (T t) | Function 의 자식. Function 과 달리 파라미터와 반환값의 타입이 같음. |
BinaryOperator | T apply (T t, T t) | BiFunction 의 자식. BiFunction 과 달리 파마미터와 반환값의 타입이 같음 |
컬렉션 프레임워크와 함수형 인터페이스
컬렉션 프레임워크의 인터페이스에 다수의 디폴트 메서드가 추가되었는데, 그 중의 일부는 함수형 인터페이스를
사용합니다.
인터페이스 | 추상 메서드 | 설명 |
Collection | boolean removeIf (Predicate<E> filter) | 조건에 맞는 요소 삭제 |
List | void replaceAll (UnaryOperator<E> operator) |
모든 요소를 변환하여 대체 |
Iterable | void forEach (Consumer<T> action) | 모든 요소에 작업 action 을 수행 |
Map | V compute (K key, BiFunction<K, V, V> f) |
지정된 키의 값에 작언 f 를 수행 |
V computeIfAbsent (K key, Function<K, V> f) |
키가 없으면, 작업 f 수행 후 추가 | |
V computeIfPresent (K key, BiFunction<K, V, V> f) |
지정된 키가 있을 때, 작업 f 수행 | |
V merge (K key, Value v, BiFunction<V, V, V> f) |
모든 요소에 병합작업 f 를 수행 | |
void forEach (BiConsumer<K, V> action) |
모든 요소에 작업 action 을 수행 | |
void eplaceAll (BiConsumer<K, V, V> f) |
모든 요소에 치환작업 f 를 수행 |
기본형 타입을 사용하는 함수형 인터페이스
함수형 인터페이스 | 추상 메서드 | 설명 |
타입ATo타입BFunction | 타입A applyAs타입B (타입A 파라미터) | 입력이 타입A 로 들어오면 출력은 타입B. |
To타입AFunction<T> | 타입A aoplyAs타입A (T value) | 입력이 제네릭 타입이면 출력은 타입A |
타입AFunction<R> | R apply (T t, U u) | 입력이 타입A 면 출력은 제네릭 타입 |
Obj타입AConsumer<T> | void accept(T t, U u) | 입력이 제네릭과 타입 A 이고 출력 없음 |
지금까지 설명한 함수형 인터페이스는 파라미터와 반환값의 타입이 모두 제네릭 타입이였습니다.
제네릭 타입에는 int 나 double 같은 기본형 타입을 넣지 못하기 때문에 기본형 타입의 값읠 처리할 때도
래퍼 클래스를 사용해야 하는데요. 이런 비효율적인 문제를 위와 같은 인터페이스로 해결할 수 있습니다.
ex)
IntToLongFunction f = new IntToLongFunction() {
@Override
public long applyAsLong(int value) {
return (long) value;
}
};
long result = f.applyAsLong(100);
Function 의 합성과 Predicate 의 결합
java.util.function 패키지의 함수형 인터페이스에는 추상 메서드 외에도
디폴트 메서드와 static 메서드가 정의되어 있습니다.
Function |
default <V> Function<T, V> andThen (Function,? super R, ? extends V> after) |
default <V> Function<T, V> compose (Function,? super R, ? extends T> before) |
static <V> Function<T, T> identify () |
Predicate |
default Prdedicate<T> and (Predicate<? super T> other) |
default Prdedicate<T> or (Predicate<? super T> other) |
default Prdedicate negate () |
static <T> Prdedicate isEqual (Object targetRef) |
̱ Function 의 합성
수학에서 두 함수를 합성해서 하나의 새로운 함수를 만들어낼 수 있는 것처럼, 두 람다식을 합성해서 새로운 람다식을
만들 수 있습니다. 함수 f 와 g 가 있으면 f.andThen(g) 는 함수 f 를 먼저 적용하고, 그 다음에 함수 g 를 적용합니다.
f.compose(g) 는 f.andThen(g) 와 순서가 반대입니다.
Function<String, Integer> f = s -> Integer.parseInt(s, 16);
Function<Integer, String> g = i -> Integer.toBinaryString(i);
Function<String, String> h = f.andThen(g);
System.out.println(h.apply("FF"));//"FF" -> 255 -> "11111111"
예제입니다. 문자열을 16진수로 변환하는 함수 f 와 숫자를 문자로 변환하는 함수 g 를 andThen() 메서드로
합성하여 새로운 함수 h 를 만들었습니다. 함수 f 가 문자열을 파라미터로 받고 함수 g 가 문자열을 반환하므로
함수 h 의 제네릭 타입은 <String, String> 입니다. 즉 위 코드에서 함수 h 에 문자열 "FF" 를 입력하면 결과로
"11111111" 을 받습니다.
Function<String, String> f = x -> x;
//Function<String, String> f = Function.identity(); //위 코드와 동일
System.out.println(f.apply("A"));
identity() 메서드는 함수를 적용하기 이전과 이후가 동일한 '항등 함수' 가 필요할 때 사용합니다. 이 함수를
람다식으로 표현하면 x -> x 입니다. 위 항등함수에 "A" 를 파라미터로 주면 결과로 "A" 가 나옵니다.
Predicate 의 결합
여러 조건식을 논리 연산자인 &&(and), ||(or), !(not) 으로 연결해서 하나의 식을 구성할 수 있는 것처럼,
여러 Predicate 를 and(), or(), negate() 로 연결해서 하나의 새로운 Predicate 를 결합할 수 있습니다.
isEqual() 메서드는 두 대상을 비교하는 Predicate 를 만들 때 사용합니다. 먼저, isEqual() 파라미터로
비교대상을 하나 지정하고, 또 다른 비교대상은 test() 메서드 파라미터로 지정합니다.
Predicate<Integer> p = i -> i < 10;
Predicate<Integer> q = i -> i < 20;
Predicate<Integer> r = i -> i%2 == 0;
Predicate<Integer> notP = p.negate();
Predicate<Integer> all = notP.and(p.or(r));
System.out.println(all.test(15));//false
String str1 = "ab";
String str2 = "ab";
//비교 대상 먼저 지정
Predicate<String> p2 = Predicate.isEqual(str1);
//test() 로 비교할 값 지정
boolean result = p2.test(str2);
System.out.println(result);//true
메서드 참조
람다식으로 메서드를 간결하게 표현할 수 있는데, 이런 람다식을 더 간결하게 표현할 수 있는 방법이 있습니다.
항상 사용할 수 있는건 아니고, 람다식이 하나의 메서드만 호출하는 경우에 메서드 참조라는 방법으로 람다식을
간략히 할 수 있습니다.
MyFunction result = new MyFunction() {
@Override
public int max(int a, int b) {
return Math.max(a,b);
}
};
앞에서 작성한 MyFunction 를 구현한 익명 클래스 객체의 반환값을 삼항 연산자가 아닌
Math 클래스의 max() 메서드로 변경했습니다.
MyFunction result = (a, b) -> Math.max(a,b);
위 코드를 람다식으로 표현하면 이렇게 작성할 수 있고
MyFunction result = Math::max;
메서드 참조를 이용하면 현재 코드처럼 더 간결하게 작성할 수 있습니다. 예제에서 람다식은단순히 Math 클래스의
max() 메소드로 인자값을 전달하는 역할만 하므로, 메소드 참조를 사용하여 다음과 같이 간단히 표현할 수 있습니다.
MyClass obj = new MyClass;
Function<String, Boolean> func = (a) -> obj.equals(a); // 람다 표현식
Function<String, Boolean> func = obj::equals(a); // 메소드 참조
메서드 참조를 사용할 수 있는 경우는 한 가지가 더 있는데요. 이미 생성된 객체의 메서드를 람다식에서 사용한
경우에는 클래스 이름 대신 그 객체의 참조변수를 적어줘야 합니다.
람다식을 메서드 종류에 따라 메서드 참조로 변환하는 표입니다.
종류 | 람다 | 메서드 참조 |
static 메서드 참조 | (x) -> ClassName.method(x) | ClassName:method |
인스턴스 메서드 참조 | (obj, x) -> obj.method(x) | ClassName:method |
특정 객체 인스턴스 메서드 참조 | (x) -> obj.method(x) | obj:method |
이처럼 하나의 메서드만 호출하는 람다식은 '클래스이름::메서드이름' 또는 '참조변수::메서드이름' 으로
바꿀 수 있습니다.
생성자의 메서드 참조
Supplier<Object> result = () -> new Object();
Supplier<Object> result = Object::new;
생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있는데요.
Supplier 는 입력 없이 객체를 생성해주는 함수입니다. Function 같은 경우는 Function<입력 T, 출력 R> 이지만
Supplier 는 Supplier<출력 타입> 이렇게 입력 없이 출력만 있는 것입니다. 위 코드는 Object 클래스 객체를 생성하는
코드입니다.
이 포스팅은 자바의 정석의 내용을 참고하여 작성하였습니다.
'Java' 카테고리의 다른 글
[Java] 입출력 (1) | 2023.02.04 |
---|---|
[Java] 스트림(Stream) (1) | 2022.11.28 |
[Java] JVM 메모리 구조 (0) | 2022.10.25 |
[Java] this 와 this() super 와 super() (0) | 2022.10.25 |
[Java] 추상클래스(Abstarct Class) 인터페이스(Interface) (0) | 2022.10.25 |
댓글