본문 바로가기
Computer Sience/Java

[JAVA8] 람다 표현식 + 변수 캡쳐 & 변수 쉐도잉

by 제우제우 2024. 9. 30.

목차 

  • 로컬 클래스 / 내부 클래스 
  • Java8 이전 익명 클래스 /로컬 클래스에서 변수 캡쳐 
  • Java8 이후 익명 클래스 /로컬 클래스에서 변수 캡쳐 
  • 변수 쉐도잉(Shadowing)
  • 변수 캡쳐 정리 
  • 참고 자료

로컬 클래스 / 내부 클래스

로컬 클래스 (Local Class)

메서드 내에서 정의된 클래스를 의미 

이 클래스는 메서드의 실행 흐름 안에서만 사용될 수 있다. 

public class LocalClassExample {
    public void doSomething() {
        // 로컬 클래스
        class LocalClass {
            void printMessage() {
                System.out.println("Hello!");
            }
        }
        // 로컬 클래스 인스턴스 생성 및 메서드 호출
        LocalClass localClass = new LocalClass();
        localClass.printMessage();
    }
    public static void main(String[] args) {
        LocalClassExample example = new LocalClassExample();
        example.doSomething(); // Hello 출력 
    }
}

 

Static 내부 클래스 (Static Nested Class)

Static 중첩 클래스는 외부 클래스의 인스턴스와는 독립적으로 존재할 수 있다.

Static 내부 클래스는 static 멤버를 가질 수 있으며, 외부 클래스의 static 멤버에만 접근할 수 있다.

Static 내부 클래스는 주로 외부 클래스의 인스턴스와 상관없이 사용될 독립적인 클래스를 만들고자 할 때 사용

public class OuterClass {
    static class StaticNestedClass {
        void printMessage() {
            System.out.println("Hello!");
        }
    }
    public static void main(String[] args) {
        // Static 내부 클래스의 인스턴스 생성
        StaticNestedClass nested = new StaticNestedClass();
        nested.printMessage();
    }
}

 

Non-static 내부 클래스 (Non-static Inner Class)

Non-static 내부 클래스는 외부 클래스의 인스턴스와 연결

이 클래스는 외부 클래스의 인스턴스 변수를 직접 참조할 수 있다.

외부 클래스의 인스턴스 없이는 Non-static 내부 클래스의 인스턴스를 생성할 수 없다. 즉, 내부 클래스 객체는 항상 외부 클래스 객체에 속한다.

public class OuterClass {
    private String message = "Hello!";
    class InnerClass {
        void printMessage() {
            // 외부 클래스의 인스턴스 변수에 접근 가능
            System.out.println(message);
        }
    }
    public static void main(String[] args) {
        // 외부 클래스의 인스턴스 생성
        OuterClass outer = new OuterClass();
        // Non-static 내부 클래스의 인스턴스 생성
        InnerClass inner = outer.new InnerClass();
        inner.printMessage();
    }
}

Java8 이전 익명 클래스 / 로컬 클래스에서 변수 캡쳐 

public class Main {
    public static void main(String[] args) {

    }
    private void run(){
        // Java8 이전은 무조건 final
        final int baseNumber = 10;

        // 로컬 클래스
        class LocalClass{
            void printBaseNumber(){
                System.out.println(baseNumber);
            }
        }
        // 익명 클래스
        Consumer<Integer> integerConsumer = new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                System.out.println(baseNumber);
            }
        };
    }
}

 

Java8 이전에 메서드 내의 로컬 변수는 무조건 final로 선언해야지 

익명 클래스나 로컬 클래스(메서드 내에서 정의된 클래스)에서 참조가 가능했다. 


Java8 이후 익명 클래스 / 로컬 클래스에서 변수 캡쳐 

Java 8 이후에는 effectively final (사실상 final)개념이 도입되었다.

이제 로컬 변수가 final로 명시되지 않더라도, 값이 한 번만 초기화되고 이후에 변경되지 않는다면 해당 변수를 로컬 클래스나 익명 클래스에서 사용할 수 있다.

다시 말해, 변수의 값이 변경되지 않으면 final로 선언하지 않아도 캡쳐가 가능하다.

private void run() {
    int baseNumber = 10; // final로 선언하지 않음
    // 로컬 클래스
    class LocalClass {
        void printBaseNumber() {
            System.out.println(baseNumber);
        }
    }
    // 익명 클래스
    Consumer<Integer> integerConsumer = new Consumer<Integer>() {
        @Override
        public void accept(Integer integer) {
            System.out.println(baseNumber);
        }
    };
}

 

