2013-03-07 69 views
16

This Java tutorial 表示不可變對象在創建後無法更改其狀態。調用java.lang.String不可變是否正確?

java.lang.String有一個字段

/** Cache the hash code for the string */ 
private int hash; // Default to 0 

這是對hashCode()方法的第一次調用初始化,因此創建後它的變化:

String s = new String(new char[] {' '}); 
    Field hash = s.getClass().getDeclaredField("hash"); 
    hash.setAccessible(true); 
    System.out.println(hash.get(s)); 
    s.hashCode(); 
    System.out.println(hash.get(s)); 

輸出

0 
32 

是它正確呼叫String不可變?

+17

反射黑客不計算不可變性。 – Perception 2013-03-07 15:44:21

+0

http://stackoverflow.com/q/11146255/758280 – Jeffrey 2013-03-07 15:45:42

+0

正如@Perception所說,反射攻擊不應該算在內。將散列值緩存在專用字段中不會影響任何非私有方法或狀態。 – 2013-03-07 15:47:00

回答

8

術語「不可變」足夠模糊,不允許有精確的定義。

我建議您從Eric Lippert的博客閱讀Kinds of Immutability。雖然它在技術上是C#文章,但它與提出的問題非常相關。特別是:

觀察不變性:

假設你已經有了它具有每一次 你調用一個方法就可以了屬性的對象,看現場等,你得到的相同的 結果。從調用者的角度來看,這樣的對象將是不可變的 。然而,你可以想象,在幕後,對象 正在進行延遲初始化,記憶函數調用的結果,哈希表等等。對象的「膽量」可能是完全可變的。

這有什麼關係?真正深不可變的對象根本不會改變其內部狀態,因此本質上是線程安全的。一個 對象可能在幕後可能仍然需要具有複雜的線程代碼,以便保護其內部可變的 狀態免受應該在兩個線程上「在同一時間」在 處調用對象的損壞。

+0

我猜想,除非你是絕對的,否則「不變性」的問題取決於它被問到誰的(即哪個對象的)視角。 – ArtB 2013-03-11 17:04:00

0

它不能從外部修改,它是一個最終的類,所以它不能被子類化和可變。這些是不變性的兩個要求。反思被認爲是黑客,它不是一種正常的發展方式。

0

反思將允許你改變任何私人領域的內容。因此,調用Java中的任何對象都是不可變的是正確的嗎?

不變性指由應用程序啓動或可察覺的更改。

在字符串的情況下,特定實現選擇懶惰地計算哈希碼的事實對於應用程序是不可察覺的。我會更進一步,並且說一個內部變量被對象增加 - 但從不暴露並且從未以任何其他方式使用 - 在「不可變」對象中也是可以接受的。

+1

其實我用公開的方法來改變字符串的狀態 – 2013-03-07 15:47:49

+0

@EvgeniyDorofeev - 而不是在你展示的例子中。至少,不是* String *類的公共方法。如果有* String *類的公共方法允許您更改其狀態,那麼我會同意:它不是不可變的。但我不知道任何這樣的方法。 – parsifal 2013-03-07 15:49:57

+0

但我在示例中調用了公共方法hashCode()並更改了散列字段。 – 2013-03-07 16:00:02

0

只要不提供對其可變字段的訪問權限,一個類可以是不可變的,同時仍具有可變字段。

它是不可變的設計。如果您使用反射(獲取聲明的字段並重置其可訪問性),您將繞過其設計。

1

是的,將它們稱爲不可變是正確的。

儘管確實可以接觸和修改private ...和final ...一個類別的變量,但在String對象上執行操作是不必要的和令人難以置信的不明智事情。這通常是假設,沒有人會足夠瘋狂的做它。

從安全角度來看,修改String狀態所需的映射調用都會執行安全檢查。除非你錯過了你的沙盒,否則這些調用將被阻止用於不可信的代碼。所以你應該擔心這個問題,因爲不受信任的代碼可能會破壞沙箱安全。

