Tôi có nên sử dụng các khối khởi tạo trong Java không?


16

Gần đây tôi đã bắt gặp một cấu trúc Java mà tôi chưa từng thấy trước đây và đang tự hỏi liệu tôi có nên sử dụng nó không. Nó dường như được gọi là khối khởi tạo .

public class Test {
  public Test() { /* first constructor */ }
  public Test(String s) { /* second constructor */ }

  // Non-static initializer block - copied into every constructor:
  {
    doStuff();
  }
}

Khối mã sẽ được sao chép vào từng hàm tạo, tức là nếu bạn có nhiều hàm tạo, bạn không phải viết lại mã.

Tuy nhiên, tôi thấy ba nhược điểm chính khi sử dụng cú pháp này:

  1. Đây là một trong số rất ít trường hợp trong Java trong đó thứ tự mã của bạn là quan trọng, vì bạn có thể định nghĩa nhiều khối mã và chúng sẽ được thực hiện theo thứ tự chúng được viết. Điều này có vẻ có hại đối với tôi vì chỉ cần thay đổi thứ tự các khối mã sẽ thực sự thay đổi mã.
  2. Tôi không thực sự thấy bất kỳ lợi ích bằng cách sử dụng nó. Trong hầu hết các trường hợp, các hàm tạo sẽ gọi nhau bằng một số giá trị được xác định trước. Ngay cả khi đây không phải là trường hợp, mã chỉ có thể được đưa vào một phương thức riêng và được gọi từ mỗi hàm tạo.
  3. Nó làm giảm khả năng đọc, vì bạn có thể đặt khối ở cuối lớp và hàm tạo thường ở đầu lớp. Sẽ khá phản trực quan khi xem xét một phần hoàn toàn khác của tệp mã nếu bạn không cho rằng điều đó là cần thiết.

Nếu các tuyên bố trên của tôi là đúng, tại sao (và khi nào) ngôn ngữ này được xây dựng? Có trường hợp sử dụng hợp pháp?


3
Ví dụ bạn đã đăng không bao gồm bất cứ thứ gì trông giống như khối khởi tạo.
Simon B

6
@SimonBarker nhìn lại - { doStuff(); }ở cấp độ lớp là một khối khởi tạo.
amon

@SimonBarker Khối mã được bao quanhdoStuff()
Tái lập Monica - dirkk


2
"[S] ngụ ý thay đổi thứ tự các khối mã sẽ thực sự thay đổi mã." Và làm thế nào khác với việc thay đổi thứ tự của các bộ khởi tạo biến hoặc các dòng mã riêng lẻ? Nếu không có phụ thuộc, thì không có tác hại nào xảy ra và nếu có phụ thuộc, thì việc đặt các phụ thuộc không theo thứ tự cũng giống như phụ thuộc sai cho các dòng mã riêng lẻ. Chỉ vì Java cho phép bạn tham khảo các phương thức và các lớp trước khi chúng được định nghĩa không có nghĩa là mã phụ thuộc đơn hàng là hiếm trong Java.
JAB

Câu trả lời:


20

Có hai trường hợp tôi sử dụng các khối khởi tạo.

Cái đầu tiên là để khởi tạo thành viên cuối cùng. Trong Java, bạn có thể khởi tạo một thành viên cuối cùng bằng nội tuyến với khai báo hoặc bạn có thể khởi tạo nó trong hàm tạo. Trong một phương thức, nó bị cấm chỉ định cho một thành viên cuối cùng.

Điều này hợp lệ:

final int val = 2;

Điều này cũng hợp lệ:

final int val;

MyClass() {
    val = 2;
}

Điều này không hợp lệ:

final int val;

MyClass() {
    init();
}

void init() {
    val = 2;  // cannot assign to 'final' field in a method
}

Nếu bạn có nhiều hàm tạo và nếu bạn không thể khởi tạo một dòng thành viên cuối cùng (vì logic khởi tạo quá phức tạp) hoặc nếu các hàm tạo không thể tự gọi, thì bạn có thể sao chép / dán mã khởi tạo hoặc bạn có thể sử dụng một khối khởi tạo.

final int val;
final int squareVal;

MyClass(int v, String s) {
    this.val = v;
    this.s = s;
}

MyClass(Point p, long id) {
    this.val = p.x;
    this.id = id;
}

{
    squareVal = val * val;
}

