《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 ...

  10. JDK源码学习--String篇(二) 关于String采用final修饰的思考

    JDK源码学习String篇中,有一处错误,String类用final[不能被改变的]修饰,而我却写成静态的,感谢CTO-淼淼的指正. 风一样的码农提出的String为何采用final的设计,阅读JD ...

随机推荐

  1. 高速幂 POW优化

    #include <iostream> #include <stdio.h> #include <stdlib.h> #include <string.h&g ...

  2. 很多人都在埋怨没有遇到好的团队,但好的团队不可能凭空出现,一流的团队不能仅靠团队成员努力,作为Leader,要有可行的规划,并坚定地执行、时势地调整(转)

    <西游记>中的唐僧团队历经千难万险,终于求得真经,目标明确.分工合理为这支队伍最终走向成功奠定了基础.唐僧从一开始,就为这个团队设定了西天取经的目标,虽然经历各种挫折与磨难,但目标从未动摇 ...

  3. iOS 购物—个人中心界面

    上一个QQ界面真实无心插柳,想不到一个新手的普通界面可以上首页推荐,在这谢谢那些csdn工作者对新手的支持,谢谢soledadzz  的特别推荐: 以下这个界面也是师傅锻炼我的题目主要是让我熟悉tab ...

  4. Google 开源项目的风格指南

    谷歌C++代码风格指南.农业所需的代码.更难得的是不FQ,决定性的最爱!! . http://zh-google-styleguide.readthedocs.org/en/latest/google ...

  5. 阅读:AirBag Boosting Smartphone Resistance to Malware Infection

  6. Linux在高铁项目的部署环境

    因为Linux和Java像开源.所以,现在在server基本上使用部署Linux平台即server.然后部署项目.在开发项目的过程中.程序员绝大多数仍采用最经典windows操作系统,尽管Linux也 ...

  7. effective c++ 条款12 copy all parts of an object

    这经常发生在更改代码的时候,当有自己的copy 赋值函数或者copy 构造函数时,编译器就不会维护这两个函数.导致发生遗忘. 可能出现的场景 class Customer { private: std ...

  8. Android 学习历程摘要(二)

    1.资源文件命名仅仅能小写,否则会报错生成不了R.java文件 2.R文件导包时应该导入自己project的包,而不是android.R 3.数据库操作使用SqliteOpenHelper 4.val ...

  9. Ognl底层使用

    今天,在得知ognl采用.在这里和大家分享一下.我希望能帮助. package com.hcj.test; import java.util.ArrayList; import java.util.L ...

  10. poj 2288 Islands and Bridges

    题意: 给你一个双向连通图,求 获得权值最大 的 哈密顿通路的 权值 和 这个权值对应的数目: 其中权值计算方法是  列如 ABCD  权值是a+b+c+d+ab+bc+cd 如果 A,B,C  和B ...