還值得注意的是,JLS指出使用反射改變final可能會破壞事情(例如在多線程中)或者可能沒有任何效果。

+0

我使用公共方法hashCode()來更改散列字段。 Docs表示不可變對象在創建後不能處於狀態。散列字段是String實例狀態的一部分嗎? – 2013-03-07 16:03:49

+0

@EvgeniyDorofeev:除了一些例外(例如'delay'方法)之外,方法花費時間執行的事實不被認爲是其行爲的一部分。 「哈希字段爲零的字符串」對象與哈希字段不爲的「字符串」對象之間唯一可觀察到的差異將是執行其'hashCode()'方法所需的時間量。請注意,如果計算出的散列值在某種程度上取決於第一次調用'hashCode()'的時間,那麼*將表示可變狀態,但不會。 – supercat 2013-07-11 21:41:41

+0

@EvgeniyDorofeev - 在你談論的情況下('String'),答案取決於你的觀點。見Ani的答案。還要記住,Java教程不是規範。它是(更多)確定性文件中信息的簡化版本。如果你看看JLS,*不變性*不是核心屬性。相反,它是類API的設計和實現方式的一個屬性。 Java教程的主要目的是幫助初學者...不是一個明確的文本。 – 2013-07-11 22:46:44

3

創建後,String實例上的所有方法(使用相同參數調用)將始終提供相同的結果。你不能改變它的行爲(用任何公共方法),所以它總是代表同一個實體。它也是final,不能被分類,所以保證所有實例的行爲都是這樣的。

因此公開視圖該對象被認爲是不可變的。在這種情況下,內部狀態並不重要。

+0

啊,是的,但在這種情況下,他使用了迂迴的技巧來改變'hash'變量,'hash'變量可以通過'hashcode()'觀察到。所以通過你的定義,String將是可變的。 – 2013-03-07 16:00:15

+0

即使在我的定義中,我也沒有重複*反射不計數的第一條評論。 – gaborsch 2013-03-07 16:03:40

13

一個更好的定義是的對象不變化,但它不能觀察已被更改。它的行爲永遠不會改變:.substring(x,y)將始終返回相同的東西,該字符串同上equals和所有其他方法。

該變量是在您第一次呼叫.hashcode()時計算出來的,並且被緩存用於進一步調用。這在功能性編程語言中基本上被稱爲「memoization」。

反射並不是真正的「編程」工具,而是用於元編程(即編程生成程序的程序),因此它不會真正計數。這相當於使用內存調試器來更改常量的值。

0

是的,這是正確的。當你像你在你的例子中那樣修改了一個字符串的時候,一個新的字符串被創建,但是舊的字符串保持它的值。

1

從使用反射的開發者的角度來看,它是而不是正確調用String不可變。有實際的Java開發人員每天都在使用反射來編寫真實的軟件。將反射視爲「黑客」是荒謬的。 但是,從不使用反射的開發者的角度來看,調用String是不可變的是正確的。是否有效假定String是不可變的取決於上下文。

不變性是一個抽象概念,因此不能以絕對意義應用於任何具有物理形式的東西(請參閱ship of Theseus)。編程語言結構像對象,變量和方法在物理上存在於存儲介質中的位中。數據降級是所有存儲介質都會發生的物理過程,所以沒有數據可以說是真正不可變的。另外,在實踐中幾乎總是可以破壞旨在防止特定數據突變的編程語言特徵。相比之下,3號是3,一直是3,而且將永遠是3

至於應用於數據編程,不變性應被視爲一個有用的假設而非基本屬性。例如,如果假定String是不可變的,那麼可以緩存其哈希代碼以供重用,並避免以後再次重新計算哈希代碼的代價。幾乎所有非平凡的軟件都依賴於假設某些數據在某段時間內不會變異。軟件開發人員通常假定程序的code segment在執行時不會改變,除非他們正在編寫自修改代碼。瞭解什麼假設在特定情況下是有效的,這是軟件開發的一個重要方面。