Trường hợp sử dụng khác mà tôi có cho các khối khởi tạo là để xây dựng các cấu trúc dữ liệu của trình trợ giúp nhỏ. Tôi khai báo một thành viên và đặt các giá trị vào nó ngay sau các khai báo của nó trong khối khởi tạo riêng của nó.

private Map<String, String> days = new HashMap<String, String>();
{
    days.put("mon", "monday");
    days.put("tue", "tuesday");
    days.put("wed", "wednesday");
    days.put("thu", "thursday");
    days.put("fri", "friday");
    days.put("sat", "saturday");
    days.put("sun", "sunday");
}

Đây không phải là cuộc gọi phương thức không hợp lệ. Đó là mã bên trong phương thức init không hợp lệ. Chỉ các hàm tạo và khối initalizer mới có thể gán cho một biến thành viên cuối cùng, do đó phép gán trong init sẽ không biên dịch.
barjak

Khối mã thứ tư của bạn không biên dịch. Các khối Initalizer chạy trước tất cả các hàm tạo, do đó squareVal = val * valsẽ phàn nàn về việc truy cập các giá trị chưa được khởi tạo. Các khối khởi tạo không thể phụ thuộc vào bất kỳ đối số nào được truyền cho hàm tạo. Giải pháp thông thường mà tôi đã thấy đối với loại vấn đề đó là xác định một hàm tạo "cơ sở" duy nhất với logic phức tạp và xác định tất cả các hàm tạo khác theo phương thức đó. Trên thực tế, hầu hết việc sử dụng các trình khởi tạo cá thể có thể được thay thế bằng mẫu đó.
Maln normalulo

11

Nói chung, không sử dụng các khối khởi tạo không tĩnh (và có thể tránh các khối tĩnh quá).

Cú pháp khó hiểu

Nhìn vào câu hỏi này, có 3 câu trả lời, nhưng bạn đã đánh lừa 4 người với cú pháp này. Tôi là một trong số họ và tôi đã viết Java được 16 năm! Rõ ràng, cú pháp có khả năng dễ bị lỗi! Tôi sẽ tránh xa nó.

Kính viễn vọng

Đối với những thứ thực sự đơn giản, bạn có thể sử dụng các hàm tạo "kính thiên văn" để tránh sự nhầm lẫn này:

public class Test {
    private String something;

    // Default constructor does some things
    public Test() { doStuff(); }

    // Other constructors call the default constructor
    public Test(String s) {
        this(); // Call default constructor
        something = s;
    }
}

Mô hình xây dựng

Nếu bạn cần doStuff () ở cuối mỗi hàm tạo hoặc khởi tạo tinh vi khác, có lẽ một mẫu trình xây dựng sẽ là tốt nhất. Josh Bloch liệt kê một số lý do tại sao các nhà xây dựng là một ý tưởng tốt. Các nhà xây dựng mất một ít thời gian để viết, nhưng viết đúng, họ là một niềm vui để sử dụng.

public class Test {
    // Value can be final (immutable)
    private final String something;

    // Private constructor.
    private Test(String s) { something = s; }

    // Static method to get a builder
    public static Builder builder() { return new Builder(); }

    // builder class accumulates values until a valid Test object can be created. 
    private static class Builder {
        private String tempSomething;
        public Builder something(String s) {
            tempSomething = s;
            return this;
        }
        // This is our factory method for a Test class.
        public Test build() {
            Test t = new Test(tempSomething);
            // Here we do your extra initialization after the
            // Test class has been created.
            doStuff();
            // Return a valid, potentially immutable Test object.
            return t;
        }
    }
}

// Now you can call:
Test t = Test.builder()
             .setString("Utini!")
             .build();

Vòng lặp khởi tạo tĩnh

Tôi đã từng sử dụng các trình khởi tạo tĩnh rất nhiều, nhưng đôi khi chạy vào các vòng lặp trong đó 2 lớp phụ thuộc vào các khối khởi tạo tĩnh của nhau được gọi trước khi lớp có thể được tải đầy đủ. Điều này tạo ra một "không tải lớp" hoặc thông báo lỗi mơ hồ tương tự. Tôi đã phải so sánh các tệp với phiên bản làm việc đã biết cuối cùng trong kiểm soát nguồn để tìm ra vấn đề là gì. Không có niềm vui nào cả.

Khởi tạo lười biếng

