Java
Lambda Expression
snail voyager
2020. 9. 14. 22:56
728x90
반응형
람다식
- 함수형 프로그래밍을 구현하는 방식
- 클래스를 생성하지 않고 함수의 호출만으로 기능 수행
- 변수처럼 사용 (자료형 기반, 매개변수로 전달, 메서드의 반환값)
- 이름이 없는 함수 (Anonymous Function)
() -> {} //파라미터가 없으며 void를 반환
() -> "Text" //파라미터가 없으며 문자열을 반환
() -> {return "Text";} //파라미터가 없으며 문자열을 반환
(Integer i) -> {return "Text" + i;} //파라미터가 있으며 문자열을 반환
(List<String> list) -> list.isEmpty() //불리언 표현식
() -> new Apple(10) //객체 생성
(Apple a) -> {
System.out.println(a.getWeight()); //객체에서 소비
}
(String s) -> s.length() //객체에서 선택/추출
(int a, int b) -> a * b //두 값을 조합
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) //두 객체 비교
- 함수형 인터페이스를 선언
- 매개변수의 타입이 유추 가능할 경우 타입 생략 가능
- 매개변수가 하나일 경우 괄호 생략 가능
- 바로 리턴하는 경우 중괄호, return 생략 가능
함수형 인터페이스
- 정확히 하나의 추상 메서드를 지정하는 인터페이스
- 람다의 전체 표현식을 함수형 인터페이스의 인스턴스로 취급
- Default method와 static method는 이미 구현이 되어있으므로 있어도 괜찮음
- 함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식 사용 가능
- @FunctionalInterface는 함수형 인터페이스임을 가리키는 어노테이션
- 추상 메서드는 람다 표현식의 시그니처(function descriptor)를 묘사
public interface Predicate<T> {
boolean test(T t); //function descriptor
}
public interface Comparator<T> {
int compare(T o1, T o2);
}
Runnable r1 = () -> System.out.println("Hello World 1"); //람다 표현식
Predicate
test()는 제너릭 형식 T의 객체를 인수로 받아 boolean을 반환
public <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for (T t : list) {
if (p.test(t)) {
results.add(t);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(Arrays.asList("aaa", "bbb", ""), nonEmptyStringPredicate);
Consumer
제네릭 형식 T 객체를 받아서 void를 반환하는 accept 정의
입력값을 받아서 그 값을 소비하거나 처리하는 역할
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
Function
- 제네릭 형식 T 객체를 인수로 받아서 제네릭 형식 R 객체를 반환하는 apply 정의
- 입력값을 변환하거나 매핑하는 작업에 유용
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
Function.identity()
- 입력 값을 그대로 반환하는 항등 함수(identity function)를 생성하는 데 사용
static <T> Function<T, T> identity() {
return t -> t;
}
public class IdentityFunctionExample {
public static void main(String[] args) {
List<String> strings = List.of("apple", "banana", "cherry", "date");
// 문자열 리스트에서 길이가 5 이상인 문자열만 선택
List<String> result = strings.stream()
.filter(s -> s.length() >= 5)
.map(Function.identity()) // Function.identity()를 사용하여 요소를 그대로 반환
.collect(Collectors.toList());
System.out.println(result);
}
}
UnaryOperator
- 하나의 입력값을 받아서 하나의 출력값을 반환하는 함수를 나타내며, 입력값과 출력값의 타입이 동일
- 주어진 입력값을 수정하거나 변환하여 동일한 타입의 출력값을 생성하는 역할
@FunctionalInterface
public interface UnaryOperator<T> {
T apply(T t);
}
UnarOperator andThen()
- 두 개의 UnaryOperator를 연결하여 순차적으로 적용할 때 사용
- 현재의 UnaryOperator를 먼저 적용하고,
그 다음에 after 파라미터로 전달된 UnaryOperator를 적용하는 새로운 UnaryOperator를 반환
default UnaryOperator<T> andThen(UnaryOperator<T> after)
public class UnaryOperatorExample {
public static void main(String[] args) {
// 문자열을 대문자로 변환하는 UnaryOperator
UnaryOperator<String> toUpperCase = s -> s.toUpperCase();
// 문자열을 "!"로 끝나게 하는 UnaryOperator
UnaryOperator<String> addExclamation = s -> s + "!";
// 첫 번째 UnaryOperator를 적용한 후 두 번째 UnaryOperator를 적용
UnaryOperator<String> combined = toUpperCase.andThen(addExclamation);
String input = "hello";
String result = combined.apply(input);
System.out.println(result); // "HELLO!"
}
}
기본형 특화
- 제네릭 파라미터에는 참조형만 사용 가능
- 오토박싱한 값은 기본형을 감싸는 래퍼며 힙에 저장
- 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정 필요
- DoublePredicate, IntConsumer, LongBinaryOperator, IntFunction
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 ==0; //박싱
oddNumbers.test(1000); //false
IntPredicate evenNumbers = (int i) -> i % 2 == 0; //박싱 없음
evenNumbers.test(1000); //true
람다 활용 : 실행 어라운드 패턴 (Execute Around Pattern)
자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태
public String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine(); //한 행을 읽는 코드
}
}
@FunctionalInterface
public interface BufferedReaderProcessor { //함수형 인터페이스 생성
String process(BufferedReader b) throws IOException;
}
public String processFileNew(BufferedReaderProcessor p) throws IOException { //인터페이스를 메서드 인수로 전달
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br); //BufferedReader 객체 처리
}
}
public void main() throws Exception {
String oneLine = processFileNew((BufferedReader br) -> br.readLine()); //람다 표현식으로 추상 메서드 구현을 직접 전달
String twoLine = processFileNew((BufferedReader br) -> br.readLine() + br.readLine());
}
예외, 람다, 함수형 인터페이스의 관계
- 함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않음
- 예외를 던지는 람다 표현식을 만들려면 확인된 예외를 선언하는 함수형 인터페이스를 직접 정의하거나
- 람다를 try/catch 블록으로 감싸야함
형식 검사
- 람다가 사용되는 context를 이용해서 람다의 형식을 추론
- context(메서드 파라미터나 람다가 할당되는 변수 등)에서 기대되는 람다 표현식의 형식을 대상 형식(target type)
List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);
filter(List<Apple> inventory, Predicate<Apple> p);
1.filter 메서드의 선언을 확인
2.두번째 파라미터로 Predicate<Apple> 형식을 기대
3.Predicate<Apple>은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스
4.test는 Apple을 입력받아 boolean을 반환하는 함수 디스크립터
5.filter 메서드로 전달된 인수는 람다의 시그니처와 일치하여 형식 검사 성공
- 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있음
Callable<Integer> c = () -> 42;
PriviledAction<Integer> p = () -> 42;
형식 추론
- context(대상 형식)을 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론
- 대상 형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론
List<Apple> greenApples = filter(inventory, apple -> GREEN.equals(apple.getColor());
지역변수 사용 제약
- 익명 함수가 하는 것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용 가능
- 람다 캡처링 capturing lambda
- 지역 변수는 명시적으로 final로 선언되어 있거나
- 실질적으로 final로 선언된 변수와 똑같이 사용되어야함
- 한 번만 할당할 수 있는 지역 변수를 캡처할 수 있음
- 인스턴스 변수는 힙에 저장, 지역 변수는 스택에 위치
- 람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면
- 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도
- 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있음
- 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공
- 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337; //compile error
메서드 참조
- 메서드를 어떻게 호출해야 하는지 설명을 참조하기보다는 메서드명을 직접 참조
- 가독성을 높임
- 람다표현식의 인수를 더 깔끔하게 전달
정적 메서드 참조
ToIntFunction<String> stringToInt = (String s) -> Integer.parseInt(s);
ToIntFunction<String> stringToInt = Integer::parseInt;
인스턴스 메서드 참조
BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element);
BiPredicate<List<String>, String> contains = List::contains;
기존 객체의 인스턴스 메서드 참조
Predicate<String> startsWithNumber = (String string) -> this.startsWithNumber(string);
Predicate<String> startsWithNumber = (String string) -> this::startsWithNumber;
생성자 참조
Supplier<Apple> c1 = () -> new Apple();
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);
BiFunction<Color, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
BiFunction<Color, Integer, Apple> c3 = Apple::new;
Apple a3 = c3.apply(GREEN, 110);
static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
static {
map.put("apple", Apple::new);
map.put("oragne", Orange::new);
}
public static Fruit giveMeFruit(String fruit, Integer weight) {
return map.get(fruit.toLowerCase()) //map에서 Function<Integer, Fruit>
.apply(weight); //Fruit 생성
}
람다 표현식을 조합할 수 있는 유용한 메서드
- 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메서드를 제공 (default method)
- 간단한 여러 개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있음
//Comparator 역정렬
inventory.sort(comparing(Apple::getWeight).reversed());
//Comparator 연결. 첫번째 비교자로 조건이 같으면 두번째 비교자 전달
inventory.sort(comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry));
//Predicate 조합
Predicate<Apple> notRedApple = redApple.negate();
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150)
.or(apple -> GREEN.equals(apple.getColor());
//Function 조합
Function<Integer, Integer> f = x -> x+1;
Function<Integer, Integer> g = x -> x*2;
Function<Integer, Integer> h = f.andThen(g); //f를 수행한 후 그 결과를 g 수행
Function<Integer, Integer> h = f.compose(g); //g를 수행한 후 그 결과를 f 수행
함수형 프로그래밍
- 순수함수를 구현하고 호출
- 매개 변수만을 사용하여 외부에 side effect가 발생하지 않도록함, 안정성
- 병렬처리 가능, 확장성
728x90
반응형