Khởi tạo Double Brace trong Java là gì?


307

Cú pháp khởi tạo Double Brace ( {{ ... }}) trong Java là gì?




10
Khởi tạo Double Brace là một tính năng rất nguy hiểm và nên được sử dụng một cách thận trọng. Nó có thể phá vỡ hợp đồng và giới thiệu rò rỉ bộ nhớ khó khăn. Bài viết này mô tả chi tiết.
Andrii Polunin

Câu trả lời:


303

Khởi tạo dấu ngoặc kép tạo ra một lớp ẩn danh xuất phát từ lớp được chỉ định ( dấu ngoặc ngoài ) và cung cấp một khối khởi tạo trong lớp đó ( dấu ngoặc trong ). ví dụ

new ArrayList<Integer>() {{
   add(1);
   add(2);
}};

Lưu ý rằng một tác dụng của việc sử dụng khởi tạo cú đúp này là bạn đang tạo các lớp bên trong ẩn danh. Lớp được tạo có một thiscon trỏ ẩn đến lớp bên ngoài xung quanh. Mặc dù thông thường không phải là một vấn đề, nó có thể gây đau buồn trong một số trường hợp, ví dụ như khi xê-ri hóa hoặc thu gom rác, và đáng để nhận ra điều này.


11
Cảm ơn đã làm rõ ý nghĩa của niềng răng bên trong và bên ngoài. Tôi đã tự hỏi tại sao đột nhiên có hai dấu ngoặc nhọn được cho phép với một ý nghĩa đặc biệt, trong khi thực tế chúng là các cấu trúc java bình thường chỉ xuất hiện như một thủ thuật mới kỳ diệu. Những điều như vậy làm cho tôi nghi ngờ cú pháp Java mặc dù. Nếu bạn chưa phải là chuyên gia, việc đọc và viết có thể rất khó khăn.
jackthehipster

4
"Cú pháp ma thuật" như thế này tồn tại trong nhiều ngôn ngữ, ví dụ như hầu hết tất cả các ngôn ngữ giống như C đều hỗ trợ cú pháp "đi về 0" của "x -> 0" trong các vòng lặp chỉ là "x--> 0" vị trí không gian.
Joachim Sauer

17
Chúng ta chỉ có thể kết luận rằng "khởi tạo cú đúp" không tồn tại một mình, nó chỉ là sự kết hợp của việc tạo một lớp ẩn danh và một khối khởi tạo , một khi được kết hợp lại trông giống như một cấu trúc cú pháp, nhưng thực tế, nó không phải là ' t.
MC Hoàng đế

Cảm ơn bạn! Gson trả về null khi chúng ta tuần tự hóa một cái gì đó với khởi tạo cú đúp vì sử dụng lớp bên trong ẩn danh.
Pradeep AJ- Msft MVP

295

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ể gây 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, điều mà tôi vừa mới viết chi tiết ở đây .

1. Bạn đang tạo 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 đồ

Để 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.


11
"Bạn đang tạo ra quá nhiều lớp ẩn danh" - nhìn vào cách (nói) Scala tạo ra các lớp ẩn danh, tôi không chắc chắn rằng đây là một vấn đề lớn
Brian Agnew

2
Nó không phải là một cách hợp lệ và tốt đẹp để khai báo bản đồ tĩnh? Nếu một HashMap được khởi tạo với {{...}}và được khai báo là một statictrường, thì không nên có bất kỳ rò rỉ bộ nhớ nào, chỉ có một lớp ẩn danh và không có tham chiếu thể hiện kèm theo, phải không?
lorenzo-s

8
@ lorenzo-s: Có, 2) và 3) không áp dụng sau đó, chỉ 1). May mắn thay, với Java 9, cuối cùng cũng Map.of()có mục đích đó, vì vậy đó sẽ là một giải pháp tốt hơn
Lukas Eder

3
Có thể đáng chú ý rằng các bản đồ bên trong cũng có các tham chiếu đến các bản đồ bên ngoài và do đó, gián tiếp đến ReallyHeavyObject. Ngoài ra, các lớp bên trong ẩn danh nắm bắt tất cả các biến cục bộ được sử dụng trong thân lớp, vì vậy nếu bạn không chỉ sử dụng các hằng số để khởi tạo các bộ sưu tập hoặc bản đồ với mẫu này, các thể hiện của lớp bên trong sẽ nắm bắt tất cả chúng và tiếp tục tham chiếu chúng ngay cả khi thực sự bị xóa khỏi bộ sưu tập hoặc bản đồ. Vì vậy, trong trường hợp đó, các trường hợp này không chỉ cần gấp đôi bộ nhớ cần thiết cho các tham chiếu, mà còn có một rò rỉ bộ nhớ khác liên quan đến vấn đề đó.
Holger

41
  • Cú đúp đầu tiên tạo ra Lớp Nội tâm Vô danh mới.
  • Bộ dấu ngoặc thứ hai tạo ra một bộ khởi tạo cá thể như khối tĩnh trong Class.

