Hiệu quả của việc khởi tạo cú đúp Java Java?


823

Trong Các tính năng ẩn của Java , câu trả lời hàng đầu đề cập đến Khởi tạo cú đúp , với cú pháp rất lôi cuốn:

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

Thành ngữ này tạo ra một lớp bên trong ẩn danh chỉ với một trình khởi tạo cá thể trong đó, "có thể sử dụng bất kỳ phương thức [...] nào trong phạm vi chứa".

Câu hỏi chính: Đây có phải là không hiệu quả như nó có vẻ? Nên sử dụng nó để giới hạn một lần khởi tạo? (Và tất nhiên là thể hiện!)

Câu hỏi thứ hai: Hashset mới phải là "cái này" được sử dụng trong trình khởi tạo cá thể ... bất cứ ai cũng có thể làm sáng tỏ cơ chế này?

Câu hỏi thứ ba: Là thành ngữ này quá tối nghĩa để sử dụng trong mã sản xuất?

Tóm tắt: Câu trả lời rất, rất hay, cảm ơn mọi người. Đối với câu hỏi (3), mọi người cảm thấy cú pháp phải rõ ràng (mặc dù tôi muốn đề xuất một nhận xét không thường xuyên, đặc biệt là nếu mã của bạn sẽ chuyển cho các nhà phát triển có thể không quen thuộc với nó).

Đối với câu hỏi (1), mã được tạo sẽ chạy nhanh. Các tệp. Class bổ sung không gây lộn xộn tệp jar và khởi động chương trình chậm một chút (nhờ @coobird để đo lường điều đó). @Thilo chỉ ra rằng bộ sưu tập rác có thể bị ảnh hưởng và chi phí bộ nhớ cho các lớp được tải thêm có thể là một yếu tố trong một số trường hợp.

Câu hỏi (2) hóa ra là thú vị nhất đối với tôi. Nếu tôi hiểu câu trả lời, điều xảy ra trong DBI là lớp bên trong ẩn danh mở rộng lớp của đối tượng được xây dựng bởi toán tử mới và do đó có giá trị "này" tham chiếu đến thể hiện đang được xây dựng. Rât gọn gang.

Nhìn chung, DBI gây ấn tượng với tôi như một sự tò mò trí tuệ. Coobird và những người khác chỉ ra rằng bạn có thể đạt được hiệu quả tương tự với Arrays.asList, các phương thức varargs, Google Collection và các bộ sưu tập Java 7 Collection được đề xuất. Các ngôn ngữ JVM mới hơn như Scala, JRuby và Groovy cũng cung cấp các ký hiệu ngắn gọn để xây dựng danh sách và tương thích tốt với Java. Cho rằng DBI làm tắc nghẽn đường dẫn lớp, làm chậm quá trình tải lớp một chút và làm cho mã trở nên khó hiểu hơn, có lẽ tôi sẽ né tránh nó. Tuy nhiên, tôi dự định sẽ giới thiệu điều này với một người bạn vừa nhận được SCJP của anh ấy và yêu thích những câu nói tốt bụng về ngữ nghĩa Java! ;-) Cảm ơn mọi người!

7/2017: Baeldung có một bản tóm tắt tốt về khởi tạo niềng răng đôi và coi đó là một mô hình chống.

12/2017: @Basil Bourque lưu ý rằng trong Java 9 mới, bạn có thể nói:

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

Đó là chắc chắn con đường để đi. Nếu bạn bị mắc kẹt với phiên bản cũ hơn, hãy xem Bộ sưu tập bất biến của Google Bộ sưu tập .


33
Mùi mã tôi thấy ở đây là, người đọc ngây thơ sẽ mong đợi flavorslà một HashSet, nhưng than ôi nó là một lớp con ẩn danh.
Elazar Leibovich

6
Nếu bạn xem xét việc chạy thay vì tải hiệu suất không có sự khác biệt, hãy xem câu trả lời của tôi.
Peter Lawrey

4
Tôi thích việc bạn tạo ra một bản tóm tắt, tôi nghĩ rằng đây là một bài tập đáng giá cho cả hai bạn để tăng sự hiểu biết và cộng đồng.
Patrick Murphy

3
Nó không tối nghĩa theo ý kiến ​​của tôi. Độc giả nên biết rằng một đôi ... o chờ đợi, @ElazarLeibovich đã nói điều đó trong bình luận của mình . Bản thân trình khởi tạo cú đúp đôi không tồn tại như một cấu trúc ngôn ngữ, nó chỉ là sự kết hợp của một lớp con ẩn danh và trình khởi tạo cá thể. Điều duy nhất là, mọi người cần phải nhận thức được điều này.
MC Hoàng đế

