深入理解JAVA字符串类

Java中字符串类主要包含3种:String、StringBuilder、StringBuffer,今天我们就从源码、性能和使用场景等角度来深入分析Java中的字符串类。

1. String

String类提供了构造和管理字符串的各种基本操作,Java中所有类似"WSYN"的字面值都是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

它是典型的Immutable(不可修改的)类,被声明为final class,这意味着String类不能被继承,并且所有属性也都是final的。由于String类是不可变性,类似拼接、裁剪字符串的操作都会产生新的String对象,所以字符串操作不当可能会产生大量临时字符串。JDK 8以前String类是使用char(字符)数组来存储字符串的,JDK 9以后改为byte(字节)数组存储。

1.1 常量池

在分析常量池前先看下java的内存区域:

  

在Java的内存区域中有三种常量池,分别是:Class常量池、运行时常量池和字符串常量池。

1.1.1 Class常量池

每一个Java文件被java编译器编译后都会生成一个Class字节码文件,Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有常量池,用于存放编译期生成的各种字面量符号引用。字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

  • 类和接口的全限定名
  • 字段名称和描述符
  • 方法名称和描述符

每个类都有一个Class常量池。

1.1.2 运行时常

运行时常量池存在于内存中的方法区。Class常量池被加载到内存之后会存放在运行时常量池中。除了保存Class文件中的符号引用外,还会将符号引用转为直接引用,并存储在运行时常量池中。

运行时常量池相对于Class常量池另一个特性就是动态性。除了在编译期产生的Class文件中常量池的内容会进入运行时常量池,运行期间也可能产生新的常量放入池中,如String类的intern()方法。

JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

1.1.3 字符串常量池

字符串常量池有两种使用方式:

(1)直接使用双引号声明出来的String对象会直接存储在常量池中;

String str = "wsyn";

(2)如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中(JDK 7后将字符串在堆的引用地址放入常量池)。

String str2 = new String("wsyn");
str2 = str2.intern();

Java中创建字符串的两种方式:

(1)直接使用双引号声明一个字符串时,JVM首先查找字符串常量池是否存在该字符串,若存在则直接返回该字符串的引用地址;若不存在则先在字符串常量池实例化该字符串,再返回引用地址。

String str = "wsyn";

(2)通过new关键字新建一个字符串时,会在堆中重新new一块内存,用于存储字符串信息,同时会在栈中创建对堆的地址引用(JDK 7后字符串堆的地址引用从栈中转到字符串常量池)。

String str2 = new String("wsyn");

字符串常量池的位置

JDK7以后字符串常量池从方法区的永久代中移入到堆中。因为方法区的内存空间太小且不方便扩展,而堆的内存空间比较大且扩展方便。

字符串常量池的内部结构

  • 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。
  • 在JDK1.6中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;
  • 在JDK1.7中,StringTable的长度可以通过参数指定:
-XX:StringTableSize=11111

1.3 == 和 equals区别以及hash冲突

  • ==是比较两个字符串的地址引用是否一致。
  • equals是判断两个字符串内容是否相等。具体逻辑为先判断两个字符串是否==,即是否指向同一个引用地址,如果引用地址一样,则表示是同一个对象,内容一定相等;若地址不一样,则逐一比较是否相等。
public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

那么问题来了,是否可以通过hashCode来比较两个字符串是否一致?

答案是否定的,两个字符串==或者equals,他们的hashCode肯定是一样的,反之不然。先看下hashCode的源码:

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

由上可以看出,hashCode的计算方式为:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],s[n] 是字符串的第 n个字符,n 是字符串的长度,^ 表示求幂。由此可知不同字符串有可能会得出相同的hashCode,虽然概率很低。

2. StringBuffer和StringBuilder

StringBuffer和StringBuilder底层都是利用可修改的char字符数组(JDK 9以后改为byte字节数组 ),为了实现修改字符序列的目的;同时他们都继承了AbstractStringBuilder,区别仅在于最终的方法是否加了synchronized。

那么这个内部数组的长度为多大合适呢?目前,内部数组长度为构建时初始字符串+16,这就意味着如果没有输入初始字符串,那么初始长度就是16。如果字符串长度值这个长度,就会进行扩容,重新创建新的足够大的数组,进行arraycopy,抛弃掉原来的数组,会产生多重开销。所以在我们能够预计字符串长度,可以指定合适大小。

2.1 StringBuffer

StringBuffer是为了解决上述提到的String类拼接产生太多中间对象的问题而提供的一个类,可以通过append或者add方法,把字符串添加到已有序列的末尾或指定位置。

StringBuffer本质是一个线程安全的可修改序列,它的线程安全是通过把各种修改数据的方法都加上synchronized关键字实现的,由此也带来了额外的性能开销,所以非线程安全的需要,一般不推荐使用。

public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
 /**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache;

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    static final long serialVersionUID = 3388685877147921107L;

    public StringBuffer() {
        super(16);
    }

    public StringBuffer(int capacity) {
        super(capacity);
    }

    public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
    }

    public StringBuffer(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }

    @Override
    public synchronized int length() {
        return count;
    }

    @Override
    public synchronized int capacity() {
        return value.length;
    }


    @Override
    public synchronized void ensureCapacity(int minimumCapacity) {
        super.ensureCapacity(minimumCapacity);
    }

    @Override
    public synchronized void trimToSize() {
        super.trimToSize();
    }

    @Override
    public synchronized void setLength(int newLength) {
        toStringCache = null;
        super.setLength(newLength);
    }

    @Override
    public synchronized char charAt(int index) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        return value[index];
    }
    .
    .
    .
    .
}

2.2 StringBuilder

StringBuilder 是 JDK 5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    /** use serialVersionUID for interoperability */
    static final long serialVersionUID = 4383685877147921099L;

    public StringBuilder() {
        super(16);
    }

    public StringBuilder(int capacity) {
        super(capacity);
    }

    public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }

    public StringBuilder(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }

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

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

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值