Java

Java 8 Functional Interfaces

기록만이살길 2020. 6. 22. 22:27
반응형

1. 소개

이 기사는 Java 8에 존재하는 다양한 기능 인터페이스, 일반적인 사용 사례 및 표준 JDK 라이브러리의 사용법에 대한 안내서입니다.

Java 8의 람다

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

Java 8 이전에는 일반적으로 단일 기능을 캡슐화해야하는 모든 경우에 대한 클래스를 작성했습니다. 이것은 원시 함수 표현으로 사용되는 것을 정의하는 불필요한 보일러 플레이트 코드가 많음을 암시했습니다.

기능 인터페이스와 함께 작동하는 모범 사례 인 Lambdas는 일반적으로 "Lambda Expressions and Functional Interfaces : Tips and Best Practices" 기사에 설명되어 있습니다. 이 안내서는 java.util.function 패키지 에있는 특정 기능 인터페이스에 중점을 둡니다 .

3. Functional Interfaces

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

SAM (Single Abstract Method)이있는 모든 인터페이스는 기능적인 인터페이스 이며 구현은 람다 식으로 취급 될 수 있습니다.

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

4. Functions

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

public interface Function<T, R> { … }

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

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

이 경우 값은 키에 함수를 적용하고 맵에 넣고 메소드 호출에서 반환하여 계산됩니다. 그건 그렇고, 우리는 람다를 전달 된 값 유형과 일치하는 메소드 참조로 바꿀 수 있습니다 .

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

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

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

Function<Integer, String> intToString = Object::toString;
Function<String, String> quote = s -> "'" + s + "'";

Function<Integer, String> quoteIntToString = quote.compose(intToString);

assertEquals("'5'", quoteIntToString.apply(5));

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

5. Primitive Function Specializations

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

예를 들어 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 Function Specializations

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

BiFunction 에는 인수와 리턴 유형이 모두 있으며 ToDoubleBiFunction 및 기타 유형을 사용하면 기본 값을 리턴 할 수 있습니다.

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

키와 이전 값을 수신하여 급여의 새 값을 계산하여 리턴 하는 BiFunction 구현을 사용 하십시오.

Map<String, Integer> 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. Suppliers

공급 기능 인터페이스는 또 다른입니다 기능의 인수를 고려하지 않습니다 전문. 일반적으로 값이 게으른 생성에 사용됩니다. 예를 들어, double 값 을 제곱하는 함수를 정의 해 봅시다 . 가치 자체가 아니라이 가치 의 공급자 를 받습니다 :

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

이를 통해 공급 업체 구현을 사용하여이 함수를 호출하기위한 인수를 느리게 생성 할 수 있습니다 . 이 인수를 생성하는 데 상당한 시간이 걸리면 유용 할 수 있습니다. 우리는 Guava의 sleepUninterruptibly 메소드 를 사용하여 시뮬레이션합니다 :

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

Double valueSquared = squareLazy(lazyValue);

공급 업체의 다른 사용 사례는 시퀀스 생성을위한 로직을 정의하는 것입니다. 이를 입증하기 위해 정적 Stream.generate 메서드를 사용하여 피보나치 수 스트림 을 만듭니다 .

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

Stream.generate 메소드에 전달 된 기능 은 공급 업체 기능 인터페이스를 구현합니다 . 발전기로 유용하려면 공급 업체는 일반적으로 일종의 외부 상태가 필요합니다. 이 경우 상태는 두 개의 마지막 피보나치 시퀀스 번호로 구성됩니다.

이 상태를 구현하려면 람다 내부에 사용 된 모든 외부 변수가 효과적으로 final이어야 하기 때문에 몇 가지 변수 대신 배열을 사용합니다 .

다른 전문 공급 기능 인터페이스를 포함 BooleanSupplier , DoubleSupplier , LongSupplierIntSupplier 그 반환 유형 프리미티브 해당됩니다.

8. Consumers

받는 반대로 공급자소비자가 제네릭 인수와 반환 값 없음을지지 않습니다. 부작용을 나타내는 기능입니다.

예를 들어, 콘솔에서 인사말을 인쇄하여 이름 목록에서 모든 사람을 맞이합시다. List.forEach 메소드에 전달 된 람다 는 소비자 기능 인터페이스를 구현합니다 .

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

