【Java 源码透析String】简单理解

最近做题的时候遇到很多的有关String相关的知识点,于是写篇博客,总结和复习吧!


String类是Java中一个比较特殊的类,字符串即String类,它不是Java的基本数据类型之一,但可以像基本数据类型一样使用,声明与初始化等操作都是相同的,是程序经常处理的对象,所以学好String的用法很重要

1.String
1.1 String源码

JDK 版本 1.8 ,String 内部实际存储结构为 char 数组,源码如下:

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

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

String 没有继承任何接口,不过实现了三个接口,分别是 Serializable、Comparable、CharSequence 接口。

  • Serializable :这个序列化接口没有任何方法和域,仅用于标识序列化的语意。
  • Comparable:实现了 Comparable 的接口可用于内部比较两个对象的大小
  • CharSequence:字符串序列接口,CharSequence 是一个可读的 char 值序列,提供了 length(), charAt(int index), subSequence(int start, int end) 等接口,StringBuilder 和 StringBuffer 也继承了这个接口

String 中有一个用于存储字符的 char 数组value[],这个数组存储了每个字符。另外一个就是 hash 属性,它用于缓存字符串的哈希码。因为 String 经常被用于比较,比如在 HashMap 中。如果每次进行比较都重新计算其 hashcode 的值的话,那无疑是比较麻烦的,而保存一个 hashcode 的缓存无疑能优化这样的操作

我们看到String 对象是由final 修饰的,一旦使用 final 修饰的类不能被继承、方法不能被重写、属性不能被修改。而且 String 不只只有类是 final 的,它其中的方法也是由 final 修饰的,也由于 String 的不可变性,类似字符串拼接、字符串截取等操作都会产生新的 Strign 对象

大家知道,下面这些字符串分别创建了几个对象?

1、String s1 = "aaa";
2、String s2 = "bbb" + "ccc";
3、String s3 = s1 + "bbb";
4、String s4 = new String("aaa");

1、s1 创建了几个对象。字符串在创建对象时,会在常量池中看有没有 aaa 这个字符串;如果没有此时还会在常量池中创建一个;如果有则不创建。我们默认是没有的情况,所以会创建一个对象

2、 s2 创建了几个对象呢?我们用idea添加JDK自带的反编译工具javap来看看反汇编代码:
在这里插入图片描述

编译器做了优化String s2 = “bbb” + "ccc"会直接被优化为bbbccc。也就是直接创建了一个 bbbccc 对象

3、s3 创建了几个对象呢?
在这里插入图片描述
我们可以看到,s3 执行"+“操作会创建一个StringBuilder对象然后执行初始化,执行”+"号相当于是执行new StringBuilder.append()操作,所以:

String s3 = new StringBuilder().append(s1).append("bbb").toString();
// Stringbuilder.toString() 方法也会创建一个 String 

所以 s3 执行完成后,相当于创建了 3 个对象

4、 s4 创建了几个对象,在创建这个对象时因为使用了 new 关键字,所以肯定会在堆中创建一个对象。然后会在常量池中看有没有 aaa 这个字符串;如果没有此时还会在常量池中创建一个;如果有则不创建。所以可能是创建一个或者两个对象,但是一定存在两个对象

1.2 String 常用方法

charAt:返回指定位置上字符的值

getChars: 复制 String 中的字符到指定的数组

equals: 用于判断 String 对象的值是否相等

indexOf: 用于检索字符串

substring: 对字符串进行截取

concat: 用于字符串拼接,效率高于 +

replace:用于字符串替换

match:正则表达式的字符串匹配

contains: 是否包含指定字符序列

split: 字符串分割

join: 字符串拼接

trim: 去掉多余空格

toCharArray: 把 String 对象转换为字符数组

valueOf: 把对象转换为字符串

2.StringBuffer

StringBuffer 对象代表一个可变的字符串序列,当一个 StringBuffer 被创建以后,通过 StringBuffer 的一系列方法可以实现字符串的拼接、截取等操作。一旦通过 StringBuffer 生成了最终想要的字符串后,就可以调用其toString方法来生成一个新的字符串
比如:

StringBuffer b = new StringBuffer("111");
b.append("222");
System.out.println(b);

StringBuffer 是线程安全的,我们可以通过它的源码可以看出:

 @Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

StringBuffer所有方法直接使用synchronized关键字加锁,从而保证了线程安全性,对于synchronized的实现原理,大家可以看我多线程那篇博客,对synchronized的实现原理详解

3.StringBuilder

StringBuilder 其实是和 StringBuffer 几乎一样,只不过 StringBuilder 是非线程安全的。并且,为什么 + 号操作符使用 StringBuilder 作为拼接条件而不是使用 StringBuffer 呢?
我猜测:加锁是一个比较耗时的操作,而加锁会影响性能,所以 String 底层使用 StringBuilder 作为字符串拼接

@Override
    public StringBuilder append(Object obj) {
        return append(String.valueOf(obj));
    }

    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

4.理解 String、StringBuilder、StringBuffer

