JavaRush /Blog Java /Random-VI /So sánh các đối tượng: thực hành
articles
Mức độ

So sánh các đối tượng: thực hành

Xuất bản trong nhóm
Đây là bài viết thứ hai dành cho việc so sánh các đối tượng. Phần đầu tiên thảo luận về cơ sở lý thuyết của so sánh - nó được thực hiện như thế nào, tại sao và nó được sử dụng ở đâu. Trong bài viết này chúng ta sẽ nói trực tiếp về việc so sánh các con số, đồ vật, trường hợp đặc biệt, sự tinh tế và những điểm không rõ ràng. Chính xác hơn, đây là những gì chúng ta sẽ nói về:
So sánh đồ vật: luyện tập - 1
  • So sánh chuỗi: ' ==' vàequals
  • Phương phápString.intern
  • So sánh nguyên thủy thực sự
  • +0.0-0.0
  • NghĩaNaN
  • Java 5.0. Phương thức tạo và so sánh thông qua ' =='
  • Java 5.0. Tự động đóng hộp/Mở hộp: ' ==', ' >=' và ' <=' cho các trình bao bọc đối tượng.
  • Java 5.0. so sánh các phần tử enum (type enum)
Vậy hãy bắt đầu!

So sánh chuỗi: ' ==' vàequals

À, những dòng này... Một trong những loại được sử dụng phổ biến nhất, gây ra rất nhiều vấn đề. Về nguyên tắc, có một bài viết riêng về họ . Và ở đây tôi sẽ đề cập đến vấn đề so sánh. Tất nhiên, các chuỗi có thể được so sánh bằng cách sử dụng equals. Hơn nữa, chúng PHẢI được so sánh thông qua equals. Tuy nhiên, có những điều tinh tế đáng để biết. Trước hết, các chuỗi giống hệt nhau thực sự là một đối tượng. Điều này có thể được xác minh dễ dàng bằng cách chạy đoạn mã sau:
String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
Kết quả sẽ là "giống nhau" . Có nghĩa là các tham chiếu chuỗi bằng nhau. Điều này được thực hiện ở cấp độ trình biên dịch, rõ ràng là để tiết kiệm bộ nhớ. Trình biên dịch tạo MỘT phiên bản của chuỗi và gán str1một str2tham chiếu cho phiên bản này. Tuy nhiên, điều này chỉ áp dụng cho các chuỗi được khai báo dưới dạng chữ trong mã. Nếu bạn soạn một chuỗi từ các mảnh, liên kết đến chuỗi đó sẽ khác. Xác nhận - ví dụ này:
String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
Kết quả sẽ là "không giống nhau" . Bạn cũng có thể tạo một đối tượng mới bằng cách sử dụng hàm tạo sao chép:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1==str2 ? "the same" : "not the same");
Kết quả cũng sẽ là "không giống nhau" . Vì vậy, đôi khi các chuỗi có thể được so sánh thông qua so sánh tham chiếu. Nhưng tốt hơn hết là không nên dựa vào điều này. Tôi muốn đề cập đến một phương pháp rất thú vị cho phép bạn có được cái gọi là biểu diễn chuẩn của một chuỗi - String.intern. Hãy nói về nó chi tiết hơn.

Phương thức String.intern

Hãy bắt đầu với thực tế là lớp này Stringhỗ trợ một nhóm chuỗi. Tất cả các chuỗi ký tự được xác định trong các lớp, và không chỉ chúng, đều được thêm vào nhóm này. Vì vậy, phương thức này interncho phép bạn lấy một chuỗi từ nhóm này bằng với chuỗi hiện có (chuỗi mà phương thức được gọi intern) theo quan điểm của equals. Nếu một hàng như vậy không tồn tại trong nhóm thì hàng hiện có sẽ được đặt ở đó và một liên kết tới nó sẽ được trả về. Do đó, ngay cả khi các tham chiếu đến hai chuỗi bằng nhau là khác nhau (như trong hai ví dụ trên), thì các lệnh gọi đến các chuỗi này internsẽ trả về một tham chiếu đến cùng một đối tượng:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1.intern()==str2.intern() ? "the same" : "not the same");
Kết quả của việc thực thi đoạn mã này sẽ là "giống nhau" . Tôi không thể nói chính xác tại sao nó lại được thực hiện theo cách này. Phương pháp này interncó nguồn gốc và thành thật mà nói, tôi không muốn đi sâu vào mã C. Nhiều khả năng điều này được thực hiện để tối ưu hóa mức tiêu thụ bộ nhớ và hiệu suất. Trong mọi trường hợp, bạn nên biết về tính năng triển khai này. Hãy chuyển sang phần tiếp theo.

