什么是不可变对象?

众所周知, 在Java中, String类是不可变的。那么到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。

区分对象和对象的引用

对于Java初学者, 对于String是不可变对象总是存有疑惑。看下面代码:
  1. String s = "ABCabc";
  2. System.out.println("s = " + s);
  3. s = "123456";
  4. System.out.println("s = " + s);

打印结果为:

s = ABCabc
s = 123456
 
首先创建一个String对象s,然后让s的值为“ABCabc”, 然后又让s的值为“123456”。 从打印结果可以看出,s的值确实改变了。那么怎么还说String对象是不可变的呢? 其实这里存在一个误区: s只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。
也就是说,s只是一个引用,它指向了一个具体的对象,当s=“123456”; 这句代码执行过之后,又创建了一个新的对象“123456”, 而引用s重新指向了这个心的对象,原来的对象“ABCabc”还在内存中存在,并没有改变。内存结构如下图所示:
 
 
Java和C++的一个不同点是, 在Java中不可能直接操作对象本身,所有的对象都由一个引用指向,必须通过这个引用才能访问对象本身,包括获取成员变量的值,改变对象的成员变量,调用对象的方法等。而在C++中存在引用,对象和指针三个东西,这三个东西都可以访问对象。其实,Java中的引用和C++中的指针在概念上是相似的,他们都是存放的对象在内存中的地址值,只是在Java中,引用丧失了部分灵活性,比如Java中的引用不能像C++中的指针那样进行加减运算。
 

为什么String对象是不可变的?