하지만 baseNumber의 값을 변경하려고 하면 컴파일 오류가 발생

Java 8 이후에는 변수의 상태만으로도 캡쳐 가능 여부를 결정하는 유연성이 생겼지만, 변수의 값이 변경되면 여전히 익명 클래스나 로컬 클래스에서 참조할 수 없다.

 

컴파일 오류 케이스 

private void run() {
    int baseNumber = 10; // final로 선언하지 않음
    // 로컬 클래스
    class LocalClass {
        void printBaseNumber() {
            System.out.println(baseNumber); // baseNumber++ 컴파일 오류 발생
        }
    }
    // 익명 클래스
    Consumer<Integer> integerConsumer = new Consumer<Integer>() {
        @Override
        public void accept(Integer integer) {
            System.out.println(baseNumber); // baseNumber++ 컴파일 오류 발생
        }
    };
    baseNumber++; 
}

변수 쉐도잉(Shadowing)

하위 스코프에서 상위 스코프에 있는 변수와 동일한 이름의 변수를 선언할 때 발생하는 현상 

이 경우. 하위 스코프의 변수는 상위 스코프의 변수를 가려서 접근하지 못하게 만든다. 

 

람다 표현식

public class LambdaShadowingExample {
    public static void main(String[] args) {
        int baseNumber = 10;

        // 람다 표현식
        Consumer<Integer> consumer = (baseNumber) -> { // 컴파일 오류 발생
            System.out.println(baseNumber);
        };
    }
}

람다 내부에서 baseNumber라는 이름의 변수를 다시 선언하려고 하면, 컴파일 오류가 발생한다.

이는 자바에서 람다 표현식은 상위 스코프의 변수를 가리는 것이 허용되지 않기 때문

 

로컬 클래스(Local Class) & 익명 클래스(Anonymous Class)

public class LocalClassShadowingExample {
    public static void main(String[] args) {
        int baseNumber = 10;

        // 로컬 클래스
        class LocalClass {
            void printBaseNumber() {
                int baseNumber = 20;  // 쉐도잉 발생
                System.out.println(baseNumber); // 20이 출력됨
            }
        }
        LocalClass localClass = new LocalClass();
        localClass.printBaseNumber(); // 내부 변수인 20 출력
        
        // 익명 클래스
        Consumer<Integer> consumer = new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                int baseNumber = 30; // 쉐도잉 발생
                System.out.println(baseNumber); // 30이 출력됨
            }
        };
    }
}

로컬 클래스 내부에서 baseNumber라는 동일한 이름의 변수를 선언하여 쉐도잉이 발생

익명 클래스 내부에서 상위 스코프와 동일한 이름의 변수를 선언하면, 그 변수는 상위 스코프의 변수를 가린다.


변수 캡쳐 정리

public class Main {
    public static void main(String[] args) {

    }
    private void run(){
        // Java8 이전은 무조건 final
        int baseNumber = 10;

        // 로컬 클래스
        class LocalClass{
            void printBaseNumber(){
                System.out.println(baseNumber); // 컴파일 에러 
            }
        }
        // 익명 클래스
        Consumer<Integer> integerConsumer = new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                int baseNumber = 30;
                System.out.println(baseNumber);
            }
        };
        baseNumber++;
        // 람다
        IntConsumer printInt = i -> System.out.println(i + baseNumber); // 컴파일 에러 
        printInt.accept(10); // 20 출력

    }
}

 

현재 baseNumber는 final이 아니다. 

그런데 run() 메서드 익명 클래스와 람다 표현식 사이를 보면 baseNumber가 변경되고 있다.

그래서 사실상 final 즉 effectively final이 아니다. 

그래서 로컬 클래스와 람다 표현식에서는 컴파일 에러가 발생하지만 

익명 클래스는 내부에 똑같은 이름의 baseNumber가 있기 때문에 쉐도잉을 통해서 컴파일 에러가 발생하지 않는다. 


참고 자료 

인프런 백기선님 더 자바, Java 8

 

더 자바, Java 8 강의 | 백기선 - 인프런

백기선 | 자바 8에 추가된 기능들은 자바가 제공하는 API는 물론이고 스프링 같은 제 3의 라이브러리 및 프레임워크에서도 널리 사용되고 있습니다. 이 시대의 자바 개발자라면 반드시 알아야 합

www.inflearn.com