[java] 자바“더블 브레이스 초기화”의 효율성?

에서 자바의 숨겨진 기능 상단의 대답은 언급 더블 중괄호 초기화를 로, 매우 유혹 구문 :

Set<String> flavors = new HashSet<String>() {{
    add("vanilla");
    add("strawberry");
    add("chocolate");
    add("butter pecan");
}};

이 관용구에는 인스턴스 이니셜 라이저가 포함 된 익명의 내부 클래스가 만들어져 “포함 범위에서 […] 메서드를 사용할 수 있습니다”.

주요 질문 : 이것은 소리가 비효율적 입니까? 일회용 초기화로만 사용해야합니까? (물론 과시!)

두 번째 질문 : 새로운 HashSet은 인스턴스 이니셜 라이저에서 사용되는 “this”여야합니다. 누구든지 메커니즘을 밝힐 수 있습니까?

세 번째 질문 :이 관용구 가 프로덕션 코드에서 사용하기에 너무 모호 합니까?

요약 : 정말 좋은 답변입니다. 모두 감사합니다. 질문 (3)에서 사람들은 구문이 명확해야한다고 느꼈습니다 (특히 코드가 익숙하지 않은 개발자에게 코드가 전달되는 경우 가끔 주석을 권장합니다).

질문 (1)에서 생성 된 코드는 빠르게 실행되어야합니다. 여분의 .class 파일은 jar 파일을 혼란스럽게 만들고 프로그램 시작을 약간 느리게 만듭니다 (@coobird 덕분에 측정). @Thilo는 가비지 수집에 영향을 줄 수 있으며 추가로드 클래스의 메모리 비용이 일부 경우 요소가 될 수 있다고 지적했습니다.

질문 (2)는 나에게 가장 흥미로운 것으로 판명되었습니다. 대답을 이해하면 DBI에서 일어나는 일은 익명의 내부 클래스가 새 연산자로 구성되는 객체의 클래스를 확장하므로 생성되는 인스턴스를 참조하는 “this”값이 있다는 것입니다. 매우 깔끔합니다.

전반적으로 DBI는 나를 지적 호기심의 대상으로 생각합니다. Coobird와 다른 사람들은 Arrays.asList, varargs 메소드, Google Collections 및 제안 된 Java 7 Collection 리터럴을 사용하여 동일한 효과를 얻을 수 있다고 지적합니다. Scala, JRuby 및 Groovy와 같은 최신 JVM 언어는 목록 구성에 대한 간결한 표기법을 제공하며 Java 와도 잘 작동합니다. DBI가 클래스 경로를 어지럽히고 클래스 로딩 속도를 느리게하고 코드를 좀 더 모호하게 만들면 아마 부끄러워 할 것입니다. 그러나 나는 SCJP를 방금 받고 Java 의미에 대해 좋은 성격의 사람들을 사랑하는 친구에게 이것을 뿌릴 계획입니다! 😉 모두 감사합니다!

7/2017 : Baeldung 이중 괄호 초기화에 대한 좋은 요약 을 가지고 있으며 반 패턴으로 간주합니다.

12/2017 : @Basil Bourque는 새로운 Java 9에서는 다음과 같이 말할 수 있다고 말합니다.

Set<String> flavors = Set.of("vanilla", "strawberry", "chocolate", "butter pecan");

그것은 확실한 길입니다. 이전 버전을 사용하고 있다면 Google 컬렉션의 ImmutableSet을 살펴보십시오 .



답변

익명의 내부 클래스로 너무 멀어지면 문제가 있습니다.

2009/05/27  16:35             1,602 DemoApp2$1.class
2009/05/27  16:35             1,976 DemoApp2$10.class
2009/05/27  16:35             1,919 DemoApp2$11.class
2009/05/27  16:35             2,404 DemoApp2$12.class
2009/05/27  16:35             1,197 DemoApp2$13.class

/* snip */

