不可变类:String

目录

1、不可变类简介

2、不可变类的设计方法

3、String对象的不可变性

4、String对象的不可变性的优缺点

5、String和StringBuilder和StringBuffer的效率比较?

5.1、StringBuilder的设计

6、小结

7、问题思考

1、不可变类简介

  • 不可变类:所谓的不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值。如JDK内部自带的很多不可变类:Interger、Long和String等。

  • 可变类:相对于不可变类,可变类创建实例后可以改变其成员变量值,开发中创建的大部分类都属于可变类。

2、不可变类的设计方法

对于设计不可变类,有以下原则和方法:

(1). 类添加final修饰符,保证类不被继承。

  • 如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。

(2). 保证所有成员变量必须私有private,并且加上final修饰

  • 通过这种方式保证成员变量不可改变。但只做到这一步还不够,因为如果是对象成员变量有可能再外部改变其值。所以第4点弥补这个不足。

(3). 不提供改变成员变量的方法,包括setter

  • 避免通过其他接口改变成员变量的值,破坏不可变特性。

(4). 通过构造器初始化所有成员,进行深拷贝(deep copy)

  • 如果构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值。

(5). 在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝

  • 这种做法也是防止对象外泄,防止通过getter获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。

3、String对象的不可变性

查看String类的设计,可以观察到以下设计细节:

  • (1). String类被final修饰,不可继承;

  • (2). String内部所有成员都设置为不可变final和变为私有变量private修饰;

  • (3). 不存在value的setter;

  • (4). 当传入可变数组value[]时,进行深拷贝copy而不是直接将value[]复制给内部变量.

  • (5). 获取value时不是直接返回对象引用,而是返回对象的copy.

//final修饰类,不可继承
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    //final修饰变量value,不可改变,属性为private;
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
    ....
    public String(char value[]) {
         this.value = Arrays.copyOf(value, value.length); // 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);  //深拷贝
        return result;
    }
    ...
}

这都符合上面总结的不变类型的特性,也保证了String类型是不可变的类。

4、String对象的不可变性的优缺点

优点:

(1).字符串常量池的需要.

  • 字符串常量池可以将一些字符常量放在常量池中重复使用,避免每次都重新创建相同的对象、节省存储空间。但如果字符串是可变的,此时相同内容的String还指向常量池的同一个内存空间,当某个变量改变了该内存的值时,其他遍历的值也会发生改变。所以不符合常量池设计的初衷。

(2).线程安全考虑。

  • 同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。

(3). 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。

  • 譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。

(4). 支持hash映射和缓存。

  • 因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

缺点:

    如果有对String对象值改变的需求,那么会创建大量的String对象,产生大量的jvm垃圾碎片,占用内存,触发gc。

5、String和StringBuilder和StringBuffer的效率比较?

实例验证:

