Study/Java

자바 기본서를 다시 읽다. 3 - 로컬클래스와 익명객체, 그리고 람다식 (1)

going.yoon 2022. 3. 15. 16:58

람다식을 공부하다보니, 로컬클래스와 익명개체 그리고 람다식의 관계가 보였다. 람다식을 더 정확하게 사용하기 위해 로컬클래스와 익명개체부터 정리해보려고 한다.

 

I. 로컬클래스

첫번째로, 로컬클래스는 중첩 클래스 중 하나로 하나의 클래스, especially 메소드 내부에서 선언되어 메소드가 실행 될 때에만 사용되는 클래스이다. 로컬클래스와 반대로 멤버클래스는 하나의 클래스 안에서 멤버변수로 선언되어 객체가 사용중에 언제든 재사용이 가능한 객체이다.

https://thisisprogrammingworld.tistory.com/29

 

 

로컬클래스는 메소드 내부에서만 사용되므로 접근을 제한할 필요가 없어 public, private 등의 접근 제한자가 사용이 불가능하다.

또한 static 필드와 메소드도 선언할 수 없다.

 

로컬클래스의 가장 중요한 특징은 다음과 같다. 

로컬 클래스 내부에서 바깥 클래스의 필드나 메소드를 사용 할 수 있지만, 메소드의 매개변수나 로컬변수를 사용할땐 문제가 발생한다.

로컬클래스 객체힙 메모리에 저장되어 메소드 실행이 끝나도 메모리에 존재하는 반면,

매개변수나 로컬변수스택 메모리에 저장되어 메소드 실행과 함께 메모리에서 사라지기 때문이다.

 

따라서 자바에서는 로컬클래스 객체를 컴파일 할 때 사용했던 매개변수와 로컬변수들의 ''을 로컬클래스 내부에 복사하여 저장하고,

매개변수나 로컬변수가 값이 수정되어 복사값과 불일치하게 되는 문제를 해결하기 위해 이들을 final로 지정하여 수정을 막는다.

개발자가 final 키워드를 명시하지 않아도 자바 8부터는 자동으로 final의 특성을 갖게 된다. 다만 키워드가 명시되었을 때는 해당 변수들이 로컬클래스 내의 로컬변수로 복사되고, 그렇지 않았을 때는 필드로 복사된다.

 

아래와 같이 코드가 작성되면

void Method(final int arg1 ,int arg2){
    final int var1 = 1;
    int var2 = 2;
    class LocalClass {
        void method(){
            int result = arg1 + arg2 + var1 + var2;
        }
    }
}

 

이렇게 컴파일 된다. final이 붙은 애들은 로컬변수로, final이 없는 애들은 필드로 복사되었다.

class LocalClass {
    int args2 = 매개값;
    int var2 = 2;
    void method(){
        int args1 = 매개값;
        int var1 = 1;
        int result = arg1 + args2 + var1 + var2;
    }
}

 

II. 익명객체

이러한 로컬클래스의 특성을 모두 가지고 있으면서 클래스 이름을 붙이지 않고 사용하는 클래스를 익명(anonymous) 객체라고 한다.

익명객체는 상속을 통하거나 인터페이스 구현을 통해 생성해야 한다. 그렇다면 상속을 통한 구현부터 알아보자.

 

상속을 통한 익명 객체 구현 - 익명 자식 객체 생성

public class A {
    
    // #1. 필드변수 - 자식객체 생성 후 부모객체 대입
    Parent field = new Child();
    
    // #2. 로컬변수  - 동일하게 자식객체 생성 후 부모 객체 대입
    void method(){
        Parent localVar = new Child();
    }

}

 

익명객체를 사용하지 않고 필드나 로컬변수에서 자식객체를 부모객체에 대입하기 위해서는 new 생성자로 객체를 생성 한 다음 대입을 해주어야 한다. 하지만 이런 방법은 자식클래스가 오로지 초기화용으로만 사용되고 재사용되지 않기 때문에 비효율적이라고 할 수 있다. 

Let me show you how to improve this code by using anonymous class!

 

아래와 같이 부모클래스에게 대입할 때, 자식객체를 직접 생성하는 것이 아니라 부모 객체를 생성하되 자식 필드와 자식 메소드를 안에 선언하여 사용한다. 자바에서는 이를 자식객체를 생성한다고 인식하여 부모 함수를 오버라이딩하여 사용할 수도 있다. 주의할 점은 익명객체 생성 후 뒤에 세미콜론을 꼭 붙여줄 것.

public class A {

    // #1. 필드변수 
    Parent field 
            = new Parent(int args1, int args2){ // #1.1 부모 클래스를 상속해서 자식 클래스를 선언하라
                                                // 단, 부모클래스의 생성자를 호출하니 부모클래스의 생성자의 매개변수를 넘겨주어야 함.
        int childField;  
        int childMethod(){};
        
        @Override
        int parentMethod(){};
    } ; // # 1.2 세미콜론 필수

    // #2. 로컬변수  
    void method(){
        Parent localVar = new Parent(int args1, int args2){
            int childField;
            int childMethod(){};
            
            @Override
            int parentMethod(){};        
        };
    }

}

 

한가지 더 주의할 점은 익명객체에서 새롭게 정의된 필드(childField, childMethod)는 외부에서 접근할 수 없다. 왜냐하면 익명자식객체는 부모 타입 변수에 대입되므로 부모 타입에 선언된 것 만 사용할 수 있기 때문이다.

 

 

인터페이스 통한 익명 객체 구현 - 익명 구현 객체 생성

먼저 아래와 같이 Car와 Vehicle 객체가 인터페이스 - 구현체의 관계를 가지고 있다고 해보자

public class Car implements Vehicle

 

그렇다면 이렇게 초기화를 해왔을 것이다.

class A {
    // #1 .필드 인터페이스 객체에 구현 객체 대입
    Vehicle field = new Car();
    
    // #2. 로컬 인터페이스 객체에 구현 객체 대입
    void method(){
        Vehicle localVar = new Car();
    }
}

 

구현객체를 직접 대입하지 않아도 익명객체를 활용해서 다음과 같이 초기화할 수 있다. 

인터페이스 객체에 인터페이스를 대입하는 순간 인터페이스의 추상메소드를 오버라이드 하라는 코드가 자동 생성된다 ㅋ

public class A {

    // #1. 필드 인터페이스 객체에 인터페이스 익명객체 대입
    Vehicle field = new Vehicle() {
        @Override
        public void run() {
            // 인터페이스의 추상 메소드를 정의해준다.
        }
    };

    // #2. 로컬 인터페이스 객체에 인터페이스 익명객체 대입
    void method() {
        Vehicle localVar = new Vehicle() {
            @Override
            public void run() {
                // 인터페이스의 추상메소드 대입
            }
        };
    }
}

 

익명객체도 로컬클래스와 마찬가지로 메소드 실행 끝나도 힙 메모리 영역에 존재하지만, 익명개체 안에서 사용된 매개변수나 로컬변수는 익명 객체 내에 값이 복사되어 저장되기 때문에 final의 속성을 가진다. final 키워드가 붙어있는 것은 로컬변수로, 없는 것은 필드 변수로 복사되어 컴파일 되는 것도 동일!