
람다 표현식이란?
기본적으로 익명 함수(anonymous function)를 말합니다.
람다는 메서드를 단순화하고 재사용성을 높이는데 도움을 줍니다.
특히, 람다는 기존에 인터페이스 구현을 위해 필요한 장황한 코드를 생략하고, 단일 메서드를 직접 정의할 수 있게 합니다.
람다가 필요한 이유
java 8 이전엔 인터페이스의 구현체는 보통 익명 클래스를 사용하여 작성하였습니다. 익명클래스는 코드가 길어질수록 중복되는 코드가 늘고, 가독성이 떨어집니다. 이런 문제를 해결하기 위해 람다가 등장했습니다.
//8버전 이전의 Runnable
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello, Lambda!");
}
};
//lambda
Runnable runnable = () -> System.out.println("Hello, Lambda!");
이처럼 람다 표현식은 가독성을 높이고, 간결한 코드를 제공합니다.
람다의 기본 문법
//매개변수 -> 실행될 블록
(parameters) -> expression
(parameters) -> { statements; }
//++자바 컴파일러는 매개변수의 타입을 추론할 수 있기 때문에, 타입을 생략할 수도 있습니다.
//++ 블록이 여러줄이라면 중괄호를 사용해야 합니다.
//ex 두 수를 더하는 람다
(a, b) -> a + b
//짝수를 출력하는 forEach를 사용한 람다 예제
public class LambdaExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.forEach(n -> {
if (n % 2 == 0) {
System.out.println(n);
}
});
}
}
위와 같이 람다 표현식은 코드의 목적을 더 직접적으로 표현해줍니다.
메서드 이름, 반환 타입, 매개변수 타입 등을 반복적으로 작성하는 것을 추상화하여 코드의 의도를 직관적으로 보여줍니다.
자바의 함수형 인터페이스(Function Interfaces)란?
함수형 인터페이스는 단 하나의 추상메서드만을 가지는 인터페이스를 뜻합니다.
이 인터페이스는 람다 표현식을 사용하여 메서드를 구현할 수 있습니다. 자바에선 이를 나타내기 위해 @FunctionalInterface 어노테이션을 사용할 수 있으며, 이는 명시적으로 함수형 인터페이스임을 나타내는데 유용합니다.
@FunctionalInterface
public interface MyFunctionalInterface {
void doSomething();
}
//람다로 구현
MyFunctionalInterface myFunc = () -> System.out.println("Doing something!");
myFunc.doSomething();
@FunctionalInterface 어노테이션
@FunctionalInterface는 컴파일러가 해당 인터페이스가 함수형 인터페이스임을 보장하도록 도와줍니다. 즉, 두 개 이상의 추상 메서드가 있을 경우 컴파일 에러가 발생합니다. 그러나 이 어노테이션을 생략해도 함수형 인터페이스는 여전히 람다로 사용할 수 있습니다.
자바의 대표적인 함수형 인터페이스
자바는 함수형 인터페이스를 제공한다. 이는 java.util.function 패키지에 정의되어 있다.
Supplier
입력 없이 값을 반환하는 함수형 인터페이스
메서드: T get()
Supplier<String> supplier = () -> "Hello, Supplier!";
System.out.println(supplier.get()); // 출력: Hello, Supplier!
주로 데이터를 지연 로딩하거나 특정 값이 필요할 때 사용하는 패턴에서 유용합니다.
Supplier<String> defaultSupplier = () -> "Default Value";
String result = Optional.ofNullable(null).orElseGet(defaultSupplier);
System.out.println(result); // 출력: Default Value
Consumer
입력을 받아 처리하고 결과를 반환하지 않는 함수형 인터페이스
메서드 : void accept(T t)
Consumer<String> consumer = message -> System.out.println("Message: " + message);
consumer.accept("Hello, Consumer!"); // 출력: Message: Hello, Consumer!
주로 특정 입력을 처리하는 작업을 정의할 때 사용됩니다.
List<String> messages = Arrays.asList("Hello", "Lambda", "Consumer");
messages.forEach(printer);
//출력
//Message: Hello
//Message: Lambda
//Message: Consumer
//내부로직
for (String message : messages) {
printer.accept(message);
}
Function<T, R>
입력을 받아 변환한 값을 반환하는 함수형 인터페이스
메서드: R apply(T t)
Function<Integer, String> function = number -> "Number is " + number;
System.out.println(function.apply(5)); // 출력: Number is 5
입력값을 처리하여 새로운 값을 반환해야 할 때 유용합니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<String> result = numbers.stream()
.map(numberToString)
.collect(Collectors.toList());
System.out.println(result); // 출력: [Number: 1, Number: 2, Number: 3, Number: 4, Number: 5]
-----------------------------
//연속적인 변환 작업
Function<Integer, Integer> multiplyByTwo = x -> x * 2;
Function<Integer, Integer> addTen = x -> x + 10;
Function<Integer, Integer> combinedFunction = multiplyByTwo.andThen(addTen);
System.out.println(combinedFunction.apply(5)); // 출력: 20
------------------------------
//조건에 따른 변환 작업
Function<Integer, String> evenToString = number -> {
if (number % 2 == 0) {
return "Even number: " + number;
} else {
return "Odd number";
}
};
System.out.println(evenToString.apply(4)); // 출력: Even number: 4
System.out.println(evenToString.apply(7)); // 출력: Odd number
Predicate
입력을 받아 논리적 테스트를 한 후, boolean 값을 반환하는 함수형 인터페이스
메서드: boolean test(T t)
Predicate<Integer> predicate = number -> number > 0;
System.out.println(predicate.test(10)); // 출력: true
필터링 조건을 정의할 때 사용됩니다.
List<Integer> numbers = Arrays.asList(-2, -1, 0, 1, 2);
List<Integer> positiveNumbers = numbers.stream()
.filter(isPositive)
.collect(Collectors.toList());
System.out.println(positiveNumbers); // 출력: [1, 2]
커스텀 함수형 인터페이스
내장 함수형 인터페이스를 사용하지 않고, 사용자 정의 함수형 인터페이스를 정의할 수 있습니다.
@FunctionalInterface
public interface Adder {
int add(int a, int b);
}
public class Main {
public static void main(String[] args) {
Adder adder = (a, b) -> a + b;
System.out.println(adder.add(10, 20)); // 출력: 30
}
}
Function 인터페이스의 장점
코드 간결화: Function 인터페이스를 사용하면 간단한 변환 작업을 훨씬 간결하게 표현할 수 있습니다. 복잡한 조건이나 여러 단계의 변환도 쉽게 처리할 수 있습니다.
재사용성: Function은 추상화된 변환 작업을 정의할 수 있기 때문에, 다양한 상황에서 재사용할 수 있습니다.
조합 가능성: Function은 andThen()과 같은 메서드를 통해 연속적인 변환 작업을 조합할 수 있어 복잡한 로직도 쉽게 관리할 수 있습니다.
예제: 데이터 처리 파이프라인
Function<String, String> toUpperCase = String::toUpperCase;
Function<String, String> addPrefix = s -> "User: " + s;
List<String> users = Arrays.asList("alice", "bob", "charlie");
List<String> result = users.stream()
.map(toUpperCase.andThen(addPrefix))
.collect(Collectors.toList());
System.out.println(result);
// 출력: [User: ALICE, User: BOB, User: CHARLIE]
람다 표현식과 메서드 참조 (Method Reference)
때로는 람다 표현식도 불필요하게 길어질 수 있습니다. 메서드 참조는 이러한 경우에 더욱 간결하고 직관적인 코드를 작성할 수 있도록 도와줍니다.
메서드 참조란?
기존의 메서드를 간단하게 참조하여 람다 표현식을 대체할 수 있는 기능입니다. 즉, 람다 표현식에서 이미 정의된 메서드나 생성자를 사용하는 경우, 메서드 참조로 바꾸어 더 간결한 코드를 작성할 수 있습니다.
//람다 표현식
Function<String, Integer> strToIntLambda = s -> Integer.parseInt(s);
System.out.println(strToIntLambda.apply("123")); // 출력: 123
//메서드 참조
Function<String, Integer> strToIntMethodRef = Integer::parseInt;
System.out.println(strToIntMethodRef.apply("123")); // 출력: 123
정적 메서드 참조
• 클래스 이름과 정적 메서드를 참조하는 형태입니다.
• 형식: ClassName::staticMethod
Function<String, Integer> strToInt = Integer::parseInt;
System.out.println(strToInt.apply("123")); // 출력: 123
---------
//정적 메서드를 참조하여 변환 작업을 수행할 수 있습니다.
Function<Double, Long> roundFunction = Math::round;
System.out.println(roundFunction.apply(4.7)); // 출력: 5
특정 객체의 인스턴스 메서드 참조
• 특정 객체의 메서드를 참조하는 형태입니다.
• 형식: instance::method
String message = "Hello, World!";
Supplier<Integer> lengthSupplier = message::length;
System.out.println(lengthSupplier.get()); // 출력: 13
-------------
//특정 객체의 인스턴스 메서드를 참조하여 작업을 수행할 수 있습니다.
String str = "Java";
Supplier<Integer> lengthSupplier = str::length;
System.out.println(lengthSupplier.get()); // 출력: 4
클래스의 임의 객체의 인스턴스 메서드 참조
• 클래스의 임의의 객체의 메서드를 참조하는 형태입니다.
• 형식: ClassName::method
Function<String, String> toUpperCase = String::toUpperCase;
System.out.println(toUpperCase.apply("hello")); // 출력: HELLO
생성자 참조
• 생성자를 참조하여 객체를 생성하는 형태입니다.
• 형식: ClassName::new
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get();
System.out.println(list); // 출력: []
--생성자를 참조하여 새로운 객체를 생성할 수 있습니다.
list.add("Hello");
System.out.println(list); // 출력: [Hello]
람다와 함수형 인터페이스의 실용적인 예제
//Predicate<String>을 사용하여 이름이 “A”로 시작하는 요소만 필터링
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
Predicate<String> startsWithA = name -> name.startsWith("A");
List<String> filteredNames = names.stream()
.filter(startsWithA)
.collect(Collectors.toList());
System.out.println(filteredNames); // 출력: [Alice]
//리스트의 문자열을 모두 대문자로 변환
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> upperCaseNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upperCaseNames); // 출력: [ALICE, BOB, CHARLIE]
3. 데이터 누적 처리 (Reduce)
BinaryOperator 인터페이스와 reduce() 메서드를 사용하면 리스트의 데이터를 누적 처리할 수 있습니다.
//리스트의 숫자들의 합을 구하는 코드
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum); // 출력: 15
4. 데이터 그룹화
Collectors.groupingBy()와 람다 표현식을 결합하여 리스트의 데이터를 특정 기준에 따라 그룹화할 수 있습니다.
//문자열의 길이를 기준으로 리스트의 문자열들을 그룹화하는 코드
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
Map<Integer, List<String>> groupedByLength = names.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println(groupedByLength);
// 출력: {3=[Bob], 5=[Alice, David], 7=[Charlie]}
5. 조건부 데이터 처리
Predicate를 사용하여 데이터가 조건을 만족할 때만 특정 작업을 수행할 수 있습니다.
//리스트에서 짝수만 출력하는 코드
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
Predicate<Integer> isEven = number -> number % 2 == 0;
numbers.stream()
.filter(isEven)
.forEach(System.out::println);
// 출력: 2, 4, 6
6. 복잡한 데이터 처리 파이프라인
람다 표현식과 함수형 인터페이스를 조합하여 복잡한 데이터 처리 파이프라인을 구축할 수도 있습니다.
//리스트에서 짝수만 필터링한 후, 각 숫자에 10을 더하고 결과를 출력하는 코드
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n + 10)
.forEach(System.out::println);
// 출력: 12, 14, 16
7. 비동기 처리와 람다
자바 8의 CompletableFuture와 람다 표현식을 결합하면 비동기 처리도 간결하게 구현할 수 있습니다.
//비동기적으로 데이터를 처리한 후 그 결과를 받아 작업을 수행하는 코드
CompletableFuture.supplyAsync(() -> "Hello, Asynchronous World!")
.thenAccept(System.out::println);
// 출력: Hello, Asynchronous World!