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


823

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

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을 살펴보십시오 .


33
내가 여기에 볼 수있는 코드 냄새는 순진한 독자가 기대하는 것입니다 flavors을 수 HashSet있지만, 슬프게도 그것은 익명의 서브 클래스입니다.
Elazar Leibovich

6
성능을로드하는 대신 실행을 고려하면 차이가 없습니다. 내 대답을 참조하십시오.
Peter Lawrey

4
나는 당신이 요약을 만든 것을 좋아합니다. 이것이 당신이 이해력과 공동체를 높이는 데 가치있는 운동이라고 생각합니다.
Patrick Murphy

3
내 의견으로는 모호하지 않습니다. @ElazarLeibovich는 자신의 의견 에서 이미 말했다 . 이중 괄호 이니셜 라이저 자체는 언어 구조로 존재하지 않으며 익명의 서브 클래스와 인스턴스 이니셜 라이저의 조합 일뿐입니다. 유일한 것은 사람들이 이것을 알아야한다는 것입니다.
MC 황제

8
Java 9는 일부 상황에서 DCI 사용을 대체 할 수있는 불변 세트 정적 팩토리 메소드 를 제공합니다 .Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Basil Bourque

답변:


607

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

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.


10
"아마도"는 작동 어입니다. 측정하지 않으면 성능에 대한 언급이 없습니다.
인스턴스 헌터

16
당신은 내가 이것을 말하고 싶지 않은 훌륭한 일을했지만 Test1 시간은 클래스로드에 의해 지배 될 수 있습니다. 누군가가 for 루프에서 1,000 번이라고 말한 각 테스트의 단일 인스턴스를 실행 한 다음 1,000 또는 10,000 번의 for 루프에서 다시 실행하고 시차 (System.nanoTime ())를 인쇄하는 것이 흥미로울 것입니다. 첫 번째 for 루프는 모든 예열 효과 (JIT, 클래스로드 등)를지나 쳐야합니다. 두 테스트는 서로 다른 유스 케이스를 모델링합니다. 나는 내일 직장에서 이것을 실행하려고합니다.
Jim Ferrans

8
@ Jim Ferrans : Test1 시간이 클래스로드에서 나온 것이 확실합니다. 그러나 이중 괄호 초기화를 사용하면 클래스로드에 대처해야합니다. 이중 괄호 init에 대한 대부분의 사용 사례를 믿습니다. 일회성 초기화의 경우 테스트는 이러한 유형의 초기화의 일반적인 사용 사례에 더 가까운 조건입니다. 각 테스트를 여러 번 반복하면 실행 시간 간격이 더 작아 질 것이라고 생각합니다.
coobird

73
이것이 증명하는 것은 a) 이중 괄호 초기화가 느리고 b) 1000 번 수행하더라도 차이를 느끼지 못할 것입니다. 그리고 이것은 내부 루프의 병목 현상이 될 수도 없습니다. 그것은 매우 최악의 작은 일회성 페널티를 부과합니다.
마이클 마이어스

15
DBI를 사용하면 코드를 더 읽기 쉽게 표현할 수 있습니다. JVM이 수행 해야하는 작업이 조금 증가한다는 사실 자체로는 유효한 인수가 아닙니다. 만약 그렇다면, 우리는 또한 추가적인 도우미 메소드 / 클래스에 대해 걱정해야하며, 적은 메소드로 더 큰 클래스를 선호합니다.
Rogério

105

지금까지 지적되지 않은이 접근법의 한 가지 특성은 내부 클래스를 작성하므로 전체 포함 클래스가 해당 범위에서 캡처된다는 것입니다. 즉, 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){

            }

        };
    }

5
포함하는 클래스에 대한 숨겨진 참조에 대해 아주 좋은 지적입니다. 원래 예제에서 인스턴스 이니셜 라이저는 Test.this.add ()가 아니라 새 HashSet <String>의 add () 메소드를 호출합니다. 그것은 다른 일이 일어나고 있음을 나에게 암시합니다. Nathan Kitchen이 제안한 것처럼 HashSet <String>에 대한 익명의 내부 클래스가 있습니까?
Jim Ferrans

