Java基础篇——String类

14 篇文章 0 订阅
2 篇文章 0 订阅

String类是一个很常用的类,它位于java.lang包下,是Java语言的核心类,用来保存代码中的字符串常量的,并且封装了很多操作字符串的方法。下面看一个定义String类型变量并初始化的例子:

String str1 = "abc";
String str2 = new String("abc");
String str3 = str1 + str2;

乍看一下,这些语句都没有问题,当然这些语句也都是正确的,符合Java语法规范的。细细推敲,又觉得有很多不明白之处,String是引用变量类型,String类型的变量为什么可以直接用一个字符串赋值,难道不是像第二句代码一样先new一个String对象,再将此对象的引用赋值给String变量,这两种赋值方式有什么不同,原理是什么,为什么两个String变量可以相“+”??????顿时,心中有无数的羊驼驼在空中乱舞,String究竟是个啥?

接下来让我们一起层层揭开String类型的神秘面纱。

俗话说的好,要了解一个人必须读懂他的心,学习知识也不例外,我们要从String的核心代码入手去认识它

这是jdk8版本中String类的部分代码,可以看出String类被final关键字修饰,类中定义了一个final修饰的字符数组类型的成员变量value,从构造方法可以看出,String对象其实就是这个字符数组。final的意思是最终的、不可变的,因此可以得到以下结论:

  • String类不可以被继承(被final修饰的类都不能被继承)
  • String的值不可变的常量(但可以通过反射机制修改),是线程安全的。

String类型变量直接赋值和使用对象赋值的区别

在这之前,先用一段代码,区分一些名词

String str1 = "abc";
String str2 = new String("abc");
String str3 = str1 + str2;
  • String变量:str1、str2、str3
  • String对象:new String()。Java创建对象的物种方式,参考:https://www.cnblogs.com/wxd0108/p/5685817.html
  • 字符串常量:"abc"。又叫Stirng匿名对象、字面量,编译时保存在常量池中,运行时保存在运行时常量池中。

 直接赋值的工作原理:

当代码中出现字面量形式创建字符串对象时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用。

使用对象赋值的工作原理:

使用了new来构造字符串对象的时候,JVM首先会对构造方法中的参数这个字面量进行检查如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池;然后再堆中分配一块内存,从字符串常量池获得这个字面量,保存再堆中分配的内存里。

字符串常量池

我们知道字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且我们使用的字符串非常多。JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串

Java中的常量池,实际上分为两种形态:静态常量池运行时常量池
静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

JDK版本不同运行时常量池和字符串常量池的关系

  • 在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
  • 在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说 字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
  • 在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

常量池、运行时常量池、字符串常量池参考:https://blog.csdn.net/qq_45737068/article/details/107149922

使用字符串常量池的优缺点

字符串常量池的好处就是减少相同内容字符串的创建,节省内存空间。

如果硬要说弊端的话,就是牺牲了CPU计算时间来换空间。CPU计算时间主要用于在字符串常量池中查找是否有内容相同对象的引用。不过其内部实现为HashTable,所以计算成本较低。

字符串常量池中保存的是字符串常量的值还是其引用

在解释这个问题前,我们先介绍一个String类的intern方法,intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

以下部分摘抄自深入解析String#intern,地址:https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html


来看一段代码:

public static void main(String[] args) {
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
}

打印结果是

  • jdk6 下false false
  • jdk7 下false true

具体为什么稍后再解释,然后将s3.intern();语句下调一行,放到String s4 = "11";后面。将s.intern(); 放到String s2 = "1";后面。是什么结果呢

public static void main(String[] args) {
    String s = new String("1");
    String s2 = "1";
    s.intern();
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    String s4 = "11";
    s3.intern();
    System.out.println(s3 == s4);
}

打印结果为:

  • jdk6 下false false
  • jdk7 下false false

####1,jdk6中的解释

jdk6图

jdk6图

注:图中绿色线条代表 string 对象的内容指向。 黑色线条代表地址指向。

如上图所示。首先说一下 jdk6中的情况,在 jdk6中上述的所有打印都是 false 的,因为 jdk6中的常量池是放在 Perm 区中的,Perm 区和正常的 JAVA Heap 区域是完全分开的。上面说过如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。所以拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用String.intern方法也是没有任何关系的。

####2,jdk7中的解释

再说说 jdk7 中的情况。这里要明确一点的是,在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 是会直接产生java.lang.OutOfMemoryError: PermGen space错误的。 所以在 jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域了。为什么要移动,Perm 区域太小是一个主要原因,当然据消息称 jdk8 已经直接取消了 Perm 区域,而新建立了一个元区域。应该是 jdk 开发者认为 Perm 区域已经不适合现在 JAVA 的发展了。

正式因为字符串常量池移动到 JAVA Heap 区域后,再来解释为什么会有上述的打印结果。

jdk7图1

jdk7图1

  • 在第一段代码中,先看 s3和s4字符串。String s3 = new String("1") + new String("1");,这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的对象。中间还有2个匿名的new String("1")我们不去讨论它们。此时s3引用对象内容是”11”,但此时常量池中是没有 “11”对象的。
  • 接下来s3.intern();这一句代码,是将 s3中的“11”字符串放入 String 常量池中,因为此时常量池中不存在“11”字符串,因此常规做法是跟 jdk6 图中表示的那样,在常量池中生成一个 “11” 的对象,关键点是 jdk7 中常量池不在 Perm 区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。
  • 最后String s4 = "11"; 这句代码中”11”是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。所以 s4 引用就指向和 s3 一样了。因此最后的比较 s3 == s4 是 true。

  • 再看 s 和 s2 对象。 String s = new String("1"); 第一句代码,生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。s.intern(); 这一句是 s 对象去常量池中寻找后发现 “1” 已经在常量池里了。

  • 接下来String s2 = "1"; 这句代码是生成一个 s2的引用指向常量池中的“1”对象。 结果就是 s 和 s2 的引用地址明显不同。图中画的很清晰。

