【JDK源码分析】String的存储区与不可变 专题
《Think in Java》中说:
“关系操作符生成的是一个boolean结果,它们计算的是操作数的值之间的关系”。
"=="判断的是两个对象的内存地址是否一样,适用于原始数据类型和枚举类型(它们的变量存储的是值本身,而引用类型变量存储的是引用);
equals是Object类的方法,Object对它的实现是比较内存地址,我们可以重写这个方法来自定义“相等”这个概念。
比如类库中的String、Date等类就对这个方法进行了重写。
综上,
对于枚举类型和原始数据类型的相等性比较,应该使用"==";
对于引用类型的相等性比较,应该使用equals方法。
// ... literals are interned by the compiler
// and thus refer to the same object
String s1 = "abcd";
String s2 = "abcd";
s1 == s2; // --> true // ... These two have the same value
// but they are not the same object
String s1 = new String("abcd");
String s2 = new String("abcd");
s1 == s2; // --> false
看上面一段代码,我们会发生疑惑:为什么通过字符串常量实例化的String类型对象是一样的,而通过new所创建String对象却不一样呢?
且看下面分解:
1. 数据存储区
String是一个比较特殊的类,除了new之外,还可以用字面常量来定义。为了弄清楚这二者间的区别,首先我们得明白JVM运行时数据存储区,这里有一张图对此有清晰的描述:
非共享数据存储区
非共享数据存储区是在线程启动时被创建的,包括:
- 程序计数器(program counter register)控制线程的执行;
- 栈(JVM Stack, Native Method Stack)存储方法调用与对象的引用等。
共享数据存储区
该存储区被所有线程所共享,可分为:
- 堆(Heap)存储所有的Java对象,当执行new对象时,会在堆里自动进行内存分配。
- 方法区(Method Area)存储常量池(run-time constant pool)、字段与方法的数据、方法与构造器的代码。
2. 两种实例化
实例化String对象:
public class StringLiterals {
public static void main(String[] args) {
String one = "Test";
String two = "Test";
String three = "T" + "e" + "s" + "t";
String four = new String("Test");
}
}
javap -c StringLiterals反编译生成字节码,我们选取感兴趣的部分如下:
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String Test
2: astore_1
3: ldc #2 // String Test
5: astore_2
6: ldc #2 // String Test
8: astore_3
9: new #3 // class java/lang/String
12: dup
13: ldc #2 // String Test
15: invokespecial #4 // Method java/lang/String."<init>": (Ljava/lang/String;)V
18: astore 4
20: return
}
ldc #2表示从常量池中取#2的常量入栈,astore_1表示将引用存在本地变量1中。
因此,我们可以看出:
对象one、two、three均指向常量池中的字面常量"Test";
对象four是在堆中new的新对象;
如下图所示:
总结如下:
- 当用字面常量实例化时,String对象存储在常量池;
- 当用new实例化时,String对象存储在堆中;
操作符==比较的是对象的引用,当其指向的对象不同时,则为false。因此,开篇中的代码会出现通过new所创建String对象不一样。
3. 不可变String
不可变性
所谓不可变性(immutability)指类不可以通过常用的API被修改。为了更好地理解不可变性,我们先来看《Thinking in Java》中的一段代码:
//: operators/Assignment.java
// Assignment with objects is a bit tricky.
import static net.mindview.util.Print.*; class Tank {
int level;
} public class Assignment {
public static void main(String[] args) {
Tank t1 = new Tank();
Tank t2 = new Tank();
t1.level = 9;
t2.level = 47;
print("1: t1.level: " + t1.level +
", t2.level: " + t2.level);
t1 = t2;
print("2: t1.level: " + t1.level +
", t2.level: " + t2.level);
t1.level = 27;
print("3: t1.level: " + t1.level +
", t2.level: " + t2.level);
}
} /* Output:
1: t1.level: 9, t2.level: 47
2: t1.level: 47, t2.level: 47
3: t1.level: 27, t2.level: 27
*///:~
上述代码中,在赋值操作t1 = t2;之后,t1、t2包含的是相同的引用,指向同一个对象。
因此对t1对象的修改,直接影响了t2对象的字段改变。显然,Tank类是可变的。
也许,有人会说s = s.concat("ef");不是修改了对象s么?而事实上,我们去看concat的实现,会发现return new String(buf, true);返回的是新String对象。
只是s1的引用改变了,如下图所示:
String源码
JDK7的String类:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; /** Cache the hash code for the string */
private int hash; // Default to 0
}
String类被声明为final,不可以被继承,所有的方法隐式地指定为final,因为无法被覆盖。字段char value[]表示String类所对应的字符串,被声明为private final;即初始化后不能被修改。
常用的new实例化对象String s1 = new String("abcd");的构造器:
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
只需将value与hash的字段值进行传递即可。
4. 反射
String的value字段是final的,可不可以通过过某种方式修改呢?答案是反射。在stackoverflow上有这样修改value字段的代码:
String s1 = "Hello World";
String s2 = "Hello World";
String s3 = s1.substring(6);
System.out.println(s1); // Hello World
System.out.println(s2); // Hello World
System.out.println(s3); // World Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[])field.get(s1);
value[6] = 'J';
value[7] = 'a';
value[8] = 'v';
value[9] = 'a';
value[10] = '!'; System.out.println(s1); // Hello Java!
System.out.println(s2); // Hello Java!
System.out.println(s3); // World
这时,有人会诧异:
为什么对象s2的值也会被修改,而对象s3的值却不会呢?根据前面的介绍,s1与s2指向同一个对象;所以当s1被修改后,s2也会对应地被修改。
至于s3对象为什么不会?我们来看看substring()的实现:
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
当beginIndex不为0时,返回的是new的String对象;当beginIndex为0时,返回的是原对象本身。
如果将上述代码String s3 = s1.substring(6);改为String s3 = s1.substring(0);,那么对象s3也会被修改了。
如果仔细看java.lang.String.java,我们会发现:
当需要改变字符串内容时,String类的方法返回的是新String对象;
如果没有改变,String类的方法则返回原对象引用。
这节省了存储空间与额外的开销。
5. 参考资料
[1] Programcreek, JVM Run-Time Data Areas.
[2] Corey McGlone, Looking "Under the Hood" with javap.
[3] Programcreek, Diagram to show Java String’s Immutability.
[4] Stackoverflow, Is a Java string really immutable?
[5] Programcreek, Why String is immutable in Java ?
http://www.cnblogs.com/en-heng/p/5121870.html
java.lang.reflect.Field对象的方法:
get
public Object get(Object obj)
throws IllegalArgumentException,
IllegalAccessException
- Returns the value of the field represented by this
Field, on the specified object. The value is automatically wrapped in an object if it has a primitive type.The underlying field's value is obtained as follows:
If the underlying field is a static field, the
objargument is ignored; it may be null.Otherwise, the underlying field is an instance field. If the specified
objargument is null, the method throws aNullPointerException.If the specified object is not an instance of the class or interface declaring the underlying field, the method throws anIllegalArgumentException.If this
Fieldobject enforces Java language access control, and the underlying field is inaccessible, the method throws anIllegalAccessException. If the underlying field is static, the class that declared the field is initialized if it has not already been initialized.Otherwise, the value is retrieved from the underlying instance or static field. If the field has a primitive type, the value is wrapped in an object before being returned, otherwise it is returned as is.
If the field is hidden in the type of
obj, the field's value is obtained according to the preceding rules. -
-
- Parameters:
obj- object from which the represented field's value is to be extracted- Returns:
- the value of the represented field in object obj; primitive values are wrapped in an appropriate object before being returned
- Throws:
IllegalAccessException- if the underlying field is inaccessible.IllegalArgumentException- if the specified object is not an instance of the class or interface declaring the underlying field (or a subclass or implementor thereof).NullPointerException- if the specified object is null and the field is an instance field.ExceptionInInitializerError- if the initialization provoked by this method fails.
【JDK源码分析】String的存储区与不可变 专题的更多相关文章
- JDK源码分析-String、StringBuilder、StringBuffer
String类的申明 public final class String implements java.io.Serializable, Comparable<String>, Char ...
- JDK源码分析(一)—— String
dir 参考文档 JDK源码分析(1)之 String 相关
- JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue
JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue 目的:本文通过分析JDK源码来对比ArrayBlockingQueue 和LinkedBlocki ...
- java-通过 HashMap、HashSet 的源码分析其 Hash 存储机制
通过 HashMap.HashSet 的源码分析其 Hash 存储机制 集合和引用 就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并非真正的把 Java 对象放入数组中.仅仅是把对象的 ...
- 【JDK】JDK源码分析-LinkedHashMap
概述 前文「JDK源码分析-HashMap(1)」分析了 HashMap 主要方法的实现原理(其他问题以后分析),本文分析下 LinkedHashMap. 先看一下 LinkedHashMap 的类继 ...
- 【JDK】JDK源码分析-Vector
概述 上文「JDK源码分析-ArrayList」主要分析了 ArrayList 的实现原理.本文分析 List 接口的另一个实现类:Vector. Vector 的内部实现与 ArrayList 类似 ...
- 【JDK】JDK源码分析-ArrayList
概述 ArrayList 是 List 接口的一个实现类,也是 Java 中最常用的容器实现类之一,可以把它理解为「可变数组」. 我们知道,Java 中的数组初始化时需要指定长度,而且指定后不能改变. ...
- 【JDK】JDK源码分析-Semaphore
概述 Semaphore 是并发包中的一个工具类,可理解为信号量.通常可以作为限流器使用,即限制访问某个资源的线程个数,比如用于限制连接池的连接数. 打个通俗的比方,可以把 Semaphore 理解为 ...
- 【JDK】JDK源码分析-HashMap(2)
前文「JDK源码分析-HashMap(1)」分析了 HashMap 的内部结构和主要方法的实现原理.但是,面试中通常还会问到很多其他的问题,本文简要分析下常见的一些问题. 这里再贴一下 HashMap ...
随机推荐
- 【t007】棋盘放置指南车问题
Time Limit: 1 second Memory Limit: 50 MB [问题描述] 按照国际象棋的规则,车可以攻击与之处在同一行或同一列上的棋子.指南车是有方向的车.横向指南车可以攻击与之 ...
- Java 中StringBuffer与StringBuilder区别(转)及String类的一些基本操作代码
String 字符串常量StringBuffer 字符串变量(线程安全) 多个线程访问时,不会产生问题(Synchronized)StringBuilder 字符串变量(非线程安全) 多个线程访问时 ...
- 192M内存的VPS,安装Centos 6 minimal x86,无法安装node.js
尝试了各种方法,始终安装不了node.偶然一次,安装了64位的Centos 6 minimal,竟然可以安装Node官网给出的命令安装node了,一切顺利.
- 【Codeforces Round #438 C】 Qualification Rounds
[链接]h在这里写链接 [题意] 给你n个问题,每个人都知道一些问题. 然后让你选择一些问题,使得每个人知道的问题的数量,不超过这些问题的数量的一半. [题解] 想法题. 只要有两个问题. 这两个问题 ...
- matlab 图像的保存
gcf:获取当前显示图像的句柄: 默认 plot 的 position 是 [232 246 560 420] 0. save >> A = randn(3, 4); >> B ...
- js typeof instanceof
一般都是用typeof推断变量存在 例如if(typeof a!="undefined"){}.不是要去使用if(a)因为假定a不存在(未申报)将是错误的. 由于typeof经验n ...
- 将gdal源码转化为VS工程编译过程记录
作者:朱金灿 来源:http://blog.csdn.net/clever101 为什么要用VS工程的方式来编译gdal库?主要还是为了调试方便,虽然理论上使用命令行方式生成库也能调试,详见:GDAL ...
- cordova打包之android应用签名
原文:cordova打包之android应用签名 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/mate_ge/article/details/78 ...
- Linux经常使用的命令(21) - find参数具体解释
一.使用name选项: 文件名称选项是find命令最经常使用的选项.要么单独使用该选项,要么和其它选项一起使用. 能够使用某种文件名称模式来匹配文件,记住要用引號将文件名称模式引起来. 无论当前路 ...
- Java编程思想里的泛型实现一个堆栈类
觉得作者写得太好了,不得不收藏一下. 对这个例子的理解: //类型参数不能用基本类型,T和U其实是同一类型. //每次放新数据都成为新的top,把原来的top往下压一级,通过指针建立链接. //末端哨 ...