8
Java 9 cung cấp các Phương thức nhà máy tĩnh không thay đổi có thể thay thế việc sử dụng DCI trong một số trường hợp:Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Basil Bourque

Câu trả lời:


607

Đây là vấn đề khi tôi quá bận tâm với các lớp bên trong ẩn danh:

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

Đây là tất cả các lớp được tạo khi tôi tạo một ứng dụng đơn giản và sử dụng số lượng lớn các lớp bên trong ẩn danh - mỗi lớp sẽ được biên dịch thành một classtệp riêng .

"Khởi tạo cú đúp", như đã đề cập, là một lớp bên trong ẩn danh với khối khởi tạo cá thể, có nghĩa là một lớp mới được tạo cho mỗi "khởi tạo", tất cả chỉ nhằm mục đích thường tạo một đối tượng.

Xem xét rằng Máy ảo Java sẽ cần phải đọc tất cả các lớp đó khi sử dụng chúng, điều đó có thể dẫn đến một thời gian trong quy trình xác minh bằng mã byte và như vậy. Chưa kể đến việc tăng dung lượng đĩa cần thiết để lưu trữ tất cả các classtệp đó.

Có vẻ như có một chút chi phí khi sử dụng khởi tạo hai nẹp, vì vậy có lẽ không nên quá nhiệt tình với nó. Nhưng như Eddie đã lưu ý trong các bình luận, không thể hoàn toàn chắc chắn về tác động.


Chỉ để tham khảo, khởi tạo cú đúp là như sau:

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

Nó trông giống như một tính năng "ẩn" của Java, nhưng nó chỉ là một bản viết lại của:

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

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

Vì vậy, về cơ bản, nó là một khối khởi tạo cá thể là một phần của lớp bên trong ẩn danh .


Đề xuất Văn học Bộ sưu tập của Joshua Bloch cho Project Coin nằm dọc theo dòng:

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

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

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

Đáng buồn thay, nó đã không đi vào cả Java 7 và 8 và bị gác lại vô thời hạn.


Thí nghiệm

Đây là thử nghiệm đơn giản mà tôi đã thử nghiệm - tạo 1000 ArrayListgiây với các yếu tố "Hello""World!"thêm vào chúng thông qua addphương thức, sử dụng hai phương pháp:

Phương pháp 1: Khởi tạo cú đúp

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

Phương pháp 2: Khởi tạo một ArrayListadd

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

Tôi đã tạo một chương trình đơn giản để viết ra một tệp nguồn Java để thực hiện 1000 lần khởi tạo bằng hai phương thức:

Kiểm tra 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);
  }
}

Bài kiểm tra 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);
  }
}

Xin lưu ý rằng thời gian đã trôi qua để khởi tạo 1000 ArrayListgiây và 1000 lớp bên trong ẩn danh ArrayListđược kiểm tra bằng cách sử dụng System.currentTimeMillis, vì vậy bộ định thời không có độ phân giải rất cao. Trên hệ thống Windows của tôi, độ phân giải khoảng 15-16 mili giây.

Kết quả cho 10 lần chạy của hai bài kiểm tra như sau:

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

Có thể thấy, việc khởi tạo nẹp đôi có thời gian thực hiện đáng chú ý là khoảng 190 ms.

Trong khi đó, ArrayListthời gian thực hiện khởi tạo là 0 ms. Tất nhiên, độ phân giải hẹn giờ nên được tính đến, nhưng nó có thể dưới 15 ms.

Vì vậy, dường như có một sự khác biệt đáng chú ý trong thời gian thực hiện của hai phương thức. Dường như có một số chi phí trong hai phương thức khởi tạo.

Và đúng vậy, đã có 1000 .classtệp được tạo bằng cách biên dịch Test1chương trình thử nghiệm khởi tạo cú đúp.


10
"Có lẽ" là từ hoạt động. Trừ khi được đo lường, không có tuyên bố nào về hiệu suất là có ý nghĩa.
Thợ săn sơ thẩm

16
Bạn đã làm một công việc tuyệt vời như vậy tôi hầu như không muốn nói điều này, nhưng thời gian Test1 có thể bị chi phối bởi tải lớp. Sẽ rất thú vị khi thấy ai đó chạy một phiên bản duy nhất của mỗi thử nghiệm trong một vòng lặp for nói 1.000 lần, sau đó chạy lại trong một giây cho vòng lặp 1.000 hoặc 10.000 lần và in ra chênh lệch thời gian (System.nanoTime ()). Vòng lặp đầu tiên sẽ vượt qua tất cả các hiệu ứng khởi động (JIT, tải lớp, v.v.). Cả hai thử nghiệm mô hình trường hợp sử dụng khác nhau mặc dù. Tôi sẽ cố gắng chạy nó vào ngày mai tại nơi làm việc.
Jim Ferrans

