String字符串详解

本文详细讲述了Java中String类的概念,重点介绍了JDK9对String存储结构的改进,以及如何在不同场景下高效使用String,包括字面量、拼接、构造器创建和字符串常量池。同时讨论了`intern()`方法在内存管理中的作用。
摘要由CSDN通过智能技术生成

String

1. 概念

String 类代表字符串,Java程序中所有的字符串文本都是这个类的实例,String是一个常量,它的值在定义后不能更改

上图可以看出,String是被final修饰的,不能被继承,可以被序列化,可以进行比较大小。

2. String在jdk9中的变更

2.1 源码对比

以下是JDK8的代码:

以下是JDK9的代码:

2.2 更改原因

JDK8中的String存储在char数组中,使用的是UTF-8(两个字节)进行编码的,但是字符串是堆使用率的重要组成部分,而且大部分的字符串对象都只是包含Latin-1字符(一个字符),因此会导致存储的内部会有一半的闲置控制,导致浪费。

2.3 新版本实现

JDK9中的String是以byte数组加上编码标志字段来实现的,会根据字符串的具体内容,以ISO-8859-1/Latin-1编码(一个字节)或者UTF-16编码(两个字节),编码标志(coder字段)将指明具体使用哪种编码方式编码——coder值为0时表示一个字节,coder值为1时表示两个字节。

2.4 优点

内存占用大幅度减少,也会较少GC的次数,也可以减少"Stop the Word"的频数,增加吞吐量,提升系统的性能。

3. 使用String

在Java代码中,String使用的频率是非常高的,我们一般有以下几种使用方式:

3.1 字面量

使用字面量的方法我们常用的使用String的方式,这种方式会将字符串中的值放在字符串常量池中,然后给变量赋值。

String name = "jack"; 

 0 ldc #2 <jack>  // 字符串常量池中取jack
 2 astore_1
 3 return

3.2 字面量拼接

字面量拼接的方式使用String,会先将拼接后的值放在字符串常量池中,然后再给变量赋值。

 String name = "abc" + "bcd";

 0 ldc #2 <abcbcd>   // 字符串常量池中取abcbcd
 2 astore_1
 3 return

3.3 含有变量拼接

当字符串拼接时,有一个值是变量,则会使用StringBuilder对象类进行拼接,然后调用toString方法将值保存在堆空间(此处是为了和字符串常量池区分开)中,因为toString方法是使用数组进行创建的对象,所以常量池中不存在字符串的值。

 String a = "123";
 String b = "abc" + a;
 
  0 ldc #2 <123>  // 字符串常量池中的123
  2 astore_1
  3 new #3 <java/lang/StringBuilder>  // 创建StringBuilder对象
  6 dup
  7 invokespecial #4 <java/lang/StringBuilder.<init> : ()V>
 10 ldc #5 <abc>  // 字符串常量池中的abc
 12 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>  // apend方法
 15 aload_1
 16 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>  // apend方法
 19 invokevirtual #7 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>  // toString方法
 22 astore_2
 23 return

3.4 使用构造器创建

构造器创建对象在开发中是非常常见的,String对象也可以使用构造器进行创建。使用构造器创建String对象,首先会在堆内存中创建一个String的对象,然后具体的值也会放在字符串常量池中。

String name = new String("jack");

 0 new #2 <java/lang/String>   // 创建一个String对象
 3 dup
 4 ldc #3 <jack>  // 字符串常量池中的jack
 6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
 9 astore_1
 10 return

3.5 构造器创建String拼接

如果是构造器创建的String对象进行拼接,首先会创建一个StringBuilder的对象,然后将两个具体的值拼接,然后调用toString方法

 String b = new String("123") + new String("abc");
 
0 new #2 <java/lang/StringBuilder>   // StringBuilder对象
  3 dup
  4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
  7 new #4 <java/lang/String>  // String 对象
 10 dup
 11 ldc #5 <123>  // 字符串常量池中的123
 13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>  
 16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>  // apend方法
 19 new #4 <java/lang/String>  // String 对象
 22 dup
 23 ldc #8 <abc>  // 字符串常量池中的abc
 25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
 28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>  // apend方法
 31 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>  // toString方法
 34 astore_1
 35 return

4. 字符串常量池

4.1 字符串常量池位置变化

  1. 在JDK6及以前,字符串常量池是在永久代中;

  2. 在JDK7时,字符串常量池放在堆空间中;

  3. 在JDK8中,元空间取代永久代,但是字符串常量池还是保存在堆空间中;

4.2 字符串常量池概述

  • 字符串常量池是一个HashTable

  • 可以使用-XX:StringTableSize来设置HashTable的长度;

  • 在JDK6中,HashTable的长度是固定的,为1009;

  • 在JDK7中,HashTable的默认长度是60013

  • 在JDK8及以后,HashTable的默认长度60013,长度可以设置的最小值为1009;

5. intern()方法

我们看到在3.5节中的使用构造器创建然后进行String拼接的方式,字符串常量池是没有拼接后的字符串。如果我们也想把拼接后的字符串放在字符串常量池中,可以调用intern()方法来完成。

jdk6中:将这个字符串对象尝试放入串池

  • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址

  • 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址

jdk7以后:将这个字符串对象尝试放入串池。

  • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址

  • 如果没有,则会把堆中对象的引用地址复制一份,放入串池,并返回串池中的引用地址

查看以下示例:

JDK6:

 public static void main(String[] args) {
     String b = new String("123") + new String("abc");
     b.intern();
 ​
     System.out.println(b == "123abc");  // false   b对象是堆中的对象,123abc字符串是在字符串常量池中的对象地址
     System.out.println(d == "123abc");  // true    调用intern方法尝试将123abc存放在字符串常量池中,然后将对象地址返回
 }

JDK8:

public static void main(String[] args) {
     String b = new String("123") + new String("abc");
     String d = b.intern();
 ​
     System.out.println(b == "123abc");  // true   b是堆中的对象,调用intern()对象之后,将引用地址复制一份,放在字符串常量池中
     System.out.println(d == "123abc");  // true   d是字符串中指向堆空间的引用
 }

讨论intern()方法的几种情况:

public static void main(String[] args) {
     String a = new String("123");   // 1. 字符串常量池中存放123对象; 2. a指向堆内存中的String对象地址;
     String b = a.intern();   // 3. 查看字符串常量池中是否存在123对象,此时是存在的,返回123字符串常量池中的对象地址;
     System.out.println(a == b);   // 4. a是堆内存中的地址   b是123字符串常量池中的对象地址  不一样  返回false
 }
 public static void main(String[] args) {
     String a = new String("12") + new String("3");   // 1. 字符串常量池中不存在123对象地址;  2. a指向堆内存中的String对象地址;
     String b = a.intern();   // 3. 因为123对象不存在,就将a的引用在字符串常量池中复制一份
     System.out.println(a == b);    // 4. a是堆内存中的地址   b是堆内存中对象地址中的引用地址  一样  返回true
 }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值