Java 8 함수형 인터페이스 예제 튜토리얼에 오신 것을 환영합니다. Java는 항상 객체 지향 프로그래밍 언어였습니다. 즉, 자바 프로그래밍에서 모든 것이 객체를 중심으로 회전합니다(간단함을 위해 일부 원시 타입 제외). 자바에는 함수만 있는 것이 아니라, 클래스의 일부이며 함수를 호출하려면 클래스/객체를 사용해야 합니다.
Java 8 함수형 인터페이스
C++, JavaScript와 같은 다른 프로그래밍 언어를 살펴보면, 필요할 때 함수를 작성하고 사용할 수 있기 때문에 함수형 프로그래밍 언어라고 합니다. 이 언어들은 객체 지향 프로그래밍과 함수형 프로그래밍을 모두 지원합니다. 객체 지향적이라는 것이 나쁜 것은 아니지만, 프로그램에 많은 장황함을 가져옵니다. 예를 들어, Runnable의 인스턴스를 생성해야 한다고 가정해 보겠습니다. 보통 우리는 아래와 같이 익명 클래스를 사용하여 이를 수행합니다.
Runnable r = new Runnable(){
@Override
public void run() {
System.out.println("My Runnable");
}};
위 코드를 보면, 실제로 사용되는 부분은 run() 메소드 안의 코드입니다. 나머지 모든 코드는 자바 프로그램의 구조 때문입니다. Java 8 함수형 인터페이스와 람다 표현식은 많은 보일러 플레이트 코드를 제거함으로써 더 작고 깔끔한 코드를 작성하는 데 도움을 줍니다.
Java 8 기능 인터페이스
정확히 하나의 추상 메서드를 갖는 인터페이스를 기능 인터페이스라고합니다. @FunctionalInterface
주석을 추가하여 인터페이스를 기능 인터페이스로 표시 할 수 있습니다. 사용은 필수가 아니지만, 기능 인터페이스에 대한 추가 메서드가 실수로 추가되는 것을 피하기 위해 사용하는 것이 좋습니다. 인터페이스가 @FunctionalInterface
주석으로 주석이 달려 있고 둘 이상의 추상 메서드를 가지려고하면 컴파일러 오류가 발생합니다. Java 8 기능 인터페이스의 주요 이점은 람다 표현식을 사용하여 인스턴스화하고 비대한 익명 클래스 구현을 피할 수 있다는 것입니다. Java 8 Collections API가 다시 작성되었으며 새로운 Stream API가 도입되었으며 이 API는 많은 기능 인터페이스를 사용합니다. Java 8은 java.util.function
패키지에 많은 기능 인터페이스를 정의했습니다. 일부 유용한 Java 8 기능 인터페이스에는 Consumer
, Supplier
, Function
및 Predicate
가 있습니다. 이에 대한 자세한 내용은 Java 8 Stream Example에서 찾을 수 있습니다. java.lang.Runnable
은 run()
이라는 단일 추상 메서드를 갖는 기능 인터페이스의 훌륭한 예입니다. 아래 코드 조각은 기능 인터페이스에 대한 일부 지침을 제공합니다:
interface Foo { boolean equals(Object obj); }
// Equals는 이미 암시적 멤버(Object 클래스)이기 때문에 기능하지 않음
interface Comparator {
boolean equals(Object obj);
int compare(T o1, T o2);
}
// Comparator에는 하나의 추상 비-Object 메서드만 있으므로 기능함
interface Foo {
int m();
Object clone();
}
// 메서드 Object.clone이 공개되어 있지 않으므로 기능하지 않음
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// 두 메서드 모두 같은 시그니처를 가지고 있지만, 기능함
interface X { Iterable m(Iterable arg); }
interface Y { Iterable m(Iterable arg); }
interface Z extends X, Y {}
// Y.m은 부시그니처 및 반환 유형 치환 가능하므로 기능함
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// 모든 추상 메서드의 부시그니처를 가진 메서드가 없으므로 기능하지 않음
interface X { int m(Iterable arg, Class c); }
interface Y { int m(Iterable arg, Class > c); }
interface Z extends X, Y {}
// 모든 추상 메서드의 부시그니처를 가진 메서드가 없으므로 기능하지 않음
interface X { long m(); }
interface Y { int m(); }
interface Z extends X, Y {}
// 컴파일러 오류: 반환 유형 치환 가능한 메서드가 없음
interface Foo { void m(T arg); }
interface Bar { void m(T arg); }
interface FooBar extends Foo, Bar {}
// 서로 다른 시그니처이지만, 같은 치환을 가지고 있어 컴파일러 오류 발생
람다 표현식
람다 표현식은 자바 객체 지향 세계에서 함수형 프로그래밍을 시각화할 수 있는 방법입니다. 객체는 자바 프로그래밍 언어의 기본이며, 객체 없이는 함수를 가질 수 없습니다. 이것이 자바 언어가 람다 표현식을 함수형 인터페이스와 함께 사용할 수 있도록 지원하는 이유입니다. 함수형 인터페이스에는 하나의 추상 함수만 있으므로 메서드에 람다 표현식을 적용할 때 혼란이 없습니다. 람다 표현식의 구문은 (인자) -> (본문)입니다. 이제 위의 익명 Runnable을 람다 표현식을 사용하여 어떻게 작성할 수 있는지 살펴보겠습니다.
Runnable r1 = () -> System.out.println("My Runnable");
위의 람다 표현식에서 무엇이 일어나고 있는지 이해해 봅시다.
- Runnable는 함수형 인터페이스이므로 람다 표현식을 사용하여 인스턴스를 만들 수 있습니다.
- run() 메서드는 인수를 사용하지 않으므로 람다 표현식도 인수를 사용하지 않습니다.
- if-else 블록과 마찬가지로 메서드 본문에 단일 문이 있으므로 중괄호 ({})를 피할 수 있습니다. 여러 문이 있는 경우 다른 메서드와 마찬가지로 중괄호를 사용해야 합니다.
왜 람다 표현식이 필요한가요
-
줄어든 코드 줄 람다 표현식을 사용하는 명백한 이점 중 하나는 코드 양이 줄어든다는 것입니다. 익명 클래스를 사용하는 대신 람다 표현식을 사용하여 함수형 인터페이스의 인스턴스를 만드는 방법을 이미 살펴보았습니다.
-
순차 및 병렬 실행 지원 람다 표현식을 사용하는 또 다른 이점은 스트림 API의 순차 및 병렬 작업 지원을 활용할 수 있다는 것입니다. 이를 설명하기 위해 숫자가 소수인지 아닌지를 테스트하는 메서드를 작성해야 하는 간단한 예를 살펴보겠습니다. 전통적으로 우리는 아래와 같이 코드를 작성할 것입니다. 이 코드는 완전히 최적화되지는 않았지만 예제 목적으로는 좋습니다.
//전통적인 방법 private static boolean isPrime(int number) { if(number < 2) return false; for(int i=2; i<number; i++){ if(number % i == 0) return false; } return true; }
위의 코드의 문제점은 순차적인 성격이라는 것입니다. 숫자가 매우 큰 경우 의미있는 시간이 소요될 것입니다. 코드의 또 다른 문제는 많은 종료 지점이 있으며 가독성이 떨어진다는 것입니다. 람다 표현식과 스트림 API를 사용하여 동일한 메서드를 작성하는 방법을 살펴보겠습니다.
//선언적 접근 private static boolean isPrime(int number) { return number > 1 && IntStream.range(2, number).noneMatch( index -> number % index == 0); }
IntStream
은 순차 및 병렬 집계 작업을 지원하는 기본 int 값 요소의 시퀀스입니다. 이것은Stream
의 int 기본 특수화입니다. 더 읽기 쉽게하기 위해 메서드를 아래와 같이 작성할 수도 있습니다.private static boolean isPrime(int number) { IntPredicate isDivisible = index -> number % index == 0; return number > 1 && IntStream.range(2, number).noneMatch( isDivisible); }
만약 IntStream에 익숙하지 않다면, range() 메서드는 시작(포함)부터 종료(제외))까지 1씩 증가하는 순차적으로 정렬된 IntStream을 반환합니다. noneMatch() 메서드는이 스트림의 요소 중 제공된 예측과 일치하는 요소가 없는지 여부를 반환합니다. 결과를 결정하는 데 필요하지 않은 경우에는 모든 요소에 대해 예측을 평가하지 않을 수 있습니다.
-
메서드로 동작 전달하기 람다 표현식을 사용하여 메서드의 동작을 전달하는 방법을 살펴보겠습니다. 간단한 예제로 메서드를 작성해야 한다고 가정해 보겠습니다. 주어진 기준과 일치하는 경우 목록의 숫자를 합산하는 메서드를 작성해야 합니다. 우리는 Predicate를 사용하여 다음과 같은 메서드를 작성할 수 있습니다.
public static int sumWithCondition(List<Integer> numbers, Predicate<Integer> predicate) { return numbers.parallelStream() .filter(predicate) .mapToInt(i -> i) .sum(); }
샘플 사용법:
//모든 숫자의 합 sumWithCondition(numbers, n -> true) //짝수의 합 sumWithCondition(numbers, i -> i%2==0) //5보다 큰 숫자의 합 sumWithCondition(numbers, i -> i>5)
-
게으름으로 인한 높은 효율성 람다 표현식을 사용하는 또 다른 장점은 게으른 평가입니다. 예를 들어, 범위 3에서 11까지의 최대 홀수를 찾고 그 제곱을 반환하는 메서드를 작성해야 한다고 가정해 보겠습니다. 보통 이 메서드에 대한 코드를 다음과 같이 작성할 것입니다:
private static int findSquareOfMaxOdd(List<Integer> numbers) { int max = 0; for (int i : numbers) { if (i % 2 != 0 && i > 3 && i < 11 && i > max) { max = i; } } return max * max; }
위의 프로그램은 항상 순차적으로 실행되지만 Stream API를 사용하여 이를 달성하고 게으름을 추구할 수 있습니다. 함수형 프로그래밍 방식과 람다 표현식을 사용하여이 코드를 어떻게 다시 작성할 수 있는지 살펴 보겠습니다.
public static int findSquareOfMaxOdd(List<Integer> numbers) { return numbers.stream() .filter(NumberTest::isOdd) //Predicate is functional interface and .filter(NumberTest::isGreaterThan3) // we are using lambdas to initialize it .filter(NumberTest::isLessThan11) // rather than anonymous inner classes .max(Comparator.naturalOrder()) .map(i -> i * i) .get(); } public static boolean isOdd(int i) { return i % 2 != 0; } public static boolean isGreaterThan3(int i){ return i > 3; } public static boolean isLessThan11(int i){ return i < 11; }
더블 콜론(::) 연산자에 놀라셨다면, 이것은 자바 8에서 도입되었으며 메서드 참조에 사용됩니다. 자바 컴파일러는 인수를 호출된 메서드에 매핑하는 것을 처리합니다. 이것은 람다 표현식
i -> isGreaterThan3(i)
또는i -> NumberTest.isGreaterThan3(i)
의 짧은 형식입니다.
람다 표현식 예제
아래에서는 람다 표현식에 대한 몇 가지 코드 조각과 그에 대한 간단한 설명을 제공합니다.
() -> {} // No parameters; void result
() -> 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
// 복잡한 블록 본문과 여러 반환
() -> {
if (true) return 10;
else {
int result = 15;
for (int i = 1; i < 10; i++)
result *= i;
return result;
}
}
(int x) -> x+1 // Single declared-type argument
(int x) -> { return x+1; } // same as above
(x) -> x+1 // Single inferred-type argument, same as below
x -> x+1 // Parenthesis optional for single inferred-type case
(String s) -> s.length() // Single declared-type argument
(Thread t) -> { t.start(); } // Single declared-type argument
s -> s.length() // Single inferred-type argument
t -> { t.start(); } // Single inferred-type argument
(int x, int y) -> x+y // Multiple declared-type parameters
(x,y) -> x+y // Multiple inferred-type parameters
(x, final y) -> x+y // Illegal: can't modify inferred-type parameters
(x, int y) -> x+y // Illegal: can't mix inferred and declared types
메서드 및 생성자 참조
A method reference is used to refer to a method without invoking it; a constructor reference is similarly used to refer to a constructor without creating a new instance of the named class or array type. Examples of method and constructor references:
System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new
그것은 모두 Java 8 기능 인터페이스와 람다 표현식 자습서입니다. 이 구문은 Java에 새로운 것이므로 그것을 사용하는 것을 강력히 제안합니다. 그것을 이해하는 데 시간이 걸릴 것입니다. 또한 Java 8 릴리스에서의 모든 개선 사항과 변경 사항을 배우기 위해 Java 8 기능을 확인하는 것이 좋습니다.
Source:
https://www.digitalocean.com/community/tutorials/java-8-functional-interfaces