람다(Lambda)
함수를 간단한 식으로 표현하는 방법이다.
람다식은 익명 함수가 아니라 익명 객체이다. 따라서 람다를 다루기 위해서는 참조변수가 필요하기 때문에 참조 변수 타입을 지정해줘야 한다.
람다식 작성하기
메서드의 이름과 반환타입을 제거하고 화살표(->)를 블록 앞에 추가한다.
반환값이 있는 경우, 식이나 값만 적고 return문을 생략한다. (세미콜론도 생략 가능)
매개변수의 타입이 추론 가능하면 생략이 가능하다. (대부분의 경우 생략이 가능함)
MyFunction f1 = new MyFunction() {
@Override
public int max(int a, int b) {
return a > b ? a : b;
}
};
함수형 인터페이스에 max라는 메서드가 있다고 가정하면 위의 코드와 같이 작성해야 한다.
MyFunction f = (a, b) -> a > b ? a : b; // 사용하려면 추상 메서드와 매개변수 개수 및 반환 타입이 일치해야 한다.
하지만 람다식으로 작성하면 한 줄로 간결하게 작성이 가능하다.
단, 추상 메서드와 매개변수 개수 및 반환 타입이 일치해야 한다.
주의사항
- 매개 변수가 하나인 경우 괄호 생략이 가능하다. (대신 타입이 없을 경우)
- 블록 안의 문장이 하나뿐일 때 괄호를 생략할 수 있다. (끝에 세미콜론 안 붙임)
람다식 메서드 참조
하나의 메서드만 호출하는 경우 메서드 참조로 간단히 작성할 수 있다.
다양한 function 패키지
자주 사용되는 다양한 함수형 인터페이스를 제공해 준다.
총 4가지의 인터페이스를 제공해 주며 하니씩 살펴보면 아래와 같다.
Supplier<Integer> supplier = () -> (int)(Math.random() * 100) + 1; // 입력값이 없고 반환값만 존재한다.
Consumer<Integer> consumer = i -> System.out.print(i + ", "); // 입력값이 존재하고 반환값이 없다.
Predicate<Integer> predicate = i -> i % 2 == 0; // 입력값을 주어진 조건을 이용하여 boolean 값을 반환한다.
Function<Integer, Integer> function = i -> i / 10 * 10; // 입력값과 출력값이 존재한다.
Supplier: 입력값이 없고 반환값만 존재한다.
Consumer: 입력값이 존재하고 반환값이 없다.
Predicate: 입력값을 주어진 조건을 이용하여 boolean 값을 반환한다.
Function: 입력값과 출력값이 존재한다.
메서드의 매개변수로 사용할 수도 있는데 아래의 예제를 통해서 이해해 보자
static <T> void makeRandomList(Supplier<T> s, ArrayList<T> list) {
for (int i = 0; i < 10; i++) {
list.add(s.get()); // Supplier로 부터 값을 가져와 리스트에 추가
}
}
먼저 매개변수로 Supplier 인터페이스를 받았는데 앞서 언급한 것과 같이 반환값만 존재하는 인터페이스다. supplier 참조 변수를 통해서 랜덤 한 숫자가 들어오게 되는데 Supplier 인터페이스의 get 메서드를 사용해 값을 가져오게 된다.
static <T> void printEvenNum(Predicate<T> p, Consumer<T> c, ArrayList<T> list) {
System.out.print("[");
for(T i : list) {
if (p.test(i)) c.accept(i); // predicate를 통해 조건에 맞는지 확인하고 true값이면 consumer를 수행한다.
}
System.out.print("]");
}
다음 예제로 Predicate와 Consumer 인터페이스를 매개변수로 받았다.
먼저 predicate 참조 변수는 짝수인지 확인하게 되는데 test() 메서드를 통해서 해당 조건이 true인지 false인지 판별하게 된다. 그 후 true 값이 나올 경우 consumer의 accept 메서드를 수행하여 i 값을 입력하게 되고 처음 코드에서 나왔던 consumer 참조 변수를 통해 출력하게 된다.
static <T> ArrayList<T> doSomething(Function<T, T> f, ArrayList<T> list) {
ArrayList<T> newList = new ArrayList<>(list.size());
for(T i : list) {
newList.add(f.apply(i)); // function을 수행하여 결과값은 새로운 리스트에 넣는다.
}
return newList;
}
마지막 예제로 Function을 매개변수로 받았는데 입력 값과 반환 값이 존재한다.
먼저 list에 존재하는 요소들을 하나씩 꺼내고 난 후 function에 해당하는 기능을 수행하고 값을 새로운 리스트에 저장하는 방식이다.
Function은 반환 값이 존재하기 때문에 마지막에 새로운 리스트를 반환해 주는 예제이다.
Predicate의 결합
Predicate는 조건을 수행하기 때문에 여러 메서드를 결합해서 조건식을 만들 수 있다.
Predicate<Integer> pTest1 = i -> i < 100;
Predicate<Integer> pTest2 = i -> i < 200;
Predicate<Integer> pTest3 = i -> i % 2 == 0;
3개의 Predicate가 있다고 가정을 했을 때 여러 가지 메서드를 하나씩 활용해 보았다.
Predicate<Integer> notP = pTest1.negate(); // i > 100
negate()는 부정을 의미하는 메서드로 pTest1이 i < 100을 뜻하면 notP는 i > 100을 의미한다.
Predicate<Integer> all = notP.and(pTest2.or(pTest3)); // i > 100 && (i < 200 || i % 2 == 0)
and와 or 메서드가 있는데 일반적인 && 연산자와 || 연산자와 같은 기능을 수행한다.
위의 코드를 조금 풀어서 보면 i > 100.and(i < 200.or(i % 2 == 0)으로 생각할 수 있고 논리 연산자를 사용하면
i > 100 && (i < 200 || i % 2 == 0)으로 해석할 수 있다.
System.out.println(all.test(150));
all 변수의 test에 150 값을 넣으면 150은 100보다 크고 200보다 작으며 2로 나눴을 때 0으로 떨어지게 되고 따라서 true 값이 나오게 된다.
String str1 = "abc";
String str3 = "ABC";
String str4 = new String("abc");
Predicate<String> pTest4 = Predicate.isEqual(str1);
System.out.println(pTest4.test(str3)); // str3로 비교하면 false가 나온다.
System.out.println(pTest4.test(str4)); // equals를 사용하기 때문에 값을 비교하게 되고 str4와 비교해도 true가 나온다.
isEqual 메서드를 사용하여 값을 비교할 수 있다.
예제 코드를 보면 str4와 비교했을 때는 "abc"로 같은 값을 가지기 때문에 결과로 true가 나오지만, str3과 비교하면 소문자와 대문자 간의 비교이기 때문에 false가 나온다.
컬렉션이 포함된 람다식
기존의 컬렉션을 람다식으로 표현하여 더 편리하게 출력할 수 있다.
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
// list의 모든 요소를 출력
list.forEach(i -> System.out.print(i + ","));
System.out.println();
// list에서 2 또는 3의 배수를 제거
list.removeIf(x -> x % 2 == 0 || x % 3 == 0);
System.out.println(list);
// list의 각 요소에 10을 곱하기
list.replaceAll(i -> i * 10);
System.out.println(list);
ArrayList를 기준으로 람다식을 작성해 보면 위의 코드와 같이 작성할 수 있다.
public void forEach(Consumer<? super E> action)
public boolean removeIf(Predicate<? super E> filter)
public void replaceAll(UnaryOperator<E> operator)
해당 메서드를 살펴보면 매개변수로 각각 Consumer와 Predicate, 마지막으로 UnaryOperator를 받고 있는 것을 알 수 있다.
UnaryOperator는 Function<T, R>을 상속받고 있기 때문에 Function 인터페이스라고 생각하면 된다.
계속해서 살펴본 function 패키지를 잘 이해하고 사용한다면 자바에서 제공하는 여러 메서드들의 매개 변수로 어떤 것을 받고, 또 어떻게 처리해야 되는지 감을 잡을 수 있다.
'자바' 카테고리의 다른 글
[Java] - JDK? JVM? JRE? 이게 다 무슨 소리지? (0) | 2023.11.26 |
---|---|
[Java] - Optional (0) | 2023.11.19 |
[Java] - 제네릭 (0) | 2023.11.15 |
[Java] - 컬렉션 (0) | 2023.11.14 |
[Java] - 애너테이션 (0) | 2023.11.13 |