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,
- Không cung cấp bất kỳ bộ định vị nào
- Đánh dấu tất cả các trường là riêng tư
- 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
?
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,
Tại sao phải có bước 3? Tại sao tôi phải đánh dấu lớp final
?
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.File
là một ví dụ thú vị hơn.
File
rấ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 File
có 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 File
có thể thay đổi giá trị giữa kiểm tra và sử dụng.
Make the class final
hoặc Đảm bảo rằng tất cả các lớp con là không thể thay đổi
Câu trả lời:
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 Mutable
lớ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 Immutable
mong đợ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ở final
ngăn điều này xảy ra.
Hi vọng điêu nay co ich!
Immutable
mộ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?
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 đó HashMap
sẽ 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?
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
là bắt buộc.
Đối số tiêu chuẩn để tạo các lớp bất biến final
là 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 final
từ 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 equals
và hashCode
cô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 đè equals
hoặc hashCode
vi 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 equals
và hashCode
đã được khai báo final
trong .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."
X
và Y
, giá trị của X.equals(Y)
sẽ là không thay đổi (miễn là X
và Y
tiế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.
double
. Người ta có thể lấy a GeneralImmutableMatrix
sử dụng một mảng làm kho dự phòng, nhưng người ta cũng có thể có ví dụ: ImmutableDiagonalMatrix
chỉ 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) .
final
hạ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 final
thay 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.
final
. Chỉ bởi vì equals
và hashcode
khô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.
Đừ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.
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.
Đ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)
final
, tôi không thể thấy câu trả lời này giúp ích như thế nào.
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.
Để 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: -
Để 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
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.
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 Foo
khô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.
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:
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 final
sau khi nó được sử dụng ở khắp mọi nơi nhưng rất dễ tạo một final
lớp final
sau đó 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ả.
java.math.BigInteger
lớ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.