💡함수형 인터페이스
함수형 인터페이스(Functional Interface)는람다 표현식을 사용하기 위한 자격을 제공하는 인터페이스로, 여기서 '함수형'이란, 이 인터페이스가 하나의 추상 메서드만을 가져야 한다는 것을 의미한다. 이 추상 메서드는 람다 표현식의 구현부가 되며 실제로 수행되는 동작을 정의한다.
함수형 인터페이스는 표준 API 함수형 인터페이스와 사용자 정의 함수형 인터페이스로 구분할 수 있다.
표준 API 함수형 인터페이스는 JDK에서 제공하는 것이며, 사용자 정의 함수형 인터페이스는 개발자가 선언한 것이다. 표준 API 함수형 인터페이스를 보다 많이 사용한다.
💡표준 API 함수형 인터페이스
표준 API 함수형 인터페이스는 자바에서 제공하는 함수형 인터페이스로, 람다 표현식을 사용할 때 편리하게 활용할 수 있도록 사전에 정의해둔 것이다.
주요 표준 함수형 인터페이스로는 Consumer, Supplier, Function, Operator, Predicate 등이 있다. 각각의 인터페이스는 추상메소드가 1개만 있다는 공통점이 있지만, 추상메소드의 생김새는 다르다.
1. Consumer (매개변수 O, 반환값 X)
- (매개변수) -> {구현부}
- 데이터를 받아서 소비만 하고 반환하지 않는 메소드를 구현한다.
- Consumer<T>
- BiConsumer<T>
- IntConsumer
- ...
2. Supplier (매개변수 X, 반환값 O)
- () -> {return 값}
- 건네주는 매개변수는 없는데 공급만 해주며, Consumer와 반대되는 행동을 할 때 사용한다.
- Supplier<T>
- ...
3. Function (매개변수 O, 반환값 O)
- (매개변수) -> {return 값}
- 매개변수와 반환값이 모두 있는 추상메소드를 가지고 있다.
- Function<T,R>
- BiFunction<T,U,R>
- DoubleToIntFunction<T,U,R>
- ...
4. Operator (매개변수 O, 반환값 O)
- (매개변수) -> {return 값}
- Function의 하위셋으로, Function의 특정한 업무를 할 수 있도록 특화되었다.
- BinaryOperator<T>
- ...
5. Predicate (매개변수 O, 반환값 O)
- (매개변수) -> {return 값}
- Function의 하위셋으로, 정보를 전달받으면 판단의 결과값을 전달해준다.
- Predicate<T>
소스코드에서 사용할 Student Class
class Student {
private String name;
private int kor;
private int eng;
private int math;
// 생성자
public Student(String name, int kor, int eng, int math) {
super();
this.name = name;
this.kor = kor;
this.eng = eng;
this.math = math;
}
public int getTotal() {
return this.kor + this.eng + this.math;
}
public String getName() {
return name;
}
public int getKor() {
return kor;
}
public int getEng() {
return eng;
}
public int getMath() {
return math;
}
@Override
public String toString() {
return "Student [name=" + name + ", kor=" + kor + ", eng=" + eng + ", math=" + math + "]";
}
}
💡Consumer 인터페이스
Consumer 인터페이스는 매개변수를 받아서 소비하는 업무를 구현하며, acceptXXX() 형태의 추상 메소드 제공한다.
매개변수만 있고, 반환값이 없다는 특징이 있다.
Consumer<T>
// 사용자 정의 함수형 인터페이스
MyConsumer m1 = num -> System.out.println(num * num);
m1.test(10); // 100
@FunctionalInterface
interface MyConsumer {
void test(int num);
}
// 표준 API 함수형 인터페이스
Consumer<Integer> c1 = num -> System.out.println(num * num);
c1.accept(10); // 100
표준 API 함수형 인터페이스를 사용하면 사용자 정의 함수형에 쓰이는 인터페이스를 만드는 비용을 줄일 수 있다.
Consumer<String> c2 = str -> System.out.println(str.length());
c2.accept("Isaac"); // 5
Consumer<Integer> c3 = count -> {
for (int i=0; i<count; i++) {
System.out.println(i);
}
System.out.println();
};
c3.accept(5);
/*
0
1
2
3
4
*/
Consumer<Student> c4 = s -> {
System.out.println("이름: " + s.getName());
System.out.println("국어: " + s.getKor());
System.out.println("영어: " + s.getEng());
System.out.println("수학: " + s.getMath());
System.out.println("총점: " + s.getTotal());
};
c4.accept(new Student("Isaac", 100, 90, 80));
/*
이름: Isaac
국어: 100
영어: 90
수학: 80
총점: 270
*/
BiConsumer<T>
BiConsumer<String, Integer> bc1 = (name, age) -> {
System.out.printf("이름: %s, 나이: %d세\n", name, age);
};
bc1.accept("Isaac", 24); // 이름: Isaac, 나이: 24세
IntConsumer
IntConsumer ic1 = num -> System.out.print(num * num);
ic1.accept(5); // 25
💡Supplier 인터페이스
Supplier 인터페이스는 매개변수 없이 반환값을 돌려주는 업무를 구현하며, getXXX() 형태의 추상 메소드를 제공한다.
Supplier<T>
Supplier<Integer> s1 = () -> 100;
System.out.println(s1.get()); // 100
Supplier<Double> s2 = () -> Math.random();
System.out.println(s2.get()); // 0.6329740582208209
Supplier<String> s3 = () -> "Isaac";
System.out.println(s3.get()); // Isaac
Supplier<Student> s4 = () -> new Student("Isaac", 100, 90, 80);
System.out.println(s4.get()); //Student [name=Isaac, kor=100, eng=90, math=80]
IntSupplier s5 = () -> 200;
System.out.println(s5.getAsInt()); // 200
💡Function 인터페이스
Function 인터페이스은 매개변수를 전달하면 처리 후, 반환값을 돌려주는 업무를 구현해야 할 때 사용하며, applyXXX() 형태의 추상 메소드를 제공한다.
input과 output이 동시에 존재하며, 가장 많이 사용하는 인터페이스이다.
Function<T,R>
Function<Integer, Boolean> f1 = num -> num > 0;
System.out.println(f1.apply(10)); // true
System.out.println(f1.apply(-10)); // false
Function<String, Integer> f2 = str -> str.length();
System.out.println(f2.apply("Isaac")); // 5
System.out.println(f2.apply("안녕하세요")); // 5
Function<Student, Boolean> f3 = s -> {
return s.getTotal() >= 180 ? true : false;
};
if (f3.apply(new Student("Isaac", 80, 95, 85))) {
System.out.println("합격");
} else {
System.out.println("불합격");
}
BiFunction<T,U,R>
BiFunction<Integer, Integer, Integer> bf1 = (a, b) -> a + b;
System.out.println(bf1.apply(10, 20)); // 30
DoubleToIntFunction<T,U,R>
DoubleToIntFunction f4 = num -> (int)num;
System.out.println(f4.applyAsInt(3.14)); // 3
UnaryOperator<T,T>
// Function<T,T>
UnaryOperator<Integer> uo1 = num -> num * num;
System.out.println(uo1.apply(10)); // 100
💡Operator 인터페이스
Operator 인터페이스는 매개변수를 전달하면 처리 후, 반환값을 돌려주는 업무를 구현하며, applyXXX() 추상 메소드를 제공한다.
Function의 하위셋이며, 매개변수와 반환값의 타입이 같은 경우(연산자의 느낌이 나는 경우)를 모아두었다는 특징이 있다.
BinaryOperator<T>
BinaryOperator<Integer> bol = (a, b) -> a + b;
System.out.println(bol.apply(10, 20));
BiFunction<Integer, Integer, Integer> bf1 = (a, b) -> a + b;
System.out.println(bf1.apply(10, 20));
BiFunction을 써도 결과는 똑같지만, 매개변수와 반환값을 타입이 같다면 BinaryOperator를 쓰면 더 간결해진다.
💡Predicate 인터페이스
Predicate 인터페이스는 매개변수를 전달하면 처리 후, 반환값을 돌려주는 업무를 구현하며, testXXX() 추상 메소드를 제공한다.
Function의 하위셋이며, 매개변수를 전달받아 반드시 Boolean을 반환한다는 특징이 있다. (리턴값이 고정되어있다.)
Predicate<T>
Function<Integer, Boolean> f1 = num -> num > 0;
Predicate<Integer> p1 = num -> num > 0;
System.out.println(f1.apply(10)); // true
System.out.println(f1.apply(-10)); // false
System.out.println(p1.test(10)); // true
System.out.println(p1.test(-10)); // false
BiPredicate<Integer, Integer> bp2 = (a, b) -> a > b;
System.out.println(bp2.test(10, 20)); // false
System.out.println(bp2.test(20, 10)); // true
💡정적 / 디폴트 메소드
표준 API 함수형 인터페이스에는 정적 / 디폴트 메소드가 있다.
정적 / 디폴트 메소드에서는 람다 객체에서 연산자를 사용할 수 있다.
Student s1 = new Student("Isaac", 100, 90, 80);
// 업무 1.
Consumer<Student> c1 = s -> System.out.println("총점: " + s.getTotal());
c1.accept(s1); // 총점: 270
// 업무 2.
Consumer<Student> c2 = s -> System.out.println("이름: " + s.getName());
c2.accept(s1); // 이름: Isaac
// 업무 3.
Consumer<Student> c3 = s -> System.out.println("평균: " + s.getTotal()/3);
c3.accept(s1); // 평균: 90
// 요구사항) 업무 1과 업무 2를 동시에 실행 > 메소드를 만든다
test(s1, c1, c2, c3);
/*
총점: 270
이름: Isaac
평균: 90
*/
private static void test(Student s1, Consumer<Student> c1, Consumer<Student> c2, Consumer<Student> c3) {
c1.accept(s1);
c2.accept(s1);
c3.accept(s1);
}
// c3() = c1() + c2()
Consumer<Student> c4 = c1.andThen(c2).andThen(c3);
c4.accept(s1);
/*
총점: 270
이름: Isaac
평균: 90
*/
여러 개의 업무를 동시에 실행해야 할 때, Consumer 인터페이스를 이용해 간결하게 하는 것이 가능하다.
연산자 사용
Function<Integer, Boolean> f1 = num -> num > 0;
System.out.println(f1.apply(10)); // true
Function<Boolean, String> f2 = flag -> flag ? "성공" : "실패";
System.out.println(f2.apply(true)); // 성공
// Function = Function + Function
// f3() = f1() + f2();
Function<Integer, String> f3 = f1.andThen(f2);
System.out.println(f3.apply(10)); // 성공
Function<Integer, String> f4 = num -> num > 0 ? "참" : "거짓";
Function<String, Integer> f5 = str -> str.length();
Function<Integer, Integer> f6 = f4.andThen(f5); // f4 + f5
System.out.println(f6.apply(-10)); // 2
Function<Integer, Integer> f7 = f5.compose(f4); // f5 + f4
System.out.println(f7.apply(-10)); // 2
// 2의 배수
Predicate<Integer> p1 = num -> num % 2 == 0;
// 3의 배수
Predicate<Integer> p2 = num -> num % 3 == 0;
int a = 10;
System.out.println(p1.test(a)); // true
System.out.println(p2.test(a)); // false
// a가 2와 3의 공배수인가?
System.out.println(p1.test(a) && p2.test(a)); // false
// p1 && p2
Predicate<Integer> p3 = p1.and(p2);
System.out.println(p3.test(a)); // false
// p1 || p2
System.out.println(p1.test(a) || p2.test(a)); // true
Predicate<Integer> p4 = p1.or(p2);
System.out.println(p4.test(a)); // true
// !p1
System.out.println(!p1.test(a)); // false
Predicate<Integer> p5 = p1.negate();
System.out.println(p5.test(a)); // false