김찬진의 개발 블로그
22/12/26 [lamda?] 본문
익명객체를 람다식으로 대체할 수 있다.
심지어 참조변수 없이 람다식만 매개변수로 주고 받을 수도 있다.
익명객체는 익명클래스로 생성한 객체를 의미한다.
(참고. 익명클래스는 클래스의 선언과 객체의 선언을 동시에 하는 방식이다.)
익명객체를 람다식으로 대체할 수 있다.
이것이 가능한 이유는
1. 람다식이 익명객체 이기 때문이고
2. 함수형 인터페이스의 추상메서드를 구현한 익명객체의 메서드와 람다식의 매개변수의 타입, 개수, 반환값이 일치하기 때문이다. (익명객체의 메서드와 람다식이 시그니처가 같기 때문)
https://cjkimhello97.tistory.com/59
23/02/09 [내부클래스?]
내부클래스의 종류는 변수의 선언위치에 따른 종류와 같다. https://cjkimhello97.tistory.com/60 23/02/09 [인스턴스변수? 클래스변수? 지역변수?] 인스턴스변수, 클래스변수, 지역변수? 인스턴스변수 인스
cjkimhello97.tistory.com
코드로 확인
@FunctionalInterface // 컴파일러가 함수형 인터페이스와 람다식이 1대1 정의되었는지 확인주므로 어노테이션을 쓰자
interface MyFunction {
public abstract int max(int a, int b);
}
public abstract class Test {
MyFunction f1 = new MyFunction(){ // 익명객체
public int max(int a, int b){return a > b ? a : b;}
};
MyFunction f2 = (int a, int b) -> a > b ? a : b; // 람다식
int big = f2.max(5,3);
}
다른 예시 코드
@FunctionalInterface
interface MyFunction{
void run(); // public abstract void run();
}
public class Test{
static void execute(MyFunction f){
f.run();
}
static MyFunction getMyFunction1(){
// 익명클래스로 run() 구현하고 익명객체를 참조변수 f1에 대입
MyFunction f1 = new MyFunction() {
public void run(){ // 반드시 public
System.out.println("f1.run()");
}
};
return f1; // 익명객체의 주소값이 들어있는 참조변수 f1 반환
}
static MyFunction getMyFunction2(){
// 익명객체를 람다식으로 대체하고 람다식을 참조변수 f2에 대입
MyFunction f2 = () -> System.out.println("f2.run()");
return f2; // 익명객체의 주소값이 들어있는 참조변수 f2 반환
}
// main
public static void main(String[] args) {
// 익명클래스로 run() 구현하고 익명객체를 참조변수 f3에 대입
MyFunction f3 = new MyFunction(){
public void run(){ // 반드시 public
System.out.println("f3.run()");
}
};
// 익명객체를 람다식으로 대체하고 람다식을 참조변수 f4에 대입
MyFunction f4 = () -> System.out.println("f4.run()");
MyFunction f5 = getMyFunction1();
MyFunction f6 = getMyFunction2();
f3.run(); // f3.run()
f4.run(); // f4.run()
f5.run(); // f1.run()
f6.run(); // f2.run()
System.out.println("");
execute(f3); // f3.run()
execute(f4); // f4.run()
execute(f5); // f1.run()
execute(f6); // f2.run()
System.out.println("");
// 익명클래스로 run() 구현하고 익명객체를 참조변수에 대입하지 않고 그대로 사용
execute(new MyFunction() {
public void run() {
System.out.println("f7.run()");
}
});
// 익명객체를 람다식으로 대체하고 람다식을 참조변수에 대입하지 않고 그대로 사용
execute(()-> System.out.println("f8.run()"));
}
}
인터페이스를 상속(implements)하는 것이 아니라
인터페이스를 참조타입으로 사용하여 익명객체를 만드는 것이다.
위 코드에 대한 설명이다.
MyFunction 인터페이스를 상속(implements)하는 것이 아니라
MyFunction 인터페이스를 참조타입으로 사용하여 익명객체(MyFunction f = new MyFunction(){...})를 만드는 것이다.
(물론 ... 안에는 MyFunction 인터페이스의 추상메서드(run())를 오버라이딩해야만 한다.)
이 때 익명객체를 람다식으로 대체한다면
MyFunction f = () -> {...} 가 될 것이다.
지금은 참조변수 f가 있으니 MyFunction 참조타입인 것을 알 수 있지만
어떤 경우에는 참조변수 없이 람다식만 매개변수로 주고받을 수도 있다.
예를 들어 이렇게 말이다.
execute(() -> System.out.println("f8.run()"));
그럼 execute 메서드의 매개변수가 람다식인 것은 알겠는데
참조변수의 타입(MyFunction)도 사라졌고
람다식으로 익명객체를 대체했으니 애초에 생성자(new Function(){...})를 알 수도 없기 때문에
참조타입이 무엇인지 모를 수 있다.
하지만 참조변수 없이 람다식만 매개변수로 주고 받을 수 있는 이유는
인터페이스의 추상메서드와 람다식의 시그니처가 같기 때문이다.
즉 인터페이스의 추상메서드와 람다식의 매개변수의 타입, 개수, 반환값이 같기 때문이라는 것이다.
바르게 쓰고 있는지 내 눈에는 보이진 않지만 아주 바르게 쓰고 있다는 것이다.
이걸 감시하는 어노테이션이 @FunctionalInterface 이다.
@FunctionalInterface를 MyFunction 인터페이스에 붙여주면
컴파일러가
MyFunction 인터페이스를 참조타입으로 사용하는
익명객체가
MyFunction 인터페이스의 추상메서드를
1대1로 오버라이딩했는지,
오버라이딩할 때 매개변수의 타입, 개수, 반환값이 동일한지
확인해준다.
그니깐 @FunctionalInterface 를 꼭 써줘야 한다.
여기서 착각하지 말아야 할 것이
@FunctionalInterface 어노테이션을 MyFunction 인터페이스에 붙여줬다고 해서
익명객체의 반환타입이 MyFunction 인터페이스 참조타입이 되는 것처럼 인식할 수 있는데 그건 아니다.
익명객체는 타입이 없다!
정확히 말하면 익명객체는 컴파일러가 임의의 이름으로 정하기 때문에 알 수 없다.
interface MyInterface {
public void method();
}
public class LamdaTest {
MyInterface f1 = ()->{};
MyInterface f2 = (MyInterface)(()->{});
}
람다식은 MyInterface 인터페이스를 직접 구현하지 않았지만, 이 인터페이스를 구현한 클래스의 객체와 완전히 동일하기 때문에 위와 같은 형변환을 허용한다.
즉, 람다식 "()->{}" 과 Myinterface 인터페이스의 추상메서드의 시그니처가 완전히 동일하기 때문에
또한 람다식이 익명객체이므로
MyInterface f1 = ()->{};
가 가능한 것이고
익명객체의 타입이 무엇인지 모르지만
형변환을 원한다면 MyInterface 참조타입으로만 형변환할 수 있다.
다른 타입으로는 형변환이 불가능하다.
심지어 Object 참조타입으로도 형변환이 불가능하다.
굳이 원한다면 MyInterface 참조타입으로 먼저 형변환한 이후에 Object 참조타입으로 형변환해야 한다.
@FunctionalInterface
interface MyFunction{
void myMethod(); // public abstract void run();
}
public class Test {
public static void main(String[] args) {
MyFunction f = ()->{};
Object obj = (MyFunction)(()->{}); // (Object)((MyFunction)(()->{}))
String str = ((Object)(MyFunction)(()->{})).toString();
System.out.println(f);
System.out.println(obj);
System.out.println(str);
// System.out.println(()->{}); // error, println()의 인자로 들어갈 수 없는 익명객체의 미지수 타입
System.out.println((MyFunction)(()->{})); // 익명객체의 타입을 MyFunction으로 변환하면 가능
// System.out.println((MyFunction)(()->{}).toString()); // error, MyFunction 인터페이스는 toString 오버라이딩 불가하므로 사용불가
System.out.println(((Object)(MyFunction)(()->{})).toString()); // 익명객체의 타입을 MyFunctoin, Object로 변환하면 가능
}
}
()->{} 는 익명객체를 만들었다는 의미이고
참조변수 없이 쓰는거라 익명객체의 타입을 더 모르니깐 (MyFunction) 이라고 참조타입으로 형변환을 해줬다.
MyFunction 인터페이스는 Object의 자손이니깐 toString 오버라이딩해서 써도 되겠지 생각하지만
MyFunction 인터페이스는 함수형인터페이스이기 때문에 오직 하나의 메서드(myMethod())만 선언해야 한다.
그래서 MyFucntion 인터페이스는 toString() 메서드를 오버라이딩할 수 없다.
(애초에 인터페이스라 선언뿐만 아니라 구현도 못함ㅋㅋ)
그래서 (MyFunction) 이라고 참조타입으로 형변환을 하더라도 toString() 을 못쓰는 것이다.
정리하자면 이 모든 일은 익명객체를 만들었기 때문이다.
다시 말해 저 부분에서는 람다식 ()->{} 을 쓸 수 없다는 것이다.
람다식 내에서 참조하는 지역변수는 final을 안붙여도 상수로 간주된다.
@FunctionalInterface
interface MyFunction{
void myMethod(); // public abstract void myMethod();
}
class Outer{
int val = 10; // Outer.this.val
class Inner{
int val = 20; // this.val
void method(int i){
int val = 30;
// i = 10; // 상수를 변경해서는 안된다!
MyFunction f = () -> {
System.out.println(" i : " + i); // i를 상수(final)로 간주하므로
System.out.println(" val : " + val);
System.out.println(" this.val : " + ++this.val);
System.out.println("Test.this.val : " + ++Outer.this.val);
};
f.myMethod();
} // Inner 내부클래스 끝
} // Outer 외부클래스 끝
}
public class Test{
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.method(100);
}
}
'1일1배움 > Java' 카테고리의 다른 글
23/01/16 [6가지 Sort] (0) | 2023.01.16 |
---|---|
22/01/03 [toString?] (0) | 2023.01.03 |
23/01/16 [array?] (0) | 2022.12.25 |
22/12/23 [객체?] (0) | 2022.12.23 |
22/12/23 [char? String? Character?] Character (0) | 2022.12.23 |