jdk7图2

jdk7图2

  • 来看第二段代码,从上边第二幅图中观察。第一段代码和第二段代码的改变就是 s3.intern(); 的顺序是放在String s4 = "11";后了。这样,首先执行String s4 = "11";声明 s4 的时候常量池中是不存在“11”对象的,执行完毕后,“11“对象是 s4 声明产生的新对象。然后再执行s3.intern();时,常量池中“11”对象已经存在了,因此 s3 和 s4 的引用是不同的。
  • 第二段代码中的 s 和 s2 代码中,s.intern();,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码String s = new String("1");的时候已经生成“1”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s 和 s2 的引用地址是不会相等的。

我使用jdk8的版本,执行了上述两端代码,与jdk7版本结果一致。

得出结论如下:

  •  jdk6版中,字符串常量池在运行时常量池中,都在方法区中,字符串常量池保存的是字符串常量本身。
  • jdk7版中,运行时常量池在方法区,字符串常量池在堆区,两者完全分离,字符串常量池保存的可能是字符串常量本身,也可能是字符串常量的引用,但只能是其中一种。代码中以字面量形式出现的字符串都会直接保存到字符串常量池中,保存的是其本身。代码中,如果通过拼接、截取等方式创建的字符串对象,如果调用了intern方法,并且字符串常量池中也没有这个对象或这个对象的引用,在常量池中保存堆中字符串对象的引用;
  • jdk8版中,取消了方法去,将运行时常量池放到了元空间中,其他与jdk7一致。

concat方法与“+”号运算符的区别

concat方法与“+”号运算符都可以实现两个字符串的拼接,得到的结果是一致的,但实现原理上存在差异。

public class TestMain {

    public static void main(String[] args) {

        String s1 = "abc";
        String s2 = "df";
        String s3 = "";
        String s4 = null;
        int i = 10;

        String str = "abcdf";

        String ss1 = "abc" + "df";
        String sc1 = "abc".concat("df");

        String ss2 = s1 + "df";
        String sc2 = s1.concat("df");

        String ss3 = s1 + s2;
        String sc3 = s1.concat(s2);

        String ss4 = str + s3;
        String sc4 = str.concat(s3);

        String ss5 = str + s4;
//        String sc5 = str.concat(s4); //执行时报错,空指针异常

        String ss6 = str + i;
//        String sc6 = str.concat(i); //语法错误,只能传入字符串参数
        
    }
}

编译成class文件,再翻译为Java文件

public class TestMain {
    public TestMain() {
    }

    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "df";
        String s3 = "";
        String s4 = null;
        int i = 10;
        String str = "abcdf";
        String ss1 = "abcdf";
        String sc1 = "abc".concat("df");
        String ss2 = s1 + "df";
        String sc2 = s1.concat("df");
        (new StringBuilder()).append(s1).append(s2).toString();
        s1.concat(s2);
        (new StringBuilder()).append(str).append(s3).toString();
        str.concat(s3);
        (new StringBuilder()).append(str).append((String)s4).toString();
        (new StringBuilder()).append(str).append(i).toString();
    }
}

 String ss2 = s1 + "df";语句的执行指令。创建一个StringBuilder对象,调用append方法拼接,最后用toString方法返回个新的String对象;

 String ss4 = str + s3;创建一个StringBuilder对象,调用append方法拼接,最后用toString方法返回个新的String对象;

String sc4 = str.concat(s3);语句的执行指令。拼接空字符串,直接返回字符串本身,没有创建新对象。所以System.out.println(str == sc4);的输出true。

查看concat方法的源码,可知参数为空字符串时,返回对象本身,传入非空串时,创建一个新的String对象并返回其引用地址。 

归纳为以下几点

“+”运算符:

  • "abc" + "df"  等价于  ”abcdf";
  • 字符串变量/字面量 + 字符串变量/字面量,编译或者执行时,将会新创建一个StringBuilder/StringBuffer对象,调用append方法拼接,最后用toString方法返回个新的String对象;
  • “+”运算符可以拼接任意类型,基础数据类型、引用数据类型、空字符串("")、null。

 concat函数:

  • concat方法只能将字符串作为输入,如果提供了任何其他类型的输入,则会要求编译时错误;
  • 仅当输出字符串的长度大于零时,concat方法才会在追加后创建新的字符串对象作为输出,否则返回与输出对象相同的目标字符串;
  • 如果此方法以null作为输入调用,则Concat方法将返回null指针异常;
  • 在concat方法的情况下,如果结果字符串的长度为零,则不会创建新对象,因此与+运算符相比,它消耗的内存更少。

参考资料:

https://blog.csdn.net/ifwinds/article/details/80849184

https://www.cnblogs.com/dolphin0520/p/3778589.html

https://www.iteye.com/blog/rednaxelafx-774673

https://www.cnblogs.com/xiaoxi/p/6036701.html

https://blog.csdn.net/qq_45737068/article/details/107149922

https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值