Java 8의 기능 인터페이스

1. 소개

이 기사는 Java 8에있는 다양한 기능 인터페이스, 일반적인 사용 사례 및 표준 JDK 라이브러리에서의 사용에 대한 가이드입니다.

2. Java 8의 Lambda

Java 8은 람다 식의 형태로 강력하고 새로운 구문 개선을 가져 왔습니다. 람다는 메서드에 전달되거나 메서드에서 반환되는 것과 같이 일류 언어 시민으로 처리 할 수있는 익명 함수입니다.

Java 8 이전에는 일반적으로 단일 기능을 캡슐화해야하는 모든 경우에 대해 클래스를 만들었습니다. 이것은 원시 함수 표현으로 사용되는 것을 정의하기 위해 불필요한 상용구 코드가 많이 있음을 의미했습니다.

일반적으로 Lambda, 기능적 인터페이스 및 작업 모범 사례는 "Lambda 표현식 및 기능적 인터페이스 : 팁 및 모범 사례"문서에 설명되어 있습니다. 이 가이드는 java.util.function 패키지 에있는 일부 특정 기능 인터페이스에 중점을 둡니다 .

3. 기능적 인터페이스

모든 기능적 인터페이스에는 유익한 @FunctionalInterface 주석 이있는 것이 좋습니다 . 이는이 인터페이스의 목적을 명확하게 전달할뿐만 아니라 어노테이션이있는 인터페이스가 조건을 충족하지 않는 경우 컴파일러가 오류를 생성 할 수 있도록합니다.

SAM (Single Abstract Method)을 사용하는 모든 인터페이스는 기능적 인터페이스 이며 해당 구현은 람다 식으로 처리 될 수 있습니다.

Java 8의 기본 메소드는 추상 이 아니며 계산되지 않습니다. 기능 인터페이스에는 여전히 여러 기본 메소드 가있을 수 있습니다 . 함수의 문서를 보면 이것을 관찰 할 수 있습니다 .

4. 기능

람다의 가장 간단하고 일반적인 경우는 하나의 값을 받고 다른 값을 반환하는 메서드가있는 기능적 인터페이스입니다. 단일 인수의이 함수는 인수 유형과 반환 값으로 매개 변수화 된 Function 인터페이스로 표시됩니다 .

public interface Function { … }

표준 라이브러리에서 Function 유형 의 용도 중 하나는 키별로 맵에서 값을 반환하지만 키가 맵에 아직없는 경우 값을 계산하는 Map.computeIfAbsent 메서드입니다. 값을 계산하기 위해 전달 된 함수 구현을 사용합니다.

Map nameMap = new HashMap(); Integer value = nameMap.computeIfAbsent("John", s -> s.length());

이 경우 값은 키에 함수를 적용하여 계산하고 맵에 넣은 다음 메서드 호출에서도 반환됩니다. 그런데 람다를 전달되고 반환 된 값 유형과 일치하는 메서드 참조로 대체 할 수 있습니다 .

메서드가 호출되는 객체는 실제로 메서드의 암시 적 첫 번째 인수이므로 인스턴스 메서드 길이 참조를 Function 인터페이스로 캐스팅 할 수 있습니다.

Integer value = nameMap.computeIfAbsent("John", String::length);

기능 인터페이스는 기본이 작성 하나에 여러 기능을 결합하고 순차적으로 실행할 수 있습니다 방법 :

Function intToString = Object::toString; Function quote = s -> "'" + s + "'"; Function quoteIntToString = quote.compose(intToString); assertEquals("'5'", quoteIntToString.apply(5));

quoteIntToString의 함수의 조합 시세 의 결과에 적용되는 함수 intToString 기능.

5. 원시 함수 전문화

