Tại sao người ta lại khai báo một lớp cuối cùng là bất biến trong Java?


83

Tôi đã đọc rằng để làm cho một lớp bất biến trong Java, chúng ta nên làm như sau,

  1. Không cung cấp bất kỳ bộ định vị nào
  2. Đánh dấu tất cả các trường là riêng tư
  3. Kết thúc lớp học

Tại sao phải có bước 3? Tại sao tôi phải đánh dấu lớp final?


java.math.BigIntegerlớp là ví dụ, các giá trị của nó là bất biến nhưng nó không phải là cuối cùng.
Nandkumar Tekale

@Nandkumar Nếu bạn có BigInteger, bạn không biết nó có phải là bất biến hay không. Đó là một thiết kế lộn xộn. / java.io.Filelà một ví dụ thú vị hơn.
Tom Hawtin - tackline

1
Không cho phép các lớp con ghi đè các phương thức - Cách đơn giản nhất để làm điều này là khai báo lớp là cuối cùng. Một cách tiếp cận phức tạp hơn là làm cho các nhà xây dựng tư nhân và xây dựng trường trong các phương pháp nhà máy - từ - docs.oracle.com/javase/tutorial/essential/concurrency/...
Raúl

1
@mkobit Filerất thú vị vì nó có khả năng được tin cậy là bất biến và do đó dẫn đến các cuộc tấn công TOCTOU (Thời gian kiểm tra / Thời gian sử dụng). Mã đáng tin cậy kiểm tra xem Filecó một đường dẫn có thể chấp nhận được không và sau đó sử dụng đường dẫn. Một phân lớp Filecó thể thay đổi giá trị giữa kiểm tra và sử dụng.
Tom Hawtin - tackline

1
Make the class finalhoặc Đảm bảo rằng tất cả các lớp con là không thể thay đổi
O.Badr

Câu trả lời:


138

Nếu bạn không đánh dấu lớp finalđó, tôi có thể đột nhiên biến lớp tưởng như bất biến của bạn thực sự có thể thay đổi được. Ví dụ: hãy xem xét mã này:

public class Immutable {
     private final int value;

     public Immutable(int value) {
         this.value = value;
     }

     public int getValue() {
         return value;
     }
}

Bây giờ, giả sử tôi làm như sau:

public class Mutable extends Immutable {
     private int realValue;

     public Mutable(int value) {
         super(value);

         realValue = value;
     }

     public int getValue() {
         return realValue;
     }
     public void setValue(int newValue) {
         realValue = newValue;
     }

    public static void main(String[] arg){
        Mutable obj = new Mutable(4);
        Immutable immObj = (Immutable)obj;              
        System.out.println(immObj.getValue());
        obj.setValue(8);
        System.out.println(immObj.getValue());
    }
}

Lưu ý rằng trong Mutablelớp con của tôi , tôi đã ghi đè hành vi getValueđọc một trường mới, có thể thay đổi được khai báo trong lớp con của mình. Kết quả là, lớp của bạn, ban đầu trông có vẻ bất biến, thực sự không phải là bất biến. Tôi có thể truyền Mutableđối tượng này vào bất cứ nơi nào Immutablemong đợi một đối tượng, điều này có thể làm Những điều Rất Xấu khi viết mã giả sử đối tượng thực sự bất biến. Đánh dấu lớp cơ sở finalngăn điều này xảy ra.

Hi vọng điêu nay co ich!


13
Nếu tôi thực hiện Mutable m = new Mutable (4); m.setValue (5); Ở đây, tôi đang chơi xung quanh với đối tượng lớp Mutable, không Immutable lớp object.So, tôi vẫn bối rối tại sao lớp Immutable không phải là bất biến
Anand

18
@ anand- Hãy tưởng tượng bạn có một hàm nhận Immutablemột đối số. Tôi có thể truyền một Mutableđối tượng cho hàm đó, kể từ Mutable extends Immutable. Bên trong hàm đó, trong khi bạn cho rằng đối tượng của mình là bất biến, tôi có thể có một luồng phụ chạy và thay đổi giá trị khi hàm chạy. Tôi cũng có thể cung cấp cho bạn một Mutableđối tượng mà hàm lưu trữ, sau đó thay đổi giá trị của nó ra bên ngoài. Nói cách khác, nếu hàm của bạn giả định giá trị là không thay đổi, nó có thể dễ dàng bị hỏng, vì tôi có thể cung cấp cho bạn một đối tượng có thể thay đổi và thay đổi nó sau này. Điều đó có ý nghĩa?
templatetypedef

