JAVA
[자바 스터디] 13주차 : 람다식
짜비
2022. 3. 20. 07:36
람다식이란
- 메서드를 하나의 식(Expression)으로 표현한 것
- 메서드의 이름과 반환값이 없어지므로 ‘익명함수’ 라고도 한다.
(참고) 익명 클래스
- 클래스 선언과 객체 생성을 동시에 함
- 단 한번만 사용되며 오직 하나의 객체만 생성
new 조상클래스이름() {
// 멤버 선언
}
new 구현인터페이스이름(){
// 멤버 선언
}
- 생성자를 가질 수 없음.
public class InnerEx6 {
Object iv = new Object() {
void method() {
}
};
static Object cv = new Object() {
void method() {
}
};
void myMethod() {
Object lv = new Object() {
void method() {
}
};
}
}
위 예제를 컴파일하면 아래와 같이 4개의 클래스파일이 생성된다.
InnerEx6.class
InnerEx6$1.class
InnerEx6$2.class
InnerEx6$3.class
익명클래스는 이름이 없기 때문에 {외부클래스명}${숫자}.class
형식으로 클래스파일명이 결정된다.
public class InnerEx7 {
public static void main(String[] args) {
Button button = new Button("Start");
button.addActionListener(new EventHandler());
}
}
class EventHandler implements ActionListener {
public void actionPerformed(ActionEvent event) {
System.out.println("actionEvent occurred!!");
}
}
위 코드를 아래와 같이 바꿀 수 있다.
public class InnerEx7 {
public static void main(String[] args) {
Button button = new Button("Start");
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("actionEvent occurred!!");
}
});
}
}
클래스를 별도로 정의할 필요가 없어졌다.
람다식을 활용하면 한번 더 간소화할 수 있다.
public class InnerEx7 {
public static void main(String[] args) {
Button button = new Button("Start");
button.addActionListener(e -> System.out.println("actionEvent occurred!!"));
}
}
람다식 사용법
- 메서드에서 이름, 반환타입을 제거하고, 매개변수 선언부와 body 사이에 ‘->’을 추가한다.
int max(int a, int b) {
return a > b ? a : b;
}
//람다식
(int a, int b) -> {
return a > b ? a : b;
}
반환값이 있는 경우, return 대신 expression을 넣을 수 있다.
(int a, int b) -> a > b ? a : b;
람다식에 선언된 매개변수의 타입은 추론이 가능하다면 생략 가능하다.
(a, b) -> a > b ? a : b;
함수형 인터페이스
- 오직 하나의 추상 메서드만 정의되어 있는 인터페이스
- (참고) static method 와 default method는 개수 제약이 없음
- 함수형 인터페이스를 구현한 익명 객체를 람다식으로 대체 가능한 이유
- 람다식이 실제로는 익명 객체
- 함수형 인터페이스를 구현한 익명 객체의 메서드와 람다식의 매개변수 타입, 개수 그리고 반환값이 일치하기 때문.
public class InnerEx7 {
public static void main(String[] args) {
/*
MyFunction f = new MyFunction() {
@Override
public int max(int a, int b) {
return a > b ? a : b;
}
};
*/
//람다식은 익명 객체
MyFunction f = (int a, int b) -> a > b ? a : b;
int big = f.max(5, 3);
}
}
interface MyFunction {
public abstract int max(int a, int b);
}
List<String> list = Arrays.asList("ab", "bc", "ca");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.compareTo(o1);
}
});
- 람다식을 이용하면 아래와 같이 코드를 간결하게 만들 수 있다.
List<String> list = Arrays.asList("ab", "bc", "ca"); Collections.sort(list, (s1, s2) -> s2.compareTo(s1));
함수형 인터페이스 타입의 매개변수 & 반환타입
@FunctionalInterface
interface MyFunction{
void myMethod(); //추상 메서드
}
void aMethod(MyFunction f){
f.myMethod();
}
// 람다식을 매개 변수로
MyFunction f = () -> System.out.println("myMethod()");
aMethod(f);
// 람다식을 매개 변수로
aMethod(() -> System.out.println("myMethod()"));
//람다식을 반환
MyFunction myMethod(){
MyFunction f = () -> {};
return f;
}
- 람다식을 참조변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고받을 수 있다는 것을 의미. 즉 변수처럼 메서드를 주고받는 것이 가능해짐. (일급 객체)
Variable Capture
- 지역 변수는 final로 선언되어 있어야 함
- final로 선언되어 있지 않은 지역변수는 final처럼 동작해야 함. 즉 값이 재할당 되면 안 됨.
- 이유 : 람다는 별도의 쓰레드에서 실행 가능. 이때 이미 사라진 쓰레드의 지역변수를 참조한다면 어떻게 될까? 에러가 발생하지 않는다. 왜냐하면 람다는 다른 쓰레드 스택에 있는 지역변수를 직접 접근하지 않고, 그 값을 자신의 스택으로 복사해 오기 때문. 이렇게 값을 복사해오기 때문에 값이 중간에 변경될 수 있다면 race condition 과 같은 문제가 발생할 것이다. 따라서 람다식에서 참조하는 지역변수는 값을 변경할 수 없도록 제한하는 것이다.
- 한편, 인스턴스 변수는 final 관련 제약 조건이 없음.
- 인스턴스 변수는 stack 영역이 아닌 heap 영역에 저장된다. heap 영역은 모든 thread가 공유한다. 따라서 직접 참조해서 값을 가져오면 되기 때문에 재할당 관련 제약이 없다.
public class LambdaCapturing {
private int fieldVar = 12;
public void test() {
final int finalLocalVar = 123;
int neverReassignedVar = 123;
int reassignedVar = 123;
final Runnable r = () -> {
// 인스턴스 변수 a는 final로 선언돼있을 필요도, final처럼 재할당하면 안된다는 제약조건도 적용되지 않는다.
fieldVar = 123;
System.out.println(fieldVar);
};
// final로 선언돼있기 때문에 OK
final Runnable r2 = () -> System.out.println(finalLocalVar);
// final로 선언돼있지 않지만 final을 선언한 것과 같이 변수에 값을 재할당하지 않았으므로 OK
final Runnable r3 = () -> System.out.println(neverReassignedVar);
// final로 선언돼있지도 않고, 값의 재할당이 일어났으므로 X
reassignedVar = 12;
final Runnable r4 = () -> System.out.println(reassignedVar);
}
}
메소드, 생성자 레퍼런스
- 람다식이 하나의 메서드만 호출하는 경우에는 메서드 참조(method reference)를 이용해 람다식을 간략히 할 수 있다.
Function<String, Integer> f = (String s) -> Integer.parseInt(s);
위 람다식을 메소드 레퍼런스로 바꾸면 아래와 같다.
Function<String, Integer> f = Integer::parseInt;
메소드 레퍼런스 사용 케이스
static 메소드 참조
//lambda messages.forEach(word -> StringUtils.capitalize(word)); //method reference messages.forEach(StringUtils::capitalize);
인스턴스 메소드 참조
//lambda numbers.stream() .sorted((a, b) -> a.compareTo(b)); //method reference numbers.stream() .sorted(Integer::compareTo);
특정 객체 인스턴스메서드 참조
BicycleComparator bikeFrameSizeComparator = new BicycleComparator(); //lambda createBicyclesList().stream() .sorted((a, b) -> bikeFrameSizeComparator.compare(a, b)); //method reference createBicyclesList().stream() .sorted(bikeFrameSizeComparator::compare);
생성자 레퍼런스
- 생성자를 호출하는 람다식도 메서드 참조로 변환 가능
//lambda
Supplier<MyClass> s = () -> new MyClass();
//method reference
Supplier<MyClass> s = MyClass::new;
//lambda
Function<Integer, MyClass> f = (i) -> new MyClass(i);
//method reference
Function<Integer, MyClass> f = MyClass::new;
- 매개변수가 있는 생성자라면 매개변수 개수에 따라 알맞은 함수형 인터페이스를 사용하면 된다.
기선님 리뷰영상
- 메서드 레퍼런스 적용 전
(o1, o2) -> o1.compareTo(o2)
- 메서드 레퍼런스 적용 후
String::compareToIgnoreCase
(this, o2) -> this.compareTo(o2)
가 메서드 레퍼런스로 변환된 것이라고 이해하는 게 좋다.
- shadowing
- 익명클래스에서는 shadowing 발생. 즉, outer scope에 있는 변수가 inner scope 에 있는 변수로 가려짐.
- 반면 람다에서는 shadowing이 발생하지 않음. 따라서 outer scope 변수와 inner scope 변수의 이름이 같으면 컴파일 에러 발생.