Java 8 - Function Interface
Java SDK 8의 java.util.function 패키지에는 수많은 Functional Interface들이 등록되어 있습니다.
이 패키지에 등록되어 있는 모든 인터페이스들은 @FunctionalInterface로 지정되어 있으며
API 문서에는 다음과 같은 설명이 추가되어 있습니다.
This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.
이것은 Functional Interface이며 그러므로 람다식이나 메서드 레퍼런스를 위한 할당 대상으로 사용될 수 있습니다.
Functional interface 에대하여 자세히 알아보고
java.util.function 패키지의 대표적인 인터페이스에 대해 알아본 뒤
이 인터페이스들을 이용하는 java.util.stream.Stream 인터페이스에 람다식을 적용하는 방법에 대해서도 알아보도록 하겠습니다.
Functional Interface
Functional Interface란 함수를 **일급 객체로 사용할 수 없는 자바 언어의 단점을 보완하기 위해 도입되었습니다.
위 덕분에 자바는 전보다 간결한 표현이 가능해졌으며, 가독성이 높아지게 되었습니다.
Functional Interface는 일반적으로
`구현해야 할 추상 메서드가 하나만 정의된 인터페이스`를 가리킵니다.
"Java Language Specification의 설명"
A functional interface is an interface that has just one abstract method (aside from the methods of Object), and thus represents a single function contract.Functional Interface는 (Object 클래스의 메서드를 제외하고) 단 하나의 추상 메서드만을 가진 인터페이스를 의미하며, 그런 이유로 단 하나의 기능적 계약을 표상하게 된다.
Funcional Interface Ex
//Functional Interface인 경우: 메소드가 하나만 있음
public interface Functional {
public boolean test(Condition condition);
}
//java.lang.Runnable도 결과적으로 Functional Interface임
public interface Runnable {
public void run();
}
//구현해야 할 메소드가 하나 이상 있는 경우는 Functional Interface가 아님
public interface NonFunctional {
public void actionA();
public void actionB();
}
Java Specification에서는Object클래스를 제외하고라는 단어가 붙어있는데 이는 왜일까요??
자바언어에서 사용되는 모든 객체들이 Object 객체를 상속하고 있다는 것을 생각하면 쉽게 알 수 있습니다.
Interface의 구현체들이 Object객체의 메서드들을 모두 갖고 있기 때문에 이 메서드들은 funcional Interface의 대상이 되지 않는 것입니다.
Funcional Interface Ex2
//Object 객체의 메소드만 인터페이스에 선언되어 있는 경우는 Functional Interface가 아님
public interface NotFunctional {
public boolean equals(Object obj);
}
//Object 객체의 메소드를 제외하고 하나의 추상 메소드만 선언되어 있는 경우는 Functional Interface임
public interface Functional {
public boolean equals(Object obj);
public void execute();
}
//Object객체의 clone 메소드는 public 메소드가 아니기 때문에 Functional Interface의 대상이 됨
public interface Functional {
public Object clone();
}
public interface NotFunctional {
public Object clone();
public void execute();
}
**일급 객체란?
일급 객체(영어: first-class object)란 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다.
보통 함수에 매개변수로 넘기기, 수정하기, 변수에 대입하기와 같은 연산을 지원할 때 일급 객체라고 한다.
출처 - https://ko.wikipedia.org/wiki/%EC%9D%BC%EA%B8%89_%EA%B0%9D%EC%B2%B4
@FunctionalInterface Annotation
Java SDK 8에서는 @FunctionalInterface이라는 어노테이션을 제공하여
작성한 인터페이스가 Fucntional Interface인지 확인할 수 있도록 하고 있습니다.
다시 Java Specification을 봐보도록 하겠습니다.
"Java Language Specification의 설명"
The annotation type FunctionalInterface is used to indicate that an interface is meant to be a functional interface (§9.8). It facilitates early detection of inappropriate method declarations appearing in or inherited by an interface that is meant to be functional.It is a compile-time error if an interface declaration is annotated with @FunctionalInterface but is not, in fact, a functional interface.
Because some interfaces are functional incidentally, it is not necessary or desirable that all declarations of functional interfaces be annotated with @FunctionalInterface.
어노테이션 타입 FunctionalInterface는 어떤 인터페이스가 Functional Interface라는 것을 나타내기 위해 사용된다. 이것을 이용하면 부적절한 메서드 선언이 포함되어 있거나 함수형이어야 하는 인터페이스가 다른 인터페이스를 상속한 경우 미리 확인할 수 있다.
@FunctionalInterface로 지정되어 있으면서 실제로는 Functional Interface가 아닌 인터페이스를 선언한 경우 컴파일 타임 에러가 발생한다.
어떤 인터페이스들은 우연히 함수형으로 정의될 수도 있기 때문에, Functional Interface들이 모두 @FunctionalInterface 어노테이션으로 선언될 필요도 없고 그렇게 하는 것이 바람직하지도 않다.
즉, @FucntionalInterface어노테이션은 인터페이스가 Fucntional Interface인지 아닌지 확실히 하기를 원할 때 사용하면 됩니다.
Lambda와 Fucntional Interface
우선 Java 8에서 함께 추가된 람다에 대하여 알아보겠습니다.
"Java Language Specification의 설명"
A lambda expression is like a method: it provides a list of formal parameters and a body - an expression or block - expressed in terms of those parameters.
(중간 생략)
Evaluation of a lambda expression produces an instance of a functional interface (§9.8). Lambda expression evaluation does not cause the execution of the expression's body; instead, this may occur at a later time when an appropriate method of the functional interface is invoked.
람다식은 메서드와 비슷하다: 규격을 갖춘 파라미터의 목록과 그 파라미터를 이용해서 표현된 몸통(표현식이든 블록이든)을 제공한다.
(중간 생략)
람다식이 평가(evaluation)되면 그 결과 Functional Interface의 인스턴스를 생성한다. 람다식의 처리 결과는 표현식 몸통을 실행하는 것이 아니다; 대신 나중에 이 Functional Interface의 적절한 메서드가 실제 호출(invoke)될 때 이것(표현식 몸통의 실행)이 일어난다.
지금 중요하게 봐야 할 부분은
람다식의 평가 결과가 Fucntional Interface의 인스턴스라는 것입니다.
람다식은 보통 아래와 같이 표기하고 사용합니다.
() -> {} // No parameters; result is void
() -> 42 // No parameters, expression body
() -> null // No parameters, expression body
() -> { return 42; } // No parameters, block body with return
() -> { System.gc(); } // No parameters, void block body
() -> { // Complex block body with returns
if (true) return 12;
else {
int result = 15;
for (int i = 1; i < 10; i++)
result *= i;
return result;
}
}
(int x) -> x+1 // Single declared-type parameter
(int x) -> { return x+1; } // Single declared-type parameter
(x) -> x+1 // Single inferred-type parameter
x -> x+1 // Parentheses optional for single inferred-type parameter
(String s) -> s.length() // Single declared-type parameter
(Thread t) -> { t.start(); } // Single declared-type parameter
s -> s.length() // Single inferred-type parameter
t -> { t.start(); } // Single inferred-type parameter
(int x, int y) -> x+y // Multiple declared-type parameters
(x, y) -> x+y // Multiple inferred-type parameters
(x, int y) -> x+y // Illegal: can't mix inferred and declared types
(x, final y) -> x+y // Illegal: no modifiers with inferred types
또 다른 예를 봐보겠습니다. 사칙 연산자를 표상하는 Functional Interface인 ArithmeticOperator와 같이 사용하는 예제입니다.
@FunctionalInterface
public interface ArithmeticOperator {
public int operate(int a, int b);
}
@FunctionalInterface
public class ArithmeticCalculator {
/**
* 실제 계산은 ArithmeticCalculator에 위임한다.
*/
public static int calculate(int a, int b, ArithmeticOperator operator){
return operator.operate(a, b);
}
}
public class ArithmeticCalculatorTest {
@Test
public void testPlus{
int firstNumber = 5;
int secondNumber = 94;
int result = ArithmeticCalculator.calculate(firstNumber, secondNumber, (a, b) -> a + b);
Assert.assertEquals(firstNumber + secondNumber, result);
}
}
ArithmeticCalculatorTest를 보게 되면 , ArithmeticOperator의 구현체가 예상되는 곳(result의 ArithmeticCalculator.calculate의 3번째 인자)에 람다식이 입력되어 있습니다.
위에서 이야기했던 것처럼 lambda 식은 functional Interface의 인스턴스를 생성합니다.
즉, 3번째 인자인 람다식은 ArithmeticOperator의 인스턴스를 생성하게 됩니다.
- (a, b) : ArithmeticOperator.operate 메서드의 두 개의 파라미터를 의미함 , 둘 다 int Type을 가질 것으로 추론(inffered 될 수 있다.
- a + b : 람다식의 몸통, 중괄호로 묶여있지 않으므로 , 위 처리 값이 리턴된다.
위와 같이 람다식은 Functional Interface의 계약에 근거한 "추론"을 통해 인스턴스를 생성합니다.
위 추론이 가능한 이유는 Functional Interface 가 1개의 메서드만을 갖기로 전제했기 때문입니다.
람다는 새로운 클래스를 만들어야만 인스턴스 생성이 가능했던 자바 언어의 한계를 넘게 해 주었습니다.
람다식과 ArithmeticOperator 인터페이스를 사용한 예제 소스
// 자체 생성한 인터페이스
@FunctionalInterface
public interface ArithmeticOperator {
public int operate(int a, int b);
}
// 자체 생성한 ArithmeticOperator 인터페이스를 사용하는 경우
public void testArithmeticOperator() {
ArithmeticOperator plusOperator = (a, b) -> a + b;
ArithmeticOperator minusOperator = (a, b) -> a - b;
ArithmeticOperator multiplyOperator = (a, b) -> a * b;
ArithmeticOperator divideOperator = (a, b) -> {
if (b == 0) {
b = 1;
}
return a / b;
};
ArithmeticOperator spareOperator = (a, b) -> {
if (b == 0) {
b = 1;
}
return a % b;
};
int a = new Random().nextInt(10000);
int b = new Random().nextInt(10000);
int plus = plusOperator.operate(a, b);
int minus = minusOperator.operate(a, b);
int multiply = multiplyOperator.operate(a, b);
int divide = divideOperator.operate(a, b);
int spare = spareOperator.operate(a, b);
Assert.assertEquals(a + b, plus);
Assert.assertEquals(a - b, minus);
Assert.assertEquals(a * b, multiply);
Assert.assertEquals(a / b, divide);
Assert.assertEquals(a % b, spare);
}
람다식과 Function인터페이스를 사용한 예제 소스
// java.util.function.Function 인터페이스의 apply 메소드가 하나의 파라미터만 받을 수 있기 때문에 정의한 클래스
public class TwoNumbers {
private int first;
private int second;
public TwoNumbers(int first, int second) {
this.first = first;
this.second = second;
}
public int getFirst() {
return first;
}
public void setFirst(int first) {
this.first = first;
}
public int getSecond() {
return second;
}
public void setSecond(int second) {
this.second = second;
}
}
// 별도의 인터페이스나 메소드 없이 java.util.function.Function 인터페이스를 직접사용
@Test
private void testFunction() {
Function<TwoNumbers, Integer> plusOperator = n -> n.getFirst() + n.getSecond();
Function<TwoNumbers, Integer> minusOperator = n -> n.getFirst() - n.getSecond();
Function<TwoNumbers, Integer> multiplyOperator = n -> n.getFirst() * n.getSecond();
Function<TwoNumbers, Integer> divideOperator = n -> {
if (n.getSecond() == 0) {
return 0;
}
return n.getFirst() / n.getSecond();
};
Function<TwoNumbers, Integer> spareOperator = n -> {
if (n.getSecond() == 0) {
return 0;
}
return n.getFirst() % n.getSecond();
};
TwoNumbers numbers = new TwoNumbers(new Random().nextInt(10000), new Random().nextInt(10000));
int plus = plusOperator.apply(numbers);
int minus = minusOperator.apply(numbers);
int multiply = multiplyOperator.apply(numbers);
int divide = divideOperator.apply(numbers);
int spare = spareOperator.apply(numbers);
Assert.assertEquals(numbers.getFirst() + numbers.getSecond(), plus);
Assert.assertEquals(numbers.getFirst() - numbers.getSecond(), minus);
Assert.assertEquals(numbers.getFirst() * numbers.getSecond(), multiply);
Assert.assertEquals(numbers.getFirst() / numbers.getSecond(), divide);
Assert.assertEquals(numbers.getFirst() % numbers.getSecond(), spare);
}
그리고 Fucntion Package에 대하여 알아보도록 하겠습니다.
Funcion Package의 대표적인 Interface
Consumer<T> |
void accept(T) 메서드가 선언되어 있는 인터페이스. 입력된 T type 데이터에 대해 어떤 작업을 수행하고자 할 때 사용합니다. 리턴 타입이 void이므로 처리 결과를 리턴해야 하는 경우에는 Function 인터페이스를 사용해야 합니다. |
Function<T, R> |
R apply(T) 메서드가 선언되어 있는 인터페이스. 입력된 T type 데이터에 대해 일련의 작업을 수행하고 R type 데이터를 리턴할 때 사용합니다. 입력된 데이터를 변환할 때 사용할 수 있습니다. |
Predicate<T> | boolean test(T) 메소드가 선언되어 있는 인터페이스. 입력된 T type 데이터가 특정 조건에 부합되는지 확인하여 boolean 결과를 리턴합니다. |
Supplier |
T get() 메서드가 선언되어있는 인터페이스입니다. 매개변수를 받지 않고 특정 타입의 결과를 리턴합니다. |
Consumer <T>
Consumner Interface원형
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
사용 예시
public class ConsumerTest {
@Test
public void test() {
andThen();
accept();
}
private void andThen() {
final Consumer<String> consumer1 = (i) -> System.out.println("consumer1 "+i);
final Consumer<String> consumer2 =(i) -> System.out.println("consumer2 "+i);
consumer1.andThen(consumer2).accept("is Consume!!");
}
private void accept() {
final Consumer<String> greetings = value -> System.out.println("Hello " + value);
greetings.accept("World"); // Hello World
}
}
andThen
Consumer 인터페이스는 처리 결과를 리턴하지 않기 때문에 andThen() 디폴트 메서드는 함수적 인터페이스의 호출 순서만 정합니다.
Function <T , R>
Function Interface원형
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
사용 예시
public class FunctionTest {
@Test
public void test() {
compose();
andThen();
identity();
}
private void compose() {
Function<Integer, String> intToString = Objects::toString;
Function<String, String> quote = s -> "'" + s + "'";
Function<Integer, String> quoteIntToString = quote.compose(intToString);
System.out.println(quoteIntToString.apply(5)); // '5'
}
private void andThen() {
Function<String, String> upperCase = v -> v.toUpperCase();
String result = upperCase.andThen(s -> s + "abc").apply("a");
System.out.println(result); // Aabc
}
private void identity() {
final Function<Integer, String> intToStr = value -> value.toString();
System.out.println(intToStr.apply(10)); // 10
final Function<Integer, Integer> identity = Function.identity();
System.out.println(identity.apply(100)); // 100
}
}
compose
before 메서드 실행 후 결과를 받아서 현재 메소드 실행합니다.
제네릭 타입은 처음 input과 마지막 output의 타입입니다.
즉 ,메서드를 순서대로 실행시키기 위한 함수입니다.
andThen
compose와 반대로
현재 메소드를 실행 후 매게 변수로 받은 람다를 실행합니다.
identity
자신의 값을 그대로 리턴하는 스테틱 메서드입니다.
compose , andThen 차이점
andThen()과 compose()의 차이점은 어떤 함수적 인터페이스부터 먼저 처리하느냐에 따라 다릅니다.
인터페이스A.compose(인터페이스B);
B실행 -> A실행
인터페이스A.andThen(인터페이스B);
A실행 -> B실행
Predicate <T>
Predicate Interface의 원형
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
public class PredicateTest {
@Test
public void test() {
tests();
and_or_negate();
}
private void and_or_negate() {
Predicate<Integer> predicateA = a -> a % 2 == 0;
Predicate<Integer> predicateB = b -> b % 3 == 0;
boolean result;
result = predicateA.and(predicateB).test(9);
System.out.println("9는 2와 3의 배수입니까? " + result);
result = predicateA.or(predicateB).test(9);
System.out.println("9는 2또는 3의 배수입니까? " + result);
result = predicateA.negate().test(9);
System.out.println("9는 홀수입니까? " + result);
}
private void tests() {
Predicate<Integer> isZero = (i) -> i == 0;
System.out.println(isZero.test(0));
;
}
}
and
인자로 받는 다른 Predicate의 람다 수행 내용과 , 자기 자신의 람다 수행 내용을 &&연산합니다.
or
인자로받는 다른 Predicate의 람다 수행내용과 , 자기자신의 람다 수행내용을 ||연산합니다.
negate
람다 수행 내용을! 연산합니다.
Supplier <T>
Supplier Inteface원형
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Supplier를 활용하면 값을 그냥 전하는 것이 아니라, 중간에 로직을 추가해서 전달할 수 있습니다.
public class SupplierTest {
@Test
public void test(){
Supplier<Integer> intSupplier = () -> {
int num = (int) (Math.random() * 6) + 1;
return num;
};
int num = intSupplier.get();
System.out.println("눈의 수 : " + num);
}
}
get
제네릭으로 전달받은 타입으로 값을 전달해줍니다.
쉽게 정리하면
-
Consumer - 어떤 데이터를 소비
-
Function - 어떤 기능을 수행
-
Predicate - 근거를 확인
-
Supplier - 특정 타입의 데이터를 공급
위의 인터페이스들을 그럼 어디에 쓸까요??
Java 8에서 같이 추가된 Stream 인터페이스와 결합하여 아주 강력한 기능을 제공합니다.
Function Interface Example with Stream
발췌 - https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html#approach3
Person이라는 데이터 목록(List)으로부터 나이가 일정 범위에 들어있는 데이터에 대해 출력하는 메서드를 작성하고자 합니다.
public static void printPersonsWithinAgeRange(
List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}
// 사용하기
printPersonsWithinAgeRange(roster, 10, 20);
위의 코드의 안 좋은 점은 Person 목록에 대하여 "나이가 일정 범위에 있는 Person에 대하여 printPerson 한다"의 기능밖에 수행하지 못합니다.
특정 이름을 포함하는 사람을 출력하거나 , 특정 성별에 대하여 등등.. 다른 조건이 필요할 때는 그때마다 새로운 메서드를 정의해주어아합니다.
위 문제를 해결하기 위해서 Java Colleciton API는 Stream이라는 인터페이스를 새롭게 선보였습니다.
Stream과 람다식을 이용하여 매우 간단하게 위의 기능을 처리할 수 있습니다.
// 메소드나 클래스 생성 없이 바로 사용. roster 객체는 List 타입의 인스턴스
roster
.stream()
.filter( p -> p.getAge() >= 10 && p.getAge() <= 20 )
.forEach( p -> p.printPerson() );
위 Stream코드를 보게 되면
roster의 stream() 메서드를 호출하여 Stream 타입의 인스턴스를 얻습니다.
그리고 Stream.filter(Predicate)와 Stream.forEach(Consumer) 메서드를
체인 형식으로 호출하고 있습니다.
즉, Stream Interface의 메서드들이 맨 위에서 봤던 Function Interface를 사용하고 있는 것입니다.
//Stream.filter
Stream<T> filter(Predicate<? super T> predicate)
//Stream.map
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
//Stream.forEach
void forEach(Consumer<? super T> action)
정리
- Functional Interface는 Object 클래스의 메서드를 제외하고 단 하나의 메서드만 가지고 있는 인터페이스를 의미합니다
- 람다식은 기본적으로 "파라미 터부 -> {몸통부}"의 형태를 띠며 평가 결과로 Functional Interface의 인스턴스를 생성할 수 있습니다,
- Stream 인터페이스는 람다식과 결합하여 List를 일괄적으로 처리할 수 있도록 도와준다. 대표적인 메서드로 filter, map, forEach 등이 있다.
이 API를 이용하면 클래스나 메서드를 만들지 않고도 효과적이고 가독성 높은 코드를 작성할 수 있습니다.
'JAVA' 카테고리의 다른 글
Java - Comparable, Comparator (1) | 2019.10.15 |
---|---|
Java 8 - More Functional Interface!! (0) | 2019.10.13 |
Java 8 - Interface바뀐점을 알아보기 (0) | 2019.10.11 |
equals,hashCode 알아보기 (2) | 2019.10.01 |
Lombok 알아보기 (4) | 2019.09.15 |
댓글