8
@Jim Ferrans: Tôi khá chắc chắn rằng lần Test1 là từ tải lớp. Nhưng, hậu quả của việc sử dụng khởi tạo cú đúp là phải đối phó với tải lớp. Tôi tin rằng hầu hết các trường hợp sử dụng cho init đôi. dành cho khởi tạo một lần, thử nghiệm gần hơn trong điều kiện sử dụng điển hình của loại khởi tạo này. Tôi tin rằng nhiều lần lặp lại của mỗi thử nghiệm sẽ làm cho khoảng cách thời gian thực hiện nhỏ hơn.
coobird

73
Điều này chứng tỏ rằng a) khởi tạo hai nẹp chậm hơn và b) ngay cả khi bạn thực hiện 1000 lần, bạn có thể sẽ không nhận thấy sự khác biệt. Và nó không giống như điều này có thể là nút cổ chai trong một vòng lặp bên trong. Nó áp dụng một hình phạt một lần nhỏ xíu TẠI RẤT NHIỀU CÔNG VIỆC.
Michael Myers

15
Nếu sử dụng DBI làm cho mã dễ đọc hoặc biểu cảm hơn, thì hãy sử dụng nó. Việc nó tăng một chút công việc mà JVM phải thực hiện không phải là một đối số hợp lệ, tự nó, chống lại nó. Nếu đúng như vậy, thì chúng ta cũng nên lo lắng về các phương thức / lớp trợ giúp bổ sung, thay vào đó thích các lớp lớn hơn với ít phương thức hơn ...
Rogério

105

Một thuộc tính của cách tiếp cận này chưa được chỉ ra cho đến nay là bởi vì bạn tạo các lớp bên trong, toàn bộ lớp chứa được nắm bắt trong phạm vi của nó. Điều này có nghĩa là chừng nào Set của bạn còn sống, nó sẽ giữ một con trỏ tới thể hiện chứa ( this$0) và giữ cho nó không bị thu gom rác, đây có thể là một vấn đề.

Điều này, và thực tế là một lớp mới được tạo ngay từ đầu mặc dù một Hashset thông thường sẽ hoạt động tốt (hoặc thậm chí tốt hơn), khiến tôi không muốn sử dụng cấu trúc này (mặc dù tôi thực sự khao khát đường cú pháp).

Câu hỏi thứ hai: Hashset mới phải là "cái này" được sử dụng trong trình khởi tạo cá thể ... bất cứ ai cũng có thể làm sáng tỏ cơ chế này? Tôi đã ngây thơ mong đợi "cái này" để chỉ đối tượng khởi tạo "hương vị".

Đây chỉ là cách các lớp bên trong hoạt động. Chúng có cái riêng của chúng this, nhưng chúng cũng có con trỏ tới thể hiện cha, để bạn có thể gọi các phương thức trên đối tượng chứa. Trong trường hợp có xung đột đặt tên, lớp bên trong (trong trường hợp của bạn là Hashset) được ưu tiên, nhưng bạn có thể đặt tiền tố "này" với một tên lớp để có được phương thức bên ngoài.

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

Để rõ ràng về lớp con ẩn danh đang được tạo, bạn cũng có thể định nghĩa các phương thức trong đó. Ví dụ ghi đèHashSet.add()

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

            @Override
            boolean add(String s){

            }

        };
    }

5
Điểm rất tốt trên tham chiếu ẩn đến lớp chứa. Trong ví dụ ban đầu, trình khởi tạo cá thể đang gọi phương thức add () của Hashset mới <String>, chứ không phải Test.this.add (). Điều đó cho tôi thấy rằng một cái gì đó khác đang xảy ra. Có một lớp bên trong ẩn danh cho Hashset <String>, như Nathan Kitchen gợi ý không?
Jim Ferrans

Tham chiếu đến lớp chứa cũng có thể nguy hiểm nếu việc tuần tự hóa cơ sở hạ tầng có liên quan. Lớp chứa cũng sẽ được tuần tự hóa và do đó phải được tuần tự hóa. Điều này có thể dẫn đến các lỗi tối nghĩa.
Chỉ là một lập trình viên Java khác vào

56

Mỗi khi ai đó sử dụng khởi tạo cú đúp, một con mèo con sẽ bị giết.