기본 유형은 제네릭 유형 인수가 될 수 없으므로 가장 많이 사용되는 기본 유형 double , int , long 및 인수 및 반환 유형의 조합에 대한 Function 인터페이스 버전이 있습니다.

  • IntFunction , LongFunction , DoubleFunction : 인수가 지정된 유형이고 반환 유형이 매개 변수화 됨
  • ToIntFunction , ToLongFunction , ToDoubleFunction : 반환 유형이 지정된 유형이고 인수가 매개 변수화 됨
  • DoubleToIntFunction , DoubleToLongFunction , IntToDoubleFunction , IntToLongFunction , LongToIntFunction , LongToDoubleFunction — 인수와 반환 유형 모두 이름에 지정된 기본 유형으로 정의 됨

예를 들어 short 를 취하고 byte를 반환하는 함수에 대한 기본 기능 인터페이스는 없지만 직접 작성하는 것을 막을 수는 없습니다.

@FunctionalInterface public interface ShortToByteFunction { byte applyAsByte(short s); }

이제 ShortToByteFunction에 의해 정의 된 규칙을 사용하여 short 배열을 바이트 배열로 변환하는 메서드를 작성할 수 있습니다 .

public byte[] transformArray(short[] array, ShortToByteFunction function) { byte[] transformedArray = new byte[array.length]; for (int i = 0; i < array.length; i++) { transformedArray[i] = function.applyAsByte(array[i]); } return transformedArray; }

다음은이를 사용하여 short 배열을 바이트 배열에 2를 곱한 배열로 변환하는 방법입니다.

short[] array = {(short) 1, (short) 2, (short) 3}; byte[] transformedArray = transformArray(array, s -> (byte) (s * 2)); byte[] expectedArray = {(byte) 2, (byte) 4, (byte) 6}; assertArrayEquals(expectedArray, transformedArray);

6. Two-Arity 함수 전문화

두 개의 인수로 람다를 정의하려면 BiFunction , ToDoubleBiFunction , ToIntBiFunctionToLongBiFunction 이라는 이름에 " Bi" 키워드 를 포함하는 추가 인터페이스를 사용해야 합니다.

BiFunction 에는 인수와 반환 유형이 모두 생성 된 반면 ToDoubleBiFunction 및 기타 기능을 사용하면 원시 값을 반환 할 수 있습니다.

표준 API에서이 인터페이스를 사용하는 일반적인 예 중 하나는 Map.replaceAll 메소드에 있습니다.이를 통해지도의 모든 값을 일부 계산 된 값으로 바꿀 수 있습니다.

급여에 대한 새 값을 계산하고 반환하기 위해 키와 이전 값을받는 BiFunction 구현을 사용합시다 .

Map salaries = new HashMap(); salaries.put("John", 40000); salaries.put("Freddy", 30000); salaries.put("Samuel", 50000); salaries.replaceAll((name, oldValue) -> name.equals("Freddy") ? oldValue : oldValue + 10000);

7. 공급자

The Supplier functional interface is yet another Function specialization that does not take any arguments. It is typically used for lazy generation of values. For instance, let's define a function that squares a double value. It will receive not a value itself, but a Supplier of this value:

public double squareLazy(Supplier lazyValue) { return Math.pow(lazyValue.get(), 2); }

This allows us to lazily generate the argument for invocation of this function using a Supplier implementation. This can be useful if the generation of this argument takes a considerable amount of time. We'll simulate that using Guava's sleepUninterruptibly method:

Supplier lazyValue = () -> { Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS); return 9d; }; Double valueSquared = squareLazy(lazyValue);

Another use case for the Supplier is defining a logic for sequence generation. To demonstrate it, let’s use a static Stream.generate method to create a Stream of Fibonacci numbers:

int[] fibs = {0, 1}; Stream fibonacci = Stream.generate(() -> { int result = fibs[1]; int fib3 = fibs[0] + fibs[1]; fibs[0] = fibs[1]; fibs[1] = fib3; return result; });

The function that is passed to the Stream.generate method implements the Supplier functional interface. Notice that to be useful as a generator, the Supplier usually needs some sort of external state. In this case, its state is comprised of two last Fibonacci sequence numbers.

To implement this state, we use an array instead of a couple of variables, because all external variables used inside the lambda have to be effectively final.