6
@ anand- Phương thức bạn chuyển Mutableđối tượng vào sẽ không thay đổi đối tượng. Mối quan tâm là phương thức đó có thể giả định rằng đối tượng là bất biến khi nó thực sự không phải như vậy. Ví dụ, phương thức có thể giả định rằng, vì nó cho rằng đối tượng là bất biến, nó có thể được sử dụng làm khóa trong a HashMap. Sau đó, tôi có thể phá vỡ hàm đó bằng cách chuyển vào a Mutable, đợi nó lưu trữ đối tượng dưới dạng khóa, sau đó thay đổi Mutableđối tượng. Bây giờ, tìm kiếm trong đó HashMapsẽ không thành công, bởi vì tôi đã thay đổi khóa được liên kết với đối tượng. Điều đó có ý nghĩa?
templatetypedef

3
@ templatetypedef- Vâng, tôi đã nhận nó now..must nói đó là một explanation..took tuyệt vời một chút thời gian để nắm bắt nó mặc dù
Anand

1
@ SAM- Đúng vậy. Tuy nhiên, nó không thực sự quan trọng, vì các hàm được ghi đè không bao giờ tham chiếu đến biến đó nữa.
templatetypedef,

47

Trái ngược với những gì nhiều người lầm tưởng, việc tạo nên một lớp bất biến không phảifinal bắt buộc.

Đối số tiêu chuẩn để tạo các lớp bất biến finallà nếu bạn không làm điều này, thì các lớp con có thể thêm khả năng thay đổi, do đó vi phạm hợp đồng của lớp cha. Khách hàng của lớp sẽ cho rằng không thay đổi, nhưng sẽ ngạc nhiên khi có thứ gì đó đột biến từ bên dưới họ.

Nếu bạn đưa đối số này đến cực điểm logic của nó, thì tất cả các phương thức nên được thực hiện final, vì nếu không thì một lớp con có thể ghi đè một phương thức theo cách không phù hợp với hợp đồng của lớp cha của nó. Thật thú vị khi hầu hết các lập trình viên Java thấy điều này là vô lý, nhưng bằng cách nào đó vẫn ổn với ý tưởng rằng các lớp bất biến nên có final. Tôi nghi ngờ rằng nó có liên quan gì đó đến việc các lập trình viên Java nói chung không hoàn toàn thoải mái với khái niệm bất biến, và có lẽ một số kiểu suy nghĩ mờ nhạt liên quan đến nhiều nghĩa của finaltừ khóa trong Java.

Việc tuân thủ hợp đồng của lớp cha của bạn không phải là điều gì đó có thể hoặc luôn luôn được thực thi bởi trình biên dịch. Trình biên dịch có thể thực thi các khía cạnh nhất định của hợp đồng của bạn (ví dụ: một tập hợp tối thiểu các phương thức và chữ ký kiểu của chúng) nhưng có nhiều phần của hợp đồng điển hình mà trình biên dịch không thể thực thi.

Tính bất biến là một phần của hợp đồng của một lớp. Nó hơi khác so với một số thứ mà mọi người quen dùng hơn, vì nó cho biết những gì mà lớp (và tất cả các lớp con) không thể làm, trong khi tôi nghĩ rằng hầu hết các lập trình viên Java (và nói chung là OOP) có xu hướng nghĩ về các hợp đồng liên quan đến những gì một lớp có thể làm, không phải những gì nó không thể làm.