2009/05/27  16:35             1,953 DemoApp2$30.class
2009/05/27  16:35             1,910 DemoApp2$31.class
2009/05/27  16:35             2,007 DemoApp2$32.class
2009/05/27  16:35               926 DemoApp2$33$1$1.class
2009/05/27  16:35             4,104 DemoApp2$33$1.class
2009/05/27  16:35             2,849 DemoApp2$33.class
2009/05/27  16:35               926 DemoApp2$34$1$1.class
2009/05/27  16:35             4,234 DemoApp2$34$1.class
2009/05/27  16:35             2,849 DemoApp2$34.class

/* snip */

2009/05/27  16:35               614 DemoApp2$40.class
2009/05/27  16:35             2,344 DemoApp2$5.class
2009/05/27  16:35             1,551 DemoApp2$6.class
2009/05/27  16:35             1,604 DemoApp2$7.class
2009/05/27  16:35             1,809 DemoApp2$8.class
2009/05/27  16:35             2,022 DemoApp2$9.class

이들은 간단한 응용 프로그램을 만들 때 생성되었으며 많은 양의 익명 내부 클래스를 사용했습니다. 각 클래스는 별도의 class파일 로 컴파일됩니다 .

이미 언급했듯이 “이중 괄호 초기화”는 인스턴스 초기화 블록이있는 익명의 내부 클래스입니다. 이는 일반적으로 단일 객체를 만들기 위해 모든 “초기화”에 대해 새 클래스가 만들어 짐을 의미합니다.

Java Virtual Machine은 클래스를 사용할 때 해당 클래스를 모두 읽어야하므로 바이트 코드 검증 프로세스 등에서 시간이 걸릴 수 있습니다 . 모든 class파일 을 저장하기 위해 필요한 디스크 공간이 증가한 것은 말할 것도 없습니다 .

이중 괄호 초기화를 사용할 때 약간의 오버 헤드가있는 것처럼 보이므로 너무 지나치게 선을 긋는 것은 좋은 생각이 아닙니다. 그러나 Eddie가 의견에서 언급했듯이 그 영향을 절대적으로 확신하는 것은 불가능합니다.


참고로 이중 괄호 초기화는 다음과 같습니다.

List<String> list = new ArrayList<String>() {{
    add("Hello");
    add("World!");
}};

Java의 “숨겨진”기능처럼 보이지만 다음과 같이 다시 작성되었습니다.

List<String> list = new ArrayList<String>() {

    // Instance initialization block
    {
        add("Hello");
        add("World!");
    }
};

따라서 기본적으로 익명의 내부 클래스의 일부인 인스턴스 초기화 블록 입니다 .


Project Coin 에 대한 Joshua Bloch의 Collection Literals 제안 은 다음과 같습니다.

List<Integer> intList = [1, 2, 3, 4];

Set<String> strSet = {"Apple", "Banana", "Cactus"};

Map<String, Integer> truthMap = { "answer" : 42 };

슬프게도, 그것은 그것의 방법을하지 않았다 도 자바 7이나 8로 무기한 보류했다.


실험

여기에 내가 테스트 한 간단한 실험이다 – 메이크업 1000 개 ArrayList요소와의 "Hello""World!"비아 그들에 추가 add하는 방법, 두 가지 방법을 사용하여이 :

방법 1 : 이중 브레이스 초기화

List<String> l = new ArrayList<String>() {{
  add("Hello");
  add("World!");
}};

방법 2 : 인스턴스화 ArrayListadd

List<String> l = new ArrayList<String>();
l.add("Hello");
l.add("World!");

Java 소스 파일을 작성하여 두 가지 방법으로 1000 개의 초기화를 수행하는 간단한 프로그램을 만들었습니다.

시험 1 :

class Test1 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    List<String> l1 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    /* snip */

    List<String> l999 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    System.out.println(System.currentTimeMillis() - st);
  }
}

시험 2 :

class Test2 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>();
    l0.add("Hello");
    l0.add("World!");

    List<String> l1 = new ArrayList<String>();
    l1.add("Hello");
    l1.add("World!");

    /* snip */

    List<String> l999 = new ArrayList<String>();
    l999.add("Hello");
    l999.add("World!");

    System.out.println(System.currentTimeMillis() - st);
  }
}

1000 ArrayList초 및 1000 개의 익명 내부 클래스 확장 을 초기화하는 데 소요되는 시간 ArrayList은을 사용하여 확인 System.currentTimeMillis되므로 타이머의 해상도가 매우 높지 않습니다. 내 Windows 시스템에서 해상도는 약 15-16 밀리 초입니다.

