new String(“xxxx“)到底创建了几个对象?

在这里插入图片描述

一、问题抛出:

我相信每个程序员可能或多或少被这个问题折磨过,比如遇到下面的问题

public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abc";
        String str3 = new String("abc");
        String str4 = new String("abc");
        System.out.println(str1 == str2);// true
        System.out.println(str2 == str3);// false
        System.out.println(str3 == str4);// false
}

如果你的答案完全正确,那么再试试看下面这个问题呢

public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "ab" + "c";
        String str3 = "a" + "b" + "c";
        System.out.println(str1 == str2);//true
        System.out.println(str1 == str3);//true
    }

如果你对这个结果有点疑惑的话,看完下面的内容你就知道为什么了

二、字符串常量池

首先需要解释一个东西,那就是字符串常量池,之所以设置这个东西是为了提高性能和减少内存开销,相当于给字符串开辟一个常量池空间类似于缓存区,需要注意是JDK1.7和1.8之间,其存在的位置发送变化,1.7的时候在方法区,1.8的时候移动到了堆中

换句话说不管采用直接赋值的方式(String str1 = “abc”)还是采用new对象的方式(new String(“abc”),它们都会判断字符串常量池中是否存在该字符串,如果不存在,则创建然后引用,否则直接引用,两者的区别在于引用的过程不一样

引用情况如下图
在这里插入图片描述
从图中我们可以回答最开始问题

直接赋值的str1和str2都是直接引用的字符串常量池中的“abc”,所以它俩的引用地址自然是一样的,也即是str1 = str2

采用new对象的方式,会首先在堆中创建对象,然后再去常量池中搜索“abc”,然后将引用给到堆中的对象,而栈中只存在指向该对象的引用,因为每次new都会在堆中创建一个新对象,那么自然堆中的地址自然不一样,也就是str3 != str4

同理str2也不可能等于str3

到这里为止我们算是基本弄明白这个引用关系

三、String创建对象的流程

主要的流程上面已经将了,总结来说就是两种情况:

如果说采用直接赋值的方式,会先去常量池中寻找有没有相等的字符串,如果没有则创建该字符串(创建一个字符串对象),然后将常量池的引用指向栈帧中的的变量,如果存在相等的字符串,则直接赋值引用(创建0个字符串对象)

如果采用new对象的方法,则首先会在堆中创建一个对象,然后再去常量池中寻找有没有相等的字符串,如果没有则创建该字符串,然后将常量池的引用指向赋值给堆中的对象,而栈帧中的变量只保存堆中对象的引用(整个过程创建了两个对象),而如果常量池中存在该字符串,则直接赋值引用(整个过程只创建一个对象,就是堆中的对象)

四、intern()方法

这个地方有个方法需要注意,那就是intern()方法,先来看一下下面这段代码

public static void main(String[] args) {
        String str1 = new String("abc");
        String str2 = str1.intern();
        String str3 = str1;
        String str4 = "abc";
        System.out.println(str1 == str2);// false
        System.out.println(str2 == str3);// false
        System.out.println(str1 == str3);//true
        System.out.println(str1 == str4);//false
        System.out.println(str2 == str4);//true
}

可能有疑惑的地方在于str1 == str2 为什么false,而str2==str4为什么是true这点上,所以我们就来看一下这个方法,首先先看一下注释

    /**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */
    public native String intern();

翻译过来的话就是当调用intern方法时,如果池中已经包含由equals(object)方法确定的等于此string对象的字符串,则返回池中的字符串。否则,此String对象将被添加到池中,并返回对此String对象的引用。

如果不太理解的话,可以看下面的图

在这里插入图片描述
我们从图中可以看到intern()会返回字符串常量池中字符串的引用,因此就会出现上面“反常”的结果

需要注意的是,这个方法在JDK1.7和JDK1.8存在差异:

JDK1.7:调用这个方法的时候,会去常量池中查看是否已经存在这个常量了,如果已经存在,那么直接返回这个常量在常量池中的地址值,如果不存在,则在常量池中创建一个,并返回其地址值。

JDK1.8:常量池从方法区搬到了堆区。intern检测到这个常量在常量池中不存在的时候,不会直接在常量池中创建该对象了,而是将堆中的这个对象的引用直接存到常量池中,减少内存开销。

五、编译器优化

当目前为止,还没有解答最开始抛出的第二个问题,毕竟按照String是常量来进行分析,String str2 = “ab”+“c”;肯定会创造ab,c两个对象,str3肯定会创造a,b,c三个对象,但是事实却不是

编译器会对代码进行优化,会直接优化成abc,也因此对于数量较少的拼接,直接使用+来进行字符串拼接效率并不低,所以对于较少数量的字符串拼接,大胆的使用+号吧!

六、变量拼接

先看一下下面的代码

public static void main(String[] args) {
        String str1 = "abc2";
        String str2 = "abc";
        String str4 = str2 + "2";
        String str5 = "abc" + "2";
        System.out.println(str1==str4);
        System.out.println(str1==str5);
    }

经过上一节对编译器优化的分析,可以知道str1==str5的结果为true,那str1 == str4的结果呢?

答案是false

也许有的人会有疑问,str2不就是指向“abc”吗,为什么结果不一样呢?我们知道String对象的char[]数组被final修饰,所以任何的拼接操作都会创造一个新的String对象,然后将引用关系赋值到新对象中,所以这里str + “2”
会在堆中创建一个新对象,而str4保存的就是这个新对象的引用,自然str1就不等于str4了

那有没有办法让str1等于str4呢?答案是可以,就是用final

相信final这个保留字,大家都很清楚,可以修饰类、方法和变量,这里我们可以使用final来让引用不能修改,代码如下

public static void main(String[] args) {
        String str1 = "abc2";
        final String str2 = "abc";
        String str4 = str2 + "2";
        String str5 = "abc" + "2";
        System.out.println(str1==str4);
        System.out.println(str1==str5);
    }

在这里插入图片描述
当我们定义str2被final修饰时,str2指向常量池中“abc”的引用也就固定下来,不可修改(差不多可以算是一个常量),而当str2和"2"进行拼接时,并不会创造新的对象,而是直接在原始值上进行计算(差不多等同于用“abc”和“2”直接进行拼接),因此str4直接指向常量池中的“abc2”

值得强调的是常量的结算结果必须用一个新变量去接收,不能用原来被final修饰的变量去接收,因为引用发生了变化,而final修饰的引用变量是不允许引用发生变化的

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值