Tính bất biến cũng ảnh hưởng nhiều hơn đến một phương thức đơn lẻ - nó ảnh hưởng đến toàn bộ phiên bản - nhưng điều này không thực sự khác nhiều so với cách thức equalshashCodecông việc của Java. Hai phương pháp đó có một hợp đồng cụ thể được đưa ra Object. Hợp đồng này rất cẩn thận đưa ra những điều mà các phương pháp này không thể làm được. Hợp đồng này được thực hiện cụ thể hơn trong các lớp con. Rất dễ bị ghi đè equalshoặc hashCodevi phạm hợp đồng. Trên thực tế, nếu bạn chỉ ghi đè một trong hai phương thức này mà không ghi đè phương thức kia, rất có thể bạn đang vi phạm hợp đồng. Vì vậy, nên equalshashCodeđã được khai báo finaltrong .Object để tránh điều này? Tôi nghĩ rằng hầu hết sẽ tranh luận rằng họ không nên. Tương tự như vậy, không cần thiết phải tạo các lớp bất biếnfinal

Điều đó nói rằng, hầu hết các lớp của bạn, không thay đổi hoặc không, có lẽ nên như vậy final. Xem phần 17 của phiên bản Java thứ hai hiệu quả : "Thiết kế và tài liệu để kế thừa nếu không thì cấm nó".

Vì vậy, một phiên bản chính xác của bước 3 của bạn sẽ là: "Tạo lớp cuối cùng hoặc, khi thiết kế cho lớp con, ghi rõ ràng rằng tất cả các lớp con phải tiếp tục là bất biến."


3
Cần lưu ý rằng Java "mong đợi", nhưng không thực thi, cho hai đối tượng XY, giá trị của X.equals(Y)sẽ là không thay đổi (miễn là XYtiếp tục tham chiếu đến các đối tượng giống nhau). Nó có những kỳ vọng tương tự liên quan đến mã băm. Rõ ràng là không ai nên mong đợi trình biên dịch thực thi tính bất biến của các quan hệ tương đương và mã băm (vì nó đơn giản là không thể). Tôi thấy không có lý do gì mọi người nên mong đợi nó được thực thi cho các khía cạnh khác của một loại.
supercat

1
Ngoài ra, có nhiều trường hợp có thể hữu ích khi có một kiểu trừu tượng có hợp đồng chỉ định tính bất biến. Ví dụ, một có thể có một kiểu ImmutableMatrix trừu tượng, được cung cấp cho một cặp tọa độ, trả về a double. Người ta có thể lấy a GeneralImmutableMatrixsử dụng một mảng làm kho dự phòng, nhưng người ta cũng có thể có ví dụ: ImmutableDiagonalMatrixchỉ lưu trữ một mảng các mục dọc theo đường chéo (đọc mục X, Y sẽ mang lại Arr [X] nếu X == y và bằng không nếu không) .
supercat

2
Tôi thích lời giải thích này nhất. Việc luôn tạo ra một lớp không thay đổi sẽ finalhạn chế tính hữu dụng của nó, đặc biệt là khi bạn đang thiết kế một API nhằm mục đích mở rộng. Vì lợi ích của sự an toàn của luồng, nên làm cho lớp của bạn trở nên bất biến nhất có thể, nhưng vẫn giữ cho nó có thể mở rộng. Bạn có thể tạo các trường protected finalthay vì private final. Sau đó, thiết lập rõ ràng một hợp đồng (trong tài liệu) về các lớp con tuân thủ các bảo đảm về tính bất biến và an toàn luồng.
curioustechizen

4
+1 - Tôi luôn ở bên bạn cho đến khi báo Bloch. Chúng ta không cần phải hoang tưởng như vậy. 99% mã hóa là hợp tác. Không có vấn đề gì lớn nếu ai đó thực sự cần ghi đè một cái gì đó, hãy để họ. 99% chúng ta không viết một số API cốt lõi như Chuỗi hoặc Bộ sưu tập. Rất nhiều lời khuyên của Bloch, thật không may, dựa trên loại trường hợp sử dụng đó. Chúng không thực sự hữu ích đối với hầu hết các lập trình viên, đặc biệt là những người mới.
ZhongYu

Tôi muốn tạo ra lớp bất biến final. Chỉ bởi vì equalshashcodekhông thể là cuối cùng không có nghĩa là tôi có thể chấp nhận điều tương tự đối với lớp không thay đổi, trừ khi tôi có lý do hợp lệ để làm như vậy và trong trường hợp này, tôi có thể sử dụng tài liệu hoặc thử nghiệm làm phòng tuyến.
O.Badr

