[자바의 정석] Ch.14 람다와 스트림
1. 람다식(Lambda expression)
1.1 람다식이란
정수 n을 매개변수로 받아 길이 n의 배열을 만들고 1부터 5까지의 숫자를 무작위로 배열에 넣는 메소드를 만들어보자.
int N = scan.nextInt();
int[] arr = new int[N];
public void maekArray(int N){
for(int i=0; i<arr.length; i++)
arr[i] = (int)(Math.random() * 5) + 1;
}
N길이의 배열을 선언하고 값을 넣는 비교적 단순한 메소드라 서 너줄 밖에 되지 않지만 더 복잡한 메소드를 작성한다 생각하면 더 복잡해질 것이다. 람다식을 공부하면 아래와 같이 간단하고 명확하게 함수를 작성할 수 있다.
int N = scan.nextInt()
int[] arr = new int[N];
Arrays.setAll(arrWithLambda, (i) -> (int)(Math.random() * 5) + 1);
람다식(Lambda expression)은 메서드를 하나의 식으로 표현하여 함수를 간략하고 명확한 식으로 표현할 수 있게 해준다. 람다식은 익명함수(anonymous function)라고도 한다. 람다식을 이용하면 메서드를 사용하기 위해 클래스와 객체를 만들어야하는 번거로움도 없고 메서드의 이름과 반환값이 없어지기 때문이다. 람다식은 그 자체로 매개변수로 전달될 수 있고 결과로 반환될 수 있다.
1.2 람다식 작성하기
람다식은 기존 메소드에서 이름과 반환타입을 제거하고 매개변수 선언부와 { } 괄호 사이에 ->를 추가하여 () -> { }의 형태로 만든다.
최대값을 구하는 max메소드를 람다식으로 바꾸면 반환타입 int와 메소드 이름인 max 는 지우고 매개변수와 본문 괄호 사이에 ->를 추가한다. 반환값이 있는 메소드는 식의 연산결과가 자동으로 반환값이 되어 return문 대신 식으로 대체할 수 있다. 매개변수의 타입이 추론 가능한 경우 생략 가능하다. 반환타입이 없는 이유도 추론이 가능하기 때문이다.
매개변수가 하나일 때는 괄호( )도 생략할 수 있다. 매개변수의 타입이 없는 경우에만 가능하다. 괄호 { } 안 문장도 한 줄인 경우 괄호{ }를 생략할 수 있다. 하지만 return문인 경우 생략할 수 없다.
람다식은 메소드의 메소드로도 쓸 수 있다. Arrays.setAll뿐만 아니라 Comparator 인터페이스에서 새로운 Comparator 인터페이스의 메서드를 선언하고 그 내부에서 정렬하는 기준을 정할 수 있다.
메서드 | 람다식 |
int max(int a, int b){ return a>b ? a : b; } |
(int a, int b) -> { return a > b ? a: b; } |
(int a, int b) -> a > b ? a : b | |
( a, b ) -> a > b ? a : b | |
void printVar(String name, int i){ System.out.println( name + " = " + i ); } |
(String name, int i) -> { System.out.println( name + " = " + i ); } |
(name, i) -> { System.out.println( name + " = " + i ); } | |
(name, i) -> System.out.println( name + " = " + i ) | |
int square(int x) { return x * x; } |
(int x) -> x * x |
(x) -> x * x | |
x -> x * x | |
int roll(){ return (int) (Math.random() * 6); } |
() -> { return (int) (Math.random() * 6); } |
() -> (int)(Math.random() * 6) | |
int sumArr(int[] arr){ int sum = 0; for(int i : arr) sum += i; return sum } |
(int[] arr) -> { int sum = 0; for(int i : arr) sum+= i; return sum; } |
1.3 함수형 인터페이스(Functional Interface)
자바에서 모든 메서드는 클래스 내에 포함되어 있어야 한다. 람다식을 메소드와 비교했지만 람다식은 익명 클래스의 객체와 동등하다. 람다식으로 정의된 익명 객체의 메소드를 호출하기 위해 익명 객체를 참조변수 anony에 저장한다면 anony의 타입은 클래스와 인터페이스 중 하나이다. 그리고 동일한 메서드가 정의되어 있어야 한다. 그 중 인터페이스를 통해 람다식을 다루기로 결정되었고, 람다식을 다루기 위한 인터페이스를 함수형 인터페이스(Functional interaface)라고 부른다. 함수형 인터페이스는 람다식과 인터페이스의 메서드를 1:1로 연결하기 위해 오직 하나의 추상 메서드만 정의되어야 한다. static 메서드와 defaul메서드의 개수는 제약이 없다. 덕분에 인터페이스의 메서드를 구현하는 것이 간단해졌다.
위에서
https://www.acmicpc.net/problem/1181
1181번: 단어 정렬
첫째 줄에 단어의 개수 N이 주어진다. (1 ≤ N ≤ 20,000) 둘째 줄부터 N개의 줄에 걸쳐 알파벳 소문자로 이루어진 단어가 한 줄에 하나씩 주어진다. 주어지는 문자열의 길이는 50을 넘지 않는다.
www.acmicpc.net
위 문제에서 Comparator 인터페이스의 메서드를 구현할 때, 람다식을 사용하면 좀 더 간단하게 구현할 수 있다. 단어의 길이에 따라 먼저 정렬하고, 길이가 같다면 사전처럼 알파벳 순으로 정렬하는 문제이다. 거의 비슷한 것 같아보여 머쓱하지만 그래도 Comparator를 선언하지 않고 바로 사용하는 람다식이 좀 더 간단해보이지 않나요...?
//인터페이스의 메서드 구현
Arrays.sort(input, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
if(o1.length() == o2.length())
return o1.compareTo(o2);
else
return o1.length() - o2.length();
}
});
//람다식을 이용
Arrays.sort(input, (o1, o2) -> {
if(o1.length() == o2.length())
return o1.compareTo(o2);
else
return o1.length() - o2.length();
});
함수형 인터페이스 타입의 매개변수와 반환타입
메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정할 수 있고 참조변수 없이 람다식을 직접 매개변수로 지정할 수 있다. 람다식을 참조변수로 다룰 수 있는 것은 메서드를 통해 람다식을 주고 받을 수 있다는 것이고 이는 변수처럼 메서드를 주고받는 것이 가능하다는 것이다.
@FunctionalInterface
interface MyFunction{
void run();
}
public class LambdaEx {
static void execute(MyFunction f){
f.run();
}
static MyFunction getMyFunction(){
MyFunction f = () -> System.out.println("f3.run()");
return f;
}
public static void main(String[] args) {
MyFunction f1 = () -> System.out.println("f1.run()");
MyFunction f2 = new MyFunction(){
@Override
public void run() {
System.out.println("f2.run()");
}
};
MyFunction f3 = getMyFunction();
f1.run();
f2.run();
f3.run();
execute(f1);
execute( ()-> System.out.println("run()") );
}
}
람다식의 타입과 형변환
함수형 인터페이스로 람다식을 참조할 수 있는 것이지 람다식의 타입이 함수형 인터페이스와 일치하는 것은 아니다. 람다식은 익명 객체이고 익명 객체는 타입이 없다. Object 타입으로 형변환할 수도 없어 대입 연산자의 양변의 타입을 일치시키기 위해 형변환이 필요하다.
@FunctionalInterface
interface MyFunction{
void myMethod();
}
public class LambdaEx {
public static void main(String[] args) {
MyFunction f = () -> {};
Object obj = (MyFunction)(()->{});
String str = ((Object)(MyFunction)(()->{})).toString();
System.out.println(f);
System.out.println(obj);
System.out.println(str);
//System.out.println(() -> {}); no suitable method found for println(()->{ })
System.out.println((MyFunction)(()->{}));
//System.out.println((MyFunction)(()->{}).toString()); lambda expression not expected here
System.out.println(((Object)(MyFunction)(()->{})).toString());
}
}
외부 변수를 참조하는 람다식
람다식도 익명 클래스의 인스턴스이므로 람다식에서 외부에 선언된 변수에 접근하는 규칙은 익명 클래스와 동일하다. 람다식 내에서 참조하는 지역변수는 상수로 간주되어 값을 변경할 수 없고 외부 지역변수와 같은 이름의 람다식 매개변수는 허용하지 않는다.
@FunctionalInterface
interface MyFunction{
void myMethod();
}
class Outer{
int val = 10;
class Inner{
int val = 20;
void method(int i) {
int val = 30;
i = 10;
MyFunction f = () -> {
//System.out.println("i : " + i); local variables referenced from a lambda expression must be final or effectively final
System.out.println("val : " + val);
System.out.println("this.val : " + ++this.val);
System.out.println("Outer.this.val : " + ++Outer.this.val);
};
f.myMethod();
}
}
}
public class LambdaEx {
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.method(100);
}
}
1.4 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 |
조건식의 표현에 사용되는 Predicate
Predicate는 조건식을 람다식으로 표현하는데 사용한다.
매개변수가 두 개인 함수형 인터페이스
매개변수의 개수가 2개인 함수형 인터페이스는 이름 앞에 접두사 'Bi'가 붙는다.
함수형 인터페이스 | 메서드 | 설명 |
BiConsumer<T, U> | void accept(T t, U u) | 두 개의 매개변수만 있고 반환값이 없음 |
BiPredicate<T, U> | boolean test(T t, U u) | 조건식을 표현하는데 사용됨 매개변수는 둘, 반환값은 boolean |
BiFunction<T, U, R> | R apply(T t, U u) | 두 개의 매개변수를 받아서 하나의 결과를 반환 |
UnaryOperator와 BinaryOperator
Function의 변형으로 UnaryOperator와 BinaryOperator가 있다. 매개변수의 타입과 반환타입의 타입이 모두 일치한다는 점을 제외하고 Function과 같다.
함수형 인터페이스 | 메서드 | 설명 |
UnaryOperator<T> | T apply(T t) | Function의 자손. Function과 달리 매개변수와 결과의 타입이 같다. |
BinaryOperator<T> | 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 computeAbsent(K key, Function<K, V> f) | 키가 없으면 작업 f 수행 후 추가 | |
V computeIfPresent(K key, BiFunction<K, V, V> f) | 지정된 키가 있을 때 작업 f 수행 | |
V merge(K key, V value, BiFunction<K, V, V> f) | 모든 요소에 병합작업 f 를 수행 | |
void forEach(BiConsumer<K, V> action | 모든 요소에 작업 action을 수행 | |
void replaceAll(BiFunction<K, V, V> f) | 모든 요소에 치환작업 f 를 수행 |
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class LambdaEx {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
for(int i=0; i<10; i++)
list.add(i);
list.forEach( i -> System.out.print(i + ", "));
System.out.println();
list.removeIf( x -> x % 2 == 0 || x % 3 == 0);
System.out.println(list);
list.replaceAll( x -> x * 10 );
System.out.println(list);
Map<String, String> map = new HashMap<>();
map.put("1", "1");
map.put("2", "2");
map.put("3", "3");
map.put("4", "4");
map.forEach( (k,v) -> System.out.print("{" + k + "," + v + "}"));
System.out.println();
}
}
기본형을 사용하는 함수형 인터페이스
위에 나온 함수형 인터페이스는 매개변수와 반환값의 타입이 모두 지네릭타입이다. 지네릭 타입은 래퍼(wrapper)클래스를 이용하여 기본형 타입의 값을 처리한다. 효율적으로 처리할 수 있도록 기본형을 사용하는 함수형 인터페이스가 있다. 매개변수의 타입과 반환타입이 일치할 때는 Function 대신 UnaryOperator를 이용하는 것이 좋다.
함수형 인터페이스 | 메서드 | 설명 |
DoubleToIntFunction | int applyAsInt(double d) | AToBFunction은 입력이 A타입 출력이 B타입 |
ToIntFunction<T> | int applyAsInt(T value) | ToBFunction은 입력은 지네릭 타입 출력은 B타입 |
IntFunction<R> | R apply(T t, U u) | AFunction은 입력이 A타입이고 출력은 지네릭 타입 |
ObjIntConsumer<T> | void accept(T t, U u) | ObjAFunction은 입력이 T, A타입이고 출력은 없음 |
1.5 Function의 합성과 Predicate의 결합
Function
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
default <V> Function<V, R> compose(Function<? super V, ? extends V> before)
static <T> Function<T, T> identity()
Predicate
default Predicate<T> and(Predicate<? super T> other)
default Predicate<T> or(Predicate<? Super T> other)
default Predicate<T> negate()
static <T> Predicate<T> isEqual(Object targetRef)
Function의 합성
두 람다식을 합성해서 새로운 람다식을 만들 수 있다. f와 g라는 함수 있다을 때 f.andThen(g)는 f를 적용한 다음 g를 적용하고 f.compose(g)는g를 적용한 다음 f를 적용한다. Identity()는 함수를 적용하기 전과 후가 동일한 항등함수가 필요할 때 사용한다. 람다식으로 x -> x이다. 주로 map()으로 변환작업할 때, 변환 없이 그대로 처리할 때 사용한다.
Predicate의 결합
여러 조건식을 &&(and), ||(or), !(not) 연산자로 연결할 수 있듯 Predicate도 and(), or(), negate()로 연결해서 사용할 수 있다.
import java.util.function.Function;
import java.util.function.Predicate;
public class LambdaEx {
public static void main(String[] args) {
Function<String, Integer> f = (s) -> Integer.parseInt(s, 16);
Function<Integer, String> g = (i) -> Integer.toBinaryString(i);
Function<String, String> h = f.andThen(g);
Function<Integer, Integer> h2 = f.compose(g);
System.out.println(h.apply("FF"));
System.out.println(h2.apply(2));
Function<String, String> f2 = x -> x;
System.out.println(f2.apply("AAA"));
Predicate<Integer> p = i -> i< 100;
Predicate<Integer> q = i -> i< 200;
Predicate<Integer> r = i -> i % 2 == 0;
Predicate<Integer> notP = p.negate();
Predicate<Integer> all = notP.and(q.or(r));
System.out.println(all.test(150));
String str1 = "abc";
String str2 = "abc";
Predicate<String> p2 = Predicate.isEqual(str1);
boolean result = p2.test(str2);
System.out.println(result);
}
}
1.6 메서드 참조
람다식이 하나의 메서드만 호출하는 경우에는 '메서드 참조(method reference)'라는 방법으로 이미 간단한 람다식을 더 간결하게 표현할 수 있다. 이미 생성된 메서드가 람다식을 사용한 경우에는 클래스 이름 대신 객체의 참조변수를 적어줘야 한다.
종류 | 람다 | 메서드 참조 |
static 메서드 참조 | (x) -> ClassName.method(x) | ClassName::method |
인스턴스메서드 참조 | (obj.x) -> obj.method(x) | ClassName::method |
특정 객체 인스턴스메서드 참조 | (x) -> obj.method(x) | obj::method |
생성자의 메서드 참조
생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있다. 매개변수가 있는 생성자도 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하면 된다. 메서드 참조는 람다식을 static 변수처럼 다룰 수 있도록 도와주고 코드를 간략하게 할 수 있어 유용하다.