1. 소개

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

2. Java 8의 Lambda

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

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

"Lambda 식 및 기능 인터페이스 : 팁 및 모범 사례" 문서에서는 기능 인터페이스 와 람다 작업 모범 사례에 대해 자세히 설명합니다. 이 가이드는 java.util.function 패키지 에있는 일부 특정 기능 인터페이스에 중점을 둡니다 .

3. 기능적 인터페이스

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

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

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

4. 기능

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

public interface Function<T, R> { … }

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

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

이 경우 키에 함수를 적용하고 맵에 넣고 메서드 호출에서 반환하여 값을 계산합니다. W e는 전달 및 반환 된 값 유형과 일치하는 메서드 참조로 람다를 대체 할 수 있습니다 .

우리가 메서드를 호출하는 객체는 사실 메서드의 암시 적 첫 번째 인수라는 것을 기억하십시오. 이를 통해 인스턴스 메서드 길이 참조를 Function 인터페이스 로 캐스팅 할 수 있습니다.

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. 원시 함수 전문화

기본 유형은 제네릭 유형 인수가 될 수 없으므로 가장 많이 사용되는 기본 유형 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;
}

단락 배열을 바이트 배열에 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 메소드에 있습니다.이를 통해Map의 모든 값을 일부 계산 된 값으로 바꿀 수 있습니다.

급여에 대한 새 값을 계산하고 반환하기 위해 키와 이전 값을받는 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. 공급자

공급 기능 인터페이스는 또 다른입니다 기능의 인수를 고려하지 않습니다 전문. 우리는 일반적으로 값의 지연 생성에 사용합니다. 예를 들어, 이중을 제곱하는 함수를 정의 해 보겠습니다 . 값 자체는받지 않지만 다음 값 공급자 :

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. 소비자

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

예를 들어, 콘솔에 인사말을 인쇄하여 이름 List에있는 모든 사람을 맞이해 봅시다. 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. 술어

수학적 논리에서 술어는 값을 받고 부울 값을 반환하는 함수입니다.

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

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를 사용하여 List을 필터링 하고 문자 "A"로 시작하는 이름 만 유지합니다. 술어 구현 필터링 로직을 캡슐화한다.

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

10. 연산자

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

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

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

List.replaceAll의 기능은 반환 무효  가 제자리에있는 값을 대체한다. 목적에 맞게 List의 값을 변환하는 데 사용되는 람다는 수신 한 것과 동일한 결과 유형을 반환해야합니다. 이것이 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 연산자 함수 의 연관 속성을 사용하면 축소 프로세스를 쉽게 병렬화 할 수 있습니다.

물론, 전문의도있다 UnaryOperatorBinaryOperator 프리미티브 값, 즉 함께 사용할 수 DoubleUnaryOperator , IntUnaryOperator , LongUnaryOperator , DoubleBinaryOperator , IntBinaryOperatorLongBinaryOperator이 .

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에서 사용할 수 있습니다 .