21

Đừng chấm điểm cuối cùng của cả lớp.

Có những lý do hợp lệ để cho phép mở rộng lớp bất biến như đã nêu trong một số câu trả lời khác, vì vậy việc đánh dấu lớp đó là cuối cùng không phải lúc nào cũng là một ý kiến ​​hay.

Tốt hơn là đánh dấu tài sản của bạn là riêng tư và cuối cùng và nếu bạn muốn bảo vệ "hợp đồng", hãy đánh dấu những người nhận của bạn là cuối cùng.

Bằng cách này, bạn có thể cho phép mở rộng lớp (có thể thậm chí bằng một lớp có thể thay đổi) tuy nhiên các khía cạnh bất biến của lớp của bạn được bảo vệ. Các thuộc tính là riêng tư và không thể được truy cập, getters cho các thuộc tính này là cuối cùng và không thể bị ghi đè.

Bất kỳ mã nào khác sử dụng một thể hiện của lớp bất biến của bạn sẽ có thể dựa vào các khía cạnh bất biến của lớp của bạn ngay cả khi lớp con mà nó được truyền có thể thay đổi ở các khía cạnh khác. Tất nhiên, vì nó chiếm một thể hiện của lớp của bạn nên nó thậm chí sẽ không biết về những khía cạnh khác này.


1
Tôi muốn gói gọn tất cả các khía cạnh bất biến trong một lớp riêng biệt và sử dụng nó theo cách bố cục. Sẽ dễ dàng lập luận hơn là trộn trạng thái có thể thay đổi và bất biến trong một đơn vị mã.
toniedzwiedz

1
Hoàn toàn đồng ý. Thành phần sẽ là một cách rất tốt để thực hiện điều này, miễn là lớp có thể thay đổi của bạn chứa lớp bất biến của bạn. Nếu cấu trúc của bạn theo cách khác, nó sẽ là một giải pháp từng phần tốt.
Justin Ohms

5

Nếu nó không phải là cuối cùng thì bất kỳ ai cũng có thể mở rộng lớp và làm bất cứ điều gì họ thích, như cung cấp bộ định tuyến, phủ bóng các biến riêng tư của bạn và về cơ bản là làm cho nó có thể thay đổi được.


Mặc dù bạn nói đúng, nhưng không rõ tại sao việc bạn có thể thêm hành vi có thể thay đổi nhất thiết sẽ phá vỡ mọi thứ nếu các trường lớp cơ sở đều là riêng tư và cuối cùng. Nguy cơ thực sự là lớp con có thể biến một đối tượng không thể thay đổi trước đó thành một đối tượng có thể thay đổi bằng cách ghi đè hành vi.
templatetypedef

4

Điều đó hạn chế các lớp khác mở rộng lớp của bạn.

lớp cuối cùng không thể được mở rộng bởi các lớp khác.

Nếu một lớp mở rộng lớp mà bạn muốn tạo thành bất biến, nó có thể thay đổi trạng thái của lớp do các nguyên tắc kế thừa.

Chỉ cần làm rõ "nó có thể thay đổi". Lớp con có thể ghi đè hành vi của lớp cha như sử dụng ghi đè phương thức (như câu trả lời templatetypedef / Ted Hop)


2
Điều đó đúng, nhưng tại sao nó lại cần thiết ở đây?
templatetypedef

@templatetypedef: Bạn quá nhanh. Tôi đang chỉnh sửa câu trả lời của mình với các điểm hỗ trợ.
kosa

4
Ngay bây giờ câu trả lời của bạn rất mơ hồ nên tôi không nghĩ nó trả lời câu hỏi nào cả. Ý bạn là gì khi "nó có thể thay đổi trạng thái của lớp do các nguyên tắc kế thừa?" Trừ khi bạn đã biết tại sao bạn nên đánh dấu nó final, tôi không thể thấy câu trả lời này giúp ích như thế nào.
templatetypedef

3

Nếu bạn không làm cho nó cuối cùng, tôi có thể mở rộng nó và làm cho nó không thể thay đổi.