So sánh nguyên thủy thực sự

Để bắt đầu, tôi muốn hỏi một câu hỏi. Rất đơn giản. Tổng sau - 0,3f + 0,4f là bao nhiêu? Tại sao? 0,7f? Hãy kiểm tra:
float f1 = 0.7f;
float f2 = 0.3f + 0.4f;
System.out.println("f1==f2: "+(f1==f2));
Kết quả là? Giống? Tôi cũng vậy. Đối với những người không hoàn thành phần này, tôi sẽ nói rằng kết quả sẽ là...
f1==f2: false
Tại sao điều này lại xảy ra?.. Hãy thực hiện một thử nghiệm khác:
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("f1="+(double)f1);
System.out.println("f2="+(double)f2);
System.out.println("f3="+(double)f3);
System.out.println("f4="+(double)f4);
Lưu ý việc chuyển đổi sang double. Điều này được thực hiện để xuất ra nhiều chữ số thập phân hơn. Kết quả:
f1=0.30000001192092896
f2=0.4000000059604645
f3=0.7000000476837158
f4=0.699999988079071
Nói đúng ra, kết quả có thể đoán trước được. Việc biểu diễn phần phân số được thực hiện bằng chuỗi hữu hạn 2-n, và do đó không cần phải nói về cách biểu diễn chính xác của một số được chọn tùy ý. Như có thể thấy từ ví dụ, độ chính xác biểu diễn floatlà 7 chữ số thập phân. Nói đúng ra, cách biểu diễn float phân bổ 24 bit cho phần định trị. Do đó, số tuyệt đối tối thiểu có thể được biểu diễn bằng cách sử dụng float (không tính đến mức độ, vì chúng ta đang nói về độ chính xác) là 2-24≈6*10-8. Với bước này, các giá trị trong biểu diễn thực sự thay đổi float. Và vì có lượng tử hóa nên cũng có lỗi. Do đó kết luận: các số trong cách biểu diễn floatchỉ có thể được so sánh với một độ chính xác nhất định. Tôi khuyên bạn nên làm tròn chúng đến vị trí thập phân thứ 6 (10-6) hoặc tốt nhất là kiểm tra giá trị tuyệt đối của chênh lệch giữa chúng:
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("|f3-f4|<1e-6: "+( Math.abs(f3-f4) < 1e-6 ));
Trong trường hợp này, kết quả thật đáng khích lệ:
|f3-f4|<1e-6: true
Tất nhiên, hình ảnh giống hệt với loại double. Sự khác biệt duy nhất là 53 bit được phân bổ cho phần định trị, do đó độ chính xác biểu diễn là 2-53≈10-16. Đúng, giá trị lượng tử hóa nhỏ hơn nhiều, nhưng nó vẫn ở đó. Và nó có thể chơi một trò đùa độc ác. Nhân tiện, trong thư viện kiểm tra JUnit , trong các phương thức so sánh số thực, độ chính xác được chỉ định rõ ràng. Những thứ kia. phương pháp so sánh chứa ba tham số - số lượng, giá trị của nó và độ chính xác của so sánh. Nhân tiện, tôi muốn đề cập đến sự tinh tế liên quan đến việc viết số theo dạng khoa học, biểu thị mức độ. Câu hỏi. Làm thế nào để viết 10-6? Thực tế cho thấy hơn 80% đáp án – 10e-6. Trong khi đó, câu trả lời đúng là 1e-6! Và 10e-6 là 10-5! Chúng tôi đã dẫm phải chiếc cào này ở một trong những dự án, khá bất ngờ. Họ đã tìm kiếm lỗi trong một thời gian rất dài, xem xét các hằng số 20 lần. Và không ai mảy may nghi ngờ về tính đúng đắn của chúng, cho đến một ngày, phần lớn là do tình cờ, hằng số 10e-3 được in ra và họ tìm thấy hai chữ số sau dấu thập phân thay vì ba chữ số như mong đợi. Vì vậy, hãy cẩn thận! Tiếp tục nào.

+0,0 và -0,0

