람다식
설명
- 자바 1.8부터 추가되었다
- 함수랑 메서드는 약간은 다르다(메서드는 객체지향의 의미를 가진 함수, 클래스가 필요함)
사용이유
- 불필요한 코드를 줄이고, 가독성을 높이기 위함, 컴파일러가 문맥을 살펴 타입을 추론하고 람다식으로 선언된 함수는 1급 객체이기 때문에 Stream API의 매개변수로 전달가능
- 람다식 작성법
//기존
반환 타입 메서드이름(매개변수 선언){
문장들
}
//람다식
(매개변수 선언) -> {
문장들
}
//return 식 대체
//기존
(매개변수 선언) ->{
return a>b ? a:b;
}
//람다식
(매개변수 선언) -> a > b ? a:b
//심지어 타입까지 생략가능(int형인 a,b가 있다고 가정)
(a, b) -> a > b ? a : b
//파라미터하나일경우
a -> a * a
//괄호안에 문장 하나인경우 {}생략가능,
//return은 불가능 그러나 람다식으로 써서 return생략시 가능
람다식의 장단점
- 장점
- 코드를 간결하게 만들 수 있다
- 식에 개발자의 의도가 명확히 드러나 가독성이 높아진다
- 함수를 만드는 과정없이 한번에 처리할 수 있어 생산성이 높아진다
- 간단한 작업에도 함수를 만드는 기존 방법과 다르게 함수 만들필요없이 람다식으로 바로 작업 가능하다
- 병렬프로그래밍이 용이하다 → 병렬 실행이나 이벤트 기반 프로그래밍 쉽게 적용가능(ex. 안드로이드 스튜디오생각하면 될듯)
- myButton.setOnClickListener(view -> { // 버튼 클릭 시 실행될 코드 작성 Log.d("ButtonClick", "Button clicked!"); });
- 단점
- 람다를 사용하면서 만든 무명함수는 재사용이 불가능하다(한번 정의된 후에 다른곳 재사용 어렵다)
- 디버깅이 어렵다: 코드가 간결해서 람다식 자체에 오류가 뜨는게 아니라 람다식을 사용된 부분에서 오류가 뜬다.
- 람다를 남발하면 비슷한 함수가 중복 생성되어 코드가 지저분해질 수 있다
- 재귀로 만들경우에 부적합하다(람다식은 자기 참조가 불가능하므로, 적합하지 않다), 일반메서드 사용해야함)
람다식 바이트코드로 분석
- 람다 바이트코드 분석 블로그 참고
- 익명클래스가 새로운 클래스를 생성하여 초기화를 해주고 인터페이스를 실행한다.
- 궁금증으로 자바에서는왜 람다를 내부적으로 익명클래스로 컴파일하지 않냐? 문제 2가지 발생
- 항상 새 인스턴스 할당
- 람다식 마다 클래스가 하나씩 생긴다
- 자바 8이전 버전에서 람다를 쓰기위한 retrolambda 라이브러리나 kotlin같은 언어에서는 컴파일 시점에 람다를 단순히 익명클래스로 치환한다.
- 자바 8이전과 이후를 놓고보면 한마디로 이전에는 새로운 익명 클래스를 만들어 사용한것과 달리 자바 8부터는 컴파일러 내부적으로 람다식을 처리한다
- 결론은 구조적으로는 유사하지만 내부적으로는 다르다, 함수형 인터페이스는 간결한 표현 제공, 익명 클래스는 더 많은 유연성 제공
함수형 인터페이스
- 결론만 말하면 자바의 람다식은 함수형 인터페이스로만 접근 가능하다
- 자바에서 모든 메서드는 클래스 내에 포함되어야한다(애초에 메서드 의미가 클래스 안에 있는 함수의미 이므로), 그리고 람다식은 익명 클래스의 객체와 동등하다.
- 람다식은 익명의 객체와 동등하다 따라서 참조변수도 타입이 클래스 또는 인터페이스 가능하다. 그리고 람다식과 동등한 메서드가 정의되어야지 호출이 가능하다.
- 결국 람다식을 인터페이스를 통해서 다루기로 했고 그러한 인터페이스를 함수형 인터페이스라고 부른다
- 추가로 FunctionalInterface를 붙이면 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인하므로 꼭 붙이자.
- 메서드의 매개변수가 인터페이스형인 MyFunction타입이라면, 람다식을 참조하는 참조변수를 매개변수로 해야한다→ 이게 결국은 메서드를 변수처럼 주고 받는것이 가능하다.(객체를 주고 받는 것이므로)
기본 함수형 인터페이스
- 종류
- Runnable
- Supplier
- Consumer
- Function<T,R>
- Predicate
- 추가적인 것은 아래 공식 페이지 참고 https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
- Runnable
- 인자를 받지 않고 리턴값도 없는 인터페이스. 앞전 쓰레드를 공부할 때 Runnable 인터페이스로 실행 했던 것
- 일전에 쓰레드의 실행시 Runnable run()호출 했던것이 Runnable 함수 인터페이스다
어노테이션을 통해 함수형 인터페이스인것을 알려줌
- Supplier
- 인자를 받지 않고 T타입의 객체를 리턴한다
public interface Supplier<T> {
T get();
}
- Consumer
- T타입의 객체를 인자로 받고 리턴 값은 없다
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
- 인터페이스 배울때 메서드 안에 내용을 추가 할 수 있게 해주던 default 사용해주고 super로 하한제한 걸어준 예시
- andThen()를 사용하면 2개 이상의 Consumer를 사용할 수 있다
- Function
- Function<T,R>은 T타입의 인자를 받아, R타입의 객체로 리턴한다
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
- Predicate
- Predicate<T>는 T타입 인자를 받고 결과로 boolean으로 리턴한다
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
예제 참고: https://sujl95.tistory.com/76
변수 캡처(Variable Capture)
스콥
- 개념: 변수를 선언했을 때, 그 변수가 접근이 가능한 범위. 예를 들어, for문 내에서 선언한 변수는 for문 밖에서 접근할 수 없다.
- 익명 클래스는 새로 *스콥을 만들지만, 람다는 람다를 감싸고 있는 범위와 같은 스콥을 가진다
변수 캡처
- 람다는 final이거나 effectivy final인 경우에만 변수 참조가능
- 아래의 경우 람다가 참조하는 변수 baseball은 final인 것이다.
package lamdaEx;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
public class Foo {
public static void main(String[] args) {
Foo foo = new Foo();
foo.run();
}
private void run() {
int baseNumber = 10;
// 로컬 클래스
class LocalClass {
int baseNumber = 11;
void printBaseNumber() {
System.out.printf("로컬 클래스: %d \\n", baseNumber); // 11
}
}
//baseNumber변수의 스코프는 여기서 끝
LocalClass localClass = new LocalClass();
localClass.printBaseNumber();
// 익명 클래스
Consumer<Integer> integerConsumer = new Consumer<Integer>() {
@Override
public void accept(Integer baseNumber) {
System.out.printf("익명 클래스: %d \\n", baseNumber); // 12
}
};
integerConsumer.accept(12);
// 람다, 익명클래스에서 사용된 변수의 스코프는 지속된다
IntConsumer printInt = (i) -> {
System.out.printf("람다: %d \\n", i + baseNumber); // 10+3=13
};
printInt.accept(3);
//외부의 변수와는 별개의 취급을 받기에 영향을 주지않는다, 그대로 10이 출력
System.out.println(baseNumber);
}
}
참고: https://velog.io/@wijoonwu/람다-표현식-변수-캡처-Variable-Capture
메서드, 생성자 레퍼런스
- Method Reference
- 메소드를 간결하게 지칭할 수 있는 방법으로 람다가 쓰이는 곳에 사용 가능하다
- 일반 함수를 람다 형태로 사용할 수 있도록 해준다
- 두 메서드의 시그니처를 맞춰야한다(시그니처: 메서드의 이름, 매개변수의 개수, 매개변수의 타입)
package lamdaEx;
interface MethodReferenceInterface{
void printMethod(int value);
}
public class MethodReferenceEx {
public static void main(String[] args){
MethodReferenceInterface methodReferenceInterface = MethodReferenceEx::printMethodDefine;
methodReferenceInterface.printMethod(7);
}
//printMethod와 같은 시그니처인 메서드 생성
public static void printMethodDefine(int value){
System.out.println("value값은 : "+value);
}
}
- 타입 다를시 오류가 뜨는 모습
- 메소드 참조 방법
- Default Use
- 특정 클래스의 인스턴스 메서드를 참조한다
- ClassName::methodName
- Constructor Reference
- 클래스의 생성자를 참조한다
- ClassName::new
- Static Method Reference
- 클래스의 정적 메서드를 참조한다
- ClassName::staticMethodName
- Instance Method Reference
- 특정 객체의 인스턴스 메서드를 참조하는 것이다
- objectName::instanceMethodName람다식
- Default Use
출처
- 자바의 정석(저자: 남궁성)
-https://sujl95.tistory.com/76
- https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
'자바(Java)' 카테고리의 다른 글
[자바] 터미널에서 Spring프로젝트 빌드 시 자바 버전 오류 해결 방법 (0) | 2023.12.01 |
---|---|
[자바] 제네릭(Generic) (1) | 2023.11.02 |
[자바] 자바 I/O (1) | 2023.10.14 |
[자바] 자바 어노테이션 (1) | 2023.10.05 |
[자바] 자바 Enum (0) | 2023.09.27 |