String类详解

在Java编程中,除Object类外,最常用的类就是String类了。本文将从String类源码出发,对String类进行一个全面的分析,以帮忙我们更好的理解和使用String类。

String类概述

Java 使用 String 类代表字符串。Java 中的所有字符串字面值(如 “abc” )都使用此类实现。字符串是常量;它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享。

String的不可变性(immutable)

在Java中,String是不可变的,这主要体现在三个方面:(1)String类使用final关键字修饰,表示其不可继承;(2)String类使用字节数组存储数据,且使用final关键字修饰,表示该字段创建后引用地址不可变。另外该字符数组的访问权限为 private,表示外部无法访问,且 String 没有对外提供可以修改该属性的方法。关键源码如下:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    @Stable
    private final byte[] value;

    // ...
}

注意,字符串的底层实现使用字节数组存储。使用字节数字而非字节数组的好处是,字节数组与数据的底层存储保持一致,无需额外转换。使用字节数组,保证使用指定的编码、解码方式,屏蔽了底层设备的差异。对于网络传输场景,无需进行额外的转换(网络数据传输使用字节流)。

String不可变性的好处

将String设计成不可变,主要有以下方面的考虑:(1) 出于性能方面的考虑。可以实现多个变量引用堆内存中的同一个字符串实例,避免创建的开销。因为String太过常用,JAVA类库的设计者在实现时做了个小小的变化,即采用了享元模式,每当生成一个新内容的字符串时,他们都被添加到一个共享池中,当第二次再次生成同样内容的字符串实例时,就共享此对象,而不是创建一个新对象,注意,这种方式仅适用于"="操作符创建的String对象。(2) 出于安全方面的考虑。由于String的不可变性,可以确保创建后不会被篡改(当然,这也不是绝对,使用反射仍可修改对象的值)。(3) 防止内存泄露。HashMap 的 key 为String类型,如果String对象可变,则会造成该key无法手动删除,从而造成内存泄露。(4)出于并发安全的考虑。由于String的不可变性,多个线程可以安全的共享String对象,则不用担心被修改。

字符串的创建

使用字符串时,遇到的第一个问题就是字符串的创建。根据是否使用运算符创建,可以将其分为两类:(1) 直接赋值创建字符串;(2)使用构造方法创建字符串。

直接赋值创建字符串

直接赋值创建字符串就是使用运算符直接赋值,可以使用的运算符有等号和加号。示例代码如下:

String strWithEqualOperator = "foo";
String strWithAddOperator = strWithEqualOperator + "test";

直接赋值创建字符串时,会优先从字符串常量池中获取已存在的字符串,如果不存在,则会将新生成的字符串添加到常量池,方便下次使用。字符串常量池是享元模式的具体应用,后面会进一步介绍。
注意,这里使用"+"运算符的语法,在Java编译阶段,会将变量替换成真实的字符串并完成拼接。

使用构造方法创建字符串

除了直接赋值创建字符串外,还可以使用构造方法创建字符串。String类支持多种场景的创建。如字节数组、字符数组、StringBuilder实例等,这里不再一一列举。关键源码如下(为避免方法过长,影响阅读,仅展示方法声明,具体实现可以参考源码):

    // 创建空字符串
    public String();

    // 基于字符串对象创建字符串对象
    @HotSpotIntrinsicCandidate
    public String(String original);

    // 基于字符数组创建字符串对象
    public String(char value[]);
    
    // 基于字符数组指定长度创建字符串对象
    public String(char value[], int offset, int count);

    // 基于整数数组指定长度创建字符串对象
    public String(int[] codePoints, int offset, int count);

    // 基于字节数组指定长度创建字符串对象
    public String(byte bytes[], int offset, int length, String charsetName)
            throws UnsupportedEncodingException;

    // 基于字节数组指定长度创建字符串对象
    public String(byte bytes[], int offset, int length, Charset charset);

    // 基于字节数组创建字符串对象
    public String(byte bytes[], String charsetName)
            throws UnsupportedEncodingException;

    // 基于字节数组创建字符串对象
    public String(byte bytes[], Charset charset);

    // 基于字节数组指定长度创建字符串对象
    public String(byte bytes[], int offset, int length);

    // 基于字节数组创建字符串对象
    public String(byte[] bytes);

    // 基于StringBuffer实例创建字符串对象
    public String(StringBuffer buffer);

    // 基于StringBuilder实例创建字符串对象
    public String(StringBuilder builder);