public static void main(String[] args) {
    String str = "";
    long start1 = new Date().getTime();
    for (int i = 0; i < 10000; i++) {
        str += i;
    }
    System.out.println("使用String生成耗时: " + (new Date().getTime() - start1));

    StringBuilder builder = new StringBuilder();
    long start2 = new Date().getTime();
    for (int i = 0; i < 10000; i++) {
        builder.append(i);
    }
    System.out.println("使用StringBuilder生成耗时: " + (new Date().getTime() - start2));

result:

使用String生成耗时: 542
使用StringBuilder生成耗时: 1
使用StringBuffer生成耗时: 1

分析:从结果可以看出,StringBuilder和StringBuilder比String的效率要高很多。

    这是因为在String在java中是不可变长的,一旦初始化就不能修改长度,简单的字符串拼接就是新建的String对象,再把拼接后的内容赋值给新的对象,在频繁修改的前提下会频繁创建对象,而SpringBuiler则不会,从头到尾都是一个对象,那StringBuilder是怎么实现的呢?

5.1、StringBuilder的设计

    从StringBuilder的源码中可以看出,使用StringBuilder时并不是用String存储,而是使用了一个value的char[] 数组来存储,字符串是固定长度的,而数组是可以扩容的,这样就不需要每次都创建一个新的对象了。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;  //存储数据的数组对象;

    /**
     * The count is the number of characters used.
     */
    int count;    //数据中使用的字符的大小;
 
    /**
     * This no-arg constructor is necessary for serialization of subclasses.
     */
    AbstractStringBuilder() {
    }

    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {  //创建初始化大小;
        value = new char[capacity];
    }

可以看到StringBuilder数组元素的原始大小为16.

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    /** use serialVersionUID for interoperability */
    static final long serialVersionUID = 4383685877147921099L;

    /**
     * Constructs a string builder with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuilder() {
        super(16);
    }

扩容的条件minimumCapacity = (append的数据长度+value.count) > value.length数组的长度的时候进行扩容。

private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code 扩容条件:minimumCapacity = (append的数据长度+value.count)> value.length
    if (minimumCapacity - value.length > 0) {
        value = Arrays.copyOf(value,
                newCapacity(minimumCapacity));
    }
}

扩容的大小value.length*2 + 2;

private int newCapacity(int minCapacity) {
    // overflow-conscious code 扩容的大小为:value.length*2 + 2;
    int newCapacity = (value.length << 1) + 2;
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
        ? hugeCapacity(minCapacity)
        : newCapacity;
}

6、小结

String、StringBuilder、StringBuffer的区别:

  • String 是不可变的,任何改变都会产生新的对象,如果改变频繁将产生很多String垃圾碎片,增加JVM的回收负担,占用大量的内存;

  • StringBuilder是可变类,通过可变数组来存储,但不是线程安全的;

  • StringBuffer也是可变类,是线程安全的,使用了Synchronized对操作进行同步控制;

StringBuilder和StringBuffer的关系类似于Hashmap和Hashtable的关系。

使用场景:

  • 字符串相加操作和改动较少的时候用引号的形式;

  • 多线程:StringBuffer;append是一个同步方法;

  • 单线程但是改动较多:StringBuilder;

7、问题思考

问题一:String str = new String(“abc”) 创建了几个对象?

分情况:

(1) 如果常量池中有”abc”存在,则创建一个堆上的返回;

(2) 如果常量池中没有“abc”存在,则创建两个如下:

  • (1) 使用引号""在编译器就确定了,放在常量池区;

  • (2) 使用new String(“”) 创建的对象存储在堆上,是运行期间创建的,返回的是堆上的数据的引用地址;

解析:可以看成是四步:

  • String  str :就是定义一个string的变量str,没有创建对象;

  • = : 赋值操作;将某个对象的引用(或者叫句柄)赋值给它,没有创建对象;

  • "abc"  :创建一个"acc"对象,常量池里有就不创建,否则创建放入常量池;

  •  new String() : 在堆上创建一个abc对象;

问题二:String str =“king” + “zz” 创建了几个对象?

    反编译查看编译器会优化 [常量折叠] 对象的产生,创建1个(kingzz)字符串对象,放在java堆的常量池中

问题三:String str = new String(“king”) + new String(“zz”) 创建了几个对象?

    5个字符串对象,3个("king","zz","kingzz")在java堆中,2个("king","zz")在java堆的常量池中。

问题四:什么是常量折叠?

常量折叠:是Java在编译期做的一个优化,简单的来说,在编译期就把一些表达式计算好,不需要在运行时进行计算。

比如: int a = 1 + 2,经过常量折叠后就变成了int a = 3。

“常量折叠”并不是所有的常量都会进行折叠,必须是编译期常量之间进行运算才会进行常量折叠,编译器常量就是编译时就能确定其值的常量,这个定义很严格,需要满足以下条件:

  • 1. 字面量是编译期常量(数字字面量,字符串字面量等)。

  • 2. 编译期常量进行简单运算的结果也是编译期常量,如1+2,”hello”+”world”。

  • 3. 被编译器常量赋值的 final 的基本类型和字符串变量也是编译期常量。因为final修饰的变量是不能被修改的,所以可以在编译期间确定。

问题五:String.intern()的使用?

  • (1) 直接使用双引号申明出来的String对像会直接存储在常量池;

  • (2) 如果不是使用双引号,则有String.intern()函数,将该字符串放入到常量池中,如果常量池中存在该字符串则返回该字符串的引用,如果不存在则在常量池中创建一个字符串,然后返回该字符串的引用。

  • (3) new会在堆上创建对象;

//todo string.intern()的应用
String s1 = new String("kinglovezz");
String s2 = s1.intern(); //将"kinglovezz放入常量池"
String s3 = "kinglovezz";
System.out.println(s2); //kinglovezz
System.out.println(s1 == s2);//false,因为一个是堆内存中的String对象一个是常量池中的String对象,
System.out.println(s3 == s2);//true,因为两个都是常量池中的String对象
注意:string是不可变类型,创建后不可改变,改变的只是变量的引用?
String s1 = "king";  //常量池
       s1="zz";      //只是改变了s1的引用,“king”并没有改变,原封不动的坐在常量池里
String s2 = "love";  //常量池
String s3 = new String("love");  //堆

实例验证:

public static void main(String[] args) {
    String s1 = new String("kinglovezz");//创建2个对象,一个Class和一个堆里面
    String s2 = "kinglovezz"; //创建1个对象,s2指向pool里面的"kinglovezz"对象
    String s3 = "kinglovezz"; //创建0个对象,指向s2指想pool里面的那个对象
    String s4 = s2;           //创建0个对象,指向s2,s3指想pool里面的那个对象
    String s5 = new String("kinglovezz");//创建1个对象在堆里面,注意,与s1没关系
    String s6 = s1.intern();

    System.out.println(s2 == "kinglovezz");//true s2=="kinglovezz"很明显true
    System.out.println(s2 == s3);    //true,因为指向的都是pool里面的那个"kinglovezz"
    System.out.println(s2 == s4);    //true,同上,那么s3和s4...:)
    System.out.println(s1 == s5);    //false,很明显,false
    System.out.println(s1 == s2);    //false,指向的对象不一样,下面再说
    System.out.println(s1 == "kinglovezz");//false,难道s1!="tset"?下面再说
    System.out.println(s6 == s2);    //true 都是指向常量池

    System.out.println("---------------");

    s1 = s2;
    System.out.println(s1 == "kinglovezz");//true, s2 == s3 == s4 == s6
}

 

 

 

水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值