《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中。
因此,我们可以看出:
对象onetwothree均指向常量池中的字面常量"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 obj argument is ignored; it may be null.

Otherwise, the underlying field is an instance field. If the specified obj argument is null, the method throws a NullPointerException. If the specified object is not an instance of the class or interface declaring the underlying field, the method throws an IllegalArgumentException.

If this Field object enforces Java language access control, and the underlying field is inaccessible, the method throws an IllegalAccessException. 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的存储区与不可变 专题的更多相关文章

  1. JDK源码分析-String、StringBuilder、StringBuffer

    String类的申明 public final class String implements java.io.Serializable, Comparable<String>, Char ...

  2. JDK源码分析(一)—— String

    dir 参考文档 JDK源码分析(1)之 String 相关

  3. JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue

    JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue 目的:本文通过分析JDK源码来对比ArrayBlockingQueue 和LinkedBlocki ...

  4. java-通过 HashMap、HashSet 的源码分析其 Hash 存储机制

    通过 HashMap.HashSet 的源码分析其 Hash 存储机制 集合和引用 就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并非真正的把 Java 对象放入数组中.仅仅是把对象的 ...

  5. 【JDK】JDK源码分析-LinkedHashMap

    概述 前文「JDK源码分析-HashMap(1)」分析了 HashMap 主要方法的实现原理(其他问题以后分析),本文分析下 LinkedHashMap. 先看一下 LinkedHashMap 的类继 ...

  6. 【JDK】JDK源码分析-Vector

    概述 上文「JDK源码分析-ArrayList」主要分析了 ArrayList 的实现原理.本文分析 List 接口的另一个实现类:Vector. Vector 的内部实现与 ArrayList 类似 ...

  7. 【JDK】JDK源码分析-ArrayList

    概述 ArrayList 是 List 接口的一个实现类,也是 Java 中最常用的容器实现类之一,可以把它理解为「可变数组」. 我们知道,Java 中的数组初始化时需要指定长度,而且指定后不能改变. ...

  8. 【JDK】JDK源码分析-Semaphore

    概述 Semaphore 是并发包中的一个工具类,可理解为信号量.通常可以作为限流器使用,即限制访问某个资源的线程个数,比如用于限制连接池的连接数. 打个通俗的比方,可以把 Semaphore 理解为 ...

  9. 【JDK】JDK源码分析-HashMap(2)

    前文「JDK源码分析-HashMap(1)」分析了 HashMap 的内部结构和主要方法的实现原理.但是,面试中通常还会问到很多其他的问题,本文简要分析下常见的一些问题. 这里再贴一下 HashMap ...

随机推荐

  1. 【SSH2(理论+实践)】--图说Struts2的执行

        前几篇文章讨论了有关Struts2的核心机制及一些基础,但同一时候也遗留下了非常多问题.这些问题主要是针对Struts2的一些使用技巧的,该篇文章将会针对Struts2的使用技巧进行讨论, ...

  2. 怎样在swift中使用cocoapods导入的第三方oc库

    假如你来到这里,说明你已经開始着手使用swift这门新语言了. 就像Java有Maven一样.Objective-C也有自己的依赖管理工具cocoapods. 可是因为swift才出来不久,眼下非常多 ...

  3. solrj 7.x Expected mime type application/octet-stream but got text/html.

    出现这种情况是因为baseurl填写错误,最开始的时候我写的是用tomcat启动后浏览器中访问solr的地址 结果就出现了如题的异常,当然提示的是404,还有可能提示405,Method not al ...

  4. iOS RunLoop了解和使用

    RunLoop介绍和使用 上次讲了runtime,这次是runloop,虽然两者都是run开头的名词术语,但是在OC中,这两个东西压根没啥联系.这篇文章主要讲讲runloop的一些概念和用法.其中包含 ...

  5. Android Studio教程-创建第一个项目Hello World

    前段时间打开Android studio 在build过程中总会出现以下错误 Error:Execution failed for task ':app:preDebugAndroidTestBuil ...

  6. 十分钟了解 spring cloud

    1 为什么需要服务发现 简单来说,服务化的核心就是将传统的一站式应用根据业务拆分成一个一个的服务,而微服务在这个基础上要更彻底地去耦合(不再共享DB.KV,去掉重量级ESB),并且强调DevOps和快 ...

  7. Arcgis api for javascript学习笔记(3.2X版本)-Map图层叠加以及基本操作

    1. 不设置默认底图,第一个图层作为底图,然后叠加另外一个图层 先添加图层1,第一个图层1作为默认底图,然后在图层1上叠加图层2,并设置图层2的透明度为50%. <!DOCTYPE html&g ...

  8. python栈--字符串反转,括号匹配

    栈的实现: # 定义一个栈类 class Stack(): # 栈的初始化 def __init__(self): self.items = [] # 判断栈是否为空,为空返回True def isE ...

  9. VS编译环境中TBB配置和C++中lambda表达式

    TBB(Thread Building Blocks),线程构建模块,是由Intel公司开发的并行编程开发工具,提供了对Windows,Linux和OSX平台的支持. TBB for Windows ...

  10. WPF中实现图片文件转换成Visual对象,Viewport3D对象转换成图片

    原文:WPF中实现图片文件转换成Visual对象,Viewport3D对象转换成图片 1.图片文件转换成Visual对象 private Visual CreateVisual(string imag ...