목표
학습할 것 (필수)
- 람다식 사용법
- 함수형 인터페이스
- Variable Capture
- 메소드, 생성자 레퍼런스
람다식 사용법
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// (parameter) -> expression
numbers.forEach(number -> System.out.println(number));
- parameter : 람다 함수의 매개변수 목록을 정의함
->
: 람다식의 구분자
- expression : 람다 함수의 본문
람다식
- Java8에 도입된 람다식은 함수형 프로그래밍 개념을 Java에 도입한 중요한 기능임
- 익명 함수를 만들어 코드를 더 간결하고 유연하게 작성할 수 있게 해 주며 컬렉션 처리, 쓰레드 처리, GUI 이벤트 처리 등 다양한 상황에서 사용됨
- 장점
- 코드 간결성: 불필요한 코드의 중복을 줄이고 가독성을 향상시킴
- 익명 함수 지원: 메서드를 정의하지 않고도 함수의 로직을 전달할 수 있음
- 병렬 처리 간소화: 병렬 처리를 더 쉽게 수행할 수 있도록 도와줌
람다가 나온 이유
- Java의 기존 버전에서는 익명 내부 클래스를 사용해 함수를 전달할 수 있었지만, 코드가 불필요하게 복잡하고 길어지는 문제가 있었음
- 멀티코어 CPU의 보편화로 인해 병렬 처리가 중요해지면서 더 유연하고 간결한 방식으로 함수를 전달하고 다루는 것이 필요해짐
함수형 인터페이스
public class FunctionalInterfaceExample {
public static void main(String[] args) {
binaryOperatorExample();
myFunctionExample(6, 3);
}
private static void binaryOperatorExample() {
// java.util.function에 정의된 함수형 인터페이스 사용
BinaryOperator<Integer> add = (a, b) -> a + b;
int result = add.apply(3, 5);
System.out.println(result);
}
private static void myFunctionExample(int x, int y) {
// 커스텀 함수형 인터페이스 사용
MyFunction add = (a, b) -> a + b;
MyFunction minus = (a, b) -> a - b;
int result1 = add.calculate(x, y);
int result2 = minus.calculate(x, y);
System.out.println("x + y : " + result1);
System.out.println("x - y " + result2);
}
}
// 커스텀 함수형 인터페이스 정의
@FunctionalInterface
public interface MyFunction {
int calculate(int a, int b);
}
- 하나의 추상 메서드만을 가지고 있는 인터페이스
- 람다식은 이 추상 메서드를 구현하는 코드임
- java.util.fucntion 패키지에 다양한 함수 인터페이스들이 정의되어 있음
@FunctionalInterface
- Java8부터 도입된 애노테이션
- 함수형 인터페이스를 선언할 때 사용하는 메타애노테이션
- 이 애노테이션을 사용하면 컴파일러가 해당 인터페이스가 함수형 인터페이스임을 검사하고, 하나의 추상 메서드만을 가지고 있는지 확인하게 됨
- 검증: 이 애노테이션을 사용하면 인터페이스가 함수형 인터페이스의 조건을 만족하는지 컴파일러가 검증함
- 문서화: @FunctionalInterface 애노테이션은 코드의 문서화에도 도움을 주며 해당 인터페이스가 함수형 인터페이스임을 명시적으로 표현 가능
주요 함수형 인터페이스
List<String> names = List.of("Lim", "Yo", "Hwan");
Consumer<String> printName = (name) -> System.out.println(name); // name -> System.out.println(name)
names.forEach(printName);
Consumer<T>
: T 타입의 인자를 받아서 처리하는 함수 인터페이스
Supplier<String> getCurrentTime = () -> java.time.LocalTime.now().toString();
String currentTime = getCurrentTime.get();
System.out.println("현재 시간 : " + currentTime);
Supplier<T>
: 값을 반환하지만 인자를 받지 않는 함수 인터페이스
Function<String, Integer> parseToInt = (s) -> Integer.parseInt(s); // s -> Integer.parseInt(s), Integer::parseInt
int result = parseToInt.apply("42");
System.out.println("Parsed integer: " + result);
Function<T, R>
: T 타입을 받아서 R 타입을 반환하는 함수 인터페이스
Predicate<Integer> isGreaterThanZero = (i) -> i > 0;
boolean result1 = isGreaterThanZero.test(10);
boolean result2 = isGreaterThanZero.test(-1);
System.out.println("10은 0보다 큰가? " + result1);
System.out.println("-1은 0보다 큰가? " + result2);
Predicate<T>
: T 타입의 인자를 받아서 boolean 값을 반환하는 함수 인터페이스
Variable Capture(변수 포획)
람다 표현식은 외부 범위에 있는 변수를 캡처할 수 있습니다.
람다 표현식 내부에서 캡처된 변수의 값을 변경할 수 없지만, 값을 읽거나 사용할 수 있습니다.
이를 람다 표현식이 해당 변수를 "포획"한다고 표현합니다.
- 람다 표현식에서 외부 범위에 있는 변수를 사용하는 것
- 람다 표현식의 스코프와 람다 표현식이 변수를 어떻게 캡처하는지에 대한 개념을 알아야 함
- Java8 이전에는 익명 내부 클래스에서 외부 변수를 사용할 때 해당 변수는 명시적으로 final로 선언되어야 했음 -> 해당 변수가 람다나 익명 클래스 내부에서 변경되지 않음을 보장하기 위함
- Java8부터는 람다 표현식에서 변수를 캡처할 때 더 유연한 방식을 제공하며 람다 표현식을 사용할 때 코드의 가독성과 유연성을 향상시킴
- 변수 캡처에는
값
을 캡처하는 것과 참조
를 캡처하는 것 두 가지 유형이 있을 수 있으며 람다 표현식의 내부 구현 방식에 따라 달라짐
- 람다 표현식에서 변수를 캡처하는 것은 매우 강력한 기능이며 함수형 프로그래밍 패러다임을 구현하거나 다양한 컨텍스트에서 유용하게 활용 가능함
람다 표현식에서 변수를 캡처하는 방법
// Effective final 변수 캡처
int num1 = 10; // final이 붙어있지않지만 람다식 안에서 사실상 final로 취급됨
Consumer<Integer> effectiveFinalConsumer = (n) -> {
// num1은 effective final로 취급됨 (값 변경하지 않음)
// num1 += n; 컴파일 에러 발생
System.out.println("Effective final consumer: " + (num1 + n));
};
effectiveFinalConsumer.accept(5);
- Effective final 변수: 람다 표현식 내부에서 값이 변경되지 않는 변수를 캡처하는 것, 이 변수는 명시적으로 final로 선언되지 않아도 상수로 취급됨
메소드, 생성자 레퍼런스
- Java8 이후에 도입된 편리한 기능이며 람다 표현식을 간단하게 표현할 수 있는 방법을 제공하고 특정 메소드나 생성자를 람다 대신 사용 가능함
- 코드의 가독성을 향상시키고 코드를 더 간결하게 작성 가능하도록 함
- 컬렉션과 같은 자주 사용되는 구조에서 레퍼런스를 적극적으로 활용하면 좋음
메소드 레퍼런스
public static void main(String[] args) {
String[] names = {"lim", "yo", "hwan"};
// 기본적으로 Comparator 사용하는 방법
Arrays.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
// 람다식
Arrays.sort(names, (o1, o2) -> o1.compareTo(o2));
// 메서드 레퍼런스
// (this, o2) -> this.compareTo(o2) 이러한 형식으로 람다식이 매핑이 되었다고 생각하면 됨
Arrays.sort(names, String::compareTo);
}
- 람다 표현식으로 단순히 메소드 호출만 하는 경우에 사용
::
연산자를 사용하여 표현하고 메소드를 직접 호출하는 대신 람다 표현식 내에서 해당 메소드를 참조하는 방식을 사용함
생성자 레퍼런스
public static void main(String[] args) {
List<String> names = List.of("Lim", "Yo", "Hwan");
// 람다식
names.forEach(name -> new User(name));
// 생성자 레퍼런스
names.forEach(User::new);
}
static class User {
String name;
public User(String name) {
this.name = name;
}
}
- 객체를 생성하는 생성자를 대신 호출하는 데 사용
::
연산자를 사용하여 생성자를 참조하고 람다 표현식에서 객체 생성을 간단하게 나타냄
백기선님 팁
- 람다식은 익명내부클래스가 아님 -> 구현 자체가 다름
- 익명내부클래스는 컴파일 시 뒤에 $가 붙는 클래스 파일이 새로 생김 -> 람다식은 생기지 않음
- 람다식은 INVOKEDYNAMIC(indy)으로 구현되어 있음
- 내부적으로 자바의 버전이 올라갈 때 INVOKEDYNAMIC으로 구현되어 있으면 하위호환성을 유지하면서 개선할 수 있는 여지가 있음
- 익명내부클래스와 다르기 때문에 scope에서 오는 차이도 가지고 있음
- 자바 API에서 제공하는 함수형 인터페이스
- Function = 트랜스포머(변신 로봇) = 값을 변환하기 때문에 = 가장 범용적으로 사용되는 것
- Consumer = 스파르탄(Spartan) = 모든 걸 빼앗고 아무것도 내주지 마라 = void 리턴
- Predicate = 판사 = 참과 거짓으로 판단하기 때문에 = boolean 리턴
- Suppliers = 게으른 공급자 = 입력값이 존재하지 않는데 내가 원하는 것을 미리 준비하기 때문에 = 파라미터가 없음
- 메서드 레퍼런스
public class MethodReferenceExample {
public static void main(String[] args) {
String[] names = {"lim", "yo", "hwan"};
// 기본적으로 Comparator 사용하는 방법
Arrays.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
// 람다식
Arrays.sort(names, (o1, o2) -> o1.compareTo(o2));
// 메서드 레퍼런스
// (this, o2) -> this.compareTo(o2) 이러한 형식으로 람다식이 매핑이 되었다고 생각하면 됨
Arrays.sort(names, String::compareTo);
}
}
- 쉐도잉 = 기존변수가 익명내부클래스에서 가려지게 됨 하지만 람다는 scope이 같기 때문에 기존변수를 또 선언할 수 없음
public class ShadowingExample {
public static void main(String[] args) {
ShadowingExample shadowingExample = new ShadowingExample();
shadowingExample.testShadowing();
}
private void testShadowing() {
int baseNumber = 10;
// 1. 로컬 클래스
// testShadowing 내의 새로운 Scope임
class LocalClass {
void printBaseNumber() {
int baseNumber = 11;
System.out.println(baseNumber);
}
}
// 2. 익명 클래스
// testShadowing 내의 새로운 Scope임
Consumer<Integer> consumer = new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
int baseNumber = 11;
System.out.println(baseNumber);
}
};
// 3. 람다
// 람다는 scope이 같으므로 같은 이름의 변수를 선언할 수 없음
IntConsumer intConsumer = (i) -> {
// int baseNumber = 11; // Variable 'baseNumber' is already defined in the scope
System.out.println(i + baseNumber);
};
intConsumer.accept(10);
}
}
- 람다식의 타입과 형변환
- Java8에서 람다식을 지원하게 된 계기는 함수형 프로그래밍이 가능하도록 하기 위해서였음
- 함수자체가 first class(일급객체)가 되어야 함 -> 무언가가 first class라는 것은 변수에 할당할 수 있고 파라미터로 메서드에 전달할 수 있고 return으로 받을 수 있어야 함
- Java8 부터 함수형프로그래밍이 가능하다는 것은 함수를 변수에 선언하거나 함수를 메서드에 전달하거나 함수를 리턴받을 수 있음
- 지연연산 = 람다를 써서 비용이 비싼 연산이나 비용이 비싼 객체 생성 작업을 뒤쪽으로 미룰 수 있음, 자바스크립트의 콜백처럼 쓰는 느낌이 있음, 필요한 순간에 function을 호출함
- 자바8 병렬처리, 스트림과 람다는 관련되어 있음
- LambdaMethodFactory를 활용하여 Reflection 대체하기