Other specializations of Supplier functional interface include BooleanSupplier, DoubleSupplier, LongSupplier and IntSupplier, whose return types are corresponding primitives.

8. Consumers

As opposed to the Supplier, the Consumer accepts a generified argument and returns nothing. It is a function that is representing side effects.

For instance, let’s greet everybody in a list of names by printing the greeting in the console. The lambda passed to the List.forEach method implements the Consumer functional interface:

List names = Arrays.asList("John", "Freddy", "Samuel"); names.forEach(name -> System.out.println("Hello, " + name));

There are also specialized versions of the ConsumerDoubleConsumer, IntConsumer and LongConsumer — that receive primitive values as arguments. More interesting is the BiConsumer interface. One of its use cases is iterating through the entries of a map:

Map ages = new HashMap(); ages.put("John", 25); ages.put("Freddy", 24); ages.put("Samuel", 30); ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));

Another set of specialized BiConsumer versions is comprised of ObjDoubleConsumer, ObjIntConsumer, and ObjLongConsumer which receive two arguments one of which is generified, and another is a primitive type.

9. Predicates

In mathematical logic, a predicate is a function that receives a value and returns a boolean value.

The Predicate functional interface is a specialization of a Function that receives a generified value and returns a boolean. A typical use case of the Predicate lambda is to filter a collection of values:

List names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David"); List namesWithA = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList());

In the code above we filter a list using the Stream API and keep only names that start with the letter “A”. The filtering logic is encapsulated in the Predicate implementation.

As in all previous examples, there are IntPredicate, DoublePredicate and LongPredicate versions of this function that receive primitive values.

10. Operators

Operator interfaces are special cases of a function that receive and return the same value type. The UnaryOperator interface receives a single argument. One of its use cases in the Collections API is to replace all values in a list with some computed values of the same type:

List names = Arrays.asList("bob", "josh", "megan"); names.replaceAll(name -> name.toUpperCase());

The List.replaceAll function returns void, as it replaces the values in place. To fit the purpose, the lambda used to transform the values of a list has to return the same result type as it receives. This is why the UnaryOperator is useful here.

Of course, instead of name -> name.toUpperCase(), you can simply use a method reference:

names.replaceAll(String::toUpperCase);

One of the most interesting use cases of a BinaryOperator is a reduction operation. Suppose we want to aggregate a collection of integers in a sum of all values. With Stream API, we could do this using a collector, but a more generic way to do it would be to use the reduce method:

List values = Arrays.asList(3, 5, 8, 9, 12); int sum = values.stream() .reduce(0, (i1, i2) -> i1 + i2); 

The reduce method receives an initial accumulator value and a BinaryOperator function. The arguments of this function are a pair of values of the same type, and a function itself contains a logic for joining them in a single value of the same type. Passed function must be associative, which means that the order of value aggregation does not matter, i.e. the following condition should hold:

op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)

The associative property of a BinaryOperator operator function allows to easily parallelize the reduction process.

Of course, there are also specializations of UnaryOperator and BinaryOperator that can be used with primitive values, namely DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator, and LongBinaryOperator.

11. Legacy Functional Interfaces

모든 기능 인터페이스가 Java 8에 등장한 것은 아닙니다. 이전 버전의 Java에서 나온 많은 인터페이스가 FunctionalInterface 의 제약 조건을 따르며 람다로 사용할 수 있습니다. 눈에 띄는 예는 동시성 API에서 사용되는 RunnableCallable 인터페이스입니다. Java 8에서는 이러한 인터페이스도 @FunctionalInterface 주석으로 표시됩니다 . 이를 통해 동시성 코드를 크게 단순화 할 수 있습니다.

Thread thread = new Thread(() -> System.out.println("Hello From Another Thread")); thread.start();

12. 결론

이 기사에서는 람다 식으로 사용할 수있는 Java 8 API에있는 다양한 기능 인터페이스를 설명했습니다. 기사의 소스 코드는 GitHub에서 사용할 수 있습니다.