Java 함수형 인터페이스 - Lambda 표현식 - 메소드 레퍼런스 정리
함수형 인터페이스
- 추상 메소드가 하나만 있는 인터페이스
- 자바8에서부터 @FunctionalInterface Annotation을 지원.
- static과 default로 선언하고 구현한 함수는 '추상 메소드'가 아니므로 괜찮다.
함수형 프로그래밍을 하기 위해서는
- 해당 언어에서 함수(메소드)가 First Class Object여야 한다.
- 해당 함수는 Pure Function이어야 한다.
는 전제조건이 필요하고, 자바는 함수를 First Class Object로 취급하므로 프로그램 언어의 제약은 없다.
cf. 함수형 인터페이스와 lambda는 자바에서 지원하는 문법일 뿐, 함수형 프로그래밍을 강제하는 게 아니다.
First Class Object?
아래의 조건을 만족하는 객체를 First Class Object라고 한다.
- 함수의 매개변수 값, 반환 값으로 사용할 수 있다.
- 변수나 자료구조에 저장할 수 있다.
정수형, 문자열, 여러 자료구조들이 이 조건에 충족한다.
Java, Python 등의 언어에서는 함수도 이 조건에 부합한다.
함수가 함수를 매개변수로 받고, 함수를 리턴할 수 있는 경우 고차원 함수 (High-Order Function) 라고 한다.
참고자료
Pure Function?
수학적 함수라고도 생각할 수 있다. 아래 조건을 만족하는 함수를 말한다.
- 함수 외부에 정의된 값을 변경하지 않는다
- 함수 외부에 정의된 값을 참조하지 않는다.
즉, 같은 입력값을 넣으면 같은 결과값이 나오는 것을 보장하는 함수.
자바의 함수형 인터페이스
java.util.function
패키지 안에 정의되어 있음.
- Function< T,R >
인풋 타입 T, 리턴 타입 R을 리턴하도록 하는 인터페이스- R apply(T) : T를 input으로, R을 output으로 하는 추상메소드
함수 조합용 디폴트 메소드 - andThen
- compose
- R apply(T) : T를 input으로, R을 output으로 하는 추상메소드
package com.inspirit941.java8to11;
import java.util.function.Function;
public class Plus10 implements Function<Integer, Integer> {
@Override
public Integer apply(Integer integer) {
return integer + 10;
}
}
public class RunSomethingImpl {
public static void main(String[] args) {
Plus10 p10 = new Plus10();
p10.apply(1);
// 아래처럼 한 번에 생성할 수도 있다.
Function<Integer, Integer> p11 = (number) -> number + 11;
p11.apply(1);
// 조합 함수 사용법
Function<Integer, Integer> multiply2 = (number) -> number * 2;
// 1. compose.
// multiply의 결과값을 받아서 p11에 입력값으로 넘긴다.
Function<Integer, Integer> multiply2AndPlus11 = p11.compose(multiply2);
multiply2AndPlus11(2);
// return 15 (2*2 + 11)
// 2. andThen
Function<Integer, Integer> Plus11Andmultiply2 = p11.andThen(multiply2);
// return 26 ((2 + 11) * 2)
}
}
- BiFunction< T, U, R >
T, U 두 개의 입력을 받아 R을 리턴하는 인터페이스- R apply(T t, U u);
-
Consumer< T >
받기만 하고 리턴하지 않는 함수.Consumer<Integer> printT = (number) -> System.out.println(number)
- void accept(T t)
-
Supplier< T >
어떤 값을 가져오는 인터페이스. 받아올 값의 타입 T를 정의한다.- T get();
Supplier<Integer> get10 = () -> return 10;
get10.get() // return 10;
- T get();
-
Predicate< T >
인자값을 받아서 True / False 리턴.Predicate<String> startsWithEbay = (string) -> string.startswith("ebay");
조합용 함수- negate() : 결과값에 not 붙이는 함수
- and(), or()
-
UnaryOperator< T >
Function에서 입력값과 결과값의 타입이 같을 경우 사용가능.- Function을 상속받았기 때문에 Function에서 제공하는 메소드 기능은 그대로다.
UnaryOperator<Integer> plus10 = (number) -> number + 10;
- Function을 상속받았기 때문에 Function에서 제공하는 메소드 기능은 그대로다.
-
BinaryOperator< T >
BiFunction의 특수 형태. BiFunction은 input 타입 세 개가 전부 다를 거라는 전제로 만들어진 인터페이스.- Input 인자값 세 개가 전부 같은 타입일 경우 사용할 수 있다.
Lambda 표현식
- 함수형 인터페이스의 인스턴스를 생성하는 방법.
- 코드를 간결하게 생성하며
- 메소드 매개변수, 리턴 타입, 변수로 만들어 사용할 수 있다.
표현식은 아래와 같다.
@FunctionalInterface
public interface RunSomething {
// 함수형 인터페이스 : 인터페이스에 추상 메소드가 "한 개"만 주어져 있는 인터페이스
int doit(int number);
// 자바8에 추가된 기능
// 추상메소드만 한 개인 것이므로, static이나 default로 구현되어 있는 메소드는 해당사항이 없음.
static void printName(){
System.out.println("inspirit941");
}
default void printAge(){
System.out.println("99");
}
}
public class RunSomethingImpl {
public static void main(String[] args) {
// 기존까지 추상메소드에 적용하던 익명 내부클래스 형태.
RunSomething runSomething1 = new RunSomething() {
@Override
public int doit(int number) {
}
};
// 아래 코드처럼로 간결하게 만들 수 있다.
RunSomething runSomething2 = (number) -> {
return number + 10;
};
// 메소드 호출방법은 동일함.
runSomething.doit(1);
lambda에서 외부 변수를 참조하는 것도 가능하다.
단, 참조할 외부 변수의 상태가 final이라는 전제를 하기 때문에,
참조한 외부 변수 값을 변화시키는 코드를 작성할 경우 컴파일 에러가 발생.
int baseNumber = 10;
RunSomething runSomething2 = (number) -> {
return number + baseNumber;
};
baseNumber ++; // 컴파일 에러 발생.
runSomething.doit(1);
lambda 표현식에서 외부 변수를 참조할 경우, 엄밀한 의미의 함수형 프로그래밍은 아니게 된다.
lambda 표현식 상세
핵심 키워드 : 변수 캡처와 Shadowing
lambda를 사용할 경우, 동일한 역할을 하는 내부클래스 / 내부익명클래스와 공통점 & 차이점이 있다.
자바8 이전까지는, 외부변수를 참조하려면 final로 선언해야 했다.
자바8에서 effective final이라는 개념이 도입됨.
- final을 생략하더라도, 정의한 변수가 코드 내에서 값이 변하지 않을 경우 참조할 수 있음.
- 값이 변할 경우 concurrency 문제가 생길 수 있기 때문에 컴파일 에러 발생.
아래 코드의 예시에서, baseNumber라는 값을 참조해도 컴파일 에러가 발생하지 않는다.
package com.inspirit941.java8to11;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
public class testVariableCapture {
public static void main(String[] args) {
testVariableCapture test = new testVariableCapture();
test.run();
}
private void run() {
// 공통점: 여기서 정의한 baseNumber를 아래 세 개에서 전부 참조할 수 있다.
// = 변수 캡쳐 (Variable Capture)
int baseNumber = 10;
// 차이점: Shadowing 여부.
// 1. 로컬 클래스
class LocalClass {
void printBaseNumber(){
int baseNumber = 11;
System.out.println(baseNumber);
// return 11;
}
}
// 2. 익명 클래스
Consumer<Integer> integerConsumer = new Consumer<Integer>(){
@Override
public void accept(Integer baseNumber) {
// 메소드 내의 baseNumber는 더 이상 run 메소드에 정의한 baseNumber가 아니다.
System.out.println(baseNumber);
// 10이 아니라, accept 메소드가 입력받은 값을 리턴한다.
}
};
// 즉, 1과 2는 내부클래스에 별도의 scope이 생성되고,
// 생성된 scope에 정의된 변수가 상위 scope 변수를 가리는 Shadowing이 발생한다.
// 3. lambda
// IntConsumer = Consumer 인터페이스 중 정수형을 받는 인터페이스.
IntConsumer printInt = (number) -> {
// int baseNumber = 11;
// 컴파일 에러 발생. 동일한 scope에서 두 번 정의했기 때문.
System.out.println(number + baseNumber);
};
printInt.accept(10);
// lambda를 사용할 경우, 별도의 scope이 생성되지 않는다.
// 따라서 외부 변수를 내부에서 재정의할 경우 컴파일 에러가 발생하고, shadowing이 발생하지 않는다.
}
}
메소드 레퍼런스
lambda가 하는 일이 기존 메소드 / 생성자를 호출하는 거라면,
메소드 레퍼런스를 사용해서 간결하게 표현할 수 있다.
- 생성자 참조하기
- 특정 객체의 인스턴스 메소드 참조하기
- static 메소드 참조하기
- 임의의 객체에 인스턴스 메소드 참조하기
코드 예시에 주석으로 설명을 달았다.
package com.inspirit941.java8to11;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
public class testMethodReference {
public static void main(String[] args) {
// 생성자 참조하기 - 1. 기본생성자.
// 입력값은 없으나 리턴값은 존재하는 인터페이스: Supplier
Supplier<Greeting> newGreeting = Greeting::new;
// Supplier만으로 객체를 생성하는 것은 아님. get 메소드까지 적용해야 함.
Greeting greeting0 = newGreeting.get();
// 생성자 참조하기 - 2. 인자값을 받는 생성자.
// 문자열을 받아서 객체를 리턴하므로 Function 사용
Function<String, Greeting> newGreeting2 = Greeting::new;
Greeting funcGreeting = newGreeting2.apply("createdByFunction");
funcGreeting.getName();
// return "createdByFunction"
// 인스턴스 메소드 참조하기
Greeting greeting1 = new Greeting();
UnaryOperator<String> hello = greeting1::hello;
System.out.println(hello.apply("this is Name"));
// return "hello this is Name";
// static 메소드 참조하기
UnaryOperator<String> hi = Greeting::hi;
}
}
package com.inspirit941.java8to11;
import java.util.Arrays;
import java.util.Comparator;
public class testInstanceMethodReference {
public static void main(String[] args) {
String[] names = {"name1", "name2", "name3"};
// 정렬 기준을 지정하는 영역에 Comparator 인터페이스를 적용하던 기존 방식.
Arrays.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return 0;
}
});
// lambda 형식으로 사용할 수 있다.
Arrays.sort(names, (Comparator<String>) (o1, o2) -> 0);
// 다시말해, 메소드 참조 방법을 사용하는 것도 가능하다.
// name1 문자열에 name2 문자열을 넣고 비교해서 결과를 리턴하고, 다시 name2에 name3읗 넣고 비교하는 식으로 작동
// 즉, 임의의 인스턴스에 해당 메소드를 참조하는 형태.
Arrays.sort(names, String::compareToIgnoreCase);
}
}