의 전문 버전도 있습니다 소비자 - DoubleConsumer , IntConsumerLongConsumer 인수로 원시 값을받을 -. 더 흥미로운 것은 BiConsumer 인터페이스입니다. 사용 사례 중 하나는 맵 항목을 반복하는 것입니다.

Map<String, Integer> 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"));

특수화 된 BiConsumer 버전 의 또 다른 세트는 ObjDoubleConsumer , ObjIntConsumerObjLongConsumer 로 구성되어 있으며 ,이 중 하나는 생성되고 다른 하나는 기본 유형입니다.

9. Predicates

수학 논리에서 술어는 값을 수신하고 부울 값을 리턴하는 함수입니다.

조건부 기능 인터페이스는 특수화의 기능 총칭 값을 수신하고, 부울을 리턴한다. 술어 람다 의 일반적인 유스 케이스 는 값 콜렉션을 필터링하는 것입니다.

List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");

List<String> namesWithA = names.stream()
  .filter(name -> name.startsWith("A"))
  .collect(Collectors.toList());

위 코드에서 Stream API를 사용하여 목록을 필터링 하고 문자 "A"로 시작하는 이름 만 유지합니다. 필터링 논리는 Predicate 구현 에서 캡슐화됩니다 .

이전의 모든 예에서 와 같이 기본 값을 수신하는이 함수의 IntPredicate , DoublePredicateLongPredicate 버전이 있습니다.

10. Operators

운영자 인터페이스는 동일한 값 유형을 수신하고 리턴하는 함수의 특수한 경우입니다. UnaryOperator의 인터페이스는 하나의 인자를 수신한다. Collections API의 사용 사례 중 하나는 목록의 모든 값을 동일한 유형의 계산 된 값으로 바꾸는 것입니다.

List<String> names = Arrays.asList("bob", "josh", "megan");

names.replaceAll(name -> name.toUpperCase());

List.replaceAll의 기능은 반환 무효 가 제자리에있는 값을 대체으로. 목적에 맞게 목록의 값을 변환하는 데 사용되는 람다는 수신 한 것과 동일한 결과 유형을 반환해야합니다. 이것이 UnaryOperator 가 유용한 이유 입니다.

물론 name-> name.toUpperCase () 대신 메소드 참조를 사용할 수 있습니다.

names.replaceAll(String::toUpperCase);

BinaryOperator 의 가장 흥미로운 사용 사례 중 하나는 축소 작업입니다. 모든 값의 합으로 정수 컬렉션을 집계한다고 가정합니다. 함께 스트림 API, 우리는 컬렉터 사용하여이 작업을 수행 할 수 있습니다 , 하지만이 (가) 사용하는 것입니다 할 수있는보다 일반적인 방법으로 줄일 방법 :

List<Integer> values = Arrays.asList(3, 5, 8, 9, 12);

int sum = values.stream()
  .reduce(0, (i1, i2) -> i1 + i2);

저감 방법은 초기 누산기 값과 수신 BinaryOperator의 기능. 이 함수의 인수는 동일한 유형의 값 쌍이며, 함수 자체는 동일한 유형의 단일 값으로 이들을 결합하는 논리를 포함합니다. 전달 된 함수는 연관되어야합니다 . 즉, 값 집계 순서는 중요하지 않습니다. 즉 다음 조건이 유지되어야합니다.

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

BinaryOperator 연산자 함수 의 연관 속성을 사용하면 축소 프로세스를 쉽게 병렬화 할 수 있습니다.

물론 DoubleUnaryOperator , IntUnaryOperator , LongUnaryOperator , DoubleBinaryOperator , IntBinaryOperatorLongBinaryOperator 와 같은 기본 값과 함께 사용할 수있는 UnaryOperatorBinaryOperator특수화 도 있습니다 .

11. 레거시 기능 인터페이스

모든 기능 인터페이스가 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에서 사용할 수 있습니다 .

참고

https://www.baeldung.com/java-8-functional-interfaces

반응형

'Java' 카테고리의 다른 글

Keycloak의 로그인 페이지 사용자 정의  (0) 2021.09.20
Java 8 Stream 불변 Collection  (0) 2020.06.27
Java 8 groupingBy Collector 예제  (1) 2020.06.25
Java 8 Collectors 베스트 예제  (0) 2020.06.24
Java 8 Stream findFirst findAny 차이  (0) 2020.06.21