Ngoài cú pháp khá bất thường và không thực sự thành ngữ (dĩ nhiên là có thể tranh cãi), bạn không cần thiết phải tạo ra hai vấn đề quan trọng trong ứng dụng của mình, mà gần đây tôi đã viết blog chi tiết hơn ở đây .

1. Bạn đang tạo ra quá nhiều lớp ẩn danh

Mỗi khi bạn sử dụng khởi tạo cú đúp, một lớp mới được tạo. Ví dụ: ví dụ này:

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

... sẽ tạo ra các lớp này:

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

Đó là một chút chi phí cho trình tải lớp của bạn - không có gì! Tất nhiên sẽ không mất nhiều thời gian khởi tạo nếu bạn làm điều đó một lần. Nhưng nếu bạn làm điều này 20.000 lần trong suốt ứng dụng doanh nghiệp của mình ... tất cả bộ nhớ heap đó chỉ vì một chút "đường cú pháp"?

2. Bạn có khả năng tạo ra rò rỉ bộ nhớ!

Nếu bạn lấy mã ở trên và trả lại bản đồ đó từ một phương thức, người gọi phương thức đó có thể không nghi ngờ gì đến việc giữ các tài nguyên rất nặng không thể thu gom được rác. Hãy xem xét ví dụ sau:

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

MapBây giờ trả về sẽ chứa một tham chiếu đến thể hiện kèm theo của ReallyHeavyObject. Bạn có thể không muốn mạo hiểm rằng:

Rò rỉ bộ nhớ ngay tại đây

Hình ảnh từ http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-potype/

3. Bạn có thể giả vờ rằng Java có bản đồ bằng chữ

Để trả lời câu hỏi thực tế của bạn, mọi người đã sử dụng cú pháp này để giả vờ rằng Java có một cái gì đó giống như chữ bản đồ, tương tự như các mảng chữ hiện có:

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

Một số người có thể tìm thấy kích thích cú pháp này.


7
Cứu mèo con! Câu trả lời tốt!
Aris2World

36

Tham gia lớp kiểm tra sau:

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

và sau đó dịch ngược tệp lớp, tôi thấy:

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

Điều này có vẻ không hiệu quả lắm đối với tôi. Nếu tôi lo lắng về hiệu suất cho một cái gì đó như thế này, tôi sẽ hồ sơ nó. Và câu hỏi số 2 của bạn được trả lời theo đoạn mã trên: Bạn đang ở trong một hàm tạo ẩn (và trình khởi tạo cá thể) cho lớp bên trong của bạn, vì vậy " this" đề cập đến lớp bên trong này.

Vâng, cú pháp này tối nghĩa, nhưng một nhận xét có thể làm rõ việc sử dụng cú pháp tối nghĩa. Để làm rõ cú pháp, hầu hết mọi người đều quen thuộc với khối khởi tạo tĩnh (JLS 8.7 Bộ khởi tạo tĩnh):

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

Bạn cũng có thể sử dụng một cú pháp tương tự (không có từ " static") để sử dụng hàm tạo (JLS 8.6 Instance khởi tạo), mặc dù tôi chưa bao giờ thấy điều này được sử dụng trong mã sản xuất. Điều này ít được biết đến hơn.