Trong biểu diễn số thực, bit quan trọng nhất được ký. Điều gì xảy ra nếu tất cả các bit khác bằng 0? Không giống như số nguyên, trong trường hợp như vậy, kết quả là số âm nằm ở giới hạn dưới của phạm vi biểu diễn, số thực chỉ có bit quan trọng nhất được đặt thành 1 cũng có nghĩa là 0, chỉ có dấu trừ. Do đó, chúng ta có hai số 0 - +0,0 và -0,0. Một câu hỏi hợp lý được đặt ra: những con số này có nên được coi là bằng nhau không? Máy ảo nghĩ chính xác theo cách này. Tuy nhiên, đây là hai số khác nhau , vì kết quả của các phép toán với chúng sẽ thu được các giá trị khác nhau:
float f1 = 0.0f/1.0f;
float f2 = 0.0f/-1.0f;
System.out.println("f1="+f1);
System.out.println("f2="+f2);
System.out.println("f1==f2: "+(f1==f2));
float f3 = 1.0f / f1;
float f4 = 1.0f / f2;
System.out.println("f3="+f3);
System.out.println("f4="+f4);
... và kết quả:
f1=0.0
f2=-0.0
f1==f2: true
f3=Infinity
f4=-Infinity
Vì vậy, trong một số trường hợp, việc coi +0,0 và -0,0 là hai số khác nhau là điều hợp lý. Và nếu chúng ta có hai đối tượng, trong đó một đối tượng là +0,0 và đối tượng kia là -0,0, thì những đối tượng này cũng có thể được coi là không bằng nhau. Câu hỏi đặt ra - làm thế nào bạn có thể hiểu rằng các con số không bằng nhau nếu so sánh trực tiếp chúng với máy ảo cho true? Câu trả lời là thế này. Mặc dù máy ảo coi những con số này là bằng nhau nhưng cách biểu diễn của chúng vẫn khác nhau. Vì vậy, điều duy nhất có thể làm là so sánh các quan điểm. Và để có được nó, có các phương thức int Float.floatToIntBits(float)and long Double.doubleToLongBits(double), trả về một biểu diễn bit ở dạng intand longtương ứng (tiếp tục ví dụ trước):
int i1 = Float.floatToIntBits(f1);
int i2 = Float.floatToIntBits(f2);
System.out.println("i1 (+0.0):"+ Integer.toBinaryString(i1));
System.out.println("i2 (-0.0):"+ Integer.toBinaryString(i2));
System.out.println("i1==i2: "+(i1 == i2));
Kết quả sẽ là
i1 (+0.0):0
i2 (-0.0):10000000000000000000000000000000
i1==i2: false
Vì vậy, nếu bạn có +0,0 và -0,0 là các số khác nhau thì bạn nên so sánh các biến thực thông qua biểu diễn bit của chúng. Có vẻ như chúng tôi đã sắp xếp được +0,0 và -0,0. Tuy nhiên, -0,0 không phải là điều ngạc nhiên duy nhất. Ngoài ra còn có chuyện như...

Giá trị NaN

NaNlà viết tắt của Not-a-Number. Giá trị này xuất hiện do các phép toán không chính xác, chẳng hạn như chia 0,0 cho 0,0, chia vô cực cho vô cực, v.v. Điểm đặc biệt của giá trị này là nó không bằng chính nó. Những thứ kia.:
float x = 0.0f/0.0f;
System.out.println("x="+x);
System.out.println("x==x: "+(x==x));
...sẽ có kết quả...
x=NaN
x==x: false
Làm thế nào điều này có thể xảy ra khi so sánh các đối tượng? Nếu trường của đối tượng bằng NaN, thì so sánh sẽ cho false, tức là. các đối tượng được đảm bảo được coi là không bằng nhau. Mặc dù, về mặt logic, chúng ta có thể muốn điều ngược lại. Bạn có thể đạt được kết quả mong muốn bằng phương pháp Float.isNaN(float). Nó trả về truenếu đối số là NaN. Trong trường hợp này, tôi sẽ không dựa vào việc so sánh các biểu diễn bit, bởi vì nó không được tiêu chuẩn hóa. Có lẽ thế là đủ về người nguyên thủy. Bây giờ chúng ta hãy chuyển sang những điểm tinh tế đã xuất hiện trong Java kể từ phiên bản 5.0. Và điểm đầu tiên tôi muốn đề cập đến là

Java 5.0. Phương thức tạo và so sánh thông qua ' =='

