<1> 함수형 인터페이스와 람다 표현식 소개
- 함수형 인터페이스 (Functional Interface)
-
- 추상 메소드를 딱 하나만 가지고 있는 인터페이스
- SAM (Single Abstract Method) 인터페이스
- 추상 메소드의 abstract 키워드는 생략 가능
- 메소드가 여러 개 들어갈 수 있으나 중요한 건 추상 메소드는 하나 뿐이란 것 - @FunctionInterface 어노테이션을 가지고 있는 인터페이스
- JSTL에 있음
- 인터페이스를 좀 더 견고하게 관리하기 위해 달아주기
인터페이스가 하나인 경우에는 줄여 쓸 수 있게 해주는 문법이 자바8 버전부터 생겼다.
람다로 바꿔주면 익명 내부 클래스 코드가 간단해진다.
- 람다 표현식 (Lambda Expressions)
- 함수형 인터페이스의 인스턴스를 만드는 방법으로 쓰일 수 있다.
- 함수처럼 생겼지만 함수형 인터페이스를 인라인으로 구현한 오브젝트라고 볼 수 있다.
- 코드를 줄일 수 있다.
- 함수 바디가 한 줄이면 중괄호 없이 쓰고, 여러 줄이면 중괄호로 묶어서 쓴다.
- 메소드의 파라미터로 전달하거나, 이 자체를 리턴하거나, 변수에 할당할 수 있다.
- 자바에서 함수형 프로그래밍
- 람다 표현식을 메소드의 매개변수, 리턴 타입, 변수로 쓸 수 있기 때문에 함수를 First class object로 사용할 수 있다.
- 순수 함수 (Pure function)
- 수학적인 함수다. 즉, 입력받은 값이 동일한 경우 결과가 같아야 한다.
- 사이드 이팩트가 없다. (함수 밖에 있는 값을 변경하지 않는다.)
- 상태가 없다. 상태값에 의존하지 않는다. (함수 밖에 있는 값을 사용하지 않는다.) - 고차 함수 (Higher-Order Function)
- 함수가 함수를 매개변수로 받을 수 있고 함수를 리턴할 수도 있다. - 불변성
함수 안에서 함수 밖에 있는 값(아래 캡처에서 int baseNumber)을 참조해서 쓰는 경우,
상태값에 의존하는 함수가 된다. 즉, 순수 함수가 아니다.
자바에서 함수형 프로그래밍을 할 때는 이런 것들을 주의해야 하므로 함수 밖에 있는 값을 참조하거나 변경하려 하면 안 된다.
<2> 자바에서 제공하는 함수형 인터페이스
- Java가 기본으로 제공하는 함수형 인터페이스
- java.lang.function 패키지 https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
- 자바에서 미리 정의해둔 자주 사용할만한 함수 인터페이스
- Function<T, R>
- BiFunction<T, U, R>
- Consumer<T>
- Supplier<T>
- Predicate<T>
- UnaryOperator<T>
- BinaryOperator<T>
- Runnable
- Function<T, R>
- T 타입을 받아서 R 타입을 리턴하는 함수 인터페이스
- R apply(T t) - 함수 조합용 메소드
- andThen : 입력값 받아서 앞의 함수를 먼저 적용
- compose : 입력값 받아서 뒤의 함수를 먼저 적용
- T 타입을 받아서 R 타입을 리턴하는 함수 인터페이스
- BiFunction<T, U, R>
- 두 개의 값(T, U)를 받아서 R 타입을 리턴하는 함수 인터페이스
- 입력값 2개, 리턴값 1개 타입이 전부 다 다를 거라고 가정함.
- R apply(T t, U u)
- Consumer<T>
- T 타입을 받아서 아무값도 리턴하지 않는 함수 인터페이스
- void accept(T t) - 함수 조합용 메소드
- andThen
- T 타입을 받아서 아무값도 리턴하지 않는 함수 인터페이스
- Supplier<T>
- T 타입의 값을 제공하는 함수 인터페이스
- 내가 받아올 값의 타입을 정의한다.
- 입력해주는 값이 없으니 lambda expression 안에 인자를 줄 필요 없다.
- T get()
- Predicate<T>
- T 타입을 받아서 boolean을 리턴하는 함수 인터페이스
- boolean test(T t) - 함수 조합용 메소드 (true/false에 대해)
- And
- Or
- Negate
- T 타입을 받아서 boolean을 리턴하는 함수 인터페이스
- UnaryOperator<T>
- Function<T, R>의 특수한 형태로, 입력값 하나를 받아서 동일한 타입을 리턴하는 함수 인터페이스
- 입력값이 하나이고 입력값과 결과값의 타입이 같은 경우 Function<T, R> 대신 쓸 수 있다.
- Function을 상속받았기 때문에 Function에서 제공하는 기본 메소드를 사용 가능 (compose, andThen 등)
↓
- BinaryOperator<T>
- BiFunction<T, U, R>의 특수한 형태로, 동일한 타입의 입력값 두개를 받아 리턴하는 함수 인터페이스
- BiFunction과 달리 입력값 2개와 리턴값 1개의 타입이 전부 같을 때
- 그래서 <T, T, T> 대신 <T>로 적어도 됨.
- Runnable
- 인자를 받지 않고 리턴값도 없는 함수 인터페이스
- 실행 메소드 이름 : run()
public interface Runnable {
public abstract void run();
}
Runnable runnable = () -> System.out.println("run!");
runnable.run();
// 출력 : run!
- 이외에도 BiConsumer, BiPredicate, BooleanSupplier 등이 있음.
<3> 람다 표현식
- 람다식
- 인자 리스트
- 인자가 없을 때 : ()
- 인자가 1개일 때 : (one) 또는 one
- 인자가 여러 개일 때 : (one, two)
- 인자의 타입은 생략 가능, 컴파일러가 추론(infer)하지만 명시할 수도 있다. (Integer one, Integer two)
- 바디
- 화살표 오른쪽에 함수 본문 정의
- 여러 줄인 경우 {}를 사용해 묶는다.
- 한 줄인 경우 생략 가능, return도 생략 가능
// 한 줄인 경우 {} 생략 가능
/* Supplier<Integer> get10 = () -> {
return 10;
}; */
Supplier<Integer> get10 = () -> 10;
// 인자가 여러 개일 때
BinaryOperator<Integer> sum = (a, b) -> a + b;
// 타입 생략 가능
// BinaryOperator<Integer> sum = (Integer a, Integer b) -> a + b;
BinaryOperator<Integer> sum = (a, b) -> a + b;
- 변수 캡처 (Variable Capture)
- 로컬 변수 캡처
- final이거나 effective final인 경우만 참조할 수 있다.
- 그렇지 않을 경우 concurrency 문제가 생길 수 있어서 컴파일러가 방지한다.
- effective final
- 이것도 역시 자바8부터 지원하는 기능으로 사실상 final인 변수(생략한 상태)
- final 키워드 사용하지 않은 effective final 변수를 익명 클래스, 로컬 클래스, 람다에서 참조할 수 있다. - 공통점
- 람다는 '쉐도윙'하지 않는다. - 차이점
- 익명 클래스와 로컬 클래스는 새로 스콥을 만들기 때문에 별도의 스콥이라 쉐도윙이 가능하다.
- 람다는 람다를 감싸고 있는 스콥과 같다.
- 이름이 같은 변수가 있으면 가려지는 게 쉐도윙
- 로컬 변수 캡처
public class Foo {
public static void main(String[] args) {
Foo foo = new Foo();
foo.run();
}
private void run() {
int baseNumber = 10; // 사실상 final인 변수
// 람다에서 로컬 변수 baseNumber 참조
IntConsumer printInt = (i) -> {
System.out.println(i + baseNumber);
};
printInt.accept(10);
}
}
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 baseNumber) { // 참조한 게 아님
// 이 스코프 안에 있는 파라미터고 이름이 baseNumber
// 하단의 printInt.accept로 값 전달됨
System.out.println(baseNumber); // 12 출력 (쉐도윙)
}
};
// 람다
// 컴파일 에러
// IntConsumer printInt = (baseNumber) -> { // 같은 스콥이므로
IntConsumer printInt = (i) -> {
System.out.println(i + baseNumber);
};
// 컴파일 에러
// baseNumber++; // effective final이 아니게 되므로 참조할 수 없음
printInt.accept(12);
<4> 메소드 레퍼런스
- 메소드 레퍼런스란?
- 람다 표현식을 더 간단하게 표현하는 방법
- 콜론을 2개 찍어서 쓴다.
- 메소드를 호출하는 것이지만 괄호는 써주지 않고 생략한다.
- 많은 코드가 생략되어 있기 때문에 사용하려는 메소드의 인자와 리턴 타입을 잘 알고 있어야 한다.
// 람다식으로 Hello 출력
Consumer<String> func = text -> System.out.println(text);
func.accept("Hello");
// 출력: Hello
// 메소드 레퍼런스로 표현한다면
Consumer<String> func = System.out::println;
func.accept("Hello");
// 출력: Hello
람다가 하는 일이 기존 메소드 또는 생성자를 호출하는 거라면, 메소드 레퍼런스를 사용해서 매우 간결하게 표현할 수 있다.
참조하려는 메소드의 형태에 따라 방식이 따르다.
- 메소드 참조하는 방법
스태틱 메소드 참조 | 타입::스태틱 메소드 |
특정 객체의 인스턴스 메소드 참조 | 객체 레퍼런스::인스턴스 메소드 |
임의 객체의 인스턴스 메소드 참조 | 타입::인스턴스 메소드 |
생성자 참조 | 타입::new |
- 메소드 또는 생성자의 매개변수로 람다의 입력값을 받는다.
- 리턴값 또는 생성한 객체는 람다의 리턴값이다.
예) App 클래스에서 Greeting 클래스의 메소드를 참조
public class Greeting {
private String name;
// 인자가 없는 생성자
public Greeting() {
}
// name을 받는 생성자
public Greeting(String name) {
this.name = name;
}
// name에게 hello 인사하는 메서드
public String hello(String name) {
return "Hello " + name;
}
// name에게 hi 인사하는 static 메서드
public static String hi(String name) {
return "Hi " + name;
}
}
public class App {
public static void main(String[] args) {
// 1. static 메소드 참조
// UnaryOperator<String> hi = (s) -> "hi " + s; // 람다식
UnaryOperator<String> hi = Greeting::hi; // 메소드 레퍼런스
System.out.println(hi.apply("world"));
// 출력: hi world
// 2. 특정 객체의 인스턴스 메소드 참조
Greeting greeting = new Greeting();
UnaryOperator<String> hello = greeting::hello;
System.out.println(hello.apply("world"));
// 출력: hello world
}
}
메소드 레퍼런스까지 했을 때는 그 메소드를 호출한 게 아니다.
그 메소드를 참조하는 함수형 인터페이스 UnaryOperator, Supplier가 만들어진 것이기 때문에 아무 일도 일어나지 않는다.
예를 들어 UnaryOperator는 apply, Supplier는 get까지 해야 만들어진다.
hello로 apply 해야 "world"라는 값이 Greeting이라는 인스턴스에 있는 hello 메소드에 전달이 되고 가져와서 출력을 하게 되는 것.
// 3-1. 인자가 없는 생성자 참조
// 생성자를 호출할 때 리턴값은 객체 Greeting의 타입
Supplier<Greeting> newGreeting = Greeting::new;
Greeting greeting = newGreeting.get();
System.out.println(greeting.name());
// 출력: default
// 3-2. 인자가 있는 생성자 참조 (입력값을 받는)
Function<String, Greeting> otherGreeting = Greeting::new;
Greeting greeting2 = otherGreeting.apply("2");
System.out.println(greeting2.getName());
// 출력: 2
인자가 없는 생성자와 인자가 있는 생성자 모두 타입::new를 사용하기 때문에 같아보이지만 다른 생성자를 참조하고 있다.
// 4. 임의 객체의 인스턴스 메서드 참조
String[] names = {"one", "two", "three"};
// 자바8 이전
/* Arrays.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return 0;
}
}); */
// 자바8 이후 (람다식 사용)
Arrays.sort(names, (o1, o2) -> 0);
// 그렇다면 메서드 레퍼런스도 가능
Arrays.sort(names, String::compareToIgnoreCase);
System.out.println(Arrays.toString(names));
// 출력: [one, two, three]
Arrays의 sort 함수에 파라미터 2개 받는 메소드가 있다.
두 번째 파라미터에 주는 함수형 인터페이스 Comparator가 자바8부터 FunctionalInterface로 바뀌었다.
함수형 인터페이스라는 소리는 람다로 바꿀 수도 있다는 소리니 저렇게 간단하게 줄일 수가 있고,
람다로 바꿨으면 메서드 레퍼런스도 가능해진다.
compareToIgnoreCase : 두 개의 인자를 받는데, 첫 번째 인자가 메서드를 호출할 객체가 되고 두 번째 인자는 호출한 메서드에 넘겨질 매개변수가 된다. 그 문자열을 비교해서 int값을 넘겨주는 인스턴스 메서드이다.
즉, String::compareToIgnoreCase는 (a, b) -> a.compareToIgnoreCase(b)와 동일하다.
"one"이 뒤에 오는 "two"를 compareToIgnoreCase에 파라미터로 넘겨서 int값을 리턴한다.
두 번째에는 "two"가 "three"와 비교해서 int값을 리턴한다.
* 백기선 님의 인프런 강의 <더 자바, Java 8>을 듣고 정리한 내용입니다.
강의 정보: https://www.inflearn.com/course/the-java-java8/
참고:
https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html
https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html#shadowing
https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
https://codechacha.com/ko/java8-method-reference/
https://kangworld.tistory.com/207
'온라인 학습 > 더 자바, Java 8 강의' 카테고리의 다른 글
5. Date와 Time (0) | 2022.11.11 |
---|---|
4. Optional (0) | 2022.11.11 |
3. Stream (0) | 2022.11.10 |
2. 인터페이스의 변화 - default method, static method (0) | 2022.11.10 |
6. CompletableFuture - Concurrent 프로그래밍, Executors, Callable, Future, CompletableFuture (0) | 2022.11.06 |