public class Immutable {
  privat final int val;
  public Immutable(int val) {
    this.val = val;
  }

  public int getVal() {
    return val;
  }
}

public class FakeImmutable extends Immutable {
  privat int val2;
  public FakeImmutable(int val) {
    super(val);
  }

  public int getVal() {
    return val2;
  }

  public void setVal(int val2) {
    this.val2 = val2;
  }
}

Bây giờ, tôi có thể chuyển FakeImmutable cho bất kỳ lớp nào mong đợi Immutable và nó sẽ không hoạt động như hợp đồng mong đợi.


2
Tôi nghĩ điều này khá giống với câu trả lời của tôi.
templatetypedef

Có, đã kiểm tra một nửa thông qua việc đăng bài, không có câu trả lời mới. Và sau đó khi được đăng, có của bạn, trước tôi 1 phút. Ít nhất chúng tôi đã không sử dụng những cái tên giống nhau cho mọi thứ.
Roger Lindsjö

Một Correction: Trong lớp FakeImmutable, tên Constructor nên FakeImmutable KHÔNG Immutable
Nắng Gupta

2

Để tạo lớp không thay đổi, không bắt buộc phải đánh dấu lớp là cuối cùng.

Hãy để tôi lấy một ví dụ như vậy từ các lớp java bản thân lớp "BigInteger" là bất biến nhưng không phải là cuối cùng.

Thực ra Immutability là một khái niệm mà đối tượng tạo ra thì nó không thể sửa đổi được.

Hãy nghĩ theo quan điểm của JVM, theo quan điểm của JVM, tất cả các luồng phải chia sẻ cùng một bản sao của đối tượng và nó được xây dựng đầy đủ trước khi bất kỳ luồng nào truy cập vào nó và trạng thái của đối tượng không thay đổi sau khi xây dựng nó.

Tính bất biến có nghĩa là không có cách nào bạn thay đổi trạng thái của đối tượng khi nó được tạo ra và điều này đạt được bằng ba quy tắc ngón tay cái khiến trình biên dịch nhận ra rằng lớp đó là bất biến và chúng như sau: -

  • tất cả các trường không riêng tư phải là trường cuối cùng
  • đảm bảo rằng không có phương thức nào trong lớp có thể thay đổi các trường của đối tượng một cách trực tiếp hoặc gián tiếp
  • bất kỳ tham chiếu đối tượng nào được xác định trong lớp không thể được sửa đổi bên ngoài từ lớp

Để biết thêm thông tin, hãy tham khảo URL bên dưới

http://javaunturnedtopics.blogspot.in/2016/07/can-we-create-immutable-class-without.html


1

Giả sử bạn có lớp học sau:

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class PaymentImmutable {
    private final Long id;
    private final List<String> details;
    private final Date paymentDate;
    private final String notes;

    public PaymentImmutable (Long id, List<String> details, Date paymentDate, String notes) {
        this.id = id;
        this.notes = notes;
        this.paymentDate = paymentDate == null ? null : new Date(paymentDate.getTime());
        if (details != null) {
            this.details = new ArrayList<String>();

            for(String d : details) {
                this.details.add(d);
            }
        } else {
            this.details = null;
        }
    }

    public Long getId() {
        return this.id;
    }

    public List<String> getDetails() {
        if(this.details != null) {
            List<String> detailsForOutside = new ArrayList<String>();
            for(String d: this.details) {
                detailsForOutside.add(d);
            }
            return detailsForOutside;
        } else {
            return null;
        }
    }

}

Sau đó, bạn mở rộng nó và phá vỡ tính bất biến của nó.

public class PaymentChild extends PaymentImmutable {
    private List<String> temp;
    public PaymentChild(Long id, List<String> details, Date paymentDate, String notes) {
        super(id, details, paymentDate, notes);
        this.temp = details;
    }

    @Override
    public List<String> getDetails() {
        return temp;
    }
}

Ở đây chúng tôi kiểm tra nó:

public class Demo {

    public static void main(String[] args) {
        List<String> details = new ArrayList<>();
        details.add("a");
        details.add("b");
        PaymentImmutable immutableParent = new PaymentImmutable(1L, details, new Date(), "notes");
        PaymentImmutable notImmutableChild = new PaymentChild(1L, details, new Date(), "notes");

        details.add("some value");
        System.out.println(immutableParent.getDetails());
        System.out.println(notImmutableChild.getDetails());
    }
}