데이터 구조의 직렬화가 관련된 경우 포함 클래스에 대한 참조도 위험 할 수 있습니다. 포함하는 클래스도 직렬화되므로 직렬화 가능해야합니다. 이로 인해 오류가 모호해질 수 있습니다.
또 다른 Java 프로그래머

56

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

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

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"); }};

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


7
새끼 고양이를 구 해주세요! 좋은 대답입니다!
Aris2World

36

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

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 문서 에서 세부 사항을 확인하십시오 .


에디, 아주 좋은 설명입니다. JVM 바이트 코드가 디 컴파일만큼 깨끗하면 추가 .class 파일 클러 터에 다소 관심이 있지만 실행 속도가 충분히 빠릅니다. 왜 인스턴스 이니셜 라이저의 생성자가 "this"를 Test 인스턴스가 아닌 새로운 HashSet <String> 인스턴스로 보는지 궁금합니다. 이 관용구를 지원하기 위해 최신 Java 언어 사양에서 명시 적으로 지정된 동작입니까?
Jim Ferrans

답변을 업데이트했습니다. 테스트 클래스의 상용구를 제외하고 혼란을 일으켰습니다. 나는 그것을 더 명확하게하기 위해 내 대답에 넣었다. 이 관용구에서 사용되는 인스턴스 이니셜 라이저 블록에 대한 JLS 섹션도 언급했습니다.
Eddie

1
@Jim "this"의 해석은 특별한 경우가 아닙니다. 단순히 가장 안쪽에있는 클래스의 인스턴스를 나타내며 HashSet <String>의 익명 하위 클래스입니다.
Nathan Kitchen

4 년 반 후에 점프해서 죄송합니다. 그러나 디 컴파일 된 클래스 파일 (두 번째 코드 블록)의 좋은 점은 유효한 Java가 아니라는 것입니다! 그것은이 super()암시 적 생성자의 두 번째 라인으로,하지만 먼저 와야한다. (나는 그것을 테스트했고, 컴파일되지 않을 것이다.)
chiastic-security

1
@ chiastic-security : 때때로 디 컴파일러는 컴파일되지 않는 코드를 생성합니다.
Eddie

35

누출되기 쉬운

차임하기로 결정했습니다. 성능 영향에는 디스크 작업 + 압축 풀기 (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"));


3
운 좋게도 Java 8부터 PermGen은 더 이상 문제가되지 않습니다. 여전히 영향이 있지만, 매우 모호한 오류 메시지가있는 것은 아닙니다.
Joey

2
@Joey, 메모리가 GC (perm gen)에 의해 직접 관리되는지 여부에 차이가 없습니다. 메타 공간의 누출은 메타가 제한되지 않는 한 여전히 유출입니다. 리눅스의 oom_killer와 같은 것들에 의해 펌 (out of perm gen)이 없을 것입니다.
bestsss

19

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

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

2
DBI를 과도하게 사용하면 PermGen 공간이 증발한다는 점을 제외하고는 차이가 없습니다. 적어도 PermGen 공간의 클래스 언로드 및 가비지 수집을 허용 하는 모호한 JVM 옵션설정 하지 않으면 그렇지 않습니다 . 서버 측 언어로서의 Java 보급률을 감안할 때 memory / PermGen 문제는 적어도 언급해야합니다.
aroth

1
@aroth 이것은 좋은 지적입니다. 나는 16 년 동안 Java에서 일하면서 PermGen (또는 메타 스페이스)을 조정 해야하는 시스템에서 결코 일하지 않았다는 것을 인정합니다. 시스템의 코드 작업은 항상 합리적으로 작습니다.
피터 로리

2
의 조건이 안 compareCollections와 결합되는 ||것이 아니라 &&? 사용 &&은 의미 상 잘못 일뿐 아니라 첫 번째 조건 만 테스트되므로 성능을 측정하려는 의도를 상쇄합니다. 또한 스마트 옵티마이 저는 반복 중에 조건이 변경되지 않음을 인식 할 수 있습니다.
Holger

@aroth는 단지 업데이트로 : Java 8 VM은 더 이상 perm-gen을 사용하지 않습니다.
Angel O'Sphere 19

16

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

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

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

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


하! ;-) 실제로 1.2 일부터 Java로 돌아 오는 Rip van Winkle 입니다 (Java의 evolution.voxeo.com 에서 VoiceXML 음성 웹 브라우저를 작성했습니다 ). 제네릭, 매개 변수화 된 유형, 콜렉션, java.util.concurrent, 새로운 for 루프 구문 등을 배우는 것은 재미있었습니다. 이제는 더 나은 언어입니다. 지금까지 DBI의 메커니즘이 처음에는 모호해 보일 수 있지만 코드의 의미는 매우 분명해야합니다.
Jim Ferrans