public class Sample2 {
    private final String someVar;

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

Nếu bạn không có hàm tạo mặc định, thì khối mã nằm giữa {}được trình biên dịch biến thành hàm tạo. Với suy nghĩ này, hãy làm sáng tỏ mã đôi:

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

Khối mã giữa các dấu ngoặc trong nhất được trình biên dịch chuyển thành hàm tạo. Hầu hết các niềng răng bên ngoài phân định lớp bên trong vô danh. Để thực hiện bước này là bước cuối cùng để biến mọi thứ thành ẩn danh:

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

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

Đối với mục đích khởi tạo, tôi muốn nói rằng không có bất kỳ chi phí nào (hoặc nhỏ đến mức có thể bỏ qua). Tuy nhiên, mọi việc sử dụng flavorssẽ không chống lại HashSetmà thay vào đó là chống lại MyHashSet. Có lẽ có một chi phí nhỏ (và khá có thể không đáng kể) cho việc này. Nhưng một lần nữa, trước khi tôi lo lắng về nó, tôi sẽ hồ sơ nó.

Một lần nữa, với câu hỏi số 2 của bạn, đoạn mã trên là tương đương logic và rõ ràng của khởi tạo cú đúp và nó làm cho nó rõ ràng trong đó " this" đề cập đến: lớp bên trong mở rộng HashSet.

Nếu bạn có câu hỏi về chi tiết của trình khởi tạo cá thể, hãy xem chi tiết trong tài liệu JLS .


Eddie, lời giải thích rất hay. Nếu các mã byte JVM sạch như dịch ngược, tốc độ thực thi sẽ đủ nhanh, mặc dù tôi có chút lo ngại về sự lộn xộn của tệp. Class. Tôi vẫn tò mò về lý do tại sao hàm tạo của trình khởi tạo cá thể thấy "cái này" là cá thể Hashset <String> mới chứ không phải là cá thể Test. Đây có phải chỉ là hành vi được chỉ định rõ ràng trong Đặc tả ngôn ngữ Java mới nhất để hỗ trợ thành ngữ không?
Jim Ferrans

Tôi cập nhật câu trả lời của tôi. Tôi đã bỏ đi bản tóm tắt của lớp Test, điều này gây ra sự nhầm lẫn. Tôi đặt nó vào câu trả lời của tôi để làm cho mọi thứ rõ ràng hơn. Tôi cũng đề cập đến phần JLS cho các khối khởi tạo cá thể được sử dụng trong thành ngữ này.
Eddie

1
@Jim Việc giải thích "này" không phải là trường hợp đặc biệt; nó chỉ đơn giản đề cập đến thể hiện của lớp kèm theo trong cùng, là lớp con ẩn danh của Hashset <String>.
Nhà bếp Nathan

Xin lỗi để nhảy trong bốn năm rưỡi sau đó. Nhưng điều thú vị về tệp lớp dịch ngược (khối mã thứ hai của bạn) là Java không hợp lệ! Nó có super()dòng thứ hai của hàm tạo ẩn, nhưng nó phải đến trước. (Tôi đã kiểm tra nó và nó sẽ không biên dịch.)
bảo mật chiastic

1
@ chiastic-security: Đôi khi trình dịch ngược tạo mã không được biên dịch.
Eddie

35

dễ bị rò rỉ

Tôi đã quyết định bấm chuông. Tác động hiệu suất bao gồm: hoạt động của đĩa + giải nén (đối với jar), xác minh lớp, không gian perm-gen (đối với JVM của Hotspot). Tuy nhiên, điều tồi tệ nhất trong tất cả: nó dễ bị rò rỉ. Bạn không thể đơn giản trở lại.

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

Vì vậy, nếu tập hợp thoát sang bất kỳ phần nào khác được tải bởi một trình nạp lớp khác và một tham chiếu được giữ ở đó, toàn bộ cây của lớp + trình nạp lớp sẽ bị rò rỉ. Để tránh điều đó, một bản sao vào HashMap là cần thiết , new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}). Không còn dễ thương nữa. Bản thân tôi không sử dụng thành ngữ này, thay vào đó nó giống như new LinkedHashSet(Arrays.asList("xxx","YYY"));


3
May mắn thay, kể từ Java 8, PermGen không còn là một điều nữa. Vẫn còn có một tác động, tôi đoán, nhưng không phải là một thông báo lỗi rất khó hiểu.
Joey

2
@Joey, tạo ra sự khác biệt bằng 0 nếu bộ nhớ được quản lý trực tiếp bởi GC (perm gen) hay không. Rò rỉ trong metaspace vẫn là một rò rỉ, trừ khi meta bị hạn chế, sẽ không có OOM (ngoài gen perm) bởi những thứ như oom_killer trong linux sẽ khởi động.
bestsss vào 6/2/2016

19

Tải nhiều lớp có thể thêm một vài phần nghìn giây để bắt đầu. Nếu khởi động không quá quan trọng và bạn đang xem hiệu quả của các lớp sau khi khởi động thì không có sự khác biệt.

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

in

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
Không có sự khác biệt ngoại trừ không gian PermGen của bạn sẽ bốc hơi nếu DBI được sử dụng quá mức. Ít nhất, nó sẽ trừ khi bạn đặt một số tùy chọn JVM tối nghĩa để cho phép dỡ lớp và thu gom rác của không gian PermGen. Với sự phổ biến của Java như một ngôn ngữ phía máy chủ, vấn đề bộ nhớ / PermGen đảm bảo ít nhất là một đề cập.
aroth

1
@aroth đây là một điểm tốt. Tôi thừa nhận rằng trong 16 năm làm việc với Java, tôi chưa bao giờ làm việc trên một hệ thống mà bạn phải điều chỉnh PermGen (hoặc Metaspace) Đối với các hệ thống tôi đã làm việc với kích thước của mã luôn luôn ở mức nhỏ.
Peter Lawrey