Kết quả đầu ra sẽ là:

[a, b]
[a, b, some value]

Như bạn có thể thấy trong khi lớp gốc vẫn giữ tính bất biến của nó, các lớp con có thể thay đổi được. Do đó, trong thiết kế của bạn, bạn không thể chắc chắn rằng đối tượng bạn đang sử dụng là bất biến, trừ khi bạn tạo lớp cuối cùng.


0

Giả sử lớp sau không phải là final:

public class Foo {
    private int mThing;
    public Foo(int thing) {
        mThing = thing;
    }
    public int doSomething() { /* doesn't change mThing */ }
}

Nó rõ ràng là bất biến vì ngay cả các lớp con cũng không thể sửa đổi mThing. Tuy nhiên, một lớp con có thể thay đổi được:

public class Bar extends Foo {
    private int mValue;
    public Bar(int thing, int value) {
        super(thing);
        mValue = value;
    }
    public int getValue() { return mValue; }
    public void setValue(int value) { mValue = value; }
}

Bây giờ một đối tượng có thể gán cho một biến kiểu Fookhông còn được đảm bảo là có thể mmutable. Điều này có thể gây ra sự cố với những thứ như băm, bình đẳng, đồng thời, v.v.


0

Bản thân thiết kế không có giá trị. Thiết kế luôn được sử dụng để đạt được mục tiêu. Mục tiêu ở đây là gì? Chúng ta có muốn giảm số lượng bất ngờ trong mã không? Chúng ta có muốn ngăn chặn lỗi không? Chúng ta có đang tuân theo các quy tắc một cách mù quáng không?

Ngoài ra, thiết kế luôn đi kèm với chi phí. Mỗi thiết kế xứng đáng với cái tên có nghĩa là bạn có xung đột về mục tiêu .

Với ý nghĩ đó, bạn cần tìm câu trả lời cho những câu hỏi sau:

  1. Có bao nhiêu lỗi rõ ràng điều này sẽ ngăn chặn?
  2. Có bao nhiêu lỗi tinh vi này sẽ ngăn chặn?
  3. Tần suất điều này sẽ làm cho mã khác phức tạp hơn (= dễ bị lỗi hơn)?
  4. Điều này làm cho việc kiểm tra dễ dàng hơn hay khó hơn?
  5. Các nhà phát triển trong dự án của bạn giỏi đến mức nào? Họ cần bao nhiêu hướng dẫn với một chiếc búa tạ?

Giả sử bạn có nhiều nhà phát triển cấp dưới trong nhóm của mình. Họ sẽ tuyệt vọng thử bất kỳ điều gì ngu ngốc chỉ vì họ chưa biết giải pháp tốt cho vấn đề của họ. Làm cho lớp cuối cùng có thể ngăn chặn lỗi (tốt) nhưng cũng có thể khiến họ nghĩ ra các giải pháp "thông minh" như sao chép tất cả các lớp này thành một lớp có thể thay đổi ở mọi nơi trong mã.

Mặt khác, sẽ rất khó để tạo một lớp finalsau khi nó được sử dụng ở khắp mọi nơi nhưng rất dễ tạo một finallớp finalsau đó nếu bạn phát hiện ra mình cần phải gia hạn.

Nếu bạn sử dụng đúng cách các giao diện, bạn có thể tránh được vấn đề "Tôi cần tạo giao diện có thể thay đổi này" bằng cách luôn sử dụng giao diện và sau đó thêm một triển khai có thể thay đổi khi cần thiết.

Kết luận: Không có giải pháp "tốt nhất" cho câu trả lời này. Nó phụ thuộc vào giá bạn muốn và bạn phải trả.


0

Ý nghĩa mặc định của equals () giống như đẳng thức tham chiếu. Đối với các kiểu dữ liệu bất biến, điều này hầu như luôn sai. Vì vậy, bạn phải ghi đè phương thức equals (), thay thế nó bằng cách triển khai của riêng bạn. liên kết

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.