Có một mẫu thiết kế được gọi là phương pháp sản xuất. Đôi khi việc sử dụng nó mang lại nhiều lợi nhuận hơn so với việc sử dụng hàm tạo. Tôi sẽ cho bạn một ví dụ. Tôi nghĩ rằng tôi biết rõ đối tượng shell Boolean. Lớp này là bất biến và chỉ có thể chứa hai giá trị. Trên thực tế, đối với bất kỳ nhu cầu nào, chỉ cần hai bản là đủ. Và nếu bạn tạo chúng trước và sau đó chỉ cần trả lại chúng, nó sẽ nhanh hơn nhiều so với việc sử dụng hàm tạo. Có một phương pháp như vậy Boolean: valueOf(boolean). Nó xuất hiện trong phiên bản 1.4. Các phương pháp sản xuất tương tự đã được giới thiệu trong phiên bản 5.0 trong các lớp Byte, Character, Shortvà . Khi các lớp này được tải, các mảng thể hiện của chúng được tạo tương ứng với các phạm vi giá trị nguyên thủy nhất định. Các phạm vi này như sau: IntegerLong
So sánh đồ vật: luyện tập - 2
Điều này có nghĩa là khi sử dụng phương thức, valueOf(...)nếu đối số nằm trong phạm vi đã chỉ định thì đối tượng tương tự sẽ luôn được trả về. Có lẽ điều này giúp tăng tốc độ. Nhưng đồng thời, các vấn đề nảy sinh có tính chất đến mức có thể khá khó khăn để đi đến tận cùng của nó. Đọc thêm về nó. Về lý thuyết, phương thức sản xuất valueOfđã được thêm vào cả lớp FloatDouble. Mô tả của họ nói rằng nếu bạn không cần một bản sao mới thì tốt hơn nên sử dụng phương pháp này, bởi vì nó có thể làm tăng tốc độ, v.v. và như thế. Tuy nhiên, trong quá trình triển khai (Java 5.0) hiện tại, một phiên bản mới được tạo trong phương thức này, tức là. Việc sử dụng nó không được đảm bảo để tăng tốc độ. Hơn nữa, tôi khó có thể tưởng tượng làm thế nào phương pháp này có thể được tăng tốc, bởi vì tính liên tục của các giá trị, bộ đệm không thể được tổ chức ở đó. Ngoại trừ số nguyên. Ý tôi là, không có phần phân số.

Java 5.0. Tự động đóng hộp/Mở hộp: ' ==', ' >=' và ' <=' cho các trình bao bọc đối tượng.

Tôi nghi ngờ rằng các phương thức sản xuất và bộ nhớ đệm cá thể đã được thêm vào các trình bao bọc cho các số nguyên gốc để tối ưu hóa hoạt động autoboxing/unboxing. Hãy để tôi nhắc bạn nó là gì. Nếu một đối tượng phải tham gia vào một thao tác, nhưng có liên quan đến một đối tượng nguyên thủy, thì đối tượng nguyên thủy này sẽ tự động được gói trong một trình bao bọc đối tượng. Cái này autoboxing. Và ngược lại - nếu một nguyên thủy phải tham gia vào hoạt động, thì bạn có thể thay thế một shell đối tượng ở đó và giá trị sẽ tự động được mở rộng từ nó. Cái này unboxing. Đương nhiên, bạn phải trả tiền cho sự tiện lợi đó. Hoạt động chuyển đổi tự động làm chậm ứng dụng một chút. Tuy nhiên, điều này không liên quan đến chủ đề hiện tại, vì vậy hãy để lại câu hỏi này. Mọi thứ đều ổn miễn là chúng ta đang xử lý các hoạt động có liên quan rõ ràng đến nguyên thủy hoặc shell. Điều gì sẽ xảy ra với ==thao tác ''? Giả sử chúng ta có hai đối tượng Integercó cùng giá trị bên trong. Họ sẽ so sánh như thế nào?
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1==i2: "+(i1==i2));
Kết quả:
i1==i2: false

