【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 ...
- JDK源码学习--String篇(二) 关于String采用final修饰的思考
JDK源码学习String篇中,有一处错误,String类用final[不能被改变的]修饰,而我却写成静态的,感谢CTO-淼淼的指正. 风一样的码农提出的String为何采用final的设计,阅读JD ...
随机推荐
- OSGI学习总结
最近的一项研究了解了一下OSGI技术,感觉OSGI尽管有一定的学习难度.可是终于掌握和推广之后将是一项对系统开发比較实用的技术.在此和大家分享一下自己的感悟. 1.什么是OSGI OSGI直译为&qu ...
- Visual Studio跨平台开发实战(5) - Xamarin Android多页面应用程式开发
原文 Visual Studio跨平台开发实战(5) - Xamarin Android多页面应用程式开发 前言 大部份的Android 都具有实体或虚拟的Back键. 因此在处理多页面应用程式时 ...
- KMP求字符串最小循环节
证明1: 对于一个字符串S,长度为L,如果由长度为len的字符串s(字符串s的最小循环节是其本身)循环k次构成,那么字符串s就是字符串S的最小循环节 那么字符串有个很重要的性质和KMP挂钩,即 i ...
- 【原创】poj ----- 1611 The Suspects 解题报告
题目地址: http://poj.org/problem?id=1611 题目内容: The Suspects Time Limit: 1000MS Memory Limit: 20000K To ...
- Arduino 数码管LED驱动 数组法
上个样例讲到驱动LED数码管,採用一种最直接的方案,对每一个LED进行高低电平的控制,这种长处是每一个LED都是受控可检的,避免了因为短路造成的假象,但对于数字变化来说,写起来就很冗余,因此这次尝试用 ...
- 探索Android该Parcel机制(上)
一.先从Serialize说起 我们都知道JAVA中的Serialize机制,译成串行化.序列化……,其作用是能将数据对象存入字节流其中,在须要时又一次生成对象. 主要应用是利用外部存储设备保存对象状 ...
- SQL Server数据库视图
1:什么是视图 2:视图和查询的区别 3:视图的优点 4:如何创建和管理视图 5:如何通过视图修改基本表的数据 6:如何通过视图实现数据的安全性 A:什么是视图: 视图(view):从一个或几个基本表 ...
- BeagleBone Black 板第三课:Debian7.5系统安装和远程控制BBB板
BBB板第三课:Debian7.5系统安装和远程控制BBB板 由于BBB板系统是Debian 7.4.据说使用Debian系统能够实现非常多BBB板的无缝连接.能够更好的学习和控制BBB板,所以就决定 ...
- BZOJ-2115-Xor-WC2011
叙述性说明 分析 我把文库里的粘了过来. 仅仅知道点1到点N的一条路径和图中若干个环.就能通过异或,表示成全部路径.那么.须要多少环才干保证必然能表示成全部路径呢?事实上.并不须要非常多, 由于一些环 ...
- Hadoop入门进阶步步高(六)-Hadoop1.x与Hadoop2的差别
六.Hadoop1.x与Hadoop2的差别 1.变更介绍 Hadoop2相比較于Hadoop1.x来说,HDFS的架构与MapReduce的都有较大的变化,且速度上和可用性上都有了非常大的提高,Ha ...