我们上面说到,使用+连接符时,JVM 会隐式创建 StringBuilder 对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意,如下这段代码:

 String s = "aaaa";
        for (int i = 0;i<10000;i++){
            s+="bbb";
        }

这是一段很普通的代码,只不过对字符串 s 进行了 + 操作,我们通过反编译代码来看一下:

// 经过反编译后
String s = "aaa";
for(int i = 0; i < 10000; i++) {
     s = (new StringBuilder()).append(s).append("bbb").toString();    
}

在每次进行循环时,都会创建一个StringBuilder对象,每次都会把一个新的字符串元素bbb拼接到aaa的后面,所以,执行几次后的结果如下:
在这里插入图片描述
每次都会创建一个 StringBuilder ,并把引用赋给 StringBuilder 对象,因此每个 StringBuilder 对象都是强引用, 这样在创建完毕后,内存中就会多了很多 StringBuilder 的无用对象

这样由于大量 StringBuilder 创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个 StringBuilder 对象调用append()方法手动拼接:

StringBuilder builder = new StringBuilder("aaa");
for (int i = 0; i < 10000; i++) {
    builder.append("bbb");
}
builder.toString();

这段代码中,只会创建一个 builder 对象,每次循环都会使用这个 builder 对象进行拼接,因此提高了拼接效率

从设计角度理解:
String 在 JDK1.6 之后提供了intern()方法,intern 方法是一个native方法,它底层由 C/C++ 实现,intern 方法的目的就是为了把字符串缓存起来,在 JDK1.6 中却不推荐使用 intern 方法,因为 JDK1.6 把方法区放到了永久代(Java 堆的一部分),永久代的空间是有限的,除了Fullgc外,其他收集并不会释放永久代的存储空间。JDK1.7 将字符串常量池移到了堆内存中

下面我们来看一段代码,来认识一下intern方法:

public static void main(String[] args) {

  String a = new String("ab");
  String b = new String("ab");
  String c = "ab";
  String d = "a";
  String e = new String("b");
  String f = d + e;

  System.out.println(a.intern() == b);
  System.out.println(a.intern() == b.intern());
  System.out.println(a.intern() == c);
  System.out.println(a.intern() == f);

}

结果:

false、true、true、false

和你预想的一样吗?为什么会这样呢?我们先来看一下 intern 方法的官方解释

在这里插入图片描述
从JDK 1.7开始去永久代,字符串常量池已经被转移至 Java 堆中,开发人员也对 intern 方法做了一些修改。因为字符串常量池和 new 的对象都存于 Java 堆中,为了优化性能和减少内存开销,当调用 intern 方法时,如果常量池中已经存在该字符串,则返回池中字符串;否则直接存储堆中的引用,也就是字符串常量池中存储的是指向堆里的对象

所以我们对上面的结论进行分析:

String a = new String("ab");
String b = new String("ab");

System.out.println(a.intern() == b);

输出为什么是false:
在这里插入图片描述
a.intern 返回的是常量池中的 ab,而 b 是直接返回的是堆中的 ab。地址不一样,肯定输出 false
所以第二个:

System.out.println(a.intern() == b.intern());

也就没问题了吧,它们都返回的是字符串常量池中的 ab,地址相同,所以输出 true

然后来看第三个:

System.out.println(a.intern() == c);

在这里插入图片描述
a 不会变,因为常量池中已经有了 ab ,所以 c 不会再创建一个 ab 字符串,这是编译器做的优化,为了提高效率

下面来看最后一个:

System.out.println(a.intern() == f);

在这里插入图片描述

StringBuilder 和 StringBuffer 的扩容问题
首先先注意一下 StringBuilder 的初始容量:

public StringBuilder() {
  super(16);
}

StringBuilder 的初始容量是 16,当然也可以指定 StringBuilder 的初始容量。

在调用 append 拼接字符串,会调用 AbstractStringBuilder 中的 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;
}

上面代码中有一个ensureCapacityInternal方法,这个就是扩容方法,我们跟进去看一下:

private void ensureCapacityInternal(int minimumCapacity) {
  // overflow-conscious code
  if (minimumCapacity - value.length > 0) {
    value = Arrays.copyOf(value,
          newCapacity(minimumCapacity));
  }
}

这个方法会进行判断,minimumCapacity 就是字符长度 + 要拼接的字符串长度,如果拼接后的字符串要比当前字符长度大的话,会进行数据的复制,真正扩容的方法是在newCapacity中:

private int newCapacity(int minCapacity) {
  // overflow-conscious code
  int newCapacity = (value.length << 1) + 2;
  if (newCapacity - minCapacity < 0) {
    newCapacity = minCapacity;
  }
  return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
    ? hugeCapacity(minCapacity)
    : newCapacity;
}

扩容后的字符串长度会是原字符串长度增加一倍 + 2,如果扩容后的长度还比拼接后的字符串长度小的话,那就直接扩容到它需要的长度 newCapacity = minCapacity,然后再进行数组的拷贝

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值