這是專門討論對像比較的第二篇文章。他們中的第一個討論了比較的理論基礎——它是如何進行的、為什麼以及在哪裡使用它。在這篇文章中,我們將直接討論比較數字、物件、特殊情況、微妙之處和非顯而易見的點。更準確地說,我們將討論以下內容:
字串比較: '
啊,這些行...最常用的類型之一,這會導致很多問題。原則上,有一篇關於它們的單獨文章。在這裡我要談談比較問題。當然,可以使用 來比較字串Java 5.0。
設計中有一種模式叫做生產方法。有時它的使用比使用構造函數更有利可圖。讓我舉一個例子。我想我很了解對像外殼
這表示使用該方法時,Java 5.0。自動裝箱/拆箱:「
我懷疑生產方法和實例快取被添加到整數基元的包裝器中以優化操作
- 字串比較: '
==
' 和equals
- 方法
String.intern
- 真實原語的比較
+0.0
和-0.0
- 意義
NaN
- Java 5.0。
==
透過「 」生成方法和比較 - Java 5.0。自動裝箱/拆箱:「
==
」、「>=
」和「<=
」用於物件包裝器。 - Java 5.0。枚舉元素的比較(型別
enum
)
字串比較: ' ==
' 和equals
啊,這些行...最常用的類型之一,這會導致很多問題。原則上,有一篇關於它們的單獨文章。在這裡我要談談比較問題。當然,可以使用 來比較字串equals
。此外,它們必須透過 進行比較equals
。然而,有一些微妙之處值得了解。首先,相同的字串實際上是一個物件。透過運行以下程式碼可以輕鬆驗證這一點:
String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
結果將是“相同”。這意味著字串引用是相等的。這是在編譯器層級完成的,顯然是為了節省記憶體。編譯器建立該字串的一個實例,並指派對此實例的參考str1
。str2
但是,這僅適用於在程式碼中聲明為文字的字串。如果您將各個片段組成一個字串,則指向它的連結將會不同。確認 - 此範例:
String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
結果將是“不一樣”。您也可以使用複製建構函式建立一個新物件:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1==str2 ? "the same" : "not the same");
結果也會是「不一樣」。 因此,有時可以透過引用比較來比較字串。但最好不要依賴於此。我想談談一種非常有趣的方法,它允許您獲得所謂的字串的規範表示 - String.intern
。讓我們更詳細地討論一下。
String.intern方法
讓我們從該類別String
支援字串池這一事實開始。類別中定義的所有字串文字(而不僅僅是它們)都會添加到此池中。因此,該方法允許您從該池中獲取一個字串,從 的角度來看,intern
該字串等於現有字串(呼叫該方法的字串) 。如果池中不存在這樣的行,則將現有行放置在那裡並傳回指向該行的連結。因此,即使對兩個相等字串的引用不同(如上面的兩個範例),對這些字串的呼叫也會傳回對相同物件的引用: intern
equals
intern
String str1 = "string";
String str2 = new String("string");
System.out.println(str1.intern()==str2.intern() ? "the same" : "not the same");
執行這段程式碼的結果將是「相同」。 我無法確切地說出為什麼要這樣做。方法intern
是本機的,說實話,我不想陷入 C 程式碼的荒野。這樣做很可能是為了優化記憶體消耗和效能。無論如何,了解這個實作特性是值得的。讓我們繼續下一部分。
真實原語的比較
首先,我想問一個問題。很簡單。以下總和是多少 – 0.3f + 0.4f?為什麼?0.7f?讓我們檢查:float f1 = 0.7f;
float f2 = 0.3f + 0.4f;
System.out.println("f1==f2: "+(f1==f2));
因此?喜歡?我也是。對於那些沒有完成這個片段的人,我會說結果將是...
f1==f2: false
為什麼會發生這種情況?...讓我們進行另一個測試:
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);
請注意轉換為double
. 這樣做是為了輸出更多的小數位。結果:
f1=0.30000001192092896
f2=0.4000000059604645
f3=0.7000000476837158
f4=0.699999988079071
嚴格來說,結果是可以預見的。小數部分的表示是使用有限級數2-n來進行的,因此無需討論任意選擇的數字的精確表示。從例子可以看出,表示精度float
為小數點後7位。 嚴格來說,該表示法float
為尾數分配了 24 位元。因此,可以使用 float
(不考慮程度,因為我們談論的是精度)表示的最小絕對數是2-24≈6*10-8。正是透過這一步,表示中的值才真正走向 float
。既然有量化,那就有誤差。 因此得出結論:表示中的數字float
只能以一定的精度進行比較。我建議將它們四捨五入到小數點後第六位 (10-6),或者最好檢查它們之間差異的 絕對值:
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 ));
在這種情況下,結果令人鼓舞:
|f3-f4|<1e-6: true
當然,圖片和文字是一模一樣的double
。唯一的差異是尾數分配了 53 位,因此表示精度為 2-53≈10-16。是的,量化值要小得多,但它是存在的。它可以開一個殘酷的玩笑。順便說一句,在JUnit測試庫中,在比較實數的方法中,明確指定了精確度。那些。比較方法包含三個參數 - 數量、應該等於什麼以及比較的準確性。 順便說一句,我想提一下以科學格式書寫數字(表示程度)相關的微妙之處。問題。10-6怎麼寫?實踐表明,80%以上的答案是——10e-6。同時,正確答案是1e-6!10e-6 就是 10-5!我們在其中一個專案中踩到了這把耙子,出乎意料。他們花了很長時間尋找錯誤,查看了常數20 次。沒有人對它們的正確性有一絲懷疑,直到有一天,很大程度上是偶然的,常數10e-3 被打印出來,他們發現了兩個小數點後的數字而不是預期的三位。因此,要小心! 讓我們繼續。
+0.0 和 -0.0
在實數的表示中,最高有效位是有符號的。如果所有其他位元均為 0,會發生什麼情況?與整數不同,在這種情況下,結果是位於表示範圍下限的負數,只有最高有效位元設定為 1 的實數也意味著 0,只是帶有一個負號。因此,我們有兩個零 - +0.0 和 -0.0。一個邏輯問題出現了:這些數字應該被認為是相等的嗎?虛擬機器正是這樣思考的。然而,這是兩個不同的數字,因為與它們運算的結果會得到不同的值: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);
……結果:
f1=0.0
f2=-0.0
f1==f2: true
f3=Infinity
f4=-Infinity
因此,在某些情況下,將 +0.0 和 -0.0 視為兩個不同的數字是有意義的。如果我們有兩個對象,其中一個物件的欄位為+0.0,另一個物件為-0.0,那麼這些物件也可以被視為不相等。問題出現了 - 如果直接與虛擬機器進行比較,您如何理解這些數字是不平等的true
?答案是這樣的。儘管虛擬機器認為這些數字是相等的,但它們的表示形式仍然不同。因此,唯一能做的就是比較觀點。為了獲得它,有方法int Float.floatToIntBits(float)
and ,它們分別以和 的long Double.doubleToLongBits(double)
形式傳回一個位元表示(上一個範例的延續): int
long
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));
結果將是
i1 (+0.0):0
i2 (-0.0):10000000000000000000000000000000
i1==i2: false
因此,如果 +0.0 和 -0.0 是不同的數字,那麼您應該透過實際變數的位表示來比較它們。我們似乎已經整理了+0.0和-0.0。然而,-0.0 並不是唯一的驚喜。還有這樣的事,比如...
南值
NaN
代表Not-a-Number
。該值是由於不正確的數學運算而出現的,例如將 0.0 除以 0.0、用無窮大除以無限大等。該值的特殊之處在於它不等於其本身。那些。:
float x = 0.0f/0.0f;
System.out.println("x="+x);
System.out.println("x==x: "+(x==x));
...將導致...
x=NaN
x==x: false
當比較物件時,結果會如何呢?如果物件的欄位等於NaN
,則比較將給出false
,即 保證對像被認為是不平等的。儘管從邏輯上講,我們可能想要相反的結果。使用此方法即可達到所需的結果Float.isNaN(float)
。true
如果參數是 則回傳NaN
。在這種情況下,我不會依賴比較位表示,因為 它沒有標準化。也許關於原語就夠了。現在讓我們繼續討論自 5.0 版以來 Java 中出現的微妙之處。我想談的第一點是
Java 5.0。==
透過「 」生成方法和比較
設計中有一種模式叫做生產方法。有時它的使用比使用構造函數更有利可圖。讓我舉一個例子。我想我很了解對像外殼Boolean
。此類別是不可變的,並且只能包含兩個值。也就是說,事實上,對於任何需求,只需要兩份就足夠了。如果你提前創建它們然後簡單地返回它們,它會比使用構造函數快得多。有這樣一個方法Boolean
:valueOf(boolean)
。它出現在1.4版本。Byte
類似的生成方法在 5.0 版本中在、Character
、Short
和Integer
類別中引入Long
。當載入這些類別時,將建立與某些範圍的原始值相對應的實例陣列。這些範圍如下:
valueOf(...)
如果參數落在指定範圍內,則始終傳回相同的物件。也許這會提高速度。但同時,問題的本質也導致了很難追根究底。閱讀更多相關內容。 理論上,生成方法valueOf
已添加到Float
和類別中Double
。他們的描述說,如果你不需要新的副本,那麼最好使用這種方法,因為 它可以提高速度等。等等。然而,在目前(Java 5.0)實作中,在此方法中建立了一個新實例,即 它的使用不能保證提高速度。而且,我很難想像這個方法如何能夠加速,因為由於值的連續性,那裡無法組織快取。整數除外。我的意思是,沒有小數部分。
Java 5.0。自動裝箱/拆箱:「==
」、「>=
」和「<=
」用於物件包裝器。
我懷疑生產方法和實例快取被添加到整數基元的包裝器中以優化操作autoboxing/unboxing
。讓我提醒你它是什麼。如果一個操作必須涉及一個對象,但涉及一個原語,那麼這個原語會自動包裝在對象包裝器中。這autoboxing
。反之亦然 - 如果操作必須涉及原語,那麼您可以在那裡替換物件外殼,並且值將自動從中擴展。這unboxing
。 當然,你必須為這種便利付費。自動轉換操作會在一定程度上降低應用程式的速度。不過,這與目前的主題無關,所以我們先離開這個問題。 只要我們處理的是與原語或 shell 明顯相關的操作,一切都很好。「 」操作會發生什麼==
?假設我們有兩個Integer
內部具有相同值的物件。他們將如何比較?
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1==i2: "+(i1==i2));
結果:
結果:i1==i2: false Кто бы сомневался... Сравниваются они How an objectы. А если так:
Integer i1 = 1; Integer i2 = 1; System.out.println("i1==i2: "+(i1==i2));
i1==i2: true
現在這更有趣了!如果autoboxing
-e 返回相同的物件!這就是陷阱所在。一旦我們發現返回相同的對象,我們將開始試驗,看看情況是否總是如此。我們將檢查多少個值?一?十?一百?我們很可能會將每個方向的數量限制在 0 左右。我們到處都平等。似乎一切都很好。然而,回頭看一下,這裡。您猜到問題是什麼了嗎?是的,自動裝箱期間物件外殼的實例是使用生成方法建立的。下面的測試很好地說明了這一點:
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));
}
}
}
結果會是這樣的:
number=-129: false
number=-128: true
number=127: true
number=128: false
對於快取範圍 內的值,傳回相同的對象,對於快取範圍之外的值,傳回不同的對象。因此,如果比較應用程式 shell 中的某個位置而不是基元,則有可能出現最可怕的錯誤:浮動錯誤。因為程式碼很可能也會在不會出現此錯誤的有限值範圍內進行測試。但在實際工作中,它要么出現,要么消失,取決於一些計算的結果。發瘋比發現這樣的錯誤更容易。因此,我建議您盡可能避免自動裝箱。事實並非如此。讓我們記住數學,不要超過五年級。設不等式A>=B
和А<=B
。A
關於和 的關係可以說些什麼B
?只有一件事——它們是平等的。你同意?我想是的。讓我們運行測試:
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));
結果:
i1>=i2: true
i1<=i2: true
i1==i2: false
這對我來說是最大的奇怪的事情。我完全不明白為什麼這個功能會引入到語言中,如果它引入了這樣的矛盾。總的來說,我會再次重複 - 如果可以不用autoboxing/unboxing
,那麼值得充分利用這個機會。我想談的最後一個主題是… Java 5.0。枚舉元素的比較(enum型) 大家知道,從5.0版本開始Java就引入了enum這樣的型別-枚舉。預設情況下,其實例在類別的實例聲明中包含名稱和序號。因此,當公告順序改變時,數字也會改變。然而,正如我在“序列化本身”一文中所說,這不會引起問題。所有枚舉元素都存在於單一副本中,這是在虛擬機器層級控制的。因此,可以使用連結直接比較它們。* * * 也許這就是今天關於實現物件比較的實際方面的全部內容。也許我錯過了什麼。一如既往,我期待您的評論!現在,讓我先告辭了。感謝大家的關注! 來源連結:比較對象:練習
GO TO FULL VERSION