String

String 底层

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
}
//从源码可以看出,String对象其实在内部就是一个个字符,存储在这个value数组里面的。

- String 为什么不可变

  • value数组用final修饰,final 修饰的变量,值不能被修改。因此value不可以指向其他对象。
  • String类内部所有的字段都是私有的,也就是被private修饰。而且String没有对外提供修改内部状态的方法,因此value数组不能改变。
    String不可变-->线程安全。

- 为什么要设计成不可变?

  • 线程安全。同一个字符串实例可以被多个线程共享,因为字符串不可变,本身就是线程安全的。
  • 支持hash映射和缓存。因为String的hash值经常会使用到,比如作为 Map 的键,不可变的特性使得 hash 值也不会变,不需要重新计算。
  • 出于安全考虑。网络地址URL、文件路径path、密码通常情况下都是以String类型保存,假若String不是固定不变的,将会引起各种安全隐患。比如将密码用String的类型保存,那么它将一直留在内存中,直到垃圾收集器把它清除。假如String类不是固定不变的,那么这个密码可能会被改变,导致出现安全隐患。
  • 字符串常量池优化。String对象创建之后,会缓存到字符串常量池中,下次需要创建同样的对象时,可以直接返回缓存的引用。

- 既然我们的String是不可变的,它内部还有很多substring, replace, replaceAll这些操作的方法。这些方法好像会改变String对象?怎么解释呢?

其实不是的,我们每次调用replace等方法,其实会在堆内存中创建了一个新的对象。然后其value数组引用指向不同的对象。

- 为何JDK9要将String的底层实现由char[]改成byte[]?

  • 主要是为了节约String占用的内存
    在大部分Java程序的堆内存中,String占用的空间最大,并且绝大多数String只有Latin-1字符,这些Latin-1字符只需要1个字节就够了。
    而在JDK9之前,JVM因为String使用char数组存储,每个char占2个字节,所以即使字符串只需要1字节,它也要按照2字节进行分配,浪费了一半的内存空间。

  • 到了JDK9之后,对于每个字符串,会先判断它是不是只有Latin-1字符,如果是,就按照1字节的规格进行分配内存,如果不是,就按照2字节的规格进行分配,这样便提高了内存使用率,同时GC次数也会减少,提升效率。

  • 不过Latin-1编码集支持的字符有限,比如不支持中文字符,因此对于中文字符串,用的是UTF16编码(两个字节),所以用byte[]和char[]实现没什么区别。

String 类的常用方法有哪些?

  • indexOf():返回指定字符的索引。
  • charAt():返回指定索引处的字符。
  • replace():字符串替换。
  • trim():去除字符串两端空白。
  • split():分割字符串,返回一个分割后的字符串数组。
  • getBytes():返回字符串的 byte 类型数组。
  • length():返回字符串长度。
  • toLowerCase():将字符串转成小写字母。
  • toUpperCase():将字符串转成大写字符。
  • substring():截取字符串。
  • equals():字符串比较。

什么是字符串常量池?

字符串常量池(String Pool)保存着所有字符串字面量,这些字面量在编译时期就确定。字符串常量池位于堆内存中,专门用来存储字符串常量。在创建字符串时,JVM首先会检查字符串常量池,如果该字符串已经存在池中,则返回其引用,如果不存在,则创建此字符串并放入池中,并返回其地址。

String一般都存储在JVM的哪块区域呢?

字符串在JVM中的存储分两种情况,一种是String对象,存储在JVM的堆中。一种是字符串常量,存储在常量池里面(虽然常量池也在堆空间中)。

String的长度限制?

字符串有长度限制

  • 在运行期,长度不能超过Int的范围,否则会抛异常。
    原因:String类提供了一个length方法,返回值为int类型,而int的取值上限为2^31 -1。
达到这个长度的话需要多大的内存?

String内部是使用一个char数组来维护字符序列的,一个char占用两个字节。如果说String最大长度是2^31 -1的话,那么最大的字符串占用内存空间约等于4GB
也就是说,我们需要有大于4GB的JVM运行内存才行。

  • 在编译期,要求字符串常量池中的常量不能超过65535,JVM需要1个字节表示结束指令,所以这个范围就为65534了,so在javac执行过程中控制了最大值为65534。
    原因:javac是将Java文件编译成class文件的一个命令,那么在Class文件生成过程中,就需要遵守一定的格式。即:JVM对常量池中存放的数据是有限制。
    根据《Java虚拟机规范》中第4.4章节常量池的定义,CONSTANT_String_info 用于表示 java.lang.String 类型的常量对象,格式如下:
CONSTANT_String_info {
 
    u1 tag;
 
    u2 string_index;
 
}

其中,string_index 项的值必须是对常量池的有效索引, 常量池在该索引处的项必须是CONSTANT_Utf8_info 结构

CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}

