[더 자바, java8] 함수형 인터페이스와 람다
본 게시물은 인프런 백기선님 강의 "더 자바, java8"을 학습하고 개인적으로 정리한 내용입니다.
https://www.inflearn.com/course/the-java-java8#
더 자바, Java 8 - 인프런 | 강의
자바 8에 추가된 기능들은 자바가 제공하는 API는 물론이고 스프링 같은 제 3의 라이브러리 및 프레임워크에서도 널리 사용되고 있습니다. 이 시대의 자바 개발자라면 반드시 알아야 합니다. 이
www.inflearn.com
목차
1. 함수형 인터페이스
2. 자바에서 제공하는 함수형 인터페이스
3. 람다 표현식
4. 메소드 레퍼런스
1. 함수형 인터페이스
public interface RunSomething {
void doIt();
static void printName() {
System.out.println("DP");
}
default void printAge() {
System.out.println("20");
}
}
다음과 같이 하나의 추상 메서드를 가진 인터페이스를 함수형 인터페이스라고 부른다.
@FunctionalInterface
컴파일러에게 해당 인터페이스가 함수형 인터페이스임을 알리는 어노테이션이다. 추상메서드가 하나여야하는 규칙을 위반하면 컴파일 에러로 경고를 줘서 더 견고하게 관리할 수 있다.
인터페이스를 사용하려면 구현체를 만들어야하는데, 자바8 이전에는 아래와 같이 익명클래스를 만들어 사용했다.
public class Foo {
public static void main(String[] args) {
RunSomething runSomething = new RunSomething() {
@Override
public void doIt() {
System.out.println("Hello");
}
};
}
}
람다 표현식을 이용하면 아래와 같이 축약할 수 있다.
public class Foo {
public static void main(String[] args) {
RunSomething runSomething = () -> System.out.println("Hello");
}
}
함수형 인터페이스 특징
1. First class object로 사용할 수 있다.
람다식을 다른 언어에서 사용하는 함수처럼 생각할 수 있지만 람다 표현식은 함수형 인터페이스를 구현한 클래스의 인스턴스이다. 자바는 객체지향 언어이기 때문에 함수를 변수에 할당하거나, 메서드에 파라미터로 쓰거나, 반환 값으로 사용할 수 있다.
High-Order Function : 람다식은 객체이기 때문에 함수가 함수를 파라미터로 받거나 반환할 수 있다.
2. 외부에서 참조하는 변수는 묵시적으로 final이다.
람다에서 참고하는 지역변수는 final이거나 혹은 실질적으로 final이어야 한다. 아래와 같이 외부값 baseNumber를 메서드 내에서 변경하거나, 두 번 이상 할당하면 컴파일 에러가 난다.
메서드 내부에서 외부값을 변경
int baseNumber = 10;
RunSomething runSomething = new RunSomething() {
@Override
public int doIt(int number) {
baseNumber++;
return number + baseNumber;
}
}
baseNumber를 두번 이상 할당
int baseNumber = 10;
RunSomething runSomething = new RunSomething() {
@Override
public int doIt(int number) {
return number + baseNumber;
}
}
baseNumber = 11;
지역 변수에 제약을 두는 이유는 지역 변수와 인스턴스 변수가 저장되는 메모리의 위치가 다르기 때문이다. 지역 변수는 스택에, 인스턴스 변수는 힙에 저장된다.
람다를 실행한 스레드에서 지역변수가 있는 스레드에 직접 접근했을 때, 지역변수가 있는 스레드가 사라지면 동기화 문제가 일어날 수 있다. 그래서 지역변수를 직접 참조하는 것이 아닌 그 복사본을 이용하는 것이다.
인스턴스 변수는 힙에 존재하고, 힙은 모든 스레드에서 공유해서 사용하므로, 동기화 문제를 걱정할 필요가 없다. 그래서 지역변수에 한 번만 값을 할당해야한다는 제약이 생긴 것이다.
2. 자바에서 제공하는 함수형 인터페이스
함수 디스크립터 : 인터페이스의 추성 메서드 시그니처인데, 람다 표현식의 시그니처를 묘사한다.
인터페이스를 직접 만들지 않아도 java.util.function 패키지에서 기본으로 여러 함수형 인터페이스를 제공해서 다양한 람다 표현식을 사용할 수 있다.
https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
java.util.function (Java Platform SE 8 )
Interface Summary Interface Description BiConsumer Represents an operation that accepts two input arguments and returns no result. BiFunction Represents a function that accepts two arguments and produces a result. BinaryOperator Represents an operation u
docs.oracle.com
함수형 인터페이스 | 함수 디스크립터 | 기본형 특화 |
Predicate<T> | T -> boolean | IntPredicate, LongPredicate, DoublePredicate ... |
Consumer<T> | T -> void | IntConsumer, LongConsumer, DoubleConsumer ... |
Function<T, R> | T -> R | IntFunction<R>, IntToDoubleFunction, DoubleToIntFunction ... |
Supplier<T> | () -> T | BooleanSupplier, IntSupplier, DoubleSupplier ... |
UnaryOperator<T> | T -> T | IntUnaryOperator, LongUnaryOperator ... |
BinaryOperator<T> | (T, T) -> T | IntBinaryOperator, LongBinaryOperator ... |
BiPredicate<T, R> | (T, R) -> boolean | |
BiConsumer<T, R> | (T, R) -> void | ObjConsumer<T>, ObjLongConsumer<T> ... |
BiFunction<T, U, R> | (T, U) -> R | ToIntBiFunction<T, U>, ToLongBiFunction<T, U> |
Interface Function<T, R>
디스크립터 : T -> R
Function의 추상메서드 apply는 제네릭 형식 T를 받아 제네릭 R 객체를 반환한다. 매핑에 주로 사용할 수 있다.
public <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<>();
for(T t: list) {
result.add(f.apply(t));
}
return result;
}
List<Integer> list = map(
Arrays.asList("lambdas", "in", "action");
(String s) -> s.length()
);
그 밖에 여러가지 함수 조합용 default 메소드를 제공한다.
compose와 identity를 예시로 들면
public class Foo {
public static void main(String[] args) {
Function<Integer, Integer> plus10 = (i) -> i+10;
Function<Integer, Integer> multiply2 = (i) -> i *2;
Function<Integer, Integer> multiply2AndPlus10 = plus10.compose(multiply2);
System.out.println(multiply2AndPlus10.apply(2));
System.out.println(plus10.andThen(multiply2).apply(2));
}
}
실행 결과 :
14
24
multiply2AndPlus10의 경우에는 multiply2가 먼저 적용되어 2*2 + 10 로 14를 출력하고
plus10.andThen(multiply2).apply(2)의 경우에는 plus10이 먼저 적용되어 (2+10) *2 로 24가 출력된다.
Interface Consumer<T>
디스크립터 : T -> void
Consumer의 경우에는 T를 입력받지만 내부에서 무언가를 반환하지 않는다.
Interface Supplier<T>
디스크립터 : () -> T
위의 Consumer와 반대로 아무 것도 입력 받지 않고 반환할 타입 T를 정의한다.
Interface Predicate<T>
디스크립터 : T -> boolean
어떤 T타입의 인자를 받아 true 혹은 false를 return한다.
and, or 등의 조합과 함께 사용할 수 있다.
Interface UnaryOperator<T>
디스크립터 : T -> T
위의 Function에서 input과 output의 type이 같은 경우에 사용할 수 있는데, Function을 상속 받았기 때문에 compose, andthen 등의 method를 사용할 수 있다.
Interface BinaryOperator<T>
디스크립터 : (T, T) -> T
BiFunction의 특별한 경우인데, 입력값 두개와 출력값 세 개의 타입이 같은 특수한 경우에 사용한다.
자바에서 제공하는 함수형 인터페이스를 충분히 학습하면 모르는 다른 인터페이스가 어떤 일을 하는지 추론할 수 있다.
예를 들면 BiConsumer는 2개의 type을 받아 return이 없는 경우겠구나, BiPredicate은 2개의 type을 받아 true false를 제공할 수 있겠구나, BooleanSupplier는 Boolean을 제공받겠구나 추론할 수 있다.
3. 람다 표현식
람다 표현식은 메서드로 전달하는 익명함수를 간단히 축약시킨 식이다.
함수형 인터페이스를 인수로 받는 메서드만 람다식으로 이용할 수 있다.
람다 문법
p -> p.getGender() == Person.Sex.Male &&
&& p.getAge() >= 18
&& p.getAge() <= 25
1. 파라미터 리스트
p는 Person 클래스의 인스턴스이다.
람다식에서 타입 Person은 컴파일러가 추론할 수 있기 때문에 생략할 수 있다.
그리고 파라미터가 하나일 때는 괄호를 생략할 수 있다.
다른 유효한 람다 표현식
(int x, int y) -> x+y
(x, y) -> x+y
(Apple apple) -> apple.getWeight()
apple -> apple.getWeight()
2. 화살표
-> : 파라미터 리스트와 바디를 구분한다.
3. 람다 바디
람다의 반환 값에 해당하는 표현식이다. {}가 생략돼있는데, 다음과 같이 표현할 수 있다.
p -> {
return p.getGender() == Person.Sex.Male &&
&& p.getAge() >= 18
&& p.getAge() <= 25
}
여러 줄인 경우 위와 같이 { }를 사용해서 묶을 수 있고 바디가 한줄인 경우는 아래와 같이 return을 생략할 수 있다.
p -> { System.out.println(p.getAge() >= 18; }
p -> System.out.println(p.getAge() >= 18)
변수 캡쳐
1. 로컬 변수 캡쳐 : final 혹은 effective final인 경우 참조할 수 있다.
2. 람다식의 스코프는 람다를 감싸는 외부 스코프와 같으므로, 람다식 내부에서 람다식 외부의 값을 변경하면 컴파일에러가 난다.
public class Foo {
public static void main(String[] args) {
Foo foo = new Foo();
foo.run();
}
private void run() {
int baseNumber = 10;
IntConsumer printInt = (i) -> {
System.out.println(i + baseNumber);
};
printInt.accept(10);
}
}
람다식 printInt에 printInt를 감싸고 있는 외부 영역의 지역변수 baseNumber가 있다.
람다식에서 지역변수 baseNumber를 참조할 수 있다. baseNumber는 final 접근제어자가 없지만, 값이 한 번만 할당돼서 사실상 final로 사용되는데 이를 effective final이라고 한다.
람다식은 람다식 scope 외부의 effective final 변수를 참조할 수 있는데, 이는 람다식 뿐만 아니라, 로컬 클래스와 익명 클래스에도 포함된 기능이다.
람다가 로컬 클래스, 익명 클래스와 차이점은 람다는 shadowing이 되지 않는다는 것이다.
int baseNumber = 10;
//로컬 클래스
class LocalClass {
void printBaseNumber() {
int baseNumber = 11;
System.out.println(baseNumber); // 출력 값 : 11 외부 스코프의 baseNumber를 shadowing
}
}
//익명 클래스
Consumer<Integer> integerConsumer = new Consumer<Integer>() {
@Override
public void accept(Integer baseNumber) { // 파라미터가 외부 스코프 baseNumber를 shadowing
System.out.println(baseNumber); // 더 이상 run 의 baseNumber 참조 X
}
};
로컬 클래스나 익명 클래스에서 외부 스코프의 변수와 같은 이름의 변수를 선언하면
더 이상 외부 스코프의 변수를 참조하지 않고 내부 스코프에서 선언한 변수를 사용한다.
그리고 새로 선언한 변수는 변경해도 외부 스코프 변수는 변경되지 않는다.
즉 로컬 클래스나 익명 클래스는 클래스 내부와 외부가 다른 스코프이다. 하지만 람다는 외부와 내부가 같은 스코프이다.
int baseNumber = 10;
IntConsumer printInt = (baseNumber) -> { // 람다는 감싸고 있는 스코프과 같은 스코프
System.out.println(i + baseNumber);
};
그래서 람다식에서 파라미터를 외부 변수와 같은 이름으로 사용하면 컴파일에러가 난다.
int baseNumber = 10;
IntConsumer printInt = (baseNumber) -> {
System.out.println(i + baseNumber);
baseNumber++; // 더 이상 effective final이 아니게 된다.
};
또한 위와같이 람다식 내부에서 baseNumber를 변경시키면, baseNumber는 더이상 effective final이 아니기 때문에 참조할 수 없어서 컴파일 에러가 난다.
4. 메소드 레퍼런스
람다 expression을 직접 구현할수 있지만, 같은 기능을 하는 메서드가 있다면 콜론 두개를 사용해서 메소드 레퍼런스를 이용할 수 있다.
Greeting.java
public class Greeting {
private String name;
public Greeting() {
}
public Greeting(String name) {
this.name = name;
}
public String hello(String name) {
return "hello " + name;
}
public static String hi(String name) {
return "hi " + name;
}
}
1. 스태틱 메서드를 참조
UnaryOperator<String> hi = Greeting::hi;
2. 인스턴스 메서드를 참조
Greeting greeting = new Greeting();
UnaryOperator<String> hello = greeting::hello;
3. 생성자를 참조
생성자의 반환 타입은 그 객체의 타입
Supplier<Greeting> newGreeting= Greeting::new;
Greeting greeting = newGreeting.get();
윗줄은 Supplier일 뿐이고 실행하기 위해서는 아랫줄 처럼 객체를 만들어서 apply를 해야한다.
4. 입력값을 받는 생성자
Function<String,Greeting> keesunGreeting = Greeting::new;
Supplier<Greeting> newGreeting = Greeting::new;
같은 생성자를 참조하는 것 같지만 같지 않다. 윗줄은 문자열이 주어진 생성자를 밑의 줄은 기본 생성자를 받는다.