class Outer { class Nested {…} }
클래스 내에 정의된 클래스를 가리켜 ‘네스티드 클래스(Nested Class)’라 하고, 이를 감싸는 클래스를 가리켜 ‘외부 클래스(Outer Class)’라 한다.
네스티드 클래스는 static의 선언 여부를 기준으로 Static 네스티드 클래스, Non-static 네스티드 클래스(이너(Inner) 클래스)로 나뉜다.
이너 클래스는 정의되는 위치나 특성에 따라 다시 세 종류로 나뉜다.
멤버 이너 클래스 (Member Inner Class), 로컬 이너 클래스 (Local Inner Class), 익명 이너 클래스 (Anonymous Inner Class) -> 멤버 클래스, 로컬 클래스, 익명 클래스로 부르는 것이 일반적
“Static 네스티드 클래스 내에서 외부 클래스의 인스턴스 변수와 메소드에 접근 불가능하다.” 즉 Static 네스티드 클래스 내에서는 외부 클래스에 static으로 선언된 변수와 메소드에만 접근이 가능하다.
멤버 클래스(Member Class) -> 인스턴스 변수, 인스턴스 메소드와 동일한 위치에 정의
로컬 클래스 (Local Class) -> 중괄호 내에, 특히 메소드 내에 정의
class Outer {
class MemberInner {…} // 멤버 클래스
void method() {
class LocalInner {…} // 로컬 클래스
}
}
멤버 클래스의 인스턴스는 외부 클래스의 인스턴스에 종속적이다. 외부 클래스의 인스턴스를 먼저 생성하고, 이를 기반으로 멤버 클래스의 인스턴스를 생성해야한다. (외부 클래스 인스턴스 없이 멤버 클래스의 인스턴스 생성이 불가능하다.) 멤버 클래스는 “클래스의 정의를 감추어야 할 때 유용하게 사용이 된다.”
멤버 클래스가 private 선언되면, 이 클래스 정의를 감싸는 클래스 내에서만 인스턴스 생성이 가능하다. 때문에 이 Printer 인스턴스는 main 메소드와 같은 방법으로만 참조가 가능하다. 이제 Papers 클래스의 외부에서는 getPrinter 메소드가 어떠한 인스턴스의 참조 값을 반환하는지 알지 못한다. 다만 반환되는 참조 값의 인스턴스가 Printable을 구현하고 있어서 Printable의 참조변수로 참조할 수 있다는 사실만 알뿐이다. 그리고 이러한 상황을 가리켜 ‘클래스의 정의가 감추어진 상황’이라고 한다. 이렇게 클래스의 정의를 감추면, getPrinter 메소드가 반환하는 인스턴스가 다른 클래스의 인스턴스로 변경되어도 Papers 클래스 외부의 코드는 조금도 수정할 필요가 없게 된다. (즉 코드에 유연성이 부여되었다. 그리고 이는 엄청난 장점이다.) / 컬렉션 프레임워크의 ‘반복자’가 이런 형식
‘로컬 클래스’는 바로 위에서 소개한 ‘멤버 클래스’와 상당 부분 유사하다. 단 클래스의 정의 위치가 if문이나 while문 또는 메소드 몸체와 같은 블록안에 정의된다는 점에서 ‘멤버 클래스’와 구분될 뿐이다.
메소드 내에 클래스를 정의하면 해당 메소드 내에서만 인스턴스 생성이 가능하다. (따라서 클래스에 대한 private 선언은 의미가 없다. 어차피 메소드 내에서만 인스턴스 생성이 가능하므로) 즉 멤버 클래스보다도 클래스를 더 깊이, 특정 블록 안으로 감추는 효과가 있다.
‘익명 클래스’의 형태로 정의하면, 클래스의 정의와 인스턴스의 생성을 하나로 묶을 수 있다. 이 문장에서는 인터페이스 Printable을 대상으로 인스턴스를 생성하고 있다. 해당 인터페이스를 구현하는 클래스의 정의를(클래스의 몸체를) 덧붙이면 인스턴스 생성이 가능하다. 이런 이름 없는 클래스의 정의를 가리켜 ‘익명 클래스’라 한다.
(a, b) -> { return a + b; }
메소드 몸체에 해당하는 내용이 return문이면 그 문장이 하나이더라도 중괄호의 생략이 불가능하다. 그러나 위의 람다식은 다음 람다식으로 대신할 수 있다.
(a, b) -> a + b; 이 경우 메소드 몸체에 연산이 등장하는데, 이 연산이 진행되면 그 결과로 값이 남게 된다. 그러면 이 값은 별도로 명시하지 않아도 반환의 대상이 된다. 따라서 return문이 메소드 몸체를 이루는 유일한 문장이면 위와 같이 작성할 수 있다. 그리고 이것이 보편적인 방식이다.
추상 메소드가 딱 하나만 존재하는 인터페이스 -> ‘함수형 인터페이스(Functional Interface)’ -> 람다식은 이러한 함수형 인터페이스를 기반으로 작성이 될 수 있다.
@FunctionalInterface은 함수형 인터페이스에 부합하는지를 확인하기 위한 어노테이션 타입이다. 인터페이스에 둘 이상의 추상 메소드가 존재하면, 이는 함수형 인터페이스가 아니기 때문에 컴파일 오류로 이어진다. 그러나 static, default 선언이 붙은 메소드의 정의는 함수형 인터페이스의 정의에 아무런 영향을 미치지 않는다. 따라서 다음 인터페이스도 함수형 인터페이스이다.
@FunctionalInterface
interface Calculate {
int cal(int a, int b);
default int add(int a, int b) { return a + b; }
static int sub(int a, int b) { return a - b; }
}
위의 인터페이스를 대상으로도 람다식을 구성할 수 있다. 어차피 채워야 할 메소드는 하나이기 때문이다.
static 메소드의 참조 - ClassName::staticMethodName
Consumer<List< Integer>> c = l -> Collections.reverse(l);
-> Consumer<List< Integer>> c = Collections::reverse;
람다식에 있는 인자 전달에 대한 정보를 생략할 수 있는 이유는 accept 메소드 호출 시 전달되는 인자를 reverse 메소드를 호출하면서 그대로 전달하기 때문이다.
참조변수를 통한 인스턴스 메소드 참조 - ReferenceName::instanceMethodName
Consumer<List< Integer>> c = e -> js.sort(e);
-> Consumer<List< Integer>> c = js::sort;
람다식에서 같은 지역에 선언된 참조변수에 접근하는 것은 가능하다. 단 람다식에서 접근 가능한 참조변수는 final로 선언되었거나 effectively final이어야 한다. 변수가 effectively final이라는 것은 ‘사실상 final 선언이 된 것과 다름없음’을 뜻한다.(인스턴스가 참조하는 대상을 수정하지 않을 때(다른 인스턴스를 참조하거나 null을 대입하거나))
클래스 이름을 통한 인스턴스 메소드 참조 - ClassName::instanceMethodName
ToIntBiFunction<IBox, IBox> bf = (b1, b2) -> b1.larger(b2);
-> ToIntBiFunction<IBox, IBox> bf = IBox::larger;
생성자 참조 - ClassName::new
Function<char[], String> f = ar -> new String(ar);
-> Function<char[], String> f = String::new;
람다식을 이루는 문장이 ‘단순히 인스턴스의 생성 및 참조 값의 반환’일 경우 이 형태의 메소드 참조로 바꿀 수 있다.