함수형 인터페이스 (Functional Interface)
- 추상 메소드를 단 하나만 가지고 있는 인터페이스
- @FuncationInterface 애노테이션을 가지고 있는 인터페이스
@FunctionalInterface
public interface RunSomething {
void doIt();
static void printName() {
System.out.println("Kang");
}
default void printAge() {
System.out.println("30");
}
}
이 인터페이스는 함수형 인터페이스가 아닌것 처럼 생겼지만 함수형 인터페이스가 맞다. 왜냐하면 추상 메소드를 단 하나만 가지고 있으며 @FuncationInterface 애노테이션을 가지고 있다. static이나 default로 선언된 함수가 존재해도 추상 메소드는 단 하나만 있기 때문에 함수형 인터페이스로 볼 수 있다.
@FunctionalInterface
public interface RunSomething {
void doIt();
void doIt2(); // 추상 메소드가 2개 이상이므로 에러가 남
static void printName() {
System.out.println("Kang");
}
default void printAge() {
System.out.println("30");
}
}
반대로 이 인터페이스는 추상 메소드를 두개 갖고 있기 때문에 함수형 인터페이스가 아니다. @FuncationInterface 애노테이션을 가지고 있는 인터페이스에 추상메소드를 두개 선언하면 @FuncationInterface 애노테이션 부분에 에러가뜬다.
람다 표현식 (Lambda Expressions)
- 함수형 인터페이스의 인스턴스를 만드는 방법으로 쓰일 수 있다.
- 코드를 줄일 수 있다.
- 메소드 매개변수, 리턴 타입, 변수로 만들어 사용할 수 있다.
public class Foo {
public static void main(String[] args) {
// 익명 내부 클래스
RunSomething runSomething1 = new RunSomething() {
@Override
public void doIt() {
System.out.println("Hello World");
}
};
runSomething1.doIt();
// 람다 표현식
RunSomething runSomething2 = () -> System.out.println("Hello World");
// 함수형 인터페이스를 인라인로 구현한 오브젝트로 볼 수 있음
runSomething2.doIt();
}
}
위 코드는 RunSomething이란 함수형 인터페이스를 선언해서 사용하는 두가지 케이스다. 첫번째는 익명 내부 클래스 형태로 선언한 케이스고 두번째는 람다 표현식으로 작성한 코드이다.
자바에서 함수형 프로그래밍
- 함수를 First Class Object로 사용할 수 있다.
- 순수 함수 (Pure Function)
- 사이드 이펙트가 없다 -> 함수 밖에 있는 값을 변경하지 않는다
- 상태가 없다 -> 함수 밖에 있는 값을 사용하지 않는다.
- 고차 함수 (Higher-Order Function)
- 함수가 함수를 매개변수로 받을 수 있고 함수를 리턴할 수도 있다.
- 불변성
자바에서 제공하는 함수형 인터페이스
Function<T, R>
- T 타입을 받아서 R 타입을 리턴하는 함수 인터페이스
- R apply(T t)
- 함수 조합용 메소드
- andThen
- compose
Function<Integer, Integer> plus5 = (i) -> i + 5;
System.out.println(plus5.apply(1));
Function<Integer, Integer> multiply2 = (i) -> i * 2;
Function<Integer, Integer> multiply2AndAdd10 = plus5.compose(multiply2); // multiply2 실행 후 add10
System.out.println(multiply2AndAdd10.apply(2));
Function<Integer, Integer> add10AndThenMultiply = plus5.andThen(multiply2); // add10 실행 후 multiply2
System.out.println(add10AndThenMultiply.apply(2));
BiFunction<T, U, R>
- 두개의 값(T, U)를 받아서 R타입을 리넡하는 함수 인터페이스
- R apply(T t, U r)
BiFunction<Integer, Integer, Integer> multiply = (i, j) -> i * j;
System.out.println(multiply.apply(3, 4));
Consumer<T>
- T 타입을 받아서 아무값도 리턴하지 않는 함수 인터페이스
- void Accept(T t)
- 함수 조합용 메소드
- andThen
Consumer<Integer> printT = i -> System.out.println(i);
printT.accept(10);
Consumer<Integer> printT = System.out::println; // 줄여서 사용 가능
Supplier<T>
- T 타입의 값을 제공하는 함수 인터페이스
- T get()
Supplier<Integer> get10 = () -> 10;
System.out.println(get10);
Predicate<T>
- T 타입을 받아서 boolean을 리턴하는 함수 인터페이스
- boolean test(T t)
- 함수 조합용 메소드
- And
- Or
- Negate
Predicate<String> startWithKang = (s) -> s.startsWith("Kang");
Predicate<Integer> isEven = (i) -> i % 2 == 0;
UnaryOperator<T>
- Function<T, R>의 특수한 형태로 입력값 하나를 받아서 동일한 타입을 리턴하는 함수 인터페이스
UnaryOperator<Integer> plus4 = (i) -> i + 4;
UnaryOperator<Integer> multiply4 = (i) -> i * 4;
BinaryOperator<T>
- BiFunction<T, U, R>의 특수한 형태로, 동일한 타입의 입력값 두개를 받아 리턴하는 함수 인터페이스
BinaryOperator<Integer> plus = (i, j) -> i + j;
System.out.println(plus.apply(10, 20));
람다 표현식
람다
- (인자 리스트) -> {바디}
인자 리스트
- 인자가 없을 때 : ()
- 인자가 한개일 때 : (one) 또는 one
- 인자가 여러개 일 때 : (one, two)
- 인자의 타입은 생략 가능, 컴파일러가 추론 하지만 명시할 수도 있다. (Integer one, Integer two)
UnaryOperator<Integer> plus4 = (i) -> i + 4;
UnaryOperator<Integer> plus4 = (Integer i) -> i + 4;
바디
- 화살표 오른쪽에 함수 본문을 정의한다
- 여러 줄인 경우에 {}를 사용해서 묶는다
- 한줄인 경우 생략 가능, return도 생략 가능
UnaryOperator<Integer> plus4 = (i) -> i + 4;
UnaryOperator<Integer> plus4 = (i) -> {
return i + 4;
};
변수 캡쳐 (Variable Capture)
- 로컬 변수 캡처
- final이거나 effective final인 경우에만 참조할 수 있다.
- 그렇지 않을 경우 concurrency 문제가 생길 수 있어서 컴파일러가 방지한다.
- effective final
- 사실상 'final'인 변수
- final 키워드를 사용하지 않은 변수를 익명 클래스 구현체 또는 람다에서 참조할 수 있다.
- 익명 클래스 구현체와 달리 쉐도잉 하지 않는다
- 익명 클래스는 새로 스콥을 만들지만, 람다는 람다를 감싸고 있는 스콥과 같다.
public class Foo3 {
public static void main(String[] args) {
Foo3 foo = new Foo3();
foo.run();
}
private void run() {
int baseNumber = 10;
// 로컬 클래스
class LocalClass {
void printBaseNumber() {
System.out.println(baseNumber);
}
}
// 익명 클래스
Consumer<Integer> integerConsumer = new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
System.out.println(baseNumber);
}
};
// 람다
IntConsumer printInt = (i) -> {
System.out.println(i + baseNumber);
};
printInt.accept(10);
}
}
공통점
위 코드에서 baseNumber는 effective final인 상태이다. final 키워드가 없지만 코드에서 값 변화가 없기 때문이다. 그렇기 때문에 로컬 클래스, 익명클래스, 람다에서 모두 참조가 가능하다.
IntConsumer printInt = (i) -> {
baseNumber++; // 에러
System.out.println(i + baseNumber);
};
하지만 위처럼 baseNumber를 조작하게되면 더이상 effective final이지 않게 되므로 에러가 발생한다
차이점
public class Foo3 {
public static void main(String[] args) {
Foo3 foo = new Foo3();
foo.run();
}
private void run() {
int baseNumber = 10;
// 로컬 클래스
class LocalClass {
void printBaseNumber() {
int baseNumber = 11;
System.out.println(baseNumber); // 11 -> 섀도잉
}
}
// 익명 클래스
Consumer<Integer> integerConsumer = new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
int baseNumber = 11;
System.out.println(baseNumber); // 11 -> 섀도잉
}
};
IntConsumer printInt = (i) -> {
int baseNumber = 11; // 에러
System.out.println(i + baseNumber);
};
printInt.accept(10);
baseNumber++; // effective final이 아니게 되어서 람다에서 참조가 불가능
}
}
로컬 클래스와 익명 클래스는 그 자체가 하나의 scope이다. 하지만 람다는 별도의 scope가 아니다. 따라서 동일한 이름을 갖는 변수를 선언 할 수 없다. 또한 람다는 외부 변수가 effective final이 아닌 경우 참조가 불가능 하다.
메소드 레퍼런스
람다가 하는일이 기존 메소드 또는 생성자를 호출하는 거라면, 메소드 레퍼런스를 사용해서 매우 간결하게 표현할 수 있다.
스태틱 메소드 참조 | 타입::스태틱 메소드 |
특정 객체의 인스턴스 메소드 참조 | 객체 레퍼런스::인스턴스 메소드 |
임의 객체의 인스턴스 메소드 참조 | 타입::인스턴스 메소드 |
생성자 참조 | 타입::new |
- 메소드 또는 생성자의 매개변수로 람다의 입력값을 받는다.
- 리턴값 또는 생성한 객체는 람다의 리턴값이다.
public class Greeting {
private String name;
public Greeting() {
}
public Greeting(String name) {
this.name = name;
}
public String getName() {
return name;
}
public String hello(String name) {
return "hello " + name;
}
public static String hi(String name) {
return "hi " + name;
}
}
public class Foo4 {
public static void main(String[] args) {
Greeting greeting = new Greeting();
UnaryOperator<String> hi = (s) -> "hi " + s;
UnaryOperator<String> hi2 = Greeting::hi; // Greeting.hi 메소드 생성
UnaryOperator<String> hello = greeting::hello; // Greeting.hello 메소드 생성
Supplier<Greeting> newGreeting = Greeting::new; // 매개변수가 없는 생성자를 사용
Greeting kang1 = newGreeting.get();
System.out.println(kang1.getName()); // null
Function<String, Greeting> kangGreeting = Greeting::new; // 매개변수가 있는 생성자를 사용
Greeting kang2 = kangGreeting.apply("Kang");
System.out.println(kang2.getName()); // Kang
String[] names = {"Kang", "Min", "Hyeong"};
Arrays.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareToIgnoreCase(o2);
}
});
Arrays.sort(names, (o1, o2) -> o1.compareToIgnoreCase(o2));
Arrays.sort(names, String::compareToIgnoreCase); // 임의의 인스턴스 메소드 참조
System.out.println(Arrays.toString(names));
}
}
- UnaryOperator<String> hi2 = Greeting::hi; 의 경우 별다른 객체 생성 없이 static으로 메소드를 생성하고 있다. 따라서 hi 함수는 static으로 선언되어 있어야 한다.
- UnaryOperator<String> hello = greeting::hello; 는 이미 선언된 객체의 hello 메소드를 생성한다.
- Supplier<Greeting> newGreeting = Greeting::new; 는 매개변수가 없는 생성자를 사용한다. Supplier가 입력값을 받지 않는 함수형 인터페이스이기 때문이다.
- Function<String, Greeting> kangGreeting = Greeting::new; 는 매개변수가 있는 생성자를 사용한다. Function이 입력값을 받아서 리턴하는 함수형 인터페이스이기 때문이다.
- 위 코드의 sort는 모두 같은 로직을 수행한다. 첫번째는 Comparator라는 함수형 인터페이스를 사용해서 comapre 함수를 로버라이딩하여 작성한 케이스이고, 두번째는 이를 람다로 표현한 형태이다. 마지막은 String에서 제공하는 메소드 레퍼런스를 사용한 형태이다.
참고자료
더 자바, Java 8 - 인프런 | 강의
자바 8에 추가된 기능들은 자바가 제공하는 API는 물론이고 스프링 같은 제 3의 라이브러리 및 프레임워크에서도 널리 사용되고 있습니다. 이 시대의 자바 개발자라면 반드시 알아야 합니다. 이
www.inflearn.com
'java' 카테고리의 다른 글
Design Patterns - Singleton (0) | 2021.11.11 |
---|---|
함수형 인터페이스를 이용한 Builder (0) | 2021.05.29 |
더 자바(java 8) - CompletableFuture (0) | 2021.05.26 |
더 자바(java 8) - Stream, Optional, Date (0) | 2021.05.19 |
더 자바(java 8) - 인터페이스의 변화 (0) | 2021.05.11 |
댓글