2
Không nên các điều kiện trong compareCollectionsđược kết hợp với ||hơn là &&? Việc sử dụng &&dường như không chỉ sai về mặt ngữ nghĩa, nó chống lại ý định đo lường hiệu suất, vì chỉ điều kiện đầu tiên sẽ được kiểm tra. Bên cạnh đó, một trình tối ưu hóa thông minh có thể nhận ra rằng các điều kiện sẽ không bao giờ thay đổi trong các lần lặp.
Holger

@aroth giống như một bản cập nhật: vì Java 8, VM không sử dụng bất kỳ perm-gen nào nữa.
Thiên thần O'Sphere

16

Để tạo bộ, bạn có thể sử dụng phương thức xuất xưởng varargs thay vì khởi tạo hai bước:

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

Thư viện Bộ sưu tập Google có rất nhiều phương pháp tiện lợi như thế này, cũng như vô số chức năng hữu ích khác.

Đối với sự tối nghĩa của thành ngữ, tôi bắt gặp nó và sử dụng nó trong mã sản xuất mọi lúc. Tôi sẽ quan tâm nhiều hơn về các lập trình viên bị nhầm lẫn bởi thành ngữ được phép viết mã sản xuất.


Hừ! ;-) Tôi thực sự là một Rip van Winkle trở lại Java sau 1,2 ngày (Tôi đã viết trình duyệt web bằng giọng nói VoiceXML tại Evolution.voxeo.com bằng Java). Thật thú vị khi học chung chung, các loại tham số hóa, Bộ sưu tập, java.util.conc hiện, cú pháp vòng lặp mới, v.v ... Bây giờ nó là ngôn ngữ tốt hơn. Theo quan điểm của bạn, mặc dù cơ chế đằng sau DBI ban đầu có vẻ mơ hồ, ý nghĩa của mã phải khá rõ ràng.
Jim Ferrans

10

Hiệu quả sang một bên, tôi hiếm khi thấy mình muốn tạo ra bộ sưu tập khai báo bên ngoài các bài kiểm tra đơn vị. Tôi tin rằng cú pháp cú đúp là rất dễ đọc.

Một cách khác để đạt được việc xây dựng danh sách cụ thể là sử dụng Arrays.asList(T ...)như vậy:

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

Tất nhiên, hạn chế của phương pháp này là bạn không thể kiểm soát loại danh sách cụ thể sẽ được tạo.


1
Arrays.asList () là những gì tôi thường sử dụng, nhưng bạn nói đúng, tình huống này phát sinh chủ yếu trong các bài kiểm tra đơn vị; mã thực sẽ xây dựng các danh sách từ các truy vấn DB, XML, v.v.
Jim Ferrans

7
Cảnh giác với asList, mặc dù: danh sách trả về không hỗ trợ thêm hoặc xóa các phần tử. Bất cứ khi nào tôi sử dụng asList, tôi chuyển danh sách kết quả vào một hàm tạo new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))để giải quyết vấn đề này.
Michael Myers

7

Nói chung không có gì đặc biệt không hiệu quả về nó. Nói chung đối với JVM, bạn đã tạo một lớp con và thêm một hàm tạo vào nó - đó là một việc bình thường, hàng ngày phải làm trong một ngôn ngữ hướng đối tượng. Tôi có thể nghĩ về các trường hợp khá khó khăn khi bạn có thể gây ra sự kém hiệu quả bằng cách thực hiện điều này (ví dụ: bạn có một phương thức được gọi nhiều lần, kết thúc bằng một hỗn hợp các lớp khác nhau vì lớp con này, trong khi lớp thông thường được truyền vào sẽ hoàn toàn có thể dự đoán được- - trong trường hợp sau, trình biên dịch JIT có thể thực hiện các tối ưu hóa không khả thi trong lần đầu tiên). Nhưng thực sự, tôi nghĩ rằng những trường hợp mà nó sẽ rất quan trọng.

Tôi sẽ thấy vấn đề nhiều hơn từ quan điểm về việc bạn có muốn "làm lộn xộn mọi thứ" với nhiều lớp ẩn danh hay không. Là một hướng dẫn sơ bộ, hãy xem xét sử dụng thành ngữ không nhiều hơn bạn sử dụng, giả sử, các lớp ẩn danh cho các trình xử lý sự kiện.

Trong (2), bạn đang ở trong hàm tạo của một đối tượng, vì vậy "cái này" chỉ đối tượng bạn đang xây dựng. Điều đó không khác với bất kỳ nhà xây dựng nào khác.

