java string不可更改_Java中的String是不可变的原因

1、对象不可变定义

不可变对象是指对象的状态在被初始化以后,在整个对象的生命周期内,不可改变。

Java中String类就是不可变的,简单来说,一个不可变的类就意味着他的实例是不可修改的,实例的所有信息都是在实例创建的时候被初始化并且不可被修改。不可变类的设计有很多优点。

2、如何不可变

通常情况下,在java中通过以下步骤实现不可变:

1、对于属性不提供设值方法

2、所有的属性定义为private final

3、类声明为final不允许继承

4、Return deep cloned objects with copied content for all mutable fields in class

注意:不用final关键字也可以实现对象不可变,使用final只是显示的声明,提示开发者和编译器为不可变。

3、Java中典型的不可变类为String类

为什么String被设计为不可变?

1、安全:首要原因是安全,不仅仅体现在你的应用中,而且在JDK中,Java的类装载机制通过传递的参数(通常是类名)加载类,这些类名在类路径下,想象一下,假设String是可变的,一些人通过自定义类装载机制分分钟黑掉应用。如果没有了安全,Java不会走到今天

2、性能:string不可变的设计出于性能考虑,当然背后的原理是string pool,当然string pool不可能使string类不可变,不可变的string更好的提高性能。

3、线程安全:当多线程访问时,不可变对象是线程安全的,不需要什么高深的逻辑解释,如果对象不可变,线程也不能改变它。

4、为什么String是不可变

区分对象和对象的引用

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

String s = "ABCabc";

System.out.println("s = " + s);

s = "123456";

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”还在内存中存在,并没有改变。内存结构如下图所示:

73092d45b4bb

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

要理解String的不可变性,首先看一下String类中都有哪些成员变量。 在JDK1.6中,String的成员变量有以下几个:

public final class String

implements java.io.Serializable, Comparable, CharSequence

{

/** The value is used for character storage. */

private final char value[];

/** The offset is the first index of the storage that is used. */

private final int offset;

/** The count is the number of characters in the String. */

private final int count;

/** Cache the hash code for the string */

private int hash; // Default to 0

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

public final class String

implements java.io.Serializable, Comparable, 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

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

73092d45b4bb

value,offset和count这三个变量都是private的,并且没有提供setValue, setOffset和setCount等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改, 并且在String类的外部不能访问这三个成员。此外,value,offset和count这三个变量都是final的, 也就是说在String类内部,一旦这三个值初始化了, 也不能被改变。所以可以认为String对象是不可变的了。

那么在String中,明明存在一些方法,调用他们可以得到改变后的值。这些方法包括substring, replace, replaceAll, toLowerCase等。例如如下代码:

String a = "ABCabc";

System.out.println("a = " + a);

a = a.replace('A', 'a');

System.out.println("a = " + a);

输出结果:

a = ABCabc

a = aBCabc

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

String ss = "123456";

System.out.println("ss = " + ss);

ss.replace('1', '0');

System.out.println("ss = " + ss);

打印结果:

ss = 123456

ss = 123456

5、从内存,同步和数据结构的角度来具体说明String这种不可变的概念

String Pool的需要

String Pool(String intern pool)是在方法区的一块特殊存储区域。当一个String被创建时如果发现当前String已经存在于String Pool,则会返回一个已存在String的引用而不会新建一个对象。

以下代码只会创建一个String对象在堆内存中。

String string1 = "abcd";

String string2 = "abcd";

下图是创建的过程:

73092d45b4bb

如果一个String是可变的,改变了一个引用指向的String会导致其他引用得到错误的值。

缓存Hashcode

在Java中,对于String的Hashcode使用是非常频繁的,例如在HashMap或HashSet中。将String设计成不可变可以保证他的Hashcode始终一致,这样Hashcode就可以被缓存并且不用担心变化。这就意味着,不需要在每次使用String的时候都去计算他的Hashcode,这也使得程序运行的更加高效。

在String类中,关于Hashcode的代码如下

private int hash;//this is used to cache hash code.

简化其他对象的使用

为了更加详细的阐述,我们考虑以下程序:

HashSet set = new HashSet();

set.add(new String("a"));

set.add(new String("b"));

set.add(new String("c"));

for(String a: set)

a.value = "a";

在这个例子中,如果String是可变的,那么就会违背set的设计初衷(set包含不重复的元素)。当然,上面的例子只是为了论证,实际上String类中没有value这个字段。

安全

String在很多Java类中被广泛用作参数,例如网络连接,文件打开等。假设String是可变的,一个连接或者一个文件就可能被改变,这会导致严重的安全隐患。某个方法以为正在连接到一个机器,实际并没有。可变的String还可能在反射的时候引发安全问题,因为反射的参数类型也是String。

以下是代码示例:

boolean connect(string s){

if (!isSecure(s)) {

throw new SecurityException();

}

//here will cause problem, if s is changed before this by using other references.

causeProblem(s);

}

不可变对象天生线程安全

因为不可变对象不能被改变,他们可以在多线程中被自由的共享,这就消除了对象同步的需求。

那么从这个层面来说,String被设计成不可变的出发点是效率和安全。这也是不可变类在很多情况下被优先使用的原因。

String对象真的不可变吗?

从上文可知String的成员变量是private final 的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗? 比如将数组中的某个位置上的字符变为下划线“_”。 至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组。

那么用什么方式可以访问私有成员呢? 没错,用反射, 可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。下面是实例代码:

public static void testReflection() throws Exception {

//创建字符串"Hello World", 并赋给引用s

String s = "Hello World";

System.out.println("s = " + s); //Hello World

//获取String类中的value字段

Field valueFieldOfString = String.class.getDeclaredField("value");

//改变value属性的访问权限

valueFieldOfString.setAccessible(true);

//获取s对象上的value属性的值

char[] value = (char[]) valueFieldOfString.get(s);

//改变value所引用的数组中的第5个字符

value[5] = '_';

System.out.println("s = " + s); //Hello_World

}

打印结果为:

s = Hello World

s = Hello_World

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值