두 테스트 중 10 회 실행 결과는 다음과 같습니다.

Test1 Times (ms)           Test2 Times (ms)
----------------           ----------------
           187                          0
           203                          0
           203                          0
           188                          0
           188                          0
           187                          0
           203                          0
           188                          0
           188                          0
           203                          0

알 수 있듯이, 이중 브레이스 초기화는 눈에 띄는 실행 시간이 약 190ms입니다.

한편, ArrayList초기화 실행 시간은 0ms로 나타났다. 물론 타이머 해상도를 고려해야하지만 15ms 미만일 수 있습니다.

따라서 두 방법의 실행 시간에 눈에 띄는 차이가있는 것으로 보입니다. 두 가지 초기화 방법에는 실제로 약간의 오버 헤드가있는 것으로 보입니다.

그리고 이중 중괄호 초기화 테스트 프로그램 .class을 컴파일하여 1000 개의 파일이 생성되었습니다 Test1.


답변

지금까지 지적되지 않은이 접근법의 한 가지 특성은 내부 클래스를 작성하므로 전체 포함 클래스가 해당 범위에서 캡처된다는 것입니다. 즉, Set이 살아있는 한 포함하는 인스턴스에 대한 포인터를 유지합니다 (this$0 )에 유지하고 가비지 수집되지 않도록 문제가 될 수 있습니다.

이것은 정규 HashSet이 잘 작동하거나 더 잘 작동하더라도 새로운 클래스가 처음부터 생성된다는 사실 때문에 (구문 설탕이 정말 길더라도)이 구문을 사용하고 싶지 않습니다.

두 번째 질문 : 새로운 HashSet은 인스턴스 이니셜 라이저에서 사용되는 “this”여야합니다. 누구든지 메커니즘을 밝힐 수 있습니까? 나는 “flavors”를 초기화하는 객체를 가리키는 “this”를 순진하게 기대했을 것이다.

이것은 내부 클래스가 작동하는 방식입니다. 그것들은 자신의 것을 얻지 만 this부모 인스턴스에 대한 포인터도 가지고 있으므로 포함하는 객체에서 메소드를 호출 할 수도 있습니다. 이름이 충돌하는 경우 내부 클래스 (귀하의 경우 HashSet)가 우선하지만 클래스 이름으로 “this”접두어를 붙여 외부 메소드도 가져올 수 있습니다.

public class Test {

    public void add(Object o) {
    }

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // HashSet
              Test.this.add("hello"); // outer instance 
            }
        };
    }
}

생성되는 익명 서브 클래스를 명확하게하기 위해 메소드를 정의 할 수도 있습니다. 예를 들어 재정의HashSet.add()

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // not HashSet anymore ...
            }

            @Override
            boolean add(String s){

            }

        };
    }


답변

누군가 이중 괄호 초기화를 사용할 때마다 새끼 고양이가 죽습니다.

구문이 다소 독특하지 않고 실제로 관용적이지는 않지만 (물론 맛은 물론 논쟁의 여지가 있습니다), 응용 프로그램에서 두 가지 중요한 문제가 불필요하게 생성 되고 있습니다 .

1. 당신은 너무 많은 익명 클래스를 만들고 있습니다

이중 괄호 초기화를 사용할 때마다 새 클래스가 작성됩니다. 예를 들어이 예 :

Map source = new HashMap(){{
    put("firstName", "John");
    put("lastName", "Smith");
    put("organizations", new HashMap(){{
        put("0", new HashMap(){{
            put("id", "1234");
        }});
        put("abc", new HashMap(){{
            put("id", "5678");
        }});
    }});
}};

…이 클래스를 생성합니다 :

Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class

클래스 로더에는 약간의 오버 헤드가 있습니다. 아무것도 없습니다! 물론 한 번 수행하면 초기화 시간이 많이 걸리지 않습니다. 그러나 엔터프라이즈 애플리케이션 전체에서이 2 만 번을 수행한다면 … “구문 설탕”에 대한 모든 힙 메모리?

