原文地址:http://www.importnew.com/16817.html

什么是不可变对象?

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

区分对象和对象的引用

对于Java初学者, 对于String是不可变对象总是存有疑惑。看下面代码:

  1. String s = "ABCabc";
  2. System.out.println("s = " + s);
  3.  
  4. s = "123456";
  5. System.out.println("s = " + s)
 

打印结果为:

  1. s = ABCabc
  2. 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.  
  7. /** The offset is the first index of the storage that is used. */
  8. private final int offset;
  9.  
  10. /** The count is the number of characters in the String. */
  11. private final int count;
  12.  
  13. /** Cache the hash code for the string */
  14. 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.  
  6. /** Cache the hash code for the string */
  7. 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. tring 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.  
  3. System.out.println("ss = " + ss);
  4.  
  5. ss.replace('1', '0');
  6.  
  7. 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.  
  3. //创建字符串"Hello World", 并赋给引用s
  4. String s = "Hello World";
  5.  
  6. System.out.println("s = " + s); //Hello World
  7.  
  8. //获取String类中的value字段
  9. Field valueFieldOfString = String.class.getDeclaredField("value");
  10.  
  11. //改变value属性的访问权限
  12. valueFieldOfString.setAccessible(true);
  13.  
  14. //获取s对象上的value属性的值
  15. char[] value = (char[]) valueFieldOfString.get(s);
  16.  
  17. //改变value所引用的数组中的第5个字符
  18. value[5] = '_';
  19.  
  20. System.out.println("s = " + s); //Hello_World
  21. }

打印结果为:

s = Hello World
s = Hello_World

在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。·员变量的值,改变对象的成员变量,调用对象的方法等。而在C++中存在引用,对象和指针三个东西,这三个东西都可以访问对象。其实,Java中的引用和C++中的指针在概念上是相似的,他们都是存放的对象在内存中的地址值,只是在Java中,引用丧失了部分灵活性,比如Java中的引用不能像C++中的指针那样进行加减运算。

Java中的String为什么是不可变的? — String源码分析的更多相关文章

  1. Java并发编程笔记之Unsafe类和LockSupport类源码分析

    一.Unsafe类的源码分析 JDK的rt.jar包中的Unsafe类提供了硬件级别的原子操作,Unsafe里面的方法都是native方法,通过使用JNI的方式来访问本地C++实现库. rt.jar ...

  2. java 集合与数组的互转方法,与源码分析

    前言 java数组与集合需要互相转换的场景非常多,但是运用不好还是容易抛出UnSupportedOperationException.下面讲解一下互转的方法,以及结合源码分异常产生的原因 集合转数组 ...

  3. SqlAlchemy 中操作数据库时session和scoped_session的区别(源码分析)

    原生session: from sqlalchemy.orm import sessionmaker from sqlalchemy import create_engine from sqlalch ...

  4. netty中的发动机--EventLoop及其实现类NioEventLoop的源码分析

    EventLoop 在之前介绍Bootstrap的初始化以及启动过程时,我们多次接触了NioEventLoopGroup这个类,关于这个类的理解,还需要了解netty的线程模型.NioEventLoo ...

  5. Java 自动拆箱 装箱 包装类的缓存问题--结合源码分析

    都0202 了 java 1.8 已经是主流 自动装箱 .拆箱已经很普遍使用了,那么有时候是不是会遇到坑呢? 我们先来看一段代码: public class TestWraperClass { pub ...

  6. Java I/O系列(二)ByteArrayInputStream与ByteArrayOutputStream源码分析及理解

    1. ByteArrayInputStream 定义 继承了InputStream,数据源是内置的byte数组buf,那read ()方法的使命(读取一个个字节出来),在ByteArrayInputS ...

  7. Java I/O系列(一)InputStream与OutputStream源码分析及理解

    1. InputStream 定义 字节输入流,是一个抽象类,核心是通过read()方法,从数据源中读取一个个字节出来,另有skip,mark功能 核心源码理解 源码: public abstract ...

  8. Disruptor中shutdown方法失效,及产生的不确定性源码分析

    版权声明:原创作品,谢绝转载!否则将追究法律责任. Disruptor框架是一个优秀的并发框架,利用RingBuffer中的预分配内存实现内存的可重复利用,降低了GC的频率. 具体关于Disrupto ...

  9. Java并发基础:了解无锁CAS就从源码分析

    https://segmentfault.com/a/1190000015881923

  10. 死磕 java并发包之AtomicStampedReference源码分析(ABA问题详解)

    问题 (1)什么是ABA? (2)ABA的危害? (3)ABA的解决方法? (4)AtomicStampedReference是什么? (5)AtomicStampedReference是怎么解决AB ...

随机推荐

  1. window.open()与window.showModalDialog区别

    window.open()与window.showModalDialog区别 弹出窗口两种方式:    1.window.showModalDialog:      var feature = &qu ...

  2. POJ 3675 Telescope(简单多边形和圆的面积交)

    Description Updog is watching a plane object with a telescope. The field of vision in the telescope ...

  3. ZOJ 3689 Digging(DP)

    Description When it comes to the Maya Civilization, we can quickly remind of a term called the end o ...

  4. vue移动音乐app开发学习(三):轮播图组件的开发

    本系列文章是为了记录学习中的知识点,便于后期自己观看.如果有需要的同学请登录慕课网,找到Vue 2.0 高级实战-开发移动端音乐WebApp进行观看,传送门. 完成后的页面状态以及项目结构如下: 一: ...

  5. Python字符串中的r前缀

    在Python中,如果字符串的前面有r/R前缀,那么,就会禁用转义符\的功能: >>>path = r'C:\new\text.dat' >>>pah 'C:\\n ...

  6. “Hello world!”团队—团队选题展示(视频展示说明)

    本次博客的主要内容基本分为以下两方面: 一.视频截图展示 二.视频简要说明 博客内容展示: 视频截图1: 简要说明:这是组长在视频前期简要介绍我们这款游戏项目的内容.从可行性和需求市场方面进行了简要阐 ...

  7. sql分页使用join提高性能

    今天在分析系统中的分页sql时意外知道了使用join可以提高分页性能. 逻辑是join部分使用单一表,单一字段排序分页,然后join大表.

  8. hexo设置permalink-避免url中出现中文

    hexo博客初始化的url是年月日+题目:year/:month/:day/:title/,这样的url不便与分享,中文会乱吗,而且一旦修改了题目(我相信大部分人的题目都是中文)就会导致之前分享的ur ...

  9. Divide two integers without using multiplication, division and mod operator.

    描述 不能使用乘法.除法和取模(mod)等运算,除开两个数得到结果,如果内存溢出则返回Integer类型的最大值.解释一下就是:输入两个数,第一个数是被除数dividend,第二个是除数divisor ...

  10. 转 Redis集群技术及Codis实践

    转  Redis集群技术及Codis实践 转自 :http://blog.51cto.com/navyaijm/1637688 codis开源地址:https://github.com/CodisLa ...