10

효율성을 제외하고는 단위 테스트 이외의 선언적 컬렉션 생성을 원하지 않습니다. 이중 중괄호 구문은 매우 읽기 쉽다고 생각합니다.

선언적으로 목록을 구성하는 또 다른 방법은 Arrays.asList(T ...)다음과 같이 사용하는 것입니다.

List<String> aList = Arrays.asList("vanilla", "strawberry", "chocolate");

물론이 접근 방식의 한계는 생성 할 특정 유형의 목록을 제어 할 수 없다는 것입니다.


1
Arrays.asList ()는 내가 일반적으로 사용하는 것이지만, 맞습니다.이 상황은 주로 단위 테스트에서 발생합니다. 실제 코드는 DB 쿼리, XML 등에서 목록을 구성합니다.
Jim Ferrans

7
그러나 asList에주의하십시오. 리턴 된 목록은 요소 추가 또는 제거를 지원하지 않습니다. asList를 사용할 때마다 결과 목록을 생성자에 전달 new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))하여이 문제를 해결합니다.
마이클 마이어스

7

일반적으로 그것에 대해 비효율적 인 것은 없습니다. 일반적으로 서브 클래스를 생성하고 생성자를 추가 한 것은 JVM에 중요하지 않습니다. 이는 객체 지향 언어에서 일상적으로하는 일입니다. 나는 당신이 이것을 수행함으로써 비 효율성을 유발할 수있는 상당히 고안된 경우를 생각할 수 있습니다 (예를 들어, 당신은이 서브 클래스로 인해 다른 클래스를 혼합하여 반복적으로 호출되는 메소드를 가지고 있지만 전달되는 클래스는 완전히 예측 가능합니다- 후자의 경우, JIT 컴파일러는 처음에는 불가능한 최적화를 만들 수 있습니다. 그러나 실제로, 나는 그것이 중요한 사건이 매우 고안된 것이라고 생각합니다.

익명 클래스가 많은 "클러스터링"여부에 대한 관점에서 문제가 더 많이 나타납니다. 대략적인 지침으로, 이벤트 핸들러에 익명 클래스를 사용하는 것 이상으로 관용구를 사용하는 것이 좋습니다.

(2)에서, 당신은 객체의 생성자 안에 있습니다. "this"는 당신이 구성하고있는 객체를 가리 킵니다. 그것은 다른 생성자와 다르지 않습니다.

(3)은 실제로 코드를 누가 관리하고 있는지에 달려 있습니다. 이것을 미리 모른다면, 내가 제안하는 벤치 마크는 "JDK의 소스 코드에서 이것을 볼 수 있습니까?"입니다. (이 경우 익명의 이니셜 라이저를 많이 본 적이 없으며 익명 클래스 의 유일한 내용 인 경우 는 아닙니다 .) 가장 적당한 규모의 프로젝트에서 나는 어느 시점에서나 JDK 소스를 이해하기 위해 프로그래머가 실제로 필요하다고 주장 할 것이다. 그 외에도, 코드를 유지 관리하는 사람을 제어 할 수 있다면 의견을 말하거나 피하십시오.


5

이중 괄호 초기화는 메모리 누수 및 기타 문제를 일으킬 수있는 불필요한 해킹입니다.

이 "트릭"을 사용해야 할 정당한 이유는 없습니다. 구아바는 좋은 제공합니다 정적 팩토리와 빌더를 모두 포함 불변 콜렉션 을 하므로 깨끗하고 읽기 쉽고 안전한 구문으로 선언 된 곳에 콜렉션을 채울 수 있습니다 .

질문의 예는 다음과 같습니다.

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

이 방법은 짧고 읽기 쉬울뿐만 아니라 이중 브레이스 패턴과 관련된 수많은 문제를 피할 수 있습니다. 다른 답변에 . 물론, 직접 구성된 것과 비슷 HashMap하지만 위험하고 오류가 발생하기 쉬우 며 더 나은 옵션이 있습니다.

이중 괄호 초기화를 고려할 때마다 API를 다시 검사하거나 새로운 API를 도입 해야합니다. 구문을 사용하는 대신 문제를 해결하기 위해 .

Error-Prone은 이제이 anti-pattern 플래그를 지정합니다 .


-1. 몇 가지 유효한 점에도 불구하고이 답변은 "불필요한 익명 클래스 생성을 피하는 방법? 더 많은 클래스가있는 프레임 워크를 사용하십시오!"로 요약됩니다.
Agent_L

1
"응용 프로그램을 손상시킬 수있는 해킹이 아니라 작업에 적합한 도구를 사용하는 것"으로 요약됩니다. 구아바는 응용 프로그램이 포함하는 꽤 일반적인 라이브러리입니다 (사용하지 않으면 분명히 빠져 있습니다).하지만 사용하지 않으려는 경우에도 이중 괄호 초기화를 피할 수 있으며 피해야합니다.
dimo414

그리고 이중 괄호 초기화가 어떻게 정확하게 메모리 누수를 유발합니까?
Angel O'Sphere '10

@ AngelO'Sphere DBI는 내부 클래스 를 만드는 난독 화 된 방법 이므로 (클래스에서만 사용되지 않는 한) 포함 클래스에 대한 암시 적 참조를 유지합니다 static. 내 질문의 맨 아래에있는 오류가 발생하기 쉬운 링크에서 이에 대해 자세히 설명합니다.
dimo414

나는 그것이 맛의 문제라고 말할 것입니다. 그리고 그것에 대해 정말 난독 한 것이 없습니다.
Angel O'Sphere

4

나는 이것을 연구하고 유효한 답변이 제공하는 것보다 더 심도있는 테스트를하기로 결정했습니다.

코드는 다음과 같습니다. https://gist.github.com/4368924

그리고 이것은 내 결론이다

대부분의 실행 테스트에서 내부 시작이 실제로 더 빠르다는 사실에 놀랐습니다 (일부 경우 거의 두 배). 많은 수의 작업을 할 때 이점이 사라지는 것 같습니다.

흥미롭게도 루프에서 3 개의 객체를 생성하는 경우 손실이 발생합니다. 다른 경우보다 이점이 더 빨리 실행됩니다. 왜 이런 일이 일어나고 있는지 확실하지 않으며 결론에 도달하기 위해 더 많은 테스트를 수행해야합니다. 구체적인 구현을 만들면 클래스 정의가 다시로드되지 않도록 할 수 있습니다 (발생하는 경우)

그러나 단일 품목 건물의 경우 많은 수의 경우에도 대부분의 경우 오버 헤드가 많지 않은 것이 분명합니다.

한 가지 단점은 각 이중 괄호 시작이 전체 디스크 블록을 응용 프로그램 크기 (또는 압축시 약 1k)에 추가하는 새 클래스 파일을 생성한다는 사실입니다. 작은 설치 공간이지만 여러 곳에서 사용하는 경우 잠재적으로 영향을 줄 수 있습니다. 이 1000 번을 사용하면 잠재적으로 응용 프로그램에 MiB 전체를 추가 할 수 있으며 이는 임베디드 환경과 관련이있을 수 있습니다.

내 결론? 남용되지 않는 한 사용하는 것이 좋습니다.

당신이 무슨 생각을하는지 제게 알려주세요 :)


