前文介绍了 运行时常量池,它和本篇文章要介绍的「字符串常量池」是包含关系
在 JDK7 及以后,字符串常量池被挪到了 Java 堆中,但这并不影响它的作用,只是位置发生了变化
通过名字,第一印象很容易看出来,「字符串常量池」就是专门用来存放字符串的常量池,所以先来介绍一下String类
注意:下面出现的代码大多都是从源码中摘出来的!有兴趣的可以结合String源码一起观看!!
首先需要明确的是String是一个final类:
xxxxxxxxxx// String 类不能被继承public final class String implements java.io.Serializable, Comparable<String>, CharSequence { // ...}其次需要知道的是String中存储数据的结构是一个char[]数组:
xxxxxxxxxx// private + final 让 String 对象不可变,这是实现字符串常量池的基础!!private final char value[];扩展:在 JDK9 之后,String的底层数据结构发生了变化,如下所示:
xxxxxxxxxxprivate final byte[] value;Motivation: The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.
解释:使用char[]数组存储数据的话,每个字符占用 2 个字节 (16 bits);然而绝大多数字符串对象中仅仅只包含 Latin-1 字符,存储这些字符花 1 个字节即可。所以如果都用 2 个字节去存储,将会浪费大量的空间
问题:如何存储必须需要 2 个字节的字符呢??
Description: We propose to change the internal representation of the String class from a UTF-16 char array to a byte array plus an encoding-flag field.
解释:String中有一个编码标志域,来标识当前存储数据的编码,如下所示:
xxxxxxxxxx// 编码标志域private final byte coder;// 是否采用压缩存储static final boolean COMPACT_STRINGS;static { // 默认使用 COMPACT_STRINGS = true;}// 返回当前编码标志域byte coder() { return COMPACT_STRINGS ? coder : UTF16;}// 当求字符串长度时,需要根据 coderpublic int length() { return value.length >> coder();}注意:String-related classes such as AbstractStringBuilder, StringBuilder, and StringBuffer will be updated to use the same representation, as will the HotSpot VM's intrinsic string operations.
解释:与字符串相关的类,如AbstractStringBuilder、StringBuilder和StringBuffer将被更新以使用相同的表示方法,HotSpot 虚拟机的内在字符串操作也是如此
最后介绍四种创建字符串对象的方法:
xxxxxxxxxxString s1 = "abc"; // 方法 1String s2 = String.valueOf("abc"); // 方法 2String s3 = new String("abc"); // 方法 3char[] chars = {'a', 'b', 'c'};String s4 = new String(chars); // 方法 4字符串常量池底层是一个固定大小的HashTable,如果长度比较小,当放进去的字符串对象特别多,就会造成严重的 Hash 冲突从而导致链表很长,更近一步导致调用String::intern方法时效率低
可以使用参数-XX:StringTableSize=size设置 StringTable 的大小
在 JDK7 及之后,「字符串常量池」被移到了堆中,而对象也是在堆中,此时「字符串常量池」中存储的只是实例引用!!下面的讨论均基于 JDK7 及之后,如果有例外,会另外说明
关于对象引用直接用= =比较的详细分析可见 「equals」「hashCode」
接着上一部分,如果s1直接和s2, s3, s4用= =做比较,结果会如何?具体代码如下:
xxxxxxxxxx// 省略前面代码 ...System.out.println(s1 == s2); // trueSystem.out.println(s1 == s3); // falseSystem.out.println(s1 == s4); // false首先通过 JProfiler 软件,查看一下这些对象在堆中的结构,如下图所示:
上图对应的可视化解释如下图所示:
对于String s1 = "abc"来说,它就是一个字符串对象,有一个字符数组类型的字段value,存储实际的数据。所有双引号"xxx"中的内容都在字符串常量池中!!所以"abc"在字符串常量池中,更准确一点,是地址为0xb5d的字符串对象在字符串常量池中
对于String s2 = String.valueOf("abc")来说,先看一下valueOf()方法,如下所示:
xxxxxxxxxxpublic static String valueOf(Object obj) { // toString() 见下方 return (obj == null) ? "null" : obj.toString();}public String toString() { return this;}可以看到,如果传入的参数是一个字符串对象,该方法就是把传入的对象返回了,所以方法 2 和方法 1 是等价的!
对于String s3 = new String("abc")来说,先看一下对应的构造函数,如下所示:
xxxxxxxxxxpublic String(String original) { this.value = original.value; this.hash = original.hash;}可以看到,将传入参数的value和hash字段赋值给自身的value和hash字段。举个很形象的例子,如果传入的对象是羊,那么new出来的新对象就是披着狼皮的羊
对于String s4 = new String(chars)来说,同样的,先看一下对应的构造函数,如下所示:
xxxxxxxxxxpublic String(char value[]) { this.value = Arrays.copyOf(value, value.length);}上面说过,String对象存储数据的结构是一个char[]数组,所以这个构造函数相当于把传入的数组 copy 了一份,然后赋值给自身的value字段
综上所述:如果只是给value和hash字段赋了相同的值,但对象还是不同的;用= =直接比较,比较的是对象的地址是否相同!!
扩展:继续看一下用equals()方法比较的原理,源码如下所示:
xxxxxxxxxxpublic boolean equals(Object anObject) { // 现在直接比较两个对象地址。如果地址相同,肯定相等 if (this == anObject) { return true; } // 看看比较的两个对象类型是否相同。如果不同,直接返回 false if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { // 可以看到比较的是两个对象的 value 字符数组是否相同 char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false;}上面的初级比较中,都是简单直接给出一个完整字符串,这一部分介绍几种高级比较!!
两个字符串直接拼接在一起的情况!
xxxxxxxxxxString s1 = "abcdef";String s2 = "abc" + "def";System.out.println(s1 == s2); // true通过 JProfiler 软件发现 Java 堆中并没有"abc"和"def",只有"abcdef",为什么会这样呢??
通过javap反编译 Class 文件后,发现常量池中只有abcdef;为s2赋值时,也是直接把abcdef赋给了s2,并没有拼接的过程,具体如下所示:
xxxxxxxxxx// 部分常量池#30 = Utf8 abcdef// 为 s2 赋值的字节码指令 3: ldc #2 // String abcdef5: astore_2这是因为在编译阶段,进行了一波优化!!(编译期优化) 不仅是字符串,整型常量也是如此,如下所示:
xxxxxxxxxx// 源码int a = 5 + 6;// 为 a 赋值的字节码指令 0: bipush 112: istore_1例子一
xxxxxxxxxxString s1 = "abc";System.out.println(s1 + "def" == "abcdef"); // false首先可以知道编译阶段没有进行和上面一样的优化,而且从底层来看,此处的拼接是借助StringBuilder完成的,上面代码和下面的代码等价 (编译后的字节指令完全一样):
xxxxxxxxxxString s1 = "abc";System.out.println(new StringBuilder().append(s1).append("def").toString() == "abcdef"); // false下面给出编译后的字节码指令:
xxxxxxxxxxCode: stack=3, locals=1, args_size=0 0: ldc #2 // String abc 2: astore_0 3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 6: new #4 // class java/lang/StringBuilder 9: dup 10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 13: aload_0 14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17: ldc #7 // String def 19: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 22: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 25: ldc #9 // String abcdef 27: if_acmpne 34 30: iconst_1 31: goto 35 34: iconst_0 35: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V 38: return上面从字节码的角度分析了等价性,下面从内存的角度分析为什么不相等!
首先搞清楚哪些字符串在字符串常量池中,在字符串常量池的有:"abc", "def", "abcdef",通过 JProfiler 验证如下:
然后在看看看StringBuilder::toString的源码:
xxxxxxxxxxpublic String toString() { // Create a copy, don't share the array return new String(value, 0, count);}public String(char value[], int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count <= 0) { if (count < 0) { throw new StringIndexOutOfBoundsException(count); } if (offset <= value.length) { this.value = "".value; return; } } // Note: offset or count might be near -1>>>1. if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); } // copy 了一份!! this.value = Arrays.copyOfRange(value, offset, offset+count);}StringBuilder中也是用char[] value来存储字符,而StringBuilder::toString方法会调用String的构造函数public String(char value[], int offset, int count)
这个构造函数是 copy 了一份value字符数组,而不是直接把value赋值给自身,和public String(char value[])差不多,注意和public String(String original)区分!!
可视化解释如下图所示:
例子二
下面再给一个稍微特殊的例子:
xxxxxxxxxxfinal String s1 = "abc";String s2 = s1 + "def";System.out.println(s2 == "abcdef"); // true唯一的不同在于s1前面被final修饰,s1是常量,不能再指向其他字符串对象
首先搞清楚编译后常量池中有什么,常量池的有:"abc", "abcdef",并没有"def"。顺便看一下编译后的字节码指令:
xxxxxxxxxxCode: stack=3, locals=3, args_size=1 0: ldc #2 // String abc 2: astore_1 3: ldc #3 // String abcdef 5: astore_2 6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 9: ldc #2 // String abc 11: aload_2 12: if_acmpne 19 15: iconst_1 16: goto 20 19: iconst_0 20: invokevirtual #5 // Method java/io/PrintStream.println:(Z)V 23: return我们发现这个时候的拼接并没有像上面那样借助StringBuilder,甚至连拼接的过程都没有发现,都没有看到"def",这是为什么呢?
肯定和final修饰有关 (废话),被final修饰后,s1是常量,而"def"也是常量
所以直接在编译阶段通过优化把s1 + "def"的结果计算出来了,这也是为什么常量池中没有"def",但是有"abcdef"的原因
那为什么上一个例子不能这样处理呢?!上面一个例子中s1不是常量,编译阶段并不能知道s1到底指向哪一个字符串对象,只有在运行期间才能知道!!!
xxxxxxxxxxString s1 = "abc" + new String("def");System.out.println(s1 == "abcdef"); // false这个和上面 字符串和变量的拼接赋值 的第一个例子差不多,字节码层面都是通过StringBuilder拼接,如下所示:
xxxxxxxxxxCode: stack=4, locals=2, args_size=1 0: new #2 // class java/lang/StringBuilder 3: dup 4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V 7: ldc #4 // String abc 9: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 12: new #6 // class java/lang/String 15: dup 16: ldc #7 // String def 18: invokespecial #8 // Method java/lang/String."<init>":(Ljava/lang/String;)V 21: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 24: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 27: astore_1 28: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream; 31: aload_1 32: ldc #11 // String abcdef 34: if_acmpne 41 37: iconst_1 38: goto 42 41: iconst_0 42: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V 45: returnxxxxxxxxxxString s1 = new String("abc") + new String("def");System.out.println(s1 == "abcdef"); // false原因同上,可以看反编译后的字节码指令,通过StringBuilder拼接,为了节约篇幅,这里就不贴出来了!
通过上面的分析,知道了字符串直接用+拼接,底层是通过StringBuilder实现,下面给出两种不同方式的性能比较:
xxxxxxxxxxpublic class CompareStringAdd { public static void main(String[] args) { int count = 1000000; long start = System.currentTimeMillis(); f1(count); long end = System.currentTimeMillis(); System.out.println(end - start); // 42392
start = System.currentTimeMillis(); f2(count); end = System.currentTimeMillis(); System.out.println(end - start); // 6 } public static void f1(int count) { String s = ""; for (int i = 0; i < count; i++) { s += "a"; } } public static void f2(int count) { StringBuilder s = new StringBuilder(); for (int i = 0; i < count; i++) { s.append("a"); } }}可以看到这差距还是很明显滴!!
对于f1()方法,循环中的每一次加操作都会经历三个步骤:创建StringBuilder对象;调用一次append()方法;调用一次toString()方法生成一个String对象
可想而知内存中会创建很多StringBuilder和String对象,内存占用大;如果进行 GC,还需要花费额外的时间开销
对于f2()方法,从循环开始到结束仅仅只创建了一个StringBuilder对象,每一轮循环仅仅是调用了一次append()方法而已,所以效率上的提升也是肉眼可见的
这个点也是阿里巴巴 Java 开发手册中所推荐的:循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展
思考:对于f2()方法,有没有可以继续优化的地方呢?-> 既然这样说了,答案肯定是有!!!
先来看看StringBuilder的无参构造器,如下所示:
xxxxxxxxxxpublic StringBuilder() { // 默认创建一个大小为 16 的 StringBuilder 对象 super(16);}再来看看append()方法,如下所示:
xxxxxxxxxxpublic StringBuilder append(String str) { // 具体见下方 super.append(str); return this;}// AbstractStringBuilder 类,StringBuilder 继承该类public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); // 判断是否会越界,具体见下方 ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this;}private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) { // 新建一个更大的 value 数组,并将原来数组的内容复制过来 // newCapacity 见下方 value = Arrays.copyOf(value, newCapacity(minimumCapacity)); }}private int newCapacity(int minCapacity) { // overflow-conscious code // 在原有的长度上 x 2 + 2 int newCapacity = (value.length << 1) + 2; // 判断新长度够不够,如果不够,直接赋值为 minCapacity if (newCapacity - minCapacity < 0) { newCapacity = minCapacity; } return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) ? hugeCapacity(minCapacity) : newCapacity;}扯了这么多,就是想说明如果初始化大小过小,会频繁的扩容,扩容又涉及到数组的复制,带来的开销较大
所以如果在实际开发中,可以基本确定需要添加的字符串长度不高于某个限定值,建议使用带参构造函数,避免频繁的扩容,如下所示:
xxxxxxxxxxStringBuilder s = new StringBuilder(highLevel);这是一道经典面试的问题,如下所示:
xxxxxxxxxxString s = new String("abc");首先字符串常量池中有一个对象"abc";其次new String("abc")创建了一个新的对象,指向字符串常量池中的"abc"
所以这个问题中创建了两个对象!!
下面是进阶版:
xxxxxxxxxxString s = new String("abc") + new String("def");首先字符串常量池中有两个对象"abc", "def";
其次new了两个String对象,分别指向字符串常量池中的两个对象;
然后+拼接的底层是通过StringBuilder实现,所以有一个StringBuilder对象
调用append()方法后,最终会调用toString()方法生成一个String对象,并返回其引用给s
所以这个问题中创建了六个对象!!
字符串字面量在上面提到过,所有双引号"xxx"中的内容都属于字符串字面量,现在讨论的问题是这种字面量什么时候进入字符串常量池呢??
首先可以确定的是,当一个源程序被编译后,字符串字面量就会被加入静态常量池中;当对应的类被加载到内存后,内存中存放静态常量池的地方就被称为运行时常量池,所以运行时常量池也有该字面量
在类加载过程的解析阶段,Java 虚拟机会在堆中创建对应 class 文件常量池中的字符串对象实例,并在字符串常量池中驻留其引用,字符串常量池中的内容全局共享!!
但但但《Java 虚拟机规范》中明确规定解析阶段可以是 lazy resolve 的!!!
《Java 虚拟机规范》中 Class 文件的常量池项目的类型,有两种东西:
后者是 String 常量的类型,但它并不直接持有 String 常量的内容,而是只持有一个符号引用,这个符号引用所指定的另一个常量池项必须是一个 CONSTANT_Utf8_info 类型的常量,这里才真正持有字符串的内容,具体如下所示:
xxxxxxxxxx #3 = String #29 // abc#29 = Utf8 abc在 HotSpot 虚拟机的运行时常量池中:
CONSTANT_Utf8_info 会在类加载的时候就被全部创建出来,而 CONSTANT_String_info 则是 lazy resolve 的。例如:在引用该项的 ldc 指令被第一次执行的时候才会 resolve
那么在尚未 resolve 的时候,HotSpot 虚拟机把它的类型叫做 JVM_CONSTANT_UnresolvedString,内容跟 Class 文件里一样只是一个符号引用
等到 resolve 过后这个项的常量类型就会变成最终的 JVM_CONSTANT_String,而内容则变成实际的引用 (直接引用)
《深入理解 Java 虚拟机》也有一段话不谋而合:虚拟机实现可以根据需要来自行判断,到底是在类被加载器加载的时候就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它
最后来一波小总结:就 HotSpot 虚拟机的实现来说,在类加载的时候字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池 (即在 StringTable 中并没有相应的引用,在堆中也没有对应的对象产生)
下面来强调一下ldc指令,它的作用:将int、float或String型常量值从常量池中推送至栈顶
上面强调过,类加载过程中的解析阶段是 lazy resolve (延迟),那到底会延迟到什么时候才会去解析呢?!
执行ldc指令就是触发 lazy resolution 动作的条件
ldc字节码在这里的执行语义是:到当前类的运行时常量池去查找该符号引用对应的项,如果该项尚未 resolve 则 resolve,并返回 resolve 后的内容
在遇到String类型常量时,resolve 的过程如果发现 StringTable 已经有了内容匹配的java.lang.String的引用,则直接返回这个引用
反之,如果 StringTable 里尚未有内容匹配的String实例的引用,则会在 Java 堆里创建一个对应内容的String对象,然后在 StringTable 记录下这个引用,并返回这个引用
可见,ldc指令是否需要创建新的String实例,全看在第一次执行这一条ldc指令时,StringTable 是否已经记录了一个对应内容的String的引用
关于这个内容的详细解释可见 Java 中new String("字面量") 中 "字面量" 是何时进入字符串常量池的? - 木女孩的回答 - 知乎
将一个字符串对象加入字符串常量池,只有两种方式:
"xxx"中的内容都在字符串常量池中!!intern()方法根据「字符串常量池」所在位置的不同,里面存的内容有些许的不同!!
JDK6 及之前,「字符串常量池」在永久代中 (非堆),而对象却是在堆中,所以会把首次遇到的字符串实例复制到「字符串常量池」中存储,返回的也是永久代里面这个字符串实例的引用
JDK7 及之后,「字符串常量池」被移到了堆中,而对象也是在堆中,所以就不需要再拷贝字符串的实例到「字符串常量池」,只需要在「字符串常量池」中记录一下首次出现的实例引用即可
xxxxxxxxxxpublic class RuntimeConstantPoolOOM { public static void main(String[] args) throws InterruptedException { String str1 = new StringBuilder("计算机").append("软件").toString(); System.out.println(str1.intern() == str1);
String str2 = new String("ja") + new String("va"); System.out.println(str2.intern() == str2); }}这里分三种情况讨论,分别在 JDK6、JDK7、JDK8 的版本下 (是不是很麻烦!)
JDK6
str1和str2引用的对象实例都在 Java 堆中str1.intern()和str2.intern()返回的是方法区中的字符串常量池里的实例引用falseJDK7
str1.intern()和str2.intern()返回的是 Java 堆中的字符串常量池里的实例引用"java"会在一开始就被加载到字符串常量池中,所以调用str2.intern()方法时并非首次遇到,返回的是一开始就被加载到字符串常量池中实例引用,和str2是不同的true,第二个会得到falseJDK8
"java"一开始不会被加载到字符串常量池中,所以调用str2.intern()方法时是首次遇到,返回的实例引用和str2相同true这里详细解释一下"java"字符串为什么一开始会被加载到字符串常量池中
在sun.misc.Version类中有一个静态常量,如下所示:
xxxxxxxxxx// JDK7public class Version { // ... private static final String launcher_name = "java"; // ...}HotSpot VM 会在初始化过程中主动触发java.lang.System类的加载和初始化,过程中会调用到java.lang.System.initializeSystemClass()静态方法:
xxxxxxxxxxprivate static void initializeSystemClass() { // ... sun.misc.Version.init(); // ...}所以sun.misc.Version类会被加载到内存中,静态常量会被加载到字符串常量池中!!但是在 JDK8 的时候,这个地方有点变化,如下所示:
xxxxxxxxxx// JDK8public class Version { private static final String launcher_name = "openjdk";}关于这个内容的详细解释可见 如何理解《深入理解java虚拟机》第二版中对String.intern()方法的讲解中所举的例子? - RednaxelaFX的回答 - 知乎
再来一个例子:
xxxxxxxxxxString str1 = new StringBuilder().append("计算机软件").toString();System.out.println(str1.intern() == str1); // false上文强调过「所有双引号"xxx"中的内容都在字符串常量池中!!」所以"计算机软件"在字符串常量池中,调用str2.intern()方法时字符串常量池中已经有了,返回的实例引用和str1是不同的
上个例子中String str1 = new StringBuilder("计算机").append("软件").toString(),"计算机"和"软件"都在字符串常量池中,但"计算机软件"不在!!
先给出一个例子:
xxxxxxxxxxString s1 = new String("abc") + new String("def"); // 第一步s1.intern(); // 第二步String s2 = "abcdef"; // 第三步System.out.println(s1 == s2); // true相信有了 字符串字面量何时进入字符串常量池?!的铺垫,不难理解输出的结果,这里再强调一下!!
当执行到「第二步」时,此时字符串常量池中并没有"abcdef",所以会把s1加入到字符串常量池中
当执行到「第三步」时,由于字符串常量池中已经有了"abcdef",所以在调用ldc指令给s2赋值时,直接返回字符串常量池中的引用
所以s1和s2的引用值是相同的!!!
xxxxxxxxxxString s = "abc"; // 第一步s += "def"; // 第二步s += "ghi"; // 第三步首先,字符串常量池中有三个字符串:"abc", "def", "ghi"。当执行到每一步时,s对应的实例引用如下图所示:
这里的+=其实是一种字符串拼接,字节码指令层面的原理和 字符串和变量的拼接赋值 相同!!
从上图可以看出,拼接的过程,变化的只有s的值 (引用),对于字符串对象本身来说并没有改变,每次都是生成了一个新的字符串对象
值的注意的是,上图中的紫色箭头是不可以变的,因为String类中的value字段被final修饰,所以值 (引用) 不可变,源码如下:
xxxxxxxxxxprivate final char value[];若在「理解一」的基础上稍加修改,如下所示:
xxxxxxxxxxfinal String s = "abc";s += "def"; // 错误,编译不通过 ❌s += "ghi"; // 错误,编译不通过 ❌原理和「理解一」中一样,由于s被final修饰,所以值 (引用) 不可变,对应上图中就是红色箭头是不可以变的
给出一段代码,想想会输出什么:
xxxxxxxxxxpublic static void f1(String s) { s = "def";}public static void f2(char[] cs) { cs[0] = 'A';}public static void main(String[] args) { String s = "abc"; char[] cs = new char[] {'a', 'b', 'c'}; f1(s); f2(cs); System.out.println(s); // abc System.out.println(cs); // Abc}在学习 C 语言的时候,肯定听过「传值」或「传引用」;Java 中也有类似的东西,但 Java 中的本质都是「传值」
如果将变量按照数据类型划分的话,可以分为两类:
之所以说 Java 中的本质都是「传值」,因为如果传的是一个基本数据类型,那就是值;如果传的是一个引用数据类型,那就是引用,其实也就是该变量的值
回到上面的代码,先解释s为什么没变?!
f1中的局部变量s的引用指向了另外一个对象 (等价于修改了上图中的红色箭头),那关main中的局部变量s什么事情!!!main中的局部变量s的值传给了f1,f1中无论怎么修改它,都不会影响main中的局部变量s再解释cs为什么变了?!
cs[i] = 'x'表示把相对起始地址偏移量为i的地址的值改为'x'假设String类中value字段不是private,即可以任意访问修改,会怎么样呢?下面我们来试试!!
可以通过反射机制访问或修改无权限的字段,关于操作运行时类的内部属性及方法的详细内容可见 反射机制
xxxxxxxxxxpublic static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { String s = "abc"; // 测试字符串对象 Class<String> stringClass = String.class; // 获取 String 类型对象 Field valueField = stringClass.getDeclaredField("value"); // 获取类型的 value 字段 valueField.setAccessible(true); // 禁止访问安全检查 char[] value = (char[]) valueField.get(s); // 获取 s 的 value 字段 value[0] = 'A'; // 修改第 0 个字符 System.out.println(s); // 输出结果为 Abc}👇👇👇👇👇👇👇👇👇👇👇👇