ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • java anonymous class 의 final
    카테고리 없음 2023. 4. 21. 15:27
    오랜만에 자바 이야기를 해볼라고 한다. 자바를 아직많이 공부해야 할 듯 하다. 모르는게 많다. spring도 spring이지만 자바를 알아야 뭘하지 않겠나 싶다. spring도 자바로 되어 있으니 말이다. 오늘 이야기 하고 싶은거는 java의 anonymous class는 final 이어야만 접근이 가능하다. (java8일 경우에는 lambda 도 해당된다.) 를 이야기 해볼것이다. final은 다 아시다시피 상수이다. 변하지 않는 값이다. 여기서는 값을 말한거지 레퍼런스(참조하고 있는 값)를 말한 것은 아니다. 즉 할당이 다시 되지 않는 것이지 참조하고 있는 변수들은 변경 가능하다. 간단하게 anonymous class 보자.
    void test() {
      int a = 0;
      Runnable runnable = new Runnable() {
        @Override
        public void run() {
          System.out.println(a);
        }
      };
    }
    
    
    이와 같은 익명 클래스가 있을 때는 우리는 다음과 같이 변수 a에 접근 할 수 없다. 왜냐하면 final이 아니라 접근하지 못했다. 하지만 java8부터는 위와 같이 해도 컴파일 에러가 나지 않는다. a라는 변수에 직접 접근할 수 있다. 람다의경우는 아래와 같다. 컴파일 에러가 나지 않는다.
    void test() {
      int a = 0;
      Runnable runnable = () -> System.out.println(a);
    }
    
    어? 그럼 final 아니니 변경도 가능하겠네?
    void test() {
      int a = 0;
      Runnable runnable = new Runnable() {
        @Override
        public void run() {
          System.out.println(a);
          a++; //컴파일 에러
        }
      };
    }
    
    //lambda의 경우
    void test() {
      int a = 0;
      Runnable runnable = () -> {
        System.out.println(a);
        a++; //컴파일 에러
      };
    }
    
    위와 같이 a라는 변수를 변경 시킬 때 컴파일 에러를 발생시킨다. 에러 내용을 보면 effectively final 라고 한다. 실질적으로 final이라는 것이다. 생략을 했을뿐 final이다. 우리는 어쨋든 익명 클래스 밖에 있는 지역변수를 접근할 때는 무조건 final이어야 한다. 하지만 우리는 꼼수를 써서 지역변수의 final의 레퍼런스를 변경 시키기도 한다. 아래와 같이 말이다.
    void test() {
      int[] a = {0};
      Runnable runnable = () -> {
        System.out.println(a[0]);
        a[0]++;
      };
    }
    
    위와 같이 배열(혹은 참조할 수 있는 어떤 객체)을 사용하여 우리는 변경 시켰다. 잘 된다. 그리고 또한 아직 까진 문제가 없다. 그런데 왜 익명클래스밖 지역변수를 접근할때는 왜 꼭 final이어야 할까? 흠.. 자바를 설계했던 사람이 괜히 final로 만든 것은 아니였을 것이다. 아무 side effect 없다면 그냥 접근해도 될 텐데 말이다.. side effect 있으니 final로 설계해 만들었을 것 아닌가? 그럼 어떤 side effect가 있어서 꼭 변하지 않는 final이어야 할까? 그건 바로 스레드 세이프하지 않기 때문이다. 예를 들어보자.
    int count = 1000;
    int[] flag = {0};
    Runnable b = new Runnable() {
      @Override
      public void run() {
        if (flag[0] == 0) {
          flag[0] = 1;
          try {
            TimeUnit.MICROSECONDS.sleep(10);
          } catch (InterruptedException e) {
          }
          System.out.println(flag[0]);
        } else {
          flag[0] = 0;
        }
      }
    };
    
    Thread[] threads = new Thread[count];
    for (int i = 0; i < count; i++) {
      threads[i] = new Thread(b);
    }
    for (Thread thread : threads) {
      thread.start();
    }
    
    위의 예가 적당한예인지는.. 아무튼 보자. 우리는 flag 변수에 1개의 배열을 할당하고 익명클래스 안에서 변경을 하고 있다. 그리고 flag[0] == 0 일때는 1로 바꾸고 어떠한 작업(시간이 걸리는)을 한다고 하고 flag에 첫번째 배열을 출력 해보았다. 만약 스레드 세이프 하다면 모두 1로 출력 되어야 정상이다. 하지만 결과를 그렇지않다. 1도 있고 0도 있고 뒤죽박죽이다. 스레드 세이프 하지 않다는 것이다. 좀더 테스트 하기 쉬운 방법으로 java8의 parallelStream을 이용해서 테스트를 해보자. parallelStream은 병렬 처리르 할 수 있는 Stream이다. 자세한 내용은 인터넷에서 찾아보거나 나중에 기회가 된다면 포스팅해보자. 병렬처리를 한다는 것만 알면 된다 지금은.. 아래 코드를 보자.
    int[] sum = {0};
    IntStream.range(0, 100).forEach(i ->
      sum[0]+=i
    );
    System.out.println(sum[0]);
    
    일단 0부터 99까지 더하는 그런 소스이다. 출력 값은 다들 아시다시피 4950이 나온다. 그런데 병렬 처리를 하면 어떻게 될까?
    int[] sum = {0};
    IntStream.range(0, 100).parallel().forEach(i ->
      sum[0]+=i
    );
    System.out.println(sum[0]);
    
    위와 같이 0부터 99까지 더한 값을 출력해보면 출력할때마다 다르다. 4000대를 왔다 갔다 한다. 위와 같이 스레드 세이프 하지 않다는 것이 증명 되었다. 그래서 스레드 세이프 하기 위해 final로 정했던거 같다. 만약 위의 방법처럼 하나의 자원을 공유해서 쓰기를 한다면 스레드 세이프 하지 않을 것이니 조심해서 개발을 해야 할 것같다. 물론 읽기는 문제가 없다. 우리가 계속 했던 저짓이 바로 java 의 closure 이다. 예전에 자바스크립트 closure에 대해 조금 설명 했었는데.. 자바에도 closure가 있다. 이거 역시 scope와 관계가 있다. 포스팅 주제와는 사뭇 다르지만 관계는 있어 짧게 살펴보자.
    private static Function<String, Integer> function() {
      int a = 100;
      return new Function<String, Integer>() {
        @Override
        public Integer apply(String s) {
          return Integer.parseInt(s) * a;
        }
      };
    }
    
    Function<String, Integer> function = function();
    Integer apply = function.apply("3");
    System.out.println(apply);
    
    //아래는 람다
    private static Function<String ,Integer> function(){
      int a = 100;
      return s -> Integer.parseInt(s) * a;
    }
    
    람다의 익숙하지 않는 사람을 위해 anonymous class도 만들어 봤다. 위와 같은 함수를 호출 할때 function()이란 메서드는 생을 마감하였지만 apply메서드는 외부 변수에 접근 할 수 있다. 우리는 이것을 클로저라 부른다. 이렇게 클로저까지 살짝 알아봤다. 람다가 훨씬 깔끔하다. 그러니 자바 버전좀 제발좀 올리자. 나온지 2년이 훨씬 넘었는데.. 아직 java6 을 쓰고 있는 회사들은 제발..ㅜ.. java9도 내년쯤 릴리즈 될 것같은데.. 또 공부를 해야되나. 만약 위의 내용이 틀렸거나 잘못 설명된 부분이 있다면 댓글로 알려주시면 감사감사 이렇게 anonymous class final 이어야만 했던 이유를 살펴봤다. 설계할 때 고민좀 했을듯? 아닌가?

    댓글

Designed by Tistory.