Ví dụ:

   public class TestHashMap {
    public static void main(String[] args) {
        HashMap<String,String> map = new HashMap<String,String>(){
        {
            put("1", "ONE");
        }{
            put("2", "TWO");
        }{
            put("3", "THREE");
        }
        };
        Set<String> keySet = map.keySet();
        for (String string : keySet) {
            System.out.println(string+" ->"+map.get(string));
        }
    }

}

Làm thế nào nó hoạt động

Cú đúp đầu tiên tạo ra một Lớp bên trong vô danh mới. Các lớp bên trong này có khả năng truy cập hành vi của lớp cha của chúng. Vì vậy, trong trường hợp của chúng tôi, chúng tôi thực sự đang tạo một lớp con của lớp Hashset, vì vậy lớp bên trong này có khả năng sử dụng phương thức put ().

bộ niềng răng thứ hai không có gì ngoài bộ khởi tạo cá thể. Nếu bạn nhắc các khái niệm java cốt lõi thì bạn có thể dễ dàng liên kết các khối khởi tạo cá thể với các trình khởi tạo tĩnh do cú đúp tương tự như struct. Chỉ khác là bộ khởi tạo tĩnh được thêm bằng từ khóa tĩnh và chỉ được chạy một lần; bất kể bạn tạo ra bao nhiêu đối tượng

hơn


24

Để biết một ứng dụng thú vị về khởi tạo cú đúp, hãy xem ở đây Dwemthy Array trong Java .

Một đoạn trích

private static class IndustrialRaverMonkey
  extends Creature.Base {{
    life = 46;
    strength = 35;
    charisma = 91;
    weapon = 2;
  }}

private static class DwarvenAngel
  extends Creature.Base {{
    life = 540;
    strength = 6;
    charisma = 144;
    weapon = 50;
  }}

Và bây giờ, hãy chuẩn bị cho thịt xông khói chunkyBattleOfGrottoOfSausageSmellschunky!


16

Tôi nghĩ điều quan trọng là phải nhấn mạnh rằng không có thứ gọi là "Khởi tạo cú đúp" trong Java . Trang web của Oracle không có thuật ngữ này. Trong ví dụ này, có hai tính năng được sử dụng cùng nhau: lớp ẩn danh và khối khởi tạo. Có vẻ như khối khởi tạo cũ đã bị các nhà phát triển lãng quên và gây ra một số nhầm lẫn trong chủ đề này. Trích dẫn từ tài liệu của Oracle :

Các khối khởi tạo cho các biến thể hiện trông giống như các khối khởi tạo tĩnh, nhưng không có từ khóa tĩnh:

{
    // whatever code is needed for initialization goes here
}

11

1- Không có thứ gọi là niềng răng đôi:
Tôi muốn chỉ ra rằng không có thứ gọi là khởi tạo niềng răng đôi. Chỉ có một khối khởi đầu truyền thống bình thường. Khối niềng răng thứ hai không liên quan gì đến khởi tạo. Câu trả lời nói rằng hai cái niềng răng đó khởi tạo một cái gì đó, nhưng nó không phải như thế.

2- Không chỉ về các lớp ẩn danh mà tất cả các lớp:
Hầu như tất cả các câu trả lời đều nói rằng đó là một thứ được sử dụng khi tạo các lớp bên trong ẩn danh. Tôi nghĩ rằng những người đọc những câu trả lời đó sẽ có ấn tượng rằng nó chỉ được sử dụng khi tạo các lớp bên trong ẩn danh. Nhưng nó được sử dụng trong tất cả các lớp. Đọc những câu trả lời đó có vẻ như là một tính năng đặc biệt hoàn toàn mới dành riêng cho các lớp ẩn danh và tôi nghĩ đó là sai lệch.

3- Mục đích chỉ là đặt các dấu ngoặc cho nhau, không phải là khái niệm mới:
Đi xa hơn, câu hỏi này nói về tình huống khi khung mở thứ hai chỉ sau khung mở đầu tiên. Khi được sử dụng trong lớp bình thường thường có một số mã giữa hai dấu ngoặc nhọn, nhưng nó hoàn toàn giống nhau. Vì vậy, nó là một vấn đề của việc đặt dấu ngoặc. Vì vậy, tôi nghĩ rằng chúng ta không nên nói rằng đây là một điều thú vị mới, bởi vì đây là điều mà tất cả chúng ta đều biết, nhưng chỉ được viết với một số mã giữa các dấu ngoặc. Chúng ta không nên tạo ra khái niệm mới gọi là "khởi tạo cú đúp".

4- Tạo các lớp ẩn danh lồng nhau không liên quan gì đến hai dấu ngoặc nhọn:
Tôi không đồng ý với lập luận rằng bạn tạo quá nhiều lớp ẩn danh. Bạn không tạo chúng vì một khối khởi tạo, mà chỉ vì bạn tạo chúng. Chúng sẽ được tạo ngay cả khi bạn không sử dụng hai lần khởi tạo niềng răng để những vấn đề đó xảy ra ngay cả khi không khởi tạo ... Khởi tạo không phải là yếu tố tạo ra các đối tượng khởi tạo.