Có thể các trình khởi tạo tĩnh tốt cho lý do hiệu suất khi chúng hoạt động và không quá khó hiểu. Nhưng nói chung, tôi thích khởi tạo lười biếng hơn so với khởi tạo tĩnh trong những ngày này. Rõ ràng những gì họ làm, tôi chưa gặp phải lỗi tải lớp với họ và họ làm việc trong nhiều tình huống khởi tạo hơn các khối khởi tạo.

Định nghĩa dữ liệu

Thay vì khởi tạo tĩnh để xây dựng cấu trúc dữ liệu, (so sánh với các ví dụ trong các câu trả lời khác), giờ đây tôi sử dụng các hàm trợ giúp định nghĩa dữ liệu bất biến của Paguro :

private ImMap<String,String> days =
        map(tup("mon", "monday"),
            tup("tue", "tuesday"),
            tup("wed", "wednesday"),
            tup("thu", "thursday"),
            tup("fri", "friday"),
            tup("sat", "saturday"),
            tup("sun", "sunday"));

Conculsion

Khi bắt đầu Java, các khối khởi tạo là cách duy nhất để thực hiện một số thứ, nhưng bây giờ chúng khó hiểu, dễ bị lỗi và trong hầu hết các trường hợp đã được thay thế bằng các lựa chọn thay thế tốt hơn (chi tiết ở trên). Thật thú vị khi biết về các khối khởi tạo trong trường hợp bạn thấy chúng trong mã kế thừa hoặc chúng xuất hiện trong một thử nghiệm, nhưng nếu tôi đang thực hiện đánh giá mã và tôi thấy một mã trong mã mới, tôi sẽ yêu cầu bạn giải thích tại sao không có mã nào các lựa chọn thay thế ở trên là phù hợp trước khi đưa ra mã của bạn.


3

Ngoài việc khởi tạo một biến đối tượng được khai báo là final(xem câu trả lời của barjak ), tôi cũng sẽ đề cập đến statickhối khởi tạo.

Bạn có thể sử dụng chúng như một loại "công cụ tĩnh".

Bằng cách đó, bạn có thể thực hiện các khởi tạo phức tạp trên một biến tĩnh lần đầu tiên lớp được tham chiếu.

Đây là một ví dụ lấy cảm hứng từ một trong những barjak:

public class dayHelper(){
    private static Map<String, String> days = new HashMap<String, String>();
    static {
        days.put("mon", "monday");
        days.put("tue", "tuesday");
        days.put("wed", "wednesday");
        days.put("thu", "thursday");
        days.put("fri", "friday");
        days.put("sat", "saturday");
        days.put("sun", "sunday");
    }
    public static String getLongName(String shortName){
         return days.get(shortName);
    }
}

1

Vì các khối khởi tạo không tĩnh có liên quan, nên chức năng trần của chúng là hoạt động như một hàm tạo mặc định trong các lớp ẩn danh. Đó là cơ bản duy nhất của họ để tồn tại.


0

Tôi hoàn toàn đồng ý với các tuyên bố 1, 2, 3. Tôi cũng không bao giờ sử dụng các công cụ khởi tạo khối vì những lý do này và tôi không biết tại sao nó tồn tại trong Java.

Tuy nhiên, tôi buộc phải sử dụng trình khởi tạo khối tĩnh trong một trường hợp: khi tôi phải khởi tạo một trường tĩnh có hàm tạo có thể ném ngoại lệ được kiểm tra.

private static final JAXBContext context = JAXBContext.newInstance(Foo.class); //doesn't compile

Nhưng thay vào đó bạn phải làm:

private static JAXBContext context;
static {
    try
    {
        context = JAXBContext.newInstance(Foo.class);
    }
    catch (JAXBException e)
    {
        //seriously...
    }
}

Tôi thấy thành ngữ này rất xấu (nó cũng ngăn bạn đánh dấu contextfinal) nhưng đây là cách duy nhất được Java hỗ trợ để khởi tạo các trường như vậy.


Tôi nghĩ rằng nếu bạn thiết lập context = null; trong khối bắt của mình, bạn có thể khai báo ngữ cảnh là cuối cùng.
GlenPeterson

@GlenPeterson Tôi đã thử nhưng nó không biên dịch:The final field context may already have been assigned
Phát hiện

Giáo sư! Tôi cá là bạn có thể làm cho bối cảnh của mình cuối cùng nếu bạn giới thiệu một biến cục bộ bên trong khối tĩnh:static { JAXBContext tempCtx = null; try { tempCtx = JAXBContext.newInstance(Foo.class); } catch (JAXBException ignored) { ; } context = tempCtx; }
GlenPeterson
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.