2
유효한 시험이 아닙니다. 코드는 객체를 사용하지 않고 객체를 생성하여 옵티마이 저가 전체 인스턴스 생성을 제거 할 수 있도록합니다. 유일하게 남아있는 부작용은이 테스트에서 오버 헤드가 다른 임의의 숫자보다 큰 난수 시퀀스의 진행입니다.
Holger

3

asList (elements)에서 암시 적 List를 생성하고 즉시 던지는 대신 루프를 사용한다는 점을 제외하고 Nat의 대답을 두 번째로 나타냅니다.

static public Set<T> setOf(T ... elements) {
    Set set=new HashSet<T>(elements.size());
    for(T elm: elements) { set.add(elm); }
    return set;
    }

1
왜? 새 개체는 eden 공간에 만들어 지므로 인스턴스화하려면 2 ~ 3 개의 포인터 만 추가하면됩니다. JVM은 절대 메소드 범위를 벗어나지 않으므로 스택에 할당합니다.
Nat

예, HashSet제안 된 용량 을 말하면 코드를 개선 할 수는 있지만로드 코드를 기억 하면 코드보다 효율적으로 처리 될 수 있습니다 .
Tom Hawtin-tackline

어쨌든 HashSet 생성자는 반복을 수행해야하므로 효율적 이지 않습니다 . 재사용을 위해 작성된 라이브러리 코드는 항상 최상의 결과를 얻으려고 노력해야 합니다.
Lawrence Dol