Ngoài ra, chúng ta không nên nói về vấn đề được tạo bằng cách sử dụng "không khởi tạo cú đúp" này hoặc thậm chí bằng cách khởi tạo một khung bình thường, bởi vì các vấn đề được mô tả chỉ tồn tại do tạo lớp ẩn danh nên không liên quan gì đến câu hỏi ban đầu. Nhưng tất cả các câu trả lời đều mang đến cho độc giả ấn tượng rằng đó không phải là lỗi khi tạo các lớp ẩn danh, mà là thứ xấu xa (không tồn tại) này được gọi là "khởi tạo cú đúp".


9

Để tránh tất cả các tác động tiêu cực của khởi tạo nẹp đôi, chẳng hạn như:

  1. Khả năng tương thích "bằng".
  2. Không có kiểm tra thực hiện, khi sử dụng bài tập trực tiếp.
  3. Rò rỉ bộ nhớ có thể.

làm những việc tiếp theo:

  1. Tạo lớp "Builder" riêng biệt, đặc biệt là cho khởi tạo cú đúp.
  2. Khai báo các trường có giá trị mặc định.
  3. Đặt phương thức tạo đối tượng trong lớp đó.

Thí dụ:

public class MyClass {
    public static class Builder {
        public int    first  = -1        ;
        public double second = Double.NaN;
        public String third  = null      ;

        public MyClass create() {
            return new MyClass(first, second, third);
        }
    }

    protected final int    first ;
    protected final double second;
    protected final String third ;

    protected MyClass(
        int    first ,
        double second,
        String third
    ) {
        this.first = first ;
        this.second= second;
        this.third = third ;
    }

    public int    first () { return first ; }
    public double second() { return second; }
    public String third () { return third ; }
}

Sử dụng:

MyClass my = new MyClass.Builder(){{ first = 1; third = "3"; }}.create();

Ưu điểm:

  1. Đơn giản chỉ để sử dụng.
  2. Không phá vỡ tính tương thích "bằng".
  3. Bạn có thể thực hiện kiểm tra trong phương pháp tạo.
  4. Không có rò rỉ bộ nhớ.

Nhược điểm:

  • Không ai.

Và, kết quả là, chúng ta có mẫu xây dựng java đơn giản nhất từ ​​trước đến nay.

Xem tất cả các mẫu tại github: java-sf-builder-Simple-example


4

Đó là - trong số các mục đích sử dụng khác - một lối tắt để khởi tạo các bộ sưu tập. Tìm hiểu thêm ...


2
Chà, đó là một ứng dụng cho nó, nhưng không có nghĩa là duy nhất.
skaffman

4

bạn có ý nghĩa như thế này?

List<String> blah = new ArrayList<String>(){{add("asdfa");add("bbb");}};

đó là một khởi tạo danh sách mảng trong thời gian tạo (hack)


4

Bạn có thể đặt một số câu lệnh Java làm vòng lặp để khởi tạo bộ sưu tập:

List<Character> characters = new ArrayList<Character>() {
    {
        for (char c = 'A'; c <= 'E'; c++) add(c);
    }
};

Random rnd = new Random();

List<Integer> integers = new ArrayList<Integer>() {
    {
         while (size() < 10) add(rnd.nextInt(1_000_000));
    }
};

Nhưng trường hợp này ảnh hưởng đến hiệu suất, kiểm tra cuộc thảo luận này


4

Như được chỉ ra bởi @Lukas Eder, việc khởi tạo niềng răng đôi của bộ sưu tập phải được tránh.

Nó tạo ra một lớp bên trong ẩn danh và vì tất cả các lớp bên trong giữ một tham chiếu đến thể hiện cha mẹ nên nó có thể - và 99% có khả năng sẽ - ngăn chặn việc thu gom rác nếu các đối tượng bộ sưu tập này được tham chiếu bởi nhiều đối tượng hơn là chỉ khai báo.

Java 9 đã giới thiệu phương pháp thuận tiện List.of, Set.ofMap.of, mà nên được sử dụng để thay thế. Chúng nhanh hơn và hiệu quả hơn bộ khởi tạo hai nẹp.


0

Niềng răng đầu tiên tạo ra một Lớp ẩn danh mới và bộ niềng thứ hai tạo ra một bộ khởi tạo cá thể giống như khối tĩnh.

Giống như những người khác đã chỉ ra, nó không an toàn để sử dụng.

Tuy nhiên, bạn luôn có thể sử dụng thay thế này để khởi tạo các bộ sưu tập.

  • Java 8
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
  • Java 9
List<String> list = List.of("A", "B", "C");

-1

Điều này có vẻ giống như với từ khóa rất phổ biến trong flash và vbscript. Đó là một phương pháp thay đổi những gì thisvà không có gì hơn.


Không hẳn vậy. Điều đó sẽ giống như việc tạo ra một lớp mới là một phương pháp để thay đổi những gì thisđang có. Cú pháp chỉ tạo một lớp ẩn danh (vì vậy mọi tham chiếu thissẽ đề cập đến đối tượng của lớp ẩn danh mới đó), sau đó sử dụng một khối {...}khởi tạo để khởi tạo thể hiện mới được tạo.
grinch
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.