남궁성님의 Java의 정석(3rd Edition)을 보고 정리한 글입니다.
1. 람다식(Lambda Expression)이란?
- java 8부터 람다식을 등장으로 Java는 객체지향 언어의 특징과 함수형 언어 특징을 함께 갖추게 되었다.
- 함수(메서드)를 간단한 식(Expression)으로 표현하는 방법
- 익명함수(anonymous function)이라고도 한다.
2. 람다식 사용 방법
(매개변수 선언) -> {
// 문장들
}
// 메서드 이름과 반환타입을 제거하고 '->'를 블록앞에 추가한다.
(int a, int b) -> {
return a > b ? a: b;
}
// 반환값이 있는 경우, 식이나 값만 적고 return문 생략 가능 (끝에 ';' 안 붙임)
(int a, int b) -> a > b ? a : b
// 매개변수의 타입이 추론 가능하면 생략가능(대부분의 경우 생략가능)
(a, b) -> a > b ? a: b
// 매개변수가 하나인 경우, 괄호() 생략가능(타입이 없을때만)
a -> a * a // OK.
int a -> a * a // error.
// 블록안의 문장이 하나뿐 일 때, 괄호{} 생략가능(끝에 ';' 안 붙임)
(int i) -> System.out.println(i)
//단 하나뿐인 문장이 return 문이면 괄호 생략불가
(int a, int b) -> {return a > b ? a : b; } // OK.
(int a, int b) -> return a ? b ? a : b // error.
(int a, int b) -> a > b ? a : b
new Object() {
int max(int a, int b) {
return a > b ? a : b'
}
}
Java에서는 모든 메서드는 클래스 내에 포함되어야 하기 때문에 사실 람다식은 익명 객체와 동일하다.
하지만 Object 타입으로 람다를 다루기 위해서는 람다 관련 기능을 Object 클래스에 넣어야 하는 부담이 있었다. Java에서는 함수형 인터페이스를 사용하여 람다식을 다룰 수 있다.
3. 함수형 인터페이스(Functional Interface)
- 함수형 인터페이스는 단 하나의 추상 메서드만 선언된 인터페이스이다. (default 메서드는 포함 가능)
- 함수형 인터페이스 타입의 참조변수로 람다식을 참조할 수 있다.
- 함수형 인터페이스의 메서드와 람다식의 매개변수 개수와 반환타입이 일치해야 함.
함수형 인터페이스 예시
@FunctionalInterface
interface MyFunction {
public abstract int max(int a, int b); // public abstract 생략 가능
// public abstract int min(int a, int b); // 컴파일 에러
}
- @FunctionalInterface는 컴파일러가 함수형 인터페이스 임을 검증한다.
- 그로 인해 단 하나의 추상 메서드만 선언할 수 있게 된다.
함수형 인터페이스의 max() 재정의
public static void main(String[] args) {
MyFunction f = new MyFunction() {
@Override
public int max(int a, int b) {
return a > b ? a : b;
}
};
System.out.println(f.max(3, 5)); // 5
}
max() 람다식 활용하여 재정의
public static void main(String[] args) {
MyFunction f = (a, b) -> a > b ? a : b;
System.out.println(f.max(3, 5)); // 5
}
함수형 인터페이스를 다루는 여러 방식
@FunctionalInterface
interface MyFunction {
void run();
}
public class Test {
static void execute(MyFunction f) { // 매개변수 타입이 MyFunction
f.run();
}
static MyFunction getMyFunction() { // 반환 타입이 MyFunction
MyFunction f = () -> System.out.println("f3.run()");
return f;
}
public static void main(String[] args) {
// 람다식으로 MyFunction의 run() 구현
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()"));
}
}
실행결과
f1.run()
f2.run()
f3.run()
f1.run()
run()
4. java.util.function 패키지
자주 사용되는 함수형 인터페이스들을 ‘java.util.function’ 패키지에 미리 정의해놨다.
a. 가장 기본적인 함수형 인터페이스
Runnable (void run())
- 매개변수도 없고, 반환값도 없음
Runnable runnable = () -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Count: " + i);
}
};
Thread thread = new Thread(runnable);
thread.start();
실행결과
Count: 1
Count: 2
Count: 3
Count: 4
Count: 5
Supplier (T get())
- 매개변수는 없고, 반환값만 있음
Supplier<String> supplier = () -> "Hello, World!";
String result = supplier.get();
System.out.println(result); // Hello, World!
Consumer (void accept(T t))
- 매개변수만 있고 반환값이 없음
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Consumer<String> printName = (name) -> System.out.println("Hello, " + name);
names.forEach(printName);
**실행결과**
Hello, Alice
Hello, Bob
Hello, Charlie
Function (R apply(T t))
- 하나의 매개변수를 받아서 결과를 반환
Function<Integer, Integer> square = (x) -> x * x;
int result = square.apply(5);
System.out.println("Square of 5 is " + result); // Square of 5 is 25
Predicate (boolean test(T t))
- 조건식을 표현하는데 사용. 매개변수는 하나. 반환타입은 boolean
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Predicate<Integer> isEven = (num) -> num % 2 == 0;
List<Integer> evenNumbers = numbers.stream().filter(isEven).collect(Collectors.toList());
System.out.println("Even numbers: " + evenNumbers; // Even numbers: [2, 4, 6, 8]
b. 파라미터가 두 개인 함수형 인터페이스
BiConsumer (void accept(T t, U u))
- 두개의 매개변수만 있고, 반환값이 없음
BiConsumer<String, Integer> printNameAndAge = (name, age) -> {
System.out.println("Name: " + name);
System.out.println("Age: " + age);
};
printNameAndAge.accept("Alice", 25);
실행결과
Name: Alice
Age: 25
BiPredicate (boolean test(T t, U u))
- 조건식을 표현하는데 사용됨. 매개변수는 둘 반환값은 boolean
BiPredicate<Integer, Integer> isSumEven = (a, b) -> (a + b) % 2 == 0;
boolean result = isSumEven.test(3, 5);
System.out.println("Is the sum of 3 and 5 even? " + result);
실행결과
Is the sum of 3 and 5 even? true
BiFunction (R apply(T t, U u))
- 두 개의 매개변수를 받아서 하나의 결과를 반환
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
int sum = add.apply(3, 5);
System.out.println("3 + 5 = " + sum);
실행결과
3 + 5 = 8
c. 하나의 파라미터를 받고 동일한 타입을 리턴하는 함수형 인터페이스
UnaryOperator (T apply(T t))
- Function의 자손, Function과 달리 매개변수와 결과의 타입이 같다.
UnaryOperator<Integer> square = (x) -> x * x;
int result = square.apply(5);
System.out.println("Square of 5 is " + result);
실행결과
Square of 5 is 25
BinaryOperator (T apply(T t1, T t2))
- BiFunction의 자손, BiFunction과 달리 매개변수와 결과의 타입이 같다
BinaryOperator<Integer> addition = (a, b) -> a + b;
int sum = addition.apply(3, 5);
System.out.println("3 + 5 = " + sum);
실행결과
3 + 5 = 8
d. 컬렉션과 함께 사용하는 함수형 인터페이스
Collection: boolean removeIf(Predicate<E> filter)
- 조건에 맞는 요소를 삭제
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
boolean removed = numbers.removeIf(n -> n % 2 == 0);
System.out.println("Removed even numbers: " + removed);
System.out.println("Remaining numbers: " + numbers);
실행결과
Removed even numbers: true
Remaining numbers: [1, 3, 5]
List: void replaceAll(UnaryOperator<E> operator)
- 모든 요소를 변환하여 대체
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
numbers.replaceAll(n -> n * 2);
System.out.println("Doubled numbers: " + numbers);
실행결과
Doubled numbers: [2, 4, 6, 8, 10]
Iterable: void forEach(Consumer<T> action)
- 모든 요소에 작업 action을 수행
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println("Hello, " + name));
실행결과
Hello, Alice
Hello, Bob
Hello, Charlie
Map: V compute(K key, BiFunction<K, V, V> f)
- 지정된 키의 값에 작업 f를 수행
Map<String, Integer> ageMap = new HashMap<>();
ageMap.put("Alice", 25);
ageMap.compute("Alice", (name, age) -> age + 1);
System.out.println("Alice's new age: " + ageMap.get("Alice"));
실행결과
Alice's new age: 26
Map: V computeIfAbsent(K key, Function<K, V> f)
- 키가 없으면, 작업 f 수행 후 추가
Map<String, Integer> ageMap = new HashMap<>();
ageMap.computeIfAbsent("Alice", key -> 30);
System.out.println("Alice's age: " + ageMap.get("Alice"));
실행결과
Alice's age: 30
Map: V computeIfPresent(K key, BiFunction<K, V, V> f)
- 지정된 키가 있을 때, 작업 f를 수행
Map<String, Integer> ageMap = new HashMap<>();
ageMap.put("Alice", 25);
ageMap.computeIfPresent("Alice", (name, age) -> age + 1);
System.out.println("Alice's new age: " + ageMap.get("Alice"));
실행결과
Alice's new age: 26
Map: V merge(K key, V value, BiFunction<V, V, V> f)
- 모든 요소에 병합작업 f를 수행
Map<String, Integer> ageMap = new HashMap<>();
ageMap.put("Alice", 25);
ageMap.merge("Alice", 5, (currentAge, additionalAge) -> currentAge + additionalAge);
System.out.println("Alice's age after merging: " + ageMap.get("Alice"));
실행결과
Alice's age after merging: 30
Map: void forEach(BiConsumer<K, V> action)
- 모든 요소에 작업 action을 수행
Map<String, Integer> ageMap = new HashMap<>();
ageMap.put("Alice", 25);
ageMap.put("Bob", 30);
ageMap.forEach((name, age) -> System.out.println(name + "'s age is " + age));
실행결과
Bob's age is 30
Alice's age is 25
Map: void replaceAll(BiFunction<K, V, V> f)
- 모든 요소에 치환작업 f를 수행
Map<String, Integer> ageMap = new HashMap<>();
ageMap.put("Alice", 25);
ageMap.put("Bob", 30);
ageMap.replaceAll((name, age) -> age + 1);
ageMap.forEach((name, age) -> System.out.println(name + "'s new age is " + age));
실행결과
Bob's new age is 31
Alice's new age is 26
5. Function의 합성
수학에서 두 함수를 합성해서 하나의 함수를 만들어낼 수 있는 것처럼, 두 람다식을 합성해서 새로운 람다식을 만들 수 있다.
a. andThen()
- f, g를 합성하여 새로운 h를 만든다.
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")); // 11111111
"FF" -> 255 -> "11111111"
b. compose()
- f, g 두 함수를 반대로 합성한다.
Function<Integer, String> g = (i) -> Integer.toBinaryString(i);
Function<String, Integer> f = (s) -> Integer.parseInt(s, 16);
Function<Integer, Integer> h = f.compose(g);
System.out.println(h.apply(2)); // 16
2 -> "10" -> 16
6. Predicate의 결합
and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있다.
Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i % 2 == 0;
Predicate<Integer> notP = p.negate(); // i >= 100
// 100 <= i && ( i < 200 || i % 2 == 0 )
Predicate<Integer> all = notP.and(q.or(r));
System.out.println(all.test(150)); // true
System.out.println(all.test(40)); // false
isEqual()은 두 대상을 비교하는 Predicate를 만들 때 사용한다.
String str1 = "abc";
String str2 = "abc";
Predicate<String> p = Predicate.isEqual(str1);
Boolean result = p.test(str2); // true
7. 메서드 참조
- 하나의 메서드만 호출하는 람다식은 ‘메서드 참조’로 간단히 할 수 있다.
종류 | 람다식 | 메서드 참조 |
static메서드 참조 | (x) → ClassName.method(x) | ClassName::method |
매개변수의 인스턴스 메서드 참조 | (x) → obj.method(x) | obj.method |
매개변수의 클래스 메서드 참조 | (x) → ClassName.method(x) | ClassName::method |
생성자 메서드 참조 | (x) → new ClassName() | (x) → ClassName::new |
a. static 메서드 참조
public class Test {
public static void main(String[] args) {
// 람다식
MyFunction f1 = ((x, y) -> MyClass.addElement(x, y));
System.out.println(f1.add(3, 5)); // 8
// static 메서드 참조
MyFunction f2 = MyClass::addElement;
System.out.println(f2.add(3, 5)); // 8
}
}
class MyClass {
public static int addElement(int x, int y) {
return x + y;
}
}
@FunctionalInterface
interface MyFunction {
int add(int x, int y);
}
b. 매개변수(인스턴스) 메서드 참조
public class Test {
public static void main(String[] args) {
MyClass myClass = new MyClass();
// 람다식
MyFunction f1 = ((x, y) -> myClass.addElement(x, y));
System.out.println(f1.add(3, 5)); // 8
// 객체 인스턴스 메서드 참
MyFunction f2 = myClass::addElement;
System.out.println(f2.add(3, 5)); // 8
}
}
class MyClass {
public int addElement(int x, int y) {
return x + y;
}
}
@FunctionalInterface
interface MyFunction {
int add(int x, int y);
}
c. 매개변수의 클래스 메서드 참조
- max(x, y) 메서드의 클래스가 Integer이기 때문에 Integer::max로 메서드 참조를 했다.
public class Test {
public static void main(String[] args) {
// 람다식
BiFunction<Integer, Integer, Integer> f1 = ((x, y) -> Integer.max(x, y));
System.out.println(f1.apply(3, 6)); // 6
// 람다식
BiFunction<Integer, Integer, Integer> f2 = Integer::max;
System.out.println(f1.apply(3, 6)); // 6
}
}
d. 생성자 메서드 참조
// 기본 생성자
Supplier<MyClass> s1 = () -> new MyClass(); // 람다식
Supplier<MyClass> s2 = MyClass::new; // 생성자 메서드 참조
// 매개변수가 있는 생성자
Function<Integer, MyClass> f1 = (i) -> new MyClass(i); // 람다식
Function<Integer, MyClass> f2 = MyClass::new; // 생성자 메서드 참조
// 배열 생성
Function<Integer, int[]> a1 = x -> new int[x]; // 람다식
Function<Integer, int[]> a2 = int[]::new; // 생성자 메서드 참조
'Programming > Java' 카테고리의 다른 글
[Java] DI 프레임워크 Google Guice 정리 (0) | 2023.11.03 |
---|---|
[Java] 네트워크 프로그래밍 - 1(IP, URL, URLConnection) (0) | 2023.11.03 |
[Java] 쓰레드(Thread) - 1(쓰레드 구현 방법) (0) | 2023.11.03 |
[Java] 컬렉션 프레임워크와 계층 구조 (0) | 2023.11.03 |
[Java] .java(소스파일) vs. .class(바이트코드파일) vs. .jar (0) | 2023.10.20 |