3

이 구문은 편리 할 수 ​​있지만이 $ 0 참조가 중첩 될 때이 참조를 많이 추가하고 각각에 중단 점이 설정되어 있지 않으면 이니셜 라이저로 디버그하기가 어려울 수 있습니다. 이러한 이유로, 특히 상수로 설정된 기본 설정자 및 익명 서브 클래스가 중요하지 않은 위치 (직렬화와 관련이없는 경우)에만이 옵션을 사용하는 것이 좋습니다.


3

Mario Gleichman Java 1.5 일반 함수를 사용하여 스칼라 목록 리터럴을 시뮬레이션하는 방법을 설명 하지만 슬프게도 불변 목록을 사용합니다.

그는이 클래스를 정의합니다.

package literal;

public class collection {
    public static <T> List<T> List(T...elems){
        return Arrays.asList( elems );
    }
}

다음과 같이 사용합니다.

import static literal.collection.List;
import static system.io.*;

public class CollectionDemo {
    public void demoList(){
        List<String> slist = List( "a", "b", "c" );
        List<Integer> iList = List( 1, 2, 3 );
        for( String elem : List( "a", "java", "list" ) )
            System.out.println( elem );
    }
}

구아바의 일부인 Google 컬렉션 은 비슷한 목록 구성 아이디어를 지원합니다. 에서 인터뷰 , 제라드 레비는 말한다 :

[...] 내가 쓰는 거의 모든 Java 클래스에 나타나는 가장 많이 사용되는 기능은 Java 코드에서 반복적 인 키 입력 횟수를 줄이는 정적 메서드입니다. 다음과 같은 명령을 입력하는 것이 매우 편리합니다.

Map<OneClassWithALongName, AnotherClassWithALongName> = Maps.newHashMap();

List<String> animals = Lists.immutableList("cat", "dog", "horse");

2014 년 7 월 10 일 : Python만큼 간단 할 수있는 경우 :

animals = ['cat', 'dog', 'horse']

2/21/2020 : Java 11에서는 다음과 같이 말할 수 있습니다.

animals = List.of(“cat”, “dog”, “horse”)


2
  1. 이것은 add()각 회원 을 요구할 것 입니다. 항목을 해시 세트에 넣는 더 효율적인 방법을 찾으면 사용하십시오. 내부 클래스는 민감한 경우 가비지를 생성 할 수 있습니다.

  2. 문맥에 의해 반환되는 객체 인 것처럼 그것은 나에게 보인다 new는 IS, HashSet.

  3. 당신이 물어볼 필요가 있다면 ... 더 많은 가능성 : 당신을 방문한 사람들이 이것을 알게 될까요? 이해하고 설명하기가 쉽습니까? 둘 다 "예"라고 대답 할 수 있다면 자유롭게 사용하십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.