Кто бы сомневался... Сравниваются они How an objectы. А если так:Integer i1 = 1;
Integer i2 = 1;
System.out.println("i1==i2: "+(i1==i2));
Kết quả:
i1==i2: true
Bây giờ điều này thú vị hơn! Nếu autoboxing-e các đối tượng tương tự được trả về! Đây là nơi cái bẫy nằm. Khi chúng tôi phát hiện ra rằng các đối tượng tương tự được trả về, chúng tôi sẽ bắt đầu thử nghiệm xem liệu điều này có luôn đúng hay không. Và chúng ta sẽ kiểm tra bao nhiêu giá trị? Một? Mười? Một trăm? Rất có thể chúng ta sẽ giới hạn bản thân ở mức một trăm theo mỗi hướng xung quanh số 0. Và chúng ta có được sự bình đẳng ở mọi nơi. Có vẻ như mọi thứ đều ổn. Tuy nhiên, hãy nhìn lại một chút, ở đây . Bạn đã đoán được nội dung bắt là gì chưa?.. Có, các phiên bản của shell đối tượng trong quá trình tự động đóng hộp được tạo bằng các phương thức sản xuất. Điều này được minh họa rõ ràng bằng thử nghiệm sau:
public class AutoboxingTest {

    private static final int numbers[] = new int[]{-129,-128,127,128};

    public static void main(String[] args) {
        for (int number : numbers) {
            Integer i1 = number;
            Integer i2 = number;
            System.out.println("number=" + number + ": " + (i1 == i2));
        }
    }
}
Kết quả sẽ như thế này:
number=-129: false
number=-128: true
number=127: true
number=128: false
Đối với các giá trị nằm trong phạm vi bộ nhớ đệm , các đối tượng giống hệt nhau sẽ được trả về, đối với những giá trị nằm ngoài phạm vi bộ nhớ đệm, các đối tượng khác nhau sẽ được trả về. Và do đó, nếu ở đâu đó trong các shell ứng dụng được so sánh thay vì các shell nguyên thủy, thì có khả năng xảy ra lỗi khủng khiếp nhất: lỗi float. Bởi vì rất có thể mã cũng sẽ được kiểm tra trên một phạm vi giá trị giới hạn mà lỗi này sẽ không xuất hiện. Nhưng trong công việc thực tế, nó sẽ xuất hiện hoặc biến mất, tùy thuộc vào kết quả của một số phép tính. Dễ phát điên hơn là tìm ra một sai lầm như vậy. Vì vậy, tôi khuyên bạn nên tránh autoboxing bất cứ khi nào có thể. Và đó không phải là nó. Hãy nhớ môn toán, không quá lớp 5. Giả sử các bất đẳng thức A>=BА<=B. Có thể nói gì về mối quan hệ AB? Chỉ có một điều - chúng bằng nhau. Bạn có đồng ý không? Tôi nghĩ là có. Hãy chạy thử nghiệm:
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1>=i2: "+(i1>=i2));
System.out.println("i1<=i2: "+(i1<=i2));
System.out.println("i1==i2: "+(i1==i2));
Kết quả:
i1>=i2: true
i1<=i2: true
i1==i2: false
Và đây là điều kỳ lạ lớn nhất đối với tôi. Tôi hoàn toàn không hiểu tại sao tính năng này lại được đưa vào ngôn ngữ nếu nó gây ra những mâu thuẫn như vậy. Nói chung, tôi sẽ nhắc lại một lần nữa - nếu có thể làm được mà không cần autoboxing/unboxing, thì bạn nên tận dụng tối đa cơ hội này. Chủ đề cuối cùng tôi muốn đề cập đến là... Java 5.0. so sánh các phần tử liệt kê (kiểu enum) Như bạn đã biết, kể từ phiên bản 5.0 Java đã giới thiệu một kiểu như enum - enumeration. Các phiên bản của nó theo mặc định chứa tên và số thứ tự trong phần khai báo phiên bản trong lớp. Theo đó, khi thứ tự thông báo thay đổi thì các con số cũng thay đổi. Tuy nhiên, như tôi đã nói trong bài viết “Serialization as it is” , điều này không gây ra vấn đề gì. Tất cả các phần tử liệt kê tồn tại trong một bản sao duy nhất, điều này được kiểm soát ở cấp độ máy ảo. Vì vậy, chúng có thể được so sánh trực tiếp bằng cách sử dụng các liên kết. * * * Có lẽ hôm nay đó là tất cả về khía cạnh thực tế của việc thực hiện so sánh đối tượng. Có lẽ tôi đã bỏ lỡ điều gì đó. Như mọi khi, tôi rất mong nhận được ý kiến ​​của bạn! Bây giờ, hãy để tôi nghỉ phép. Cảm ơn tất cả các bạn đã quan tâm! Liên kết đến nguồn: So sánh các đối tượng: thực hành
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION