1. 什么是不可变对象?

      众所周知, 在Java中, String类是不可变的。那么到底什么是不可变的对象呢?

  可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。

  不能改变状态的意思是:不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。

2. 区分对象和对象的引用

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

  1. String s = "ABCabc";
  2. System.out.println("s = " + s);
  3. s = "123456";
  4. 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++中的指针那样进行加减运算。

3. 为什么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中,数组也是对象。

  所以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);

上面代码反编译结果如下:

  1. String s = "ABCabc";
  2. System.out.println((new StringBuilder()).append("a= ").append(s).toString());
  3. s = s.replace('A', 'a');
  4. System.out.println((new StringBuilder()).append("a= ").append(s).toString());

打印结果为:

  1. a = ABCabc
  2. a = aBCabc

那么a的值看似改变了,其实也是同样的误区。再次说明, a只是一个引用, 不是真正的字符串对象,在调用a.replace('A','a')时, 方法内部创建了一个新的String对象,并把这个新的对象重新赋给了引用a。String中replace方法的源码可以说明问题:

  1. /**
  2. * Copies this string replacing occurrences of the specified character with
  3. * another character.
  4. *
  5. * @param oldChar
  6. * the character to replace.
  7. * @param newChar
  8. * the replacement character.
  9. * @return a new string with occurrences of oldChar replaced by newChar.
  10. */
  11. public String replace(char oldChar, char newChar) {
  12. char[] buffer = value;
  13. int _offset = offset;
  14. int _count = count;
  15. int idx = _offset;
  16. int last = _offset + _count;
  17. boolean copied = false;
  18. while (idx < last) {
  19. if (buffer[idx] == oldChar) {
  20. if (!copied) {
  21. char[] newBuffer = new char[_count];
  22. System.arraycopy(buffer, _offset, newBuffer, 0, _count);
  23. buffer = newBuffer;
  24. idx -= _offset;
  25. last -= _offset;
  26. copied = true;
  27. }
  28. buffer[idx] = newChar;
  29. }
  30. idx++;
  31. }
  32. return copied ? new String(0, count, buffer) : this;
  33. }

     读者可以自己查看其他方法,都是在方法内部重新创建新的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);

打印结果:

  1. ss = 123456
  2. ss = 123456

4. 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 =
  7. String.class.getDeclaredField("value");
  8. //改变value属性的访问权限
  9. valueFieldOfString.setAccessible(true);
  10. //获取s对象上的value属性的值
  11. char[] value = (char[]) valueFieldOfString.get(s);
  12. //改变value所引用的数组中的第5个字符
  13. value[5] = '_';
  14. System.out.println("s = " + s); //Hello_World
  15. }

打印结果如下:

  1. s = Hello World
  2. s = Hello_World

      在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的"不可变"对象的。但是一般我们不这么做。

  这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。

Java基础知识强化101:Java 中的 String对象真的不可变吗 ?的更多相关文章

  1. Java基础知识强化之集合框架笔记32:集合之可变参数的概述和使用

    1. 可变参数的概述和使用: (1)可变参数:定义方法的时候不知道该定义多少个参数(2)格式:     修饰符  返回值类型  方法名(数据类型… 变量名){   }  注意: 这里的变量其实是一个数 ...

  2. Java基础知识强化之集合框架笔记76:ConcurrentHashMap之 ConcurrentHashMap简介

    1. ConcurrentHashMap简介: ConcurrentHashMap是一个线程安全的Hash Table,它的主要功能是提供了一组和Hashtable功能相同但是线程安全的方法.Conc ...

  3. java基础知识回顾之---java String final类普通方法

    辞职了,最近一段时间在找工作,把在大二的时候学习java基础知识回顾下,拿出来跟大家分享,如果有问题,欢迎大家的指正. /*     * 按照面向对象的思想对字符串进行功能分类.     *      ...

  4. Java基础知识强化之集合框架笔记27:ArrayList集合练习之去除ArrayList集合中的重复字符串元素

    1. 去除ArrayList集合中的重复字符串元素(字符串内容相同) 分析: (1)创建集合对象 (2)添加多个字符串元素(包含重复的) (3)创建新的集合 (4)遍历旧集合,获取得到每一个元素 (5 ...

  5. Java基础知识强化03:Java中的堆与栈

    1.在JVM中,内存分为两个部分,Stack(栈)和Heap(堆),这里,我们从JVM的内存管理原理的角度来认识Stack和Heap,并通过这些原理认清Java中静态方法和静态属性的问题. 一般,JV ...

  6. Java基础知识强化69:基本类型包装类之Character案例(统计字符串中大写小写以及数字的次数)

    我们直接看案例如下: package cn.itcast_03; import java.util.Scanner; /* * 统计一个字符串中大写字母字符,小写字母字符,数字字符出现的次数.(不考虑 ...

  7. Java基础知识强化24:Java中异常

    1.什么是异常 ?       Java程序运行中,常常会遇到非正常的现象,这种情况称为运行错误.根据性质可以分为错误和异常.Java程序中(无论谁写的代码),所有抛出(throw)的异常都必须从Th ...

  8. Java基础知识强化之多线程笔记01:多线程基础知识(详见Android(java)笔记61~76)

    1. 基础知识: Android(java)学习笔记61:多线程程序的引入    ~    Android(java)学习笔记76:多线程-定时器概述和使用 

  9. Java基础知识强化14:Java死亡竞赛题目解析

      一个小型网站上发布了一个称为Java“死亡竞赛”的新项目.测验发布后,超过20000位开发者参加了测验.网站以20道关于Java的多选题为主.我们得到了众多开发者的测验统计数据,今天,我们非常乐意 ...

随机推荐

  1. leetcode—Swap Nodes in Pairs

    1.题目描述 Given a linked list, swap every two adjacent nodes and return its head.   For example, Given ...

  2. 将大数据利用 BCP 导出SqlServer数据到CSV

    --导出数据 --BCP [数据库]..[数据库表] out "d:\abc.csv" -c -t "|" -T bcp "SELECT * FROM ...

  3. linux appear packet loss solution

    故障排查: 早上突然收到nagios服务器check_icmp的报警,报警显示一台网站服务器的内网网络有问题.因为那台服务器挂载了内网的NFS,因此内网的网络就采用nagios的check_icmp来 ...

  4. HDU4456-Crowd(坐标旋转+二位树状数组+离散化)

    转自:http://blog.csdn.net/sdj222555/article/details/10828607 大意就是给出一个矩阵 初始每个位置上的值都为0 然后有两种操作 一种是更改某个位置 ...

  5. MVC使用Google OAuth[OWIN]注意事項

    1.前提條件,申請一個client id,頁面:https://console.developers.google.com/ 2.添加連接域名,javascript那欄位為域名即可,另一欄需要加上具體 ...

  6. BAT-使用BAT方法删除目录下0KB文件

    @Echo Off For /f "tokens=*" %%i in ('dir /a-d /b /s "*.*"') do ( If " (Del ...

  7. C++问题-无法打开某个自定义源文件

    问题经过:需要做一个工具,是在某个产品的基础上做的,所以要来了同事的代码.用VS打开后,提示如下问题.1>c1xx : fatal error C1083: 无法打开源文件:“..\..\GUX ...

  8. POJ 2828 Buy Tickets (线段树 or 树状数组+二分)

    题目链接:http://poj.org/problem?id=2828 题意就是给你n个人,然后每个人按顺序插队,问你最终的顺序是怎么样的. 反过来做就很容易了,从最后一个人开始推,最后一个人位置很容 ...

  9. POJ 3671 Dining Cows (DP,LIS, 暴力)

    题意:给定 n 个数,让你修改最少的数,使得这是一个不下降序列. 析:和3670一思路,就是一个LIS,也可以直接暴力,因为只有两个数,所以可以枚举在哪分界,左边是1,右边是2,更新答案. 代码如下: ...

  10. iOS开发60分钟入门

    原文:https://github.com/qinjx/30min_guides/blob/master/ios.md 本文面向已有其它语言(如Java,C,PHP,Javascript)编程经验的i ...