2. 메모리 누수가 발생할 가능성이 있습니다!

위의 코드를 가져와 메소드에서 해당 맵을 리턴하면 해당 메소드의 호출자가 의심 할 여지없이 가비지 콜렉션 할 수없는 매우 많은 자원을 보유하고있을 수 있습니다. 다음 예제를 고려하십시오.

public class ReallyHeavyObject {

    // Just to illustrate...
    private int[] tonsOfValues;
    private Resource[] tonsOfResources;

    // This method almost does nothing
    public Map quickHarmlessMethod() {
        Map source = new HashMap(){{
            put("firstName", "John");
            put("lastName", "Smith");
            put("organizations", new HashMap(){{
                put("0", new HashMap(){{
                    put("id", "1234");
                }});
                put("abc", new HashMap(){{
                    put("id", "5678");
                }});
            }});
        }};

        return source;
    }
}

반환 된 Map에의 닫는 인스턴스에 대한 참조가 포함됩니다 ReallyHeavyObject. 다음과 같은 위험을 감수하고 싶지 않을 것입니다.

바로 메모리 누수

http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/의 이미지

3. Java에 맵 리터럴이 있다고 가정 할 수 있습니다.

실제 질문에 답하기 위해 사람들은이 구문을 사용하여 Java에 기존 배열 리터럴과 비슷한 맵 리터럴과 같은 것으로 가장했습니다.

String[] array = { "John", "Doe" };
Map map = new HashMap() {{ put("John", "Doe"); }};

어떤 사람들은 이것이 문법적으로 자극적 인 것을 발견 할 수 있습니다.


답변

다음과 같은 시험 수업을 듣습니다.

public class Test {
  public void test() {
    Set<String> flavors = new HashSet<String>() {{
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }};
  }
}

클래스 파일을 디 컴파일하면 다음과 같습니다.

