개발자로 후회없는 삶 살기
[문법] 자바에서 제공하는 함수형 인터페이스 본문
서론
※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.
https://www.youtube.com/playlist?list=PLW2UjW795-f6xWA2_MUhEVgPauhGl3xIp
본론
자바 8부터 자바에 oop 기능에 함수형 언어 기능을 추가했습니다. 대표적인 함수형 언어 기능인 람다를 알아봅니다.
- 람다식
public int max(int a, int b) {
return a > b ? a : b;
}
함수(메서드)를 간단한 식으로 표현하는 방법입니다. max 함수(메서드)를 람다식을 이용하면 짧게 표현할 수 있습니다.
(int a, int b) -> a > b ? a : b
람다식은 이름이 없는 익명 함수로 함수(메서드)를 람다로 바꿀 때 함수 이름(+반환 타입)을 없애면 됩니다. 함수(메서드)는 클래스에 독립적인 개념으로 자바에서 메서드라고 불리며 모든 메서드는 클래스 안에 있어야하는데 메서드를 람다식으로 함수형으로 바꾸면 클래스에 독립적으로 쓸 수 있습니다.
-> 익명함수 vs 익명객체
new Object() {
int max(int a, int b) {
return a > b ? a : b;
}
}
람다식은 메서드를 클래스 독립적으로 쓸 수 있게 합니다. 하지만 자바의 원칙은 메서드가 따로 존재할 수 없습니다. 사실 람다식은 익명 함수가 아니라 익명 객체입니다. new 클래스() { 메서드 }인 익명 객체를 람다식으로 간단히 쓸 수 있게 허용하는 것 입니다. 결국은 람다식으로 표현된 함수는 객체입니다. 처음에는 메서드를 간단히 한 것이 람다라고 했는데 사실 객체인 것입니다.
Object o = (int a, int b) -> a > b ? a : b
따라서 람다식도 레퍼런스로 객체 타입에 대입할 수 있습니다. 하지만 Object를 보면 max() 메서드가 객체 내부에 없어서 o.max()로 직접 호출해서 사용할 수 없습니다. 이를 어떻게 해결하는지 함수형 인터페이스에서 알아봅니다.
- 함수형 인터페이스
public interface Myfunctional {
public abstract int max(int a, int b);
}
단 하나의 추상 메서드만 선언된 인터페이스입니다. 이 함수형 인터페이스의 단 하나의 메서드를 익명 클래스로 구현할 수 있습니다.
public class test {
public static void main(String[] args) {
Myfunctional f = new Myfunctional() {
@Override
public int max(int a, int b) {
return a > b ? a: b;
}
};
System.out.println(f.max(5, 3));
}
}
이를 레퍼런스로 참조하면 max 메서드를 이름으로 호출할 수 있습니다.
Myfunctional f2 = (a, b) -> a > b ? a : b;
System.out.println(f2.max(5, 3));
Comparator<String> comparator1 =
(String s1, String s2) -> s2.compareTo(s1);
comparator1.compare()
익명 객체를 간단이 사용한 것이 람다식이라고 했으니 람다식도 레퍼런스로 참조할 수 있습니다. 위에서 참조는 가능하나 호출은 불가능하다고 했는데 그것은 Object 객체에 호출할 함수명이 없어서 그런 것이고 구현할 단일 추상 메서드를 가지고 있는 함수형 인터페이스를 선언하고 람다식을 참조하면 호출할 수 있습니다. 호출할 때는 추상 메서드 이름을 호출해야합니다.
Comparator<String> c = new Comparator<>() {
@Override
public int compare(String s1, String s2) {
return s2.compareTo(s1);
}
};
Comparator c1 = (String s1, String s2) -> s2.compareTo(s1);
자바에는 수 많은 함수형 인터페이스가 있습니다. 그 중 하나인 Comparator도 람다식을 사용하여 익명 객체를 간단하게 사용할 수 있습니다.
@FunctionalInterface
interface MyFunction {
void run();
}
public class Lambda3 {
static MyFunction getMyFunction() { // 익명 객체 반환
MyFunction f = () -> System.out.println("f1.run");
return f;
// return() -> System.out.println("f1.run");
}
public static void main(String[] args) {
MyFunction f1 = getMyFunction();
f1.run();
}
}
람다식은 익명 객체를 구현하고 간편하게 사용할 수 있게 해주는 것입니다. 따라서 단순히 생각하면 익명 객체이므로 람다식을 인자로 넣거나 반환하여 익명 객체를 인자로 넣거나 반환하는 효과를 얻을 수 있습니다.
- java에서 제공하는 함수형 인터페이스
자주 사용하는 다양한 함수형 인터페이스를 살펴봅니다. 자바에서는 자주 사용하는 함수형 인터페이스를 미리 만들어 두고 제공합니다. 제네릭은 입력과 반환 타입이며 Function만 반환 타입이 2번째 제네릭으로 있습니다.
1. Predicate 함수형 인터페이스
조건식을 표현할 때 사용됩니다. 입력을 받아 반환은 boolean 타입입니다.
이들은 전부 함수형 인터페이스로 내부에 하나의 추상 메서드가 선언이 되어있습니다. 따라서 람다식으로 사용할 때 선언된 메서드 이름으로 호출해서 사용해야 합니다.
public class test {
public static void main(String[] args) {
Predicate<String> isEmptyStr = s -> s.length() == 0;
String s = "";
if (isEmptyStr.test(s)) {
System.out.println("empty");
}
}
}
반환을 "s -> s.length == 0" 이렇게 하여 boolean 타입을 반환하도록 합니다.
2. Supplier 함수형 인터페이스
public static void main(String[] args) {
Supplier<Integer> s = () -> (int) (Math.random() * 100) + 1;
List<Integer> list = new ArrayList<>();
makeRandomList(s,list);
System.out.println(list);
}
static <T> void makeRandomList(Supplier<T> s, List<T> list) {
for (int i = 0; i < 10; i++) {
list.add(s.get());
}
}
공급자로 입력이 없고 출력만 있습니다. 따라서 인자에 빈 괄호입니다. makeRandomList 메서드는 공급자 람다식을 매개로 받아 램덤 값을 10번 생성하고 list에 담는 작업을 수행합니다.
3. Consumer 함수형 인터페이스
public static void main(String[] args) {
Supplier<Integer> s = () -> (int) (Math.random() * 100) + 1;
Consumer<Integer> c = integer -> System.out.println(integer);
Predicate<Integer> p = integer -> integer % 2 == 0;
List<Integer> list = new ArrayList<>();
makeRandomList(s, list);
System.out.println(list);
printEvenNum(p, c, list);
}
static <T> void printEvenNum(Predicate<T> p, Consumer<T> c, List<T> list) {
for (T i : list) {
if(p.test(i))
c.accept(i);
}
}
소비자로 입력만 있고 출력이 없습니다. 람다식을 보면 integer를 입력받고 반환값이 없도록 하였습니다. printEvenNun 함수는 조건자와 소비자 람다식을 매개로 받아 list의 값을 조건자로 검사하고 참인 경우 소비자로 출력합니다.
이렇게 하면 좋은 것이 미리 만들어 놓은 인터페이스를 사용하는 것이기 때문에 개발자는 단일 추상 메서드를 직접 만들 때와 같이 람다식을 만들고 5개 중 조건에 맞는 인터페이스로 참조하여 사용할 수 있습니다.
- predicate의 결합
predicate 여러 개를 논리 연산자로 결합할 수 있습니다. 3개의 조건자가 있을 때 and, or, negate로 결합합니다. 조건인 것을 생각하면 굉장히 당연한 일입니다.
Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i % 2 == 0;
Predicate<Integer> notP = p.negate();
Predicate<Integer> all = notP.and(q.or(r));
System.out.println(all.test(150));
람다식과 람다식을 연결하는 것이므로 인자로 람다식을 넣으면 됩니다.
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
인터페이스에는 추상 메서드와 default 메서드, static 메서드를 가질 수 있습니다. isEqual은 static 메서드로 람다식을 반환합니다.
String str1 = "abc";
String str2 = "abc";
Predicate<String> p2 = Predicate.isEqual(str1);
boolean result = p2.test(str2);
등가 비교를 위한 Predicate의 작성에는 isEqual을 사용하고 람다식을 반환받아 익명 객체를 만들어 Predicate 레퍼런스로 참조하고 test() 추상 메서드로 사용하면 됩니다.
이처럼 함수형 인터페이스는 단일 추상 메서드를 가지기에 default나 static으로 람다식을 반환하는 메서드들을 가지고 있고 람다식을 반환 받아 단일 추상 메서드로 호출해 사용하면 됩니다.
- Function의 결합
함수도 결합할 수 있습니다. 함수 f의 출력을 함수 g의 입력으로 넣을 때 이처럼 작성하면 됩니다. 이때 f의 출력 타입과 g의 입력 타입이 같아야합니다.
public static void main(String[] args) {
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"));
}
andThen()으로 연결하면 됩니다. 이렇게 하면 여러 함수를 하나로 연결하여 여러 작업을 하나의 함수로 수행할 수 있습니다.
- 람다식과 컬랙션 프레임워크
람다식을 이용해서 컬랙션의 메서드를 쉽게 사용할 수 있게 되었습니다. 컬랙션의 인자로 람다식을 주면 람다식에 맞는 작업을 하게 됩니다. 컬랙션을 사용하는데 코드가 너무 길어서 쓰기 불편한 점이 있었는데 람다식을 이용해서 코드를 간단히 했습니다.
1) list와 foreach
리스트에 있는 모든 요소를 출력합니다. 인자로 Consumer를 받기에 소비자 형식의 람다식을 넣어야합니다.
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
원래는 이렇게 해야하는 것인데 훨씬 간단해집니다.
2) list와 removeIf
리스트에서 조건에 해당하는 요소를 삭제합니다.
3) list와 replaceAll
list.replaceAll(i -> i * 10); // 단항 연산자
리스트에 있는 모든 요소에 연산을 수행합니다.
4) map과 foreach
map.forEach((k, v) -> System.out.println(k + " " + v)); biconsumer
map의 모든 요소를 k, v 형태로 출력합니다.
- 메서드 참조
람다식을 더 간단히 할 수 있습니다. 클래스 이름 :: 메서드 이름을 써서 표현합니다.
interface Met {
static Integer method(String a) {
return Integer.parseInt(a);
}
}
int result = Integer.parseInt("123");
예를 들어서 str을 매개로 줘서 int로 반환해주는 메서드가 있습니다. 이 메서드가 하는 일은 단순히 str을 문자로 변환하는 것으로 메서드 없이 Integer.parseInt를 바로 써도 됩니다.
Function<String, Integer> f = (String s) -> Integer.parseInt(s);
이는 단일 추상 메서드를 재정의한 모양과 같으므로 람다식으로 표현할 수 있습니다. 인자를 받아 반환을 하니 Function 인터페이스로 하면 될 것입니다. 이 람다식을 보면 참조하는 부분에 String이라는 입력 정보가 있어서 ()에 String을 없애도 됩니다. 근데 그러면 s 자체도 없애도 됩니다.
Function<String, Integer> f2 = Integer::parseInt;
System.out.println(f2.apply("123"));
그래서 남는 것을 보면 Interger.parseInt만 남고 메서드 참조형으로 표현됩니다.
람다식의 Integer.parseInt의 클래스이름.메서드 이름이 Integer::parseInt의 클래스이름::메서드 이름이 됩니다.
메서드 참조는 그냥 클래스이름 :: 메서드 이름으로 알면 되고 메서드 참조를 람다식으로, 람다식을 메서드 참조 형식으로 바꾸는 것을 이해해야 합니다. 메서드 참조는 람다식을 더 간단히 쓴 것인데 그렇게 할 수 있는 이유는 참조 부분의 함수형 인터페이스에 정보가 다 있기 때문입니다.
※ 메서드 참조식을 람다식으로 그릴 때나 람다식을 참조식으로 그릴 때나, 최초에 람다식을 그릴 때나 입력과 출력 그림을 그리는 것이 좋습니다.
- 생성자의 메서드 참조
Supplier<Met> s = () -> new Met();
객체를 생성해서 주는 supplier가 있습니다. 입력이 없고 출력만 있는 기본 생성자를 만들어주는 것으로 역시 짧게 쓸 수 있습니다.
Supplier<Met> s2 = Met::new;
"생성할 클래스 타입::new"로 표현하면 됩니다. 입력이 없는 람다라서 함수형 인터페이스를 Supplier로 해야합니다.
Function<Integer, Met> s3 = Met::new;
기본 생성자가 아닌 매개가 있는 생성자의 경우 함수형 인터페이스를 입력이 있기에 Function으로 해야하고 참조 형식은 똑같이 하면 됩니다.
BiFunction<Integer, Integer, Met> s4 = Met::new;
매개의 갯수가 두개인 생성자면 BiFunction을 쓰면 됩니다.
Function<Integer, int[]> arr = int[]::new;
배열은 반환값으로 배열 타입을 주면 됩니다.
Met met = s.get();
Met apply = s3.apply(1);
Met apply1 = s4.apply(1, 2);
int[] apply2 = arr.apply(100); // 길이가 100인 빈 배열
역시 람다식과 함수형 인터페이스를 쓰는 것이므로 함수형 인터페이스의 get, apply 단일 메서드를 호출해야 최종적으로 생성자를 반환해줍니다.
'[백엔드] > [Java | 학습기록]' 카테고리의 다른 글
디자인 패턴 PART.빌더 패턴 (0) | 2023.08.17 |
---|---|
디자인 패턴 PART.추상 팩토리 패턴 (0) | 2023.08.13 |
디자인 패턴 PART.팩토리 메서드 패턴 (0) | 2023.08.13 |
디자인 패턴 PART.싱글톤 패턴 (0) | 2023.07.25 |
[문법] Stream (0) | 2023.06.18 |