Java核心技术面试精讲-05 从String、StringBuffer、StringBuilder的区别学习可变类与不可变类

1.不可变类(immutable)

1.1 不可变类简介

不可变类:所谓的不可变类是指这个类的实例一旦创建完毕后,就不能改变其成员变量值。
可变类:相对于不可变类,可变类创建实例之后可以改变其成员变量值,开发中创建的大部分类都属于可变类。
Java 中八个基本类型的包装类和 String 类都属于不可变类,而其他的大多数类都属于可变类。

1.2 不可变类的优点

线程安全:不可变对象是线程安全的,在线程之间可以互相共享,不需要利用特殊机制来保证同步问题,因为对象的值无法改变。
降低并发错误可能性:因为不需要用一些锁机制来保证内存一致性问题,同时也降低了同步开销。

2.String对象的不可变性

2.1 不可变设计的五个原则

对于不可变类的设计,有以下五个原则:
1.类添加final修饰符,保证类不可以被继承。
2.保证所有成员变量必须私有,并且加上final进行修饰。
3.不提供改变成员变量的方法,包括setter。
4.通过构造器初始化所有成员,进行深拷贝。
如果构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值。例如:

public final class ImmutableDemo{
	private final int[] myArray;
	public ImmutableDemo(int[] array){
		this.myArray=array;
	}
}

这种方式并不能保证不可变性,myArray和array指向同一块内存地址,用户可以在ImmutableDemo之外通过修改array对象的值来改变myArray内部的值。
为了保证内部的值不被修改,可以采用深度copy来创建一个新内存保存传入的值。正确做法:

public final class ImmutableDemo{
	private final int[] myArray;
	public ImmutableDemo(int[] array){
		this.myArray=array.clone();
	}
}

5.在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝。
这种做法也是防止对象外泄,防止通过getter获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。

2.2 String的不可变实现

String对象在内存创建后就不可改变,不可变对象的创建一般满足以上5个原则,我们看看String代码是如何实现的。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
{//1.类添加final修饰符,保证类不可以被继承。
    /** The value is used for character storage. */
    private final char value[];//保证所有成员变量必须私有,并且加上final修饰
    /** The offset is the first index of the storage that is used. */
    private final int offset;//保证所有成员变量必须私有,并且加上final修饰
    /** The count is the number of characters in the String. */
    private final int count;//保证所有成员变量必须私有,并且加上final修饰
    /** Cache the hash code for the string */
    private int hash; // Default to 0保证所有成员变量必须私有,并且加上final修饰
    ....
    public String(char value[]) {
         this.value = Arrays.copyOf(value, value.length); // deep copy操作通过构造器初始化所有成员,进行深拷贝(deep copy)
     }
    ...
     public char[] toCharArray() {
     // Cannot use Arrays.copyOf because of class initialization order issues
        char result[] = new char[value.length];
        System.arraycopy(value, 0, result, 0, value.length);//5.在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝。
        return result;
    }
    ...
}

如上代码所示,可以观察到以下设计细节:
1.String类被final修饰,不可继承
2.String内部所有成员都设置为私有变量
3.不存在value的setter
4.并将value和offset设置为final。
5.当传入可变数组value[]时,进行copy而不是直接将value[]复制给内部变量.
6.获取value时不是直接返回对象引用,而是返回对象的copy.
这都符合上面总结的不变类型的特性,也保证了String类型是不可变的类。

2.3 String对象不可变的优缺点

从上一节分析,String数据不可变类,那设置这样的特性有什么好处呢?我总结为以下几点:

1.线程安全考虑
同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
2.类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。
譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。
3. 支持hash映射和缓存。
因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

那么缺点是什么呢?
如果有对String对象值改变的需求,那么会创建大量的String对象。

2.4 String对象是否真的不可变?

虽然String对象将value设置为final,并且还通过各种机制保证其成员变量不可改变。但是还是可以通过反射机制的手段改变其指。例如:

//创建字符串“Hello world”并赋给引用s
String s="Hello World";
System.out.println("s="+s);
//获取String类中的value字段
Field valueFieldOfString=String.class.getDeclaredField("value");
//改变value属性的访问权限
valueFieldOfString.setAccessible("true");
//获取s对象上的value属性的值
char[] value=(char [])valueFieldOfString.get(s);
//改变value所引用的数组中的第五个字符
value[5]="_";
System.out.println("s="+s);

打印结果为:

s = Hello World
s = Hello_World

发现String的值已经发生了改变。也就是说,通过反射是可以修改所谓的“不可变”对象的。

2.5 总结

不可变类是实例创建后就不可以改变成员变量值的类。
这种特性使得不可变类提供了线程安全的特性但同时也带来了对象创建的开销,每更改一个属性都是重新创建一个新的对象。JDK内部也提供了很多不可变类如Integer、Double、String等。String的不可变特性主要为了满足常量池、线程安全、类加载的需求。合理使用不可变类可以带来极大的好处。但是这种不可变也不是绝对意义上的不可变,使用诸如反射这样的技术还是可以修改这种“不可变”对象的

2.String、StringBuffer以及StringBuilder的区别与联系

String是Java语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。
StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。
StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。

3.StringBuffer以及StringBuilder的实现原理

我们再来看看 StringBuffer 实现的一些细节,它的线程安全是通过把各种修改数据的方法都加上 synchronized 关键字实现的,非常直白。
为了实现修改字符序列的目的,StringBuffer 和 StringBuilder 底层都是利用可修改的(char,JDK 9 以后是 byte)数组,二者都继承了 AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了 synchronized。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值