您好,登錄后才能下訂單哦!
本篇內容主要講解“String對象不可變嗎”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“String對象不可變嗎”吧!
注:本文基于JDK8
String相信是Java中很基礎的一個類,也是很多初級面試中很喜歡問的,包括String在JVM中的池化(常量池)、String的intern方法、如何避免內存溢出(主要存在JDK1.7之前)等,而String是創建后就不可變的對象從最開始學習Java我們就會從各種書籍、教程、面試寶典上看到,然而事實真的如此嗎?你對這個不可變理解的正確嗎?
首先我們看以下代碼:
String text = "hello : "; text += "JoeKerouac"; System.out.println(text);
上述代碼運行結果相信大多數人都會知道,最終會打印出"hello : JoeKerouac",而這是我們最常用的“改變”字符串的方法,為什么改變加引號呢?是因為這個方法其實并沒有真正改變字符串本身,而是重新創建了一個新的String對 象,然后將text引用指向了這個新建的對象,原String對象"hello : "并沒有被改變,似乎String確實是無法改變的,然而真的就沒辦法改變嗎?首先我們來看String的class文件,可以看到用來存儲字符串的實際數據的char數組是final類型的,而該數組也并沒有相應的get方法,看起來是很完美,那如果用反射去更改呢?下面我們來上代碼:
import java.lang.reflect.Field; public class StringTest { public static void main(String[] args) { String text = "JoeKerouac"; change(text); System.out.println(text); } /** * 更改字符串值,將字符串的第一個字符更改為j * * @param txt * 要更改的字符串 */ static void change(String txt){ try { Field valueField = txt.getClass().getDeclaredField("value"); valueField.setAccessible(true); char[] value = (char[]) valueField.get(txt); value[0] = 'j'; } catch (IllegalAccessException e) { e.printStackTrace(); System.err.println("當前系統不允許反射調用"); } catch (NoSuchFieldException e) { } } }
上述代碼運行后你猜結果是什么?是"joeKerouac",是的,你沒猜錯,是"joeKerouac"而不是"JoeKerouac",下面我們再來看一個例子:
import java.lang.reflect.Field; public class StringTest { public static void main(String[] args) { String text = "JoeKerouac"; change(text); String textCopy = "JoeKerouac"; System.out.println(textCopy); } /** * 更改字符串值,將字符串的第一個字符更改為j * * @param txt * 要更改的字符串 */ static void change(String txt){ try { Field valueField = txt.getClass().getDeclaredField("value"); valueField.setAccessible(true); char[] value = (char[]) valueField.get(txt); value[0] = 'j'; } catch (IllegalAccessException e) { e.printStackTrace(); System.err.println("當前系統不允許反射調用"); } catch (NoSuchFieldException e) { } } }
上述代碼運行后結果是什么呢?機智點兒的小伙伴應該猜出來了,還是"joeKerouac",可能還有人會一臉懵逼,上個例子還能看的懂,是用反射將text對象更改了,所以結果是"joeKerouac",可是這個明明textCopy是在運行完change方法后才聲明出來的,怎么也被修改了呢?這是因為這兩個字符串的字面值一樣,所以實際上他們指向的String對象都是同一個,這是Java對String池化了(當然,這個不是本文重點),所以當text更改后textCopy實際也就被更改了 ,同時也說明textCopy的聲明雖然在change方法調用的后一行,但是他在change方法調用前就與text一樣通過某種手段鏈接到了同一個String對象上。
看到這里可能有的同學覺得這就結束了,String對象確實被我們用反射改變了,但是此時內存是怎么樣的呢?內存中的"JoeKerouac"這個最早聲明的字符串是被改變了還是只是跟文章開頭的代碼段一樣僅僅是重新創建了一個對象,然 后讓字符串的引用指向了這個新對象,原對象沒有更改呢?其實大概我們也是可以猜出來的,我們是用反射更改的String中的value數組,并沒有使用公共API去更改,從邏輯上來說JVM是無感知的,他不應該也沒有時機去創建一個新>的字符串然后返回,那么事實是否如此呢?下面我們來看下面這個例子:
import java.lang.reflect.Field; public class StringTest { public static void main(String[] args) throws InterruptedException { String text = "JoeKerouac"; System.out.println("now text is : " + text); //在此時做一次heap dump Thread.sleep(1000 * 15); change(text); System.out.println("now text is : " + text); //在此時重新heap dump一下 Thread.sleep(1000 * 60 * 60); } /** * 更改字符串值,將字符串的第一個字符更改為j * * @param txt * 要更改的字符串 */ static void change(String txt) { try { Field valueField = txt.getClass().getDeclaredField("value"); valueField.setAccessible(true); char[] value = (char[]) valueField.get(txt); value[0] = 'j'; } catch (IllegalAccessException e) { e.printStackTrace(); System.err.println("當前系統不允許反射調用"); } catch (NoSuchFieldException e) { } } }
在上述的兩個注釋處分別做heap dump操作,推薦使用JVisual VM,方便后續操作,heap dump完畢后使用JVisual VM對兩次dump出來的文件進行分析,可以使用JVisual VM中的OQL控制臺查詢,具體查詢語句如下:
select {instance: s, content: s.toString(), id: objectid(s)} from java.lang.String s where s.toString() == 'joeKerouac' || s.toString() == 'JoeKerouac'
最后結果應該是第一次dump文件會有一個JoeKerouac字符串,第二次dump文件會有一個joeKerouac字符串,而且兩個字符串的ID是一樣的,說明我們上面的猜測是正確的,JVM并沒有去創建一個新的字符串返回,而我們確實做到了更>改String的值,真正的更改內存值。
我們從第三段代碼可以看出使用該種方法可以做到更改用戶編碼時定義的字符串,假如在你的系統里有一個password字符串,而你恰巧在某個地方用這個方法將這個字符串更改了,別人要是不知道估計調好久都找不到哪兒的原因,明 明聲明的對著的,但是打印就是不對,哈哈~ 另外如果你要是某天遇到這個問題說不定就是某個調皮的小鬼給你這樣搞了一波~
最后,我們看到String并不是如傳說那樣一成不變的,依托于Java強大的反射系統,即使是String這樣的不可變對象也會改變~(PS:有些安全性要求高的系統會限制反射的使用)
到此,相信大家對“String對象不可變嗎”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。