表示一组 Unicode 码点序列(16位),这组 Unicode 码点序列最终会被初始化为一个 String 对象。length在这里就是代表字符串的长度,length的类型是u2,u2是无符号的16位整数,也就是说最大长度可以做到2^16-1 即 65535。Class文件中常量池的格式规定了,其字符串常量的长度不能超过65535。

上面提到的这种String长度的限制是编译期的限制,也就是使用String s= “ ”;这种字面值方式定义的时候才会有的限制。

String创建对象

1. 赋字面量

String str = "Jxy";

解析:在字符串常量池中找"Jxy"

  • 找到了,直接返回引用给str;
  • 没找到,在字符串常量池中创建"Jxy"字符串字面量,再把创建好的字面量的地址给str。

2. new String(" ")

String str = new String("Jxy");

解析:先在字符串常量池中找"Jxy",

  • 找到了,在堆内存中创建"Jxy"对象,该对象存的内容是字符串常量池中找到的"Jxy"的地址,然后把该堆内存中"Jxy"对象的地址给str;
  • 没找到,在字符串常量池中创建该"Jxy"字面量,然后在堆内存中创建"Jxy"对象,该对象存的内容是字符串常量池中创建的"Jxy"的地址,然后把该堆内存中"Jxy"对象的地址给str。
    总之,str存的是堆内存中对象的地址,即str是堆内存中对象的引用

3. 用intern()
String的intern方法,根据JDK版本的不同:
JDK6:
如果常量池中已经存在该字符串,则直接返回常量池内该字符串的地址,即该字符串的引用。

否则,在常量池中加入该字符串,然后返回该常量池中刚加入的字符串的地址

JDK7:
如果常量池中已经存在该字符串,则直接返回常量池内该字符串的地址,即该字符串的引用;如果已经存在该字符串的引用,则返回该字符串的引用的引用,即堆内存中该字符串对象的地址

否则,在常量池中加入该字符串的引用(相当于堆内存中该字符串对象的地址),然后返回堆内存中该字符串对象的地址

注意:关键字问题,如果是关键字,关键字是在常量池中是一定有的。eg.java

public class Demo {
    public static void main(String args[]) {
        String s1 = new String("1");
        String s2 = s1.intern();//"1"已经存在
        System.out.println(s1 == s2);
 
        String s3 = new String("1") + new String("1");
        String s4 = s3.intern();//"11"不存在
        String s5 = s3.intern();//"11"存在
        System.out.println(s3 == s4);
        System.out.println(s3 == s5);
    }
}
//结果
jdk6:false false false
jdk7:false true true

4. 字符串连接
1). 字符串常量与字符串常量的拼接

String s1 = "a"+"b"+"c";//常量池中存的直接是"abc",没有存"a","b","c"

字符串常量之间的拼接操作在未加载到内存之前就已经完成了。在前端编译期间(即将.java源文件编译为.class字节码文件),会对字符串常量之间的拼接操作进行优化,结果放在常量池
(只要挨着的常量就会合并, 前后声明不影响, 例如 “1” + “2” + def + “4” + “5” 会自动合并为 “12” + def + “45” )

2). 字符串常量与变量的拼接

String s1 = "a";
String s2 = s1+"b";

3). 变量与变量的拼接

String s1 = "a";
String s2 = "b";
String s4 = s1+s2;

字符串拼接操作中只要其中有一个是变量,拼接结果就在堆中。与常量池无关。且变量拼接的原理是创建一个StringBuilder类的对象,每次相加都 append(str),拼接完成后隐式调用 toString() (该方法已经被重写了)返回一个字符串对象。

1,常量与常量的拼接结果在常量池,原理是编译器优化。
2,常量池中不会存在相同内容的常量,但是堆内存可以。
3,只要其中一个是变量,结果就在堆内存中。变量的拼接原理是StringBuilder。
4,如果拼接的结果调用intern()方法,根据jdk版本不同,酌情分析。

注意:需要注意的是, 使用 += 拼接, 例如 a += 123; 等效于 a = a + 123; //a是变量
每次 += 都会实例化一个新的 StringBuilder 对象, append(a).append(123) 再隐式调用 toString() (注意: 每次 += 因为要再次赋值给字符串 a 都会隐式 toString() )
由此可知, 如果在循环中使用 +=, 会产生大量 StringBuilder 对象 (每次相加前都会实例化 StringBuilder 对象) 和 String 对象 (每次都会 toString()), 造成资源浪费。

所以

  • 普通的几个字符串拼接, 可以使用 + 拼接字符串 (与调用 append() 等效)
    注意:如果 + 号两边的字符串都在字符串常量池中话,“+” 的性能比 append 快。
  • 大量字符串拼接或者循环拼接, 推荐手动创建 StringBuilder 对象调用 append(), 而不是 += (循环中 += 会创建大量 StringBuilder 和 String 对象),+效率也会很低。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值