关于String相关知识点

String

String不可变的理解与原因

在面向对象编程中,对象不可变指的是对象被创建后,其状态不可在变化。在Java中通过关键字final来实现变量的不可变。举个例子,比如:

String str = "A";
System.out.print(str);  //A
str = "B";
System.out.print(str);  //B

从以上来看似乎String类型是可变的(因为str的值变了啊),这样理解是错误的,我们从内剖析来看。在JVM中提到,对象的引用放在虚拟机栈中,对象放Java堆中(该部分有兴趣可以去了解)。
String str = "A"在JVM中可以简单地分解为以下步骤:

  1. 在栈中创建一个String对象的引用str;
  2. 在String常量池中创建字符串“A”对象实例(字符串池中不存在“A”字符串,所以会创建实例,后面会解释);
  3. 对象引用str指向字符串池中“A"的对象实例。
    图1
    str = "B"的步骤为:
  4. 在String常量池中创建字符串“B”对象实例
  5. 对象引用str指向字符串池中“B"的对象实例
    图2
    从这我们可以清楚看到,str的引用指向了一个新的对象实例“B”内存地址,所以我们不能从这说明String的值发生变化了。
    回到开头说的,对象不可变指创建对象后,其状态不可变。查看String源码
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];   //字符数组

我们发现String的字符串是由字符数组char组成,所以假设我们要使String可变化,那就可以通过set方法,或者对象.属性改变其状态。但因为String对象的value属性是私有的,也没有暴露set方法,最主要的原因是因为value被关键字final所修饰。final关键字可修饰类,方法与变量(区别在此不谈)。
当final修饰变量时,该变量只能被赋值一次,赋值后不可在变,也就是说明,final修饰的变量必须要显示初始化,初始化后不可再变化,这就导致了String对象不可变的原因。

String两种创建方式的区别

我们经常使用的字符串创建方式是

String str = "A";

还有一种初始化创建方式是通过new String();

String newStr = new String("A");

两者是有区别的,我们举个例子:

String str1 = "A";
String str2 = "A“;
String str3 = new String("A");  
System.out.print(str1 == str2); // true
System.out.print(str3== str1);  //false

一步一步来理解。我们第一节讲过String常量池,字符串常量池是Java设计者为提高性能所设计的。当要实例化字符串时,都会在字符串常量池中判断是否存在该字符串。存在则返回字符串引用,不存在则实例化字符串放在字符串常量池中。
所以当执行String str1 = "A"时,字符串常量池中不存在“A”,所以会创建该实例存储在字符串常量池中,当执行String str2 = “A"时,字符串常量池中已存在"A”,所以直接返回该实例。
在这里插入图片描述
所以两引用指向的都是同一对象,所以System.out.print(str1 == str2); 输出为true。

String str3 = new String("A");  

通过new String(“XXX”)方式实例化字符串有点特别,我们查看其构造方法源码:

    /**
     * Initializes a newly created {@code String} object so that it represents
     * the same sequence of characters as the argument; in other words, the
     * newly created string is a copy of the argument string. Unless an
     * explicit copy of {@code original} is needed, use of this constructor is
     * unnecessary since Strings are immutable.
     *
     * @param  original
     *         A {@code String}
     */
    public String(String original) {
            this.value = original.value;
            this.hash = original.hash;
    }

我们查看该构造方法的注释说明,简单翻译就是该构造方法初始化String对象时,字符数组即value与参数的String对象即original的value的引用指向同一个字符数组,也可以说新创建的String是参数字符串的一个副本。
回到new String("A");来看,这一操作分为两种情况:

  1. 字符串常量池中不存在"A"实例
    在堆中创建一个String实例,并进行相应的初始化,然后执行该构造函数时,会在字符串常量池中实例化一个字符串"A",并将在字符串常量池中的对象"A"的value数组的引用给在堆中新创建的字符串的value,还有相同的hash值。此时可以说创建了两个对象,一个在字符串常量池中,一个在Java堆中。
  2. 字符串常量池中存在"A"实例
    此过程与上面的过程差不多,只是不在字符串常量池中实例化"A"了,直接将已经存在的"A"的value字符数组引用给堆中新创建的String的value。所以只创建了一个对象,即堆中的String。

所以System.out.print(str3== str1); 输出为false,因为str3的实例在堆中,str1的实力在字符串常量池中,比较当然为false,因为不是同一个对象。

关于String hashCode的的补充

查看String的hashCode的方法的源码(JDK8):

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
}

从源码可以看出,因为String重写了equals方法,String的hashCode也需要重写,hashcode的值是根据value所计算出来的,并且只计算一次(在调用hashCode方法的时候计算,否则hash值默认为0)。我们可以通过写例子打断点来看hash值是在调用hashCode方法进行计算后才赋值的:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

从这也验证出上面所讲的,

堆中新建的字符串字符数组即value与参数的String对象即original的value的引用指向同一个字符数组,也可以说新创建的String是参数字符串的一个副本。

字符数组引用相同

另外补充,不能通过判断hashCode的值相同而断定String的对象是同一个对象(可通过判断hashCode不同而断定不是同一个对象),也不能断定其字符数组引用指向同一对象,即String.equals的判断不能仅仅通过hashCode相等来进来判断字符串的值相等,因为会出现字符串不同,而所计算的hashCode的值是相同的情况。

String str1="Aa";
String str2="BB";
System.out.println(str1.hashCode());   //2112
System.out.println(str2.hashCode());   //2112

举一个例子反证法就能证明该命题的错误。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值