목표
자바의 람다식에 대해 학습하세요.
학습할 것 (필수)
- 람다식 사용법
- 함수형 인터페이스
- Variable Capture
- 메소드, 생성자 레퍼런스
마감일시
2021년 3월 6일 토요일 오후 1시까지.
람다식 사용법
람다식이란?
람다식은(Lambda Expression)은 메서드를 하나의 '식(expression)'으로 표현한 것이다.( 자바에서 함수형 프로그래밍(functional programming)을 구현하는 방식)
람다식은 함수를 간략하게 하면서 명확한 식으로 표현할 수 있게 해 준다.
메서드를 람다식으로 표현하면 메서드의 이름과 반환 값이 없어지므로, 람다식을 '익명 함수(anonymous function)'이라고도 한다.
자바 8부터 지원되는 기능
함수형 프로그래밍이란?
순수 함수(pure function)를 구현하고 호출하는 프로그래밍 방식.
매개 변수만을 사용하도록 만든 함수로 외부 자료에 부수적인 영향(side effect)가 발생하지 않는 것이 목적이다.
입력받은 자료를 기반으로 수행되고 외부에 영향을 미치지 않으므로 병렬 처리 등에 가능 안정적인 확장성 있는 프로그래밍 방식.
람다가 없던 시절
public class noLambdaBefore {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
for(int number : numbers) {
System.out.printf(String.valueOf(number));
}
System.out.println();
List<Integer> numbers2 = Arrays.asList(1, 2, 3, 4, 5);
numbers2.forEach(new Consumer<Integer>() {
public void accept(Integer value) {
System.out.printf(String.valueOf(value));
}
});
}
}
람다를 활용하여 간결하게 표현
List<Integer> number3 = Arrays.asList(1, 2, 3, 4, 5, 6);
number3.forEach(System.out::println);
람다식 예제
람다 표현식의 특징
-
함수형 인터페이스의 인스턴스를 만드는 방법으로 쓰일 수 있다.
-
코드를 줄일 수 있다.
-
메소드 매개변수, 리턴 타입, 변수로 만들어 사용할 수도 있다.
-
함수를 정의한 것처럼 보이나 실질적으로 자바에서는 특수한 형태의 Object로 봐야 한다.
자바에서 함수형 프로그래밍
-
함수를 First class object(일급 객체)로 사용할 수 있다.
- 위와 같이 lamda expression 형태를 변수에 저장하고, 메소드의 파라미터로 전달하고, 리턴 타입으로 리턴이 가능하다.
-
순수 함수 (Pure function) : 항상 같은 값을 넣으면 같은 값을 반환하는 함수
- 사이드 이팩트가 없다. (함수 밖에 있는 값을 변경하지 않는다.)
- 상태가 없다. (함수 밖에 있는 값을 사용하지 않는다.)
-
고차 함수 (Higher-Order Function)
- 함수가 함수를 매개변수로 받을 수 있고 함수를 리턴할 수도 있다.
-
불변성
람다식 작성하기
람다식은 메서드의 이름과 반환타입을 제거하고, 매개변수 선언부와 몸통 사이 {...}에 ->를 추가한다.
예를 들어 큰 값을 반환하는 메서드 max를 람다식으로 변환하면 아래와 같다.
// 메소드
int max(int a, int b) {
return a > b ? a : b;
}
// 람다식 변환
(int a, int b) -> {
return a > b ? a : b;
}
람다 표현식의 인자
인자가 없을 경우
()만 작성하면 된다.
public interface NoParameterInterface {
public void notParam();
}
public class NoParamApp {
public static void main(String[] args) {
NoParameterInterface noParameterInterface = () -> System.out.println("noParam");
noParameterInterface.notParam();
}
}
인자가 한 개일 경우
매개변수의 괄호()가 생략 가능하며, 타입 생략 가능하다.
public interface ParameterOne {
public void oneMethod(int a);
}
public class App {
public static void main(String[] args) {
ParameterOne one = a -> System.out.println("값은 " + a);
one.oneMethod(11);
}
}
매개변수 타입이 있으면 괄호를 생략할 수 없다.
: int a -> System.out.println("값은" + a); // 컴파일 오류
인자가 여러 개일 경우
이 역시 타입 생략 가능하다, 단 어느 하나의 타입만 생략하는 것은 불가능하다
인자 타입은 생략 가능하다(컴파일러가 추론)
람다 표현식의 바디와 return
바디가 여러 줄인 경우 {}를 사용해서 묶으며, 한 줄인 경우 생략 가능하다.
return 타입이 한줄인 경우 return생략 가능하다(컴파일러가 추론)
함수형 인터페이스
추상 메소드를 딱 하나만 가지고 있는 인터페이스 (추상 메소드가 아니면 괜찮다)
SAM (Single Abstract Method) 인터페이스라고도 한다.
@FunctionalInterface 애노테이션을 붙이면 컴파일 시점에 checking 한다.
오류 내용 : 추상메소드가 2개 이상이다.
Java가 기본으로 제공하는 함수형 인터페이스
1. Runnable
매개변수도 없고, 반환 값도 없다.
메소드 : void run();
2. Supplier<T>
매개변수는 없고 반환 값만 있다.
메소드 T get();
public class SupplierApp {
public static void main(String[] args) {
Supplier<Integer> get10 = () -> 10;
System.out.println(get10.get()); //10
}
}
3. Consumer<T>
Supplier와 반대로 매개 변수만 있고, 반환 값이 없다.
메소드 : void Accept(T t)
함수 조합용 메소드
- andThen
Consumer<Integer> printT = (i) -> System.out.println(i);
printT.accept(10); //10
4. Function<T,R>
하나의 매개변수를 받아서 결과를 반환
메소드 : R apply(T t)
함수 조합용 메소드
- andThen : 뒤에 있는 함수를 먼저 적용하고 그 결과값을 다시 입력값으로 사용
- compose : 뒤에 있는 함수를 나중에 적용
public class Plus10 implements Function<Integer, Integer> {
@Override
public Integer apply(Integer integer) {
return integer + 10;
}
}
public class Foo {
public static void main(String[] args) {
// 방법 1
Plus10 plus10 = new Plus10();
System.out.println(plus10.apply(1)); // 11
// 방법 2
Function<Integer, Integer> plus20 = (i) -> i + 20;
System.out.println(plus20.apply(1)); // 21
// 조합
Function<Integer, Integer> multiply2 = (i) -> i * 2;
// 뒤에 함수를 먼저 적용하고 그 결과값을 다시 입력값으로 사용
Function<Integer, Integer> multiply2AndPlus10 = plus10.compose(multiply2);
// 먼저 더하고 x2
Function<Integer, Integer> andThen = plus10.andThen(multiply2);
System.out.println(multiply2AndPlus10.apply(2)); // 14
System.out.println(andThen.apply(2)); //24
}
}
5. Predicate<T>
조건식을 표현하는 데 사용
매개변수는 하나, 반환 타입은 boolean
메소드 : boolean test(T t)
함수 조합용 메소드
- And
- Or
- Negate
public class Foo {
public static void main(String[] args) {
// ~로 시작하는지 검사사
Predicate<String> startsWithHyang = (s) -> s.startsWith("hyang");
// 짝수인지 검사
Predicate<Integer> isEven = (i) -> i%2 == 0;
}
}
매개변수가 두 개인 함수형 인터페이스
BiFunction<T, U, R>
두 개의 매개변수를 받아서 하나의 결과를 반환
메소드 : R apply(T t, U u)
BiConsumer<T,U>
두개의 매개 변수만 있고, 반환 값이 없다.
메소드 : void accept(T t, U u)
BiPredicate<T,U>
조건식을 표현하는 데 사용된다
두 개의 매개변수를 받고 반환 값은 boolean이다.
메소드 : boolean test(T t, U u)
입력값과 반환 값의 타입이 같을 때 사용하는 함수형 인터페이스
UnaryOperator
Function의 특수한 형태
입력값 하나를 받아서 동일한 타입을 리턴
public class Foo {
public static void main(String[] args) {
UnaryOperator<Integer> plus10 = (i) -> i+10;
System.out.println(plus10.apply(10)); //20
}
}
BinaryOperator
BiFunction의 특수한 형태
동일한 타입의 입력값 두 개를 받아서, 동일한 타입의 리턴 값을 반환
변수 캡처 (Variable Capture)
로컬 변수 캡처
람다식을 사용할 때 람다식 내부에서 외부의 지역변수를 참조하기 위해서는 지켜야 할 조건이 있다.
1. 지역변수가 final로 선언되어 있어야 한다.
2. effective final이어도 된다. (final 키워드가 붙지 않아도 final처럼 사용하는 경우)
왜 final이라는 조건이 붙었을까?
JVM에서 인스턴스 변수는 heap영역에 저장되고, 지역 변수는 stack에 저장된다.
인스턴스 변수는 쓰레드끼리 공유가 가능하며, 지역변수는 쓰레드끼리 공유가 되지 않는다.
람다가 쓰레드에서 실행되고, 또 다른 쓰레드의 지역변수를 참조하고 있다가 이 지역변수가 사라지게 되면 (stack에서 해제) 어떻게 될까?
람다에서 실행되고 있는 쓰레드는 사라진 지역변수를 참조하고 있기 때문에 오류가 발생할 것으로 예상되지만 실제로 람다는 지역변수를 직접 접근하는 것이 아닌 자신의 스택에 복사해서 사용한다. 따라서 실제 값과 복사된 값이 상이하면 안되기 때문에 final이거나 final처럼 동작해야 된다는 조건이 생기게 된 것이다.
쉐도윙
람다와 익명 클래스 구현체의 차이는 람다는 쉐도윙을 하지 않는다는 것이다.
쉐도윙이란? : 클래스 안에 name이라는 변수가 있고 또 지역변수로 name이라는 변수가 있으면 클래스에 있는 name이라는 변수가 지역변수 name에 가려지는 것을 말한다.
메소드, 생성자 레퍼런스
메소드 레퍼런스
람다식이 하나의 메서드만 호출하는 경우에는 '메서드 참조'라는 방법으로 럼다식을 간략히 할 수 있다.
예를들어 문자열을 정수로 변환하는 람다식은 아래와 같다.
Function<String, Integer> f = (String s) -> Integer.parseInt(s);
이 람다식을 더욱 메서드 참조를 통해 아래와 같이 더욱 간결하게 표현할 수 있다.
Function<String, Integer> f = Integer::parseInt;
메서드 참조에서 람다식의 일부가 생략되었지만, 컴파일러는 생략되는 부분을 아래에서 알아낼 수있다.
1. 우변의 parseInt메서드의 선언부
2. 좌변의 Function인터페이스에 지정된 지네릭 타입
메소드 참조하는 방법
종류 | 표현방법 |
스태틱 메소드 참조 | 타입::스태틱 메소드 |
특정 객체의 인스턴스 메소드 참조 | 객체 레퍼런스::인스턴스 메소드 |
임의 객체의 인스턴스 메소드 참조 | 타입::인스턴스 메소드 |
생성자 참조 | 타입::new |
생성자의 메서드 레퍼런스
생성자를 호출하는 람다식도 메서드 참조로 변환 할 수 있다.
Supplier<MyClass> s = () -> new MyClass(); // 람다식
Supplier<MyClass> s = MyClass:new; // 생성자의 메서드 레퍼런스
매개변수가 있는 생성자 레퍼런스
매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하면 된다.
필요한 기본 함수형 인터페이스가 없다면 새로 정의하면 된다.
Function<Integer, MyClass> f2 = MyClass:new;
BiFunction<Integer, String, MyClass> bf2 = MyClass:new;
Reference
자바의 정석
더 자바 8(백기선)
'Java' 카테고리의 다른 글
스터디 할래 13주차 과제: I/O (0) | 2021.03.06 |
---|---|
스터디할래 14주차 과제: 제네릭 (0) | 2021.03.06 |
스터디 할래 14주차 과제: 제네릭(feedback, 피드백) (0) | 2021.02.28 |
스터디할래 12주차 과제: 애노테이션 (0) | 2021.02.28 |
스터디 할래 12주차 과제: 애노테이션(피드백, feedback) (0) | 2021.02.14 |