public class Test {
  public void test() {
    java.util.Set flavors = new HashSet() {

      final Test this$0;

      {
        this$0 = Test.this;
        super();
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
    };
  }
}

이것은 나에게 비효율적으로 보이지 않습니다. 이와 같은 성능에 대해 걱정이된다면 프로파일 링합니다. 그리고 당신의 질문 # 2는 위의 코드에 의해 답변됩니다 : 당신은 당신의 내부 클래스를위한 암시 적 생성자 (및 인스턴스 초기화 자) 안에 있습니다.this “는이 내부 클래스를 가리 킵니다.

예,이 구문은 모호하지만 주석이 모호한 구문 사용법을 명확하게 설명 할 수 있습니다. 구문을 명확히하기 위해 대부분의 사람들은 정적 초기화 블록 (JLS 8.7 정적 초기화 기)에 익숙합니다.

public class Sample1 {
    private static final String someVar;
    static {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

static생성자 사용법 (JLS 8.6 인스턴스 이니셜 라이저)에 유사한 구문 ( ” ” 없이 )을 사용할 수 있지만 프로덕션 코드에서는이 구문을 사용한 적이 없습니다. 이것은 덜 일반적으로 알려져 있습니다.

public class Sample2 {
    private final String someVar;

    // This is an instance initializer
    {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

기본 생성자가없는 경우 {와 코드 사이의 코드 블록은 }컴파일러에 의해 생성자로 바뀝니다. 이를 염두에두고 이중 중괄호 코드를 풀어보십시오.

public void test() {
  Set<String> flavors = new HashSet<String>() {
      {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
  };
}

가장 안쪽 중괄호 사이의 코드 블록은 컴파일러에 의해 생성자로 바뀝니다. 가장 바깥 쪽 중괄호는 익명의 내부 클래스를 구분합니다. 이것을 익명이 아닌 것으로 만드는 마지막 단계를 수행하려면 :

public void test() {
  Set<String> flavors = new MyHashSet();
}

class MyHashSet extends HashSet<String>() {
    public MyHashSet() {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }
}

초기화 목적으로, 오버 헤드가 없다고 말하거나 무시할 수 없습니다. 그러나 모든 사용은 flavors반대가 HashSet아니라 반대 MyHashSet입니다. 이것에는 아마도 (아마도 무시할만한) 오버 헤드가있을 것입니다. 그러나 다시 걱정하기 전에 프로파일 링합니다.

다시 질문 # 2에서, 위의 코드는 이중 중괄호 초기화와 논리적이고 명시 적으로 동일하며 ” this“가 참조하는 위치를 명확하게합니다 HashSet.

인스턴스 이니셜 라이저의 세부 사항에 대한 질문이있는 경우 JLS 문서 에서 세부 사항을 확인하십시오 .


답변

누출되기 쉬운

차임하기로 결정했습니다. 성능 영향에는 디스크 작업 + 압축 풀기 (jar의 경우), 클래스 확인, perm-gen 공간 (Sun의 핫스팟 JVM의 경우)이 포함됩니다. 그러나, 최악의 경우 : 누출이 발생하기 쉽습니다. 당신은 단순히 돌아올 수 없습니다.

Set<String> getFlavors(){
  return Collections.unmodifiableSet(flavors)
}

따라서 집합이 다른 클래스 로더에 의해로드 된 다른 부분으로 이스케이프되고 참조가 유지되면 클래스 + 클래스 로더의 전체 트리가 누출됩니다. 이를 피하려면 HashMap에 대한 사본이 필요 new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}})합니다. 더 이상 귀엽지 않습니다. 나는 관용구를 사용하지 않고 대신 new LinkedHashSet(Arrays.asList("xxx","YYY"));


답변

많은 클래스를로드하면 시작에 몇 밀리 초가 추가 될 수 있습니다. 시작이 그렇게 중요하지 않고 시작 후 클래스의 효율성을 보는 경우 아무런 차이가 없습니다.

package vanilla.java.perfeg.doublebracket;

import java.util.*;

/**
 * @author plawrey
 */
public class DoubleBracketMain {
    public static void main(String... args) {
        final List<String> list1 = new ArrayList<String>() {
            {
                add("Hello");
                add("World");
                add("!!!");
            }
        };
        List<String> list2 = new ArrayList<String>(list1);
        Set<String> set1 = new LinkedHashSet<String>() {
            {
                addAll(list1);
            }
        };
        Set<String> set2 = new LinkedHashSet<String>();
        set2.addAll(list1);
        Map<Integer, String> map1 = new LinkedHashMap<Integer, String>() {
            {
                put(1, "one");
                put(2, "two");
                put(3, "three");
            }
        };
        Map<Integer, String> map2 = new LinkedHashMap<Integer, String>();
        map2.putAll(map1);

        for (int i = 0; i < 10; i++) {
            long dbTimes = timeComparison(list1, list1)
                    + timeComparison(set1, set1)
                    + timeComparison(map1.keySet(), map1.keySet())
                    + timeComparison(map1.values(), map1.values());
            long times = timeComparison(list2, list2)
                    + timeComparison(set2, set2)
                    + timeComparison(map2.keySet(), map2.keySet())
                    + timeComparison(map2.values(), map2.values());
            if (i > 0)
                System.out.printf("double braced collections took %,d ns and plain collections took %,d ns%n", dbTimes, times);
        }
    }

    public static long timeComparison(Collection a, Collection b) {
        long start = System.nanoTime();
        int runs = 10000000;
        for (int i = 0; i < runs; i++)
            compareCollections(a, b);
        long rate = (System.nanoTime() - start) / runs;
        return rate;
    }

    public static void compareCollections(Collection a, Collection b) {
        if (!a.equals(b) && a.hashCode() != b.hashCode() && !a.toString().equals(b.toString()))
            throw new AssertionError();
    }
}

인쇄물

double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 34 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns


답변

세트를 만들려면 이중 괄호 초기화 대신 varargs 팩토리 메소드를 사용할 수 있습니다.

public static Set<T> setOf(T ... elements) {
    return new HashSet<T>(Arrays.asList(elements));
}

Google 컬렉션 라이브러리에는 이와 같은 편리한 방법과 기타 유용한 기능이 많이 있습니다.

관용구의 모호함에 관해서는, 나는 그것을 항상 만나서 프로덕션 코드에서 사용합니다. 생산 코드 작성이 허용되는 관용구에 혼란스러워하는 프로그래머에 대해 더 걱정하고 있습니다.