String的那些事儿

String作为我们最常用的Java类之一,在日常开发过程中充当着重要角色?那么大家真的了解String吗?让我们一起看看下面的问题:

  • String内存结构?对象存储在堆上还是栈上?
  • 一个String有多长?占内存多大?
  • 字符串拼接过程中内存对象是怎么管理的?
  • String.intern是用来干嘛的?
  • String底层数据结构是什么?
  • String为什么设计成final类?
  • 请简单实现下String的equals方法
  • String的hashCode方法有了解吗?有没有可能两个String的值不同,但是hashCode相同?这种情况被称为什么?有什么解决方案?

String内存分配与最大长度

要谈String的内存分配与最大大小,我们首先应该清楚String中Java中有两种创建方式,如下所示:

// 直接使用""定义
String hello = "Hello";

// 使用new关键词创建
String nextHello = new String("nextHello");

那么这两种创建的String有什么不同呢?我们一起来看下,编写测试代码如下:

public class StringTest {
    public void testString(){
        String s1 = "Hello";
        String s2 = new String("nextHello");
        System.out.println(s1);
        System.out.println(s2);
        s1 = "World";
        System.out.println(s1);
    }
}

查看StringTest类字节码,如下所示:

1-3-5-1

1-3-5-2

1-3-5-3

可以看出使用"“创建的Hello和World字符串均声明在常量池中,而常量池中一个字符串的最大长度为65535个字符,故使用”"创建的字符串最大长度为65535.最大内存大小为65535*2 byte 约等于 128 KB。

继续查看StringTest的字节码文件,可以看到String s2 = new String("nextHello");对应的字节码如下所示:

1-3-5-4

可以看出这里首先在常量池中生成nextHello字符串,随后在堆上创建String对象,通过astore_2指令将新创建的对象引用赋值给s2。

ldc,astore_2这两个指令的说明也能印证上述结论,两个指令的说明如下所示:

1-3-5-6

1-3-5-5

那么通过new String创建的字符串最大长度是多少呢?由于String底层使用char数组实现,故其最大长度为Integer.MAX_VALUE个字符(理论上),最大内存大小为2*Integer.MAX_VALUE byte 约等于 4GB。

String为什么被设计成final?

从final那些事儿一文中可以知道,final修饰类时,则该类不可被继承,也就意味着其内部的实现和依赖是不可更改的,进而导致不论如何创建的String实例,其必然关联的是String对象,而不可能是其他类的对象,String不可变,那么什么是String不可变,为什么要设计成不可变的呢?

String不可变

以下述代码为例:

String s  = "abc";
s = "abcd";

其执行顺序如下,常量池创建"abc"字符串,变量s指向常量池新创建的字符串,随后在常量池中创建字符串"abcd",再次将s指向"abcd"所在地址,而不是将字符串"abc"修改为字符串"abcd"。

为什么String不可变

在源码中可以看到,为了实现String不可变,在String内部,底层数据结构是char数组,该数组同样final修饰,从final那些事儿一文可知,如果final修饰数组,只要数组指向地址不变,其内部元素值可以发生改变,故而进一步也用final修饰了String类,同样在该类并没有暴露任何访问内部成员字段的接口,进而最终实现String不可变。

当然反射修改String底层数组中的元素值仍然是可行的,但没有必要

String不可变的优势

String不可变主要有以下优势:

  • 安全:在Java中,函数传参使用引用传递,String作为广泛应用的类型,如果其可变,在多个函数的层次传递过程中,极有可能被修改导致异常,同时HashMap,HashSet等类都可以使用String作为key,如果String可变,则键值,整个存储结构就乱套了。
  • 内存利用率高,提升效率:在大量使用字符串的场景下,字符串重用,减少内存开销,避免频繁GC

String拼接原理

在日常开发中,我们经常使用 " + " 号进行字符串拼接,那么其底层是如何实现的呢?编写测试代码如下:

public class Person {
    private int mAge = 100;

    public Person(int mAge) {
        this.mAge = mAge;
    }

    public int getAge() {
        return mAge;
    }
}

public class StringTest {
    public void testString() {
        Person person = new Person(200);
        String s1 = "String Test";
        int a = 300;
        System.out.println("Test String append:" + s1 + ",person.Age:" + person.getAge() + ",a:" + a);
    }
}

public class Main {
    public static void main(String[] args) {
        StringTest stringTest = new StringTest();
        stringTest.testString();
    }
}

查看StringTest的testString函数字节码,如下图所示:

1-3-5-7