要理解String的不可变性,首先看一下String类中都有哪些成员变量。 在JDK1.6中,String的成员变量有以下几个:
  1. public final class String
  2. implements java.io.Serializable, Comparable<String>, CharSequence
  3. {
  4. /** The value is used for character storage. */
  5. private final char value[];
  6. /** The offset is the first index of the storage that is used. */
  7. private final int offset;
  8. /** The count is the number of characters in the String. */
  9. private final int count;
  10. /** Cache the hash code for the string */
  11. private int hash; // Default to 0
 

在JDK1.7中,String类做了一些改动,主要是改变了substring方法执行时的行为,这和本文的主题不相关。JDK1.7中String类的主要成员变量就剩下了两个:

  1. public final class String
  2. implements java.io.Serializable, Comparable<String>, CharSequence {
  3. /** The value is used for character storage. */
  4. private final char value[];
  5. /** Cache the hash code for the string */
  6. private int hash; // Default to 0

由以上的代码可以看出, 在Java中String类其实就是对字符数组的封装。JDK6中, value是String封装的数组,offset是String在这个value数组中的起始位置,count是String所占的字符的个数。在JDK7中,只有一个value变量,也就是value中的所有字符都是属于String这个对象的。这个改变不影响本文的讨论。 除此之外还有一个hash成员变量,是该String对象的哈希值的缓存,这个成员变量也和本文的讨论无关。在Java中,数组也是对象(可以参考我之前的文章java中数组的特性)。 所以value也只是一个引用,它指向一个真正的数组对象。其实执行了String s = “ABCabc”; 这句代码之后,真正的内存布局应该是这样的:

value,offset和count这三个变量都是private的,并且没有提供setValue, setOffset和setCount等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改, 并且在String类的外部不能访问这三个成员。此外,value,offset和count这三个变量都是final的, 也就是说在String类内部,一旦这三个值初始化了, 也不能被改变。所以可以认为String对象是不可变的了。
 
那么在String中,明明存在一些方法,调用他们可以得到改变后的值。这些方法包括substring, replace, replaceAll, toLowerCase等。例如如下代码:
  1. String a = "ABCabc";
  2. System.out.println("a = " + a);
  3. a = a.replace('A', 'a');
  4. System.out.println("a = " + a);

打印结果为:

a = ABCabc
a = aBCabc
 
那么a的值看似改变了,其实也是同样的误区。再次说明, a只是一个引用, 不是真正的字符串对象,在调用a.replace('A', 'a')时, 方法内部创建了一个新的String对象,并把这个心的对象重新赋给了引用a。String中replace方法的源码可以说明问题:
读者可以自己查看其他方法,都是在方法内部重新创建新的String对象,并且返回这个新的对象,原来的对象是不会被改变的。这也是为什么像replace, substring,toLowerCase等方法都存在返回值的原因。也是为什么像下面这样调用不会改变对象的值:
 
  1. String ss = "123456";
  2. System.out.println("ss = " + ss);
  3. ss.replace('1', '0');
  4. System.out.println("ss = " + ss);

打印结果:

ss = 123456
ss = 123456
 
 

String对象真的不可变吗?

从上文可知String的成员变量是private final 的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗? 比如将数组中的某个位置上的字符变为下划线“_”。 至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组。
那么用什么方式可以访问私有成员呢? 没错,用反射, 可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。下面是实例代码:
 
  1. public static void testReflection() throws Exception {
  2. //创建字符串"Hello World", 并赋给引用s
  3. String s = "Hello World";
  4. System.out.println("s = " + s); //Hello World
  5. //获取String类中的value字段
  6. Field valueFieldOfString = String.class.getDeclaredField("value");
  7. //改变value属性的访问权限
  8. valueFieldOfString.setAccessible(true);
  9. //获取s对象上的value属性的值
  10. char[] value = (char[]) valueFieldOfString.get(s);
  11. //改变value所引用的数组中的第5个字符
  12. value[5] = '_';
  13. System.out.println("s = " + s);  //Hello_World
  14. }

打印结果为:

s = Hello World
s = Hello_World
 
在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变

java String不可变对象,但StringBuffer是可变对象的更多相关文章

  1. JAVA String、StringBuilder、和StringBuffer的区别,及如何使用

    目录 String类 一.String类的理解和创建对象 二.String类创建的方式 两种创建String对象的区别 测试题 三.String常用方法 四.StringBuffer类 1.Strin ...

  2. Java String, StringBuffer和StringBuilder实例

    1- 分层继承2- 可变和不可变的概念3- String3.1- 字符串是一个非常特殊的类3.2- String 字面值 vs. String对象3.3- String的方法3.3.1- length ...

  3. Java学习之String对象为什么是不可变的

    转自:http://www.2cto.com/kf/201401/272974.html,感谢作者的总结 什么是不可变对象? 众所周知, 在Java中, String类是不可变的.那么到底什么是不可变 ...

  4. Java String StringBuffer StringBuilder

    String  字符串常量存储在常量区,每次追加操作会创建新的对象: StringBuffer  字符串变量  线程安全 在堆上创建,每次追加操作在原对象上进行操作:  速度 StringBuffer ...

  5. 关于Java String对象创建的几点疑问

    我们通过JDK源码会知道String实质是字符数组,而且是不可被继承(final)和具有不可变性(immutable).可以如果想要了解String的创建我们需要先了解下JVM的内存结构. 1.JVM ...

  6. Java——String对象

    前言 实际上任何语言都没有提供字符串这个概念,而是使用字符数组来描述字符串.Java里面严格来说也是没有字符串的,在所有的开发里面字符串的应用有很多,于是Java为了应对便创建了String类这个字符 ...

  7. 【转载】关于Java String, StringBuilder, StringBuffer, Hashtable, HashMap的面试题

    REF: http://blog.csdn.net/fightforyourdream/article/details/15333405 题目是一道简单的小程序,像下面这样:[java] view p ...

  8. java String,StringBuilder和StringBuffer

    String:1.java语言中的字符串值属于String类,虽然有其它方法表示字符串(如字符数组),但java一般使用Sting类作为字符串的标准格式,java编译器把字符串值作为String对象. ...

  9. Java String、StringBuilder和StringBuffer

    转载: Java String.StringBuilder和StringBuffer 概览 在Android/Java开发中,用来处理字符串常用的类有3种: String.StringBuilder. ...

随机推荐

  1. 不要在 _ASSERT() 宏中执行功能代码 AND RELEASE版调试办法

            程序在DEBUG版正常运行,在RELEASE版就崩溃了,想调试找问题,却发现RELEASE版根本走不到断点... 需要在工程配置中将 Configuration Properties- ...

  2. Openjudge-计算概论(A)-苹果和虫子

    描述 你买了一箱n个苹果,很不幸的是买完时箱子里混进了一条虫子.虫子每x小时能吃掉一个苹果,假设虫子在吃完一个苹果之前不会吃另一个,那么经过y小时你还有多少个完整的苹果? 输入输入仅一行,包括n,x和 ...

  3. Neutron 不健全的HA ROUTER

    首先介绍下HA,所谓的HA就是高可用性,但HA有双主.主备两种工作模式,其中主备模式又包含抢占与 非抢占两种方式,而Neutron Router采用的无疑是HA中最简单的工作方式非抢占主备模式. HA ...

  4. python脚本文件删除

    昨天有需求需要用python脚本删除一个目录下的文件.遇到了点麻烦. 使用的是shutil.rmtree(dir)函数,这个函数可以删除有内容的目录,而shutil.rmdir(dir)只能删除空目录 ...

  5. 不自动切换eclipse视图

    刚开始使用eclipse进行调试时,当弹出"Confir Perspective Switch"视图时,不小心点了“No”.以后每次debug的时候都不切换到debug视图. 后发 ...

  6. asp脱离源代码管理

    当项目中出现“未能找到与此解决方案关联的源代码管理提供程序.项目将视为不受源代码管理” 解决方法:1.vs2013打开项目, 2.提示“您正在打开的解决方案已绑定到以下Team Foundation ...

  7. windows7所有版本

    windows7所有版本迅雷地址下载集合(含32位和64位) Windows7 SP1旗舰版 32位官方原版下载: ed2k://|file|/cn_windows_7_ultimate_with_s ...

  8. 【第一篇】Python基础

    Python学习 学习站点:https://www.shiyanlou.com/ 1 hello world code如下: $ python [15:50:40] Python2.7.6(defau ...

  9. linux的pvtrace环境配置

    1.查看当前ubuntu版本号 froid@ubuntu:~/Desktop$ lsb_release -aNo LSB modules are available.Distributor ID:   ...

  10. 如何在VS2013中显示代码行号

    http://jingyan.baidu.com/article/af9f5a2d2e193543140a4533.html