本篇內(nèi)容主要講解“String對(duì)象不可變嗎”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“String對(duì)象不可變嗎”吧!
創(chuàng)新互聯(lián)專(zhuān)業(yè)為企業(yè)提供和平網(wǎng)站建設(shè)、和平做網(wǎng)站、和平網(wǎng)站設(shè)計(jì)、和平網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁(yè)設(shè)計(jì)與制作、和平企業(yè)網(wǎng)站模板建站服務(wù),十余年和平做網(wǎng)站經(jīng)驗(yàn),不只是建網(wǎng)站,更提供有價(jià)值的思路和整體網(wǎng)絡(luò)服務(wù)。
注:本文基于JDK8
String相信是Java中很基礎(chǔ)的一個(gè)類(lèi),也是很多初級(jí)面試中很喜歡問(wèn)的,包括String在JVM中的池化(常量池)、String的intern方法、如何避免內(nèi)存溢出(主要存在JDK1.7之前)等,而String是創(chuàng)建后就不可變的對(duì)象從最開(kāi)始學(xué)習(xí)Java我們就會(huì)從各種書(shū)籍、教程、面試寶典上看到,然而事實(shí)真的如此嗎?你對(duì)這個(gè)不可變理解的正確嗎?
首先我們看以下代碼:
String text = "hello : "; text += "JoeKerouac"; System.out.println(text);
上述代碼運(yùn)行結(jié)果相信大多數(shù)人都會(huì)知道,最終會(huì)打印出"hello : JoeKerouac",而這是我們最常用的“改變”字符串的方法,為什么改變加引號(hào)呢?是因?yàn)檫@個(gè)方法其實(shí)并沒(méi)有真正改變字符串本身,而是重新創(chuàng)建了一個(gè)新的String對(duì) 象,然后將text引用指向了這個(gè)新建的對(duì)象,原String對(duì)象"hello : "并沒(méi)有被改變,似乎String確實(shí)是無(wú)法改變的,然而真的就沒(méi)辦法改變嗎?首先我們來(lái)看String的class文件,可以看到用來(lái)存儲(chǔ)字符串的實(shí)際數(shù)據(jù)的char數(shù)組是final類(lèi)型的,而該數(shù)組也并沒(méi)有相應(yīng)的get方法,看起來(lái)是很完美,那如果用反射去更改呢?下面我們來(lái)上代碼:
import java.lang.reflect.Field; public class StringTest { public static void main(String[] args) { String text = "JoeKerouac"; change(text); System.out.println(text); } /** * 更改字符串值,將字符串的第一個(gè)字符更改為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("當(dāng)前系統(tǒng)不允許反射調(diào)用"); } catch (NoSuchFieldException e) { } } }
上述代碼運(yùn)行后你猜結(jié)果是什么?是"joeKerouac",是的,你沒(méi)猜錯(cuò),是"joeKerouac"而不是"JoeKerouac",下面我們?cè)賮?lái)看一個(gè)例子:
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); } /** * 更改字符串值,將字符串的第一個(gè)字符更改為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("當(dāng)前系統(tǒng)不允許反射調(diào)用"); } catch (NoSuchFieldException e) { } } }
上述代碼運(yùn)行后結(jié)果是什么呢?機(jī)智點(diǎn)兒的小伙伴應(yīng)該猜出來(lái)了,還是"joeKerouac",可能還有人會(huì)一臉懵逼,上個(gè)例子還能看的懂,是用反射將text對(duì)象更改了,所以結(jié)果是"joeKerouac",可是這個(gè)明明textCopy是在運(yùn)行完change方法后才聲明出來(lái)的,怎么也被修改了呢?這是因?yàn)檫@兩個(gè)字符串的字面值一樣,所以實(shí)際上他們指向的String對(duì)象都是同一個(gè),這是Java對(duì)String池化了(當(dāng)然,這個(gè)不是本文重點(diǎn)),所以當(dāng)text更改后textCopy實(shí)際也就被更改了 ,同時(shí)也說(shuō)明textCopy的聲明雖然在change方法調(diào)用的后一行,但是他在change方法調(diào)用前就與text一樣通過(guò)某種手段鏈接到了同一個(gè)String對(duì)象上。
看到這里可能有的同學(xué)覺(jué)得這就結(jié)束了,String對(duì)象確實(shí)被我們用反射改變了,但是此時(shí)內(nèi)存是怎么樣的呢??jī)?nèi)存中的"JoeKerouac"這個(gè)最早聲明的字符串是被改變了還是只是跟文章開(kāi)頭的代碼段一樣僅僅是重新創(chuàng)建了一個(gè)對(duì)象,然 后讓字符串的引用指向了這個(gè)新對(duì)象,原對(duì)象沒(méi)有更改呢?其實(shí)大概我們也是可以猜出來(lái)的,我們是用反射更改的String中的value數(shù)組,并沒(méi)有使用公共API去更改,從邏輯上來(lái)說(shuō)JVM是無(wú)感知的,他不應(yīng)該也沒(méi)有時(shí)機(jī)去創(chuàng)建一個(gè)新>的字符串然后返回,那么事實(shí)是否如此呢?下面我們來(lái)看下面這個(gè)例子:
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); //在此時(shí)做一次heap dump Thread.sleep(1000 * 15); change(text); System.out.println("now text is : " + text); //在此時(shí)重新heap dump一下 Thread.sleep(1000 * 60 * 60); } /** * 更改字符串值,將字符串的第一個(gè)字符更改為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("當(dāng)前系統(tǒng)不允許反射調(diào)用"); } catch (NoSuchFieldException e) { } } }
在上述的兩個(gè)注釋處分別做heap dump操作,推薦使用JVisual VM,方便后續(xù)操作,heap dump完畢后使用JVisual VM對(duì)兩次dump出來(lái)的文件進(jìn)行分析,可以使用JVisual VM中的OQL控制臺(tái)查詢(xún),具體查詢(xún)語(yǔ)句如下:
select {instance: s, content: s.toString(), id: objectid(s)} from java.lang.String s where s.toString() == 'joeKerouac' || s.toString() == 'JoeKerouac'
最后結(jié)果應(yīng)該是第一次dump文件會(huì)有一個(gè)JoeKerouac字符串,第二次dump文件會(huì)有一個(gè)joeKerouac字符串,而且兩個(gè)字符串的ID是一樣的,說(shuō)明我們上面的猜測(cè)是正確的,JVM并沒(méi)有去創(chuàng)建一個(gè)新的字符串返回,而我們確實(shí)做到了更>改String的值,真正的更改內(nèi)存值。
我們從第三段代碼可以看出使用該種方法可以做到更改用戶(hù)編碼時(shí)定義的字符串,假如在你的系統(tǒng)里有一個(gè)password字符串,而你恰巧在某個(gè)地方用這個(gè)方法將這個(gè)字符串更改了,別人要是不知道估計(jì)調(diào)好久都找不到哪兒的原因,明 明聲明的對(duì)著的,但是打印就是不對(duì),哈哈~ 另外如果你要是某天遇到這個(gè)問(wèn)題說(shuō)不定就是某個(gè)調(diào)皮的小鬼給你這樣搞了一波~
最后,我們看到String并不是如傳說(shuō)那樣一成不變的,依托于Java強(qiáng)大的反射系統(tǒng),即使是String這樣的不可變對(duì)象也會(huì)改變~(PS:有些安全性要求高的系統(tǒng)會(huì)限制反射的使用)
到此,相信大家對(duì)“String對(duì)象不可變嗎”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢(xún),關(guān)注我們,繼續(xù)學(xué)習(xí)!