可以看出,对于System.out.println("Test String append:" + s1 + ",person.Age:" + person.getAge() + ",a:" + a);从其字节码可以看出,“Test String append:”,",person.Age:“以及”,a:"这三个字符串,存储在字符串常量池中,通过ldc指令取出,在整个代码执行流程中,主要是StringBuilder.append来实现字符串拼接的,最终将拼接后的内容输出。

以上例子都是变量参与字符串拼接,那么如果有常量参与,又会怎样呢?

1-3-5-8

可以看出,当常量参与字符串拼接时,编译器会直接替换内容(如变量s3),如果参与拼接的全是常量,则在编译时就会直接完成拼接操作(如变量s4)。

String.intern

查看String类的源码,可以看到String.intern是个native方法,声明如下所示:

/**
 * Returns a canonical representation for the string object.
 * <p>
 * A pool of strings, initially empty, is maintained privately by the
 * class {@code String}.
 * <p>
 * When the intern method is invoked, if the pool already contains a
 * string equal to this {@code String} object as determined by
 * the {@link #equals(Object)} method, then the string from the pool is
 * returned. Otherwise, this {@code String} object is added to the
 * pool and a reference to this {@code String} object is returned.
 * <p>
 * It follows that for any two strings {@code s} and {@code t},
 * {@code s.intern() == t.intern()} is {@code true}
 * if and only if {@code s.equals(t)} is {@code true}.
 * <p>
 * All literal strings and string-valued constant expressions are
 * interned. String literals are defined in section 3.10.5 of the
 * <cite>The Java&trade; Language Specification</cite>.
 *
 * @return  a string that has the same contents as this string, but is
 *          guaranteed to be from a pool of unique strings.
 */
public native String intern();

从函数注释可以看出,当该函数被调用时,如果字符串常量池中已经有一个相同的字符串(equals返回true),则直接返回常量池中的字符串,如果常量池中不存在,则添加字符串到常量池并返回该字符串的引用

对于直接使用""创建的字符串而言,我们知道其创建在常量池中,故而调用其intern方法与直接访问并没有内存上的实际差异,我们重点来看下使用new关键字创建字符串的场景。首先我们来思考下String s2 = new String("Hello") + new String(" String");这行代码会创建几个对象?

其实前文中已有答案,其一共会有6个对象生成,如下图所示:

1-3-5-9

1-3-5-10

了解了new String参与时,相关对象的创建过程,我们接下来看下下述代码:

public void testString() {
    String s1 = new String("Hello  String");
    String s3 = s1.intern();
    String s2 = "Hello String";
    System.out.println(s1 == s2);
    System.out.println(s2 == s3);
}

运行后可以看到 s1 == s2返回false,s2 == s3返回true,也进一步说明String.intern的含义,s1不等于s2主要是因为s1指向的是堆内存的字符串地址,而s2指向的是常量池中的内存地址,s3等于s2,主要是经过s1.intern后,s3指向的是常量池中已经存在的"Hello String"字符串,后面创建s2时,由于常量池中已经存在,故两个变量指向的是同一个内存地址。

那么我们再来看下如下字符串拼接代码:

public class StringTest {
    public void testString() {
        String s1 = new String("Hello") + new String(" String");
        s1.intern();
        String s2 = "Hello String";
        System.out.println(s1 == s2);
    }
}

这里应该输出true还是false呢?这里返回的是true,大家猜对了吗?

查看这段代码对应的字节码如下:

1-3-5-11

可以看出,s1的赋值代码实际上等价于String s1 = StringBuilder.toString()而StringBuilder.toString是通过new String创建一个堆内存对象,此时再执行s1.intern,由于字符串常量池中无该字符串,则会将该字符串的引用添加到常量池中,进而后续创建的s2指向的也是常量池中该字符串的引用地址(内容一致)

String.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;
}

String.equals源码如上所示,可以看出其首先比较的时两个对象的引用地址是否相同,如果相同的话直接返回true,随后逐位比较字符串的内容,如果有一位不相同,则返回false,否则返回true。

String.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;
}

String.hashCode源码如上所示,可以看到其是逐位递归,依此对每一位字符应用算法, 将最后一位的算法取值作为全串的hash值。

最后一位的算法取值 = 31 * 前一位的算法取值 + 当前位的字符编码

从其算法可以看出,必然会出现两个不同的字符串hash取值一样的情况,这种情况被称为hash冲突,比较常见的hash冲突解决方案有链地址法和开放地址法,链地址法的经典实现就是HashMap,将hash值相同的key以链表形式存储下来,而开放地址法指的是在已有的hash值相同的元素后方,寻找空余位置处存放当前元素,当然也有其他的hash冲突解决方案,我们后续单独介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值