需要说明的是,使用构造方法创建字符串对象时,不会复用字符串常量池。如果两个字符串对象分别使用相同的字符串字面量由构造函数创建,且使用等号运算进行比较,因为是两个不同的对象,所以尽管其值相同,但不相等。示例如下:

String str1 = new String("foo");
String str2 = new String("foo");
// 打印false
System.out.println(str1 == str2);

所以,在进行字符串比较时,尽量使用equals方法,而不要使用相等运算符。

字符串常量池与享元模式

为了提高对象的复用率,减少重复对象的内存占用,Java语言引入了字符串常量池。针对使用赋值运算符创建的字符串对象,Java会优先从字符串常量池中尝试获取该对象,如果对象存在,则直接复用。如果对象不存在,则新生成一个字符串并将其放到常量池中。熟悉缓存使用的同学可能会发现,字符串常量池的使用与缓存的使用一致,这里缓存的对象是字符串对象。从设计模式角度来说,字符串常量池是享元设计模式的具体应用。
所以,在以后的字符串创建操作中,为了提高内存的复用率,尽量使用赋值运算符创建对象

字符串的比较

针对字符串,最常用的功能就是字符串的相等比较。针对字符串的相等比较有两种选择:使用==运算符和使用equals方法。对于Java语言来说,==运算符,对于“值类型”和“空类型”是比较他们的值;对于“引用类型”是是比较对象在内存中的存放地址,即是否指向指向同一个对象。对Object类的equals方法来说,其功能与 == 运算符一致,但是String类重写了该方法,使其可以进行值相等比较。关键源码如下:


public class Object {
    //...

    public boolean equals(Object obj) {
        return (this == obj);
    }
}

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    // ...

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String aString = (String)anObject;
            // 优先比较hashCode是否相等
            if (coder() == aString.coder()) {
                return isLatin1() ? StringLatin1.equals(value, aString.value)
                                    : StringUTF16.equals(value, aString.value);
            }
        }
        return false;
    }
}

所以,在对字符串进行相等比较时,为了减少字符串底层存储的优化带来的可能影响,尽量使用equals方法。更多equals方法和==运算符使用细节上的差异,可以参考笔者之前的文章

除了相等比较,String类还实现了Comparable接口,支持自定义比较。关键代码如下:

public int compareTo(String anotherString) {
    byte v1[] = value;
    byte v2[] = anotherString.value;
    if (coder() == anotherString.coder()) {
        return isLatin1() ? StringLatin1.compareTo(v1, v2)
                            : StringUTF16.compareTo(v1, v2);
    }
    return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
                        : StringUTF16.compareToLatin1(v1, v2);
    }

其他常用方法

除了构造方法、比较方法,String类还提供了不少方便的功能,如:字符串分割、字符串转换、字符串格式化。这里不再进一步介绍,有兴趣的同学可以阅读相关源代码。
请添加图片描述

总结

String类是Java语言中使用频率极高的类,针对String类的源码分析有利于更好的理解和使用String类。String对象的最大特点是其不可变性。String的不可变性,使其在性能、不可篡改、并发安全等方面展现优越性。在使用String类时,要善于利用该特性。如HashMap使用String类型作为key。在查询key时,优先对其hash-code进行比较。在创建字符串时,为了提高内存的复用率,尽量使用赋值运算符创建对象。这种方式会优先从字符串常量池中获取重复对象,减少不必要的内存。不同的创建方式(直接赋值创建字符串、使用构造方法创建字符串)会带来字符串比较上的不同。为减少字符串底层存储的策略差异带来的影响,推荐使用equals方法来进行字符串的相等比较。为了更好的使用String类,还应熟悉其提供的常用方法,如:字符串比较、字符串分割、字符串转换、字符串格式化等。

参考

https://www.cnblogs.com/zhangyinhua/p/7689974.html Java常用类(二)String类详解
https://zhuanlan.zhihu.com/p/94228628 String 的不可变性
https://www.alpharithms.com/byte-array-sequences-of-8-bit-groups-183908/ 字节数组

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值