Java 的String

String是一个比较特殊的引用类型,String类使用final修饰,是所谓不可变类,不可被继承

设计成不可变的理由

  1. 节省空间:为了提高效率和空间使用率,把字符串常量存储在字符串常量池中,这些字符在多线程中可被共享,保证一个用户对字符串的修改不会影响到其他用户使用。
  2. 提高效率:由于String会被不同用户共享,在多线程时是线程安全的,不需要对String进行同步。此外,计算String的Hash值也因其是不可变量,则它的Hash值也保持不变,就可以把Hash值缓存起来,而不需要每次都计算Hash值,可以显著提高效率。
  3. 安全性:可以帮助避免一些安全问题,防止意外或恶意的代码修改。

底层数据结构

在JDK9之前,String的底层存储结构是一个char[]数组,一个char占两个字节的存储单位。

JDK开发人员发现,一个字符串只包含英文字符或ASC码时只需要一个字节表示,(JDK是UTF-16编码的),这意味着字符串实际的存储空间比需要的多一倍,

在JDK9引入Compact String优化,将String的char[]改成byte[],在存储非ASC码的字符时才使用char[],纯ASC码字符就用byte[]表示,从而减少内存消耗。

这种优化是在编译器和运行环境实现的,可以通过

-XX:+UserCompressedStrings

来设置是否进行优化。

字符串常量池

在HotSpot中实现字符串常量池功能的是一个StringTable类,它是一个Hash表,默认大小为1009,在每个虚拟机中只有一份,被所有类共享。字符串常量由一个个字符组成,放在了StringTable上。

在JDK7之后,String的常量池被移动到堆内存中。

对于一个String的字符串类型来说,它的引用都是保存在栈中的,但是如果在编译期就能确定值->双引号赋值,这个值就会存储在字符串常量池中,如:

String a = "123";
String b = null;
b = "123";

通过new关键词创建就会保存在堆中,new几个就会保存几个。

new String()创建了几个对象

当我们通过new String("abc");先检索常量池中是否存在“abc”,如果不存在“abc”这个字符串,则会先在常量池中创建这个一个字符串。然后再执行new操作,会在堆内存中创建一个存储“abc”的String对象,对象的引用赋值给new的对象。此过程创建了2个对象。

当然,如果检索常量池时发现已经存在了对应的字符串,那么只会在堆内创建一个新的String对象,此过程只创建了1个对象。

在上述过程中检查常量池是否有相同Unicode的字符串常量时,使用的方法便是String中的intern()方法。

intern()方法的作用

  1. 如果当前字符串内容存在于字符串常量池(即equals()方法为true,也就是内容一样),那直接返回此字符串在常量池的引用;
  2. 如果当前字符串不在字符串常量池中,那么在常量池创建一个引用并指向堆中已存在的字符串,然后返回常量池中的引用。

简单说intern方法就是判断并将字符串是否存在于字符串常量池,如果不存在则创建,存在则返回。(返回的是此字符串常量的地址)

如果字符串常量池中已经存在对于的字符串时,此时返回的是字符串常量的地址(引用),如果不存在,此时会把堆中的引用放在常量池对于位置(常量池存储的是堆中字符串的引用),此时返回的是堆中字符串的引用(地址)

String字符串拼接

因为String的不可变性,每当+操作,都会在内存中生成新的字符串,生成的String对象位于堆中,如果拼接多次,那么会产生多个中间对象。

但是,在JDK8中对+进行了优化,基于StringBuilder的append方法进行处理

StringBuffer和StringBuilder

StringBuffer和StringBuilder实现的核心代码基本一致,很多代码都是公用的。这两个类均继承自抽象类AbstractStringBuilder。两者的构造方法是一样的,而append()方法,在StringBuffer除了内部将toStringCache变量赋值为null,还在方法上使用synchronized进行了同步处理。

toStringCache是用来缓存最后一次调用toString方法时生成的字符串,当StringBuffer内容变动时,改值也会变动。

通过上面的append方法的对比,我们可以很轻易的发现StringBuffer是线程安全的,StringBuilder是非线程安全的。当然,使用synchronized进行同步处理,性能便会降低很多。

底层实现

StringBuffer与StringBuilder都调用了父类的构造方法:

AbstractStringBuilder(int capacity) {
    value = new char[capacity];
}

在初始化时先初始化一个长度为传入字符串长度+16的char[]数组,也就是value值,用来存储实际的字符串。

在调用父类构造方法之后便是调用各自的append方法(见前面的代码),而其中的核心处理又的调用父类的append方法:

public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

其中str.getChars方法用来对传入的str字符串进行拼接,在原有的value数组后面进行填充。而count用来记录当前value数字中已经使用的长度。上面代码中count+=len并不是原子操作,所以StringBuilder有线程安全问题。

String转Interger

其最终调用都是parseInt(String s,int radix),用负的值累减遍历,负数最终直接返回,正事取反,以boolean negtive来标记正负数。

public static int parseInt(String s, int radix)
throws NumberFormatException
{
    int result = 0;
    //是否是负数
    boolean negative = false;
    //char字符数组下标和长度
    int i = 0, len = s.length();
    // ……
    int digit;
    //判断字符长度是否大于0,否则抛出异常
    if (len > 0) {
    // ……
        while (i < len) {
            // Accumulating negatively avoids surprises near MAX_VALUE
            //返回指定基数中字符表示的数值。(此处是十进制数值)
             digit = Character.digit(s.charAt(i++),radix);
             //进制位乘以数值
             result *= radix;
             result -= digit;
         }
     }
     //根据上面得到的是否负数,返回相应的值
     return negative ? result : -result;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bear程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值