Đối với (3), điều đó thực sự phụ thuộc vào người duy trì mã của bạn, tôi đoán vậy. Nếu bạn không biết trước điều này, thì một điểm chuẩn mà tôi muốn đề xuất sử dụng là "bạn có thấy điều này trong mã nguồn của JDK không?" (trong trường hợp này, tôi không nhớ đã thấy nhiều trình khởi tạo ẩn danh và chắc chắn không phải trong trường hợp đó là nội dung duy nhất của lớp ẩn danh). Trong hầu hết các dự án có quy mô vừa phải, tôi cho rằng bạn thực sự sẽ cần các lập trình viên của mình hiểu nguồn JDK vào lúc này hay lúc khác, vì vậy bất kỳ cú pháp hoặc thành ngữ nào được sử dụng đều có "trò chơi công bằng". Ngoài ra, tôi muốn nói, hãy huấn luyện mọi người về cú pháp đó nếu bạn có quyền kiểm soát ai đang duy trì mã, nếu không thì nhận xét hoặc tránh.


5

Khởi tạo cú đúp là một hack không cần thiết có thể gây rò rỉ bộ nhớ và các vấn đề khác

Không có lý do chính đáng để sử dụng "mánh khóe" này. Guava cung cấp các bộ sưu tập bất biến đẹp bao gồm cả các nhà máy và nhà xây dựng tĩnh, cho phép bạn đưa vào bộ sưu tập của mình ở nơi nó được khai báo theo một cú pháp sạch, dễ đọc và an toàn .

Ví dụ trong câu hỏi trở thành:

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

Điều này không chỉ ngắn hơn và dễ đọc hơn mà còn tránh được nhiều vấn đề với mẫu hình đôi được mô tả trong các câu trả lời khác . Chắc chắn, nó hoạt động tương tự như được xây dựng trực tiếp HashMap, nhưng nó nguy hiểm và dễ bị lỗi, và có những lựa chọn tốt hơn.

Bất cứ khi nào bạn thấy mình cân nhắc việc khởi tạo hai lần, bạn nên kiểm tra lại API của mình hoặc giới thiệu API mới để giải quyết vấn đề một cách chính xác, thay vì tận dụng các thủ thuật cú pháp.

Error-Prone hiện gắn cờ chống mẫu này .


-1. Mặc dù có một số điểm hợp lệ, câu trả lời này rút gọn thành "Làm thế nào để tránh tạo lớp ẩn danh không cần thiết? Sử dụng một khung, với nhiều lớp hơn nữa!"
Đặc vụ_L

1
Tôi sẽ nói rằng nó tập trung vào "sử dụng công cụ phù hợp cho công việc, thay vì một vụ hack có thể làm hỏng ứng dụng của bạn". Quả ổi là một thư viện khá phổ biến cho các ứng dụng bao gồm (bạn chắc chắn bỏ lỡ nếu bạn không sử dụng nó), nhưng ngay cả khi bạn không muốn sử dụng nó, bạn vẫn có thể và vẫn nên tránh khởi tạo hai lần.
dimo414

Và chính xác làm thế nào một khởi tạo cú đúp sẽ gây rò rỉ meory?
Thiên thần O'Sphere

@ AngelO'Sphere DBI là một cách khó hiểu trong việc tạo ra một lớp bên trong và do đó giữ lại một tham chiếu ngầm cho lớp kèm theo của nó (trừ khi chỉ được sử dụng trong staticngữ cảnh). Liên kết dễ bị lỗi ở cuối câu hỏi của tôi sẽ thảo luận thêm về vấn đề này.
dimo414

Tôi sẽ nói đó là một vấn đề của hương vị. Và không có gì thực sự khó hiểu về nó.
Thiên thần O'Sphere

4

Tôi đã nghiên cứu điều này và quyết định làm một bài kiểm tra chuyên sâu hơn bài kiểm tra được cung cấp bởi câu trả lời hợp lệ.

Đây là mã: https://gist.github.com/4368924

và đây là kết luận của tôi

Tôi đã rất ngạc nhiên khi thấy rằng trong hầu hết các bài kiểm tra chạy, sự khởi đầu bên trong thực sự nhanh hơn (gần gấp đôi trong một số trường hợp). Khi làm việc với số lượng lớn, lợi ích dường như mất dần.

Điều thú vị là, trường hợp tạo ra 3 đối tượng trên vòng lặp sẽ mất lợi ích sớm hơn so với các trường hợp khác. Tôi không chắc tại sao điều này lại xảy ra và cần phải thử nghiệm nhiều hơn để đạt được bất kỳ kết luận nào. Tạo các triển khai cụ thể có thể giúp tránh định nghĩa lớp được tải lại (nếu đó là những gì đang xảy ra)

Tuy nhiên, rõ ràng là không có nhiều chi phí mà nó quan sát được trong hầu hết các trường hợp cho việc xây dựng một mặt hàng, ngay cả với số lượng lớn.

Một thiết lập lại sẽ là thực tế rằng mỗi lần khởi tạo cú đúp sẽ tạo ra một tệp lớp mới có thêm một khối đĩa cho kích thước của ứng dụng của chúng tôi (hoặc khoảng 1k khi được nén). Một dấu chân nhỏ, nhưng nếu nó được sử dụng ở nhiều nơi, nó có khả năng có thể có tác động. Sử dụng 1000 lần này và bạn có khả năng thêm toàn bộ MiB cho bạn, điều này có thể liên quan đến môi trường nhúng.

Kết luận của tôi? Nó có thể được sử dụng miễn là nó không bị lạm dụng.

Cho tôi biết bạn nghĩ gì :)


2
Đó không phải là một bài kiểm tra hợp lệ. Mã tạo các đối tượng mà không sử dụng chúng cho phép trình tối ưu hóa hoàn thành việc tạo toàn bộ cá thể. Tác dụng phụ duy nhất còn lại là sự tiến bộ của chuỗi số ngẫu nhiên có chi phí vượt trội hơn bất kỳ thứ gì khác trong các thử nghiệm này.
Holger

3

Tôi thứ hai câu trả lời của Nat, ngoại trừ tôi sẽ sử dụng một vòng lặp thay vì tạo và ngay lập tức ném Danh sách ẩn từ asList (phần tử):

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

1
Tại sao? Đối tượng mới sẽ được tạo trong không gian eden và do đó chỉ cần thêm hai hoặc ba con trỏ để khởi tạo. JVM có thể nhận thấy rằng nó không bao giờ thoát ra ngoài phạm vi phương thức và do đó phân bổ nó trên ngăn xếp.
Nat

Vâng, nó có khả năng kết thúc hiệu quả hơn mã đó (mặc dù bạn có thể cải thiện nó bằng cách nói với HashSetcông suất được đề xuất - hãy nhớ hệ số tải).
Tom Hawtin - tackline

Chà, dù sao thì hàm tạo Hashset cũng phải thực hiện việc lặp lại, vì vậy nó sẽ không hiệu quả hơn. Mã thư viện được tạo để tái sử dụng phải luôn luôn cố gắng trở thành tốt nhất có thể.
Lawrence Dol

3

Mặc dù cú pháp này có thể thuận tiện, nhưng nó cũng thêm rất nhiều tham chiếu $ 0 này khi các tham chiếu này trở nên lồng nhau và khó có thể bước gỡ lỗi vào các trình khởi tạo trừ khi các điểm dừng được đặt trên mỗi điểm. Vì lý do đó, tôi chỉ khuyên bạn nên sử dụng điều này cho các setters ban, đặc biệt là đặt thành hằng số và những nơi mà các lớp con ẩn danh không quan trọng (như không có sự tuần tự hóa liên quan).


3

Mario Gleichman mô tả cách sử dụng các hàm chung Java 1.5 để mô phỏng các danh sách Scala List, mặc dù thật đáng buồn khi bạn kết thúc với các Danh sách bất biến .

Ông định nghĩa lớp này:

package literal;

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

và sử dụng nó như vậy:

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

Bộ sưu tập của Google, giờ là một phần của Guava hỗ trợ một ý tưởng tương tự để xây dựng danh sách. Trong cuộc phỏng vấn này , Jared Levy nói:

[...] Các tính năng được sử dụng nhiều nhất, xuất hiện trong hầu hết mọi lớp Java tôi viết, là các phương thức tĩnh giúp giảm số lần nhấn phím lặp đi lặp lại trong mã Java của bạn. Thật tiện lợi khi có thể nhập các lệnh như sau:

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

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

7/10/2014: Nếu chỉ có thể đơn giản như Python:

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

21/2/2020: Trong Java 11 bây giờ bạn có thể nói:

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


2
  1. Điều này sẽ gọi add()cho từng thành viên. Nếu bạn có thể tìm thấy một cách hiệu quả hơn để đặt các mục vào một bộ băm, thì hãy sử dụng nó. Lưu ý rằng lớp bên trong có thể sẽ tạo ra rác, nếu bạn nhạy cảm về điều đó.

  2. Dường như với tôi như thể bối cảnh là đối tượng được trả về new, đó là HashSet.

  3. Nếu bạn cần hỏi ... Nhiều khả năng: những người đến sau bạn có biết điều này hay không? Có dễ hiểu và giải thích không? Nếu bạn có thể trả lời "có" cho cả hai, vui lòng sử dụng nó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.