运维开发网

Java基础-重新认识字符串

运维开发网 https://www.qedev.com 2020-10-13 08:10 出处:51CTO 作者:俗世游子
字符串作为我们开发中最常用的类型之一,我们真的了解这个类型么?接下来我们好好聊一聊字符串。

来,进来的Java程序猿,我们认识一下。

我是俗世游子,在外流浪多年的Java程序猿

重新认识String

首先,我们来看个小栗子,保证你看了既陌生又熟悉:

public class St {
    public static void main(String[] args) {
        System.out.println("Hello World! ! !");
    }
}

熟悉不,有没有感觉回到了刚刚入门的时刻?

上面,我们输出了一个字符串,在之后的开发生涯中,用String定义的字符串对象也频繁的出现在我们的代码中,比如下面的两种方式,就是我们使用的定义方式:

String s1 = "abc";
String s3 = "abc";
String s2 = new String("abc");

// ----- s1 == s2 true
s2.intern();

这里,我想到了一道常见的面试题:已上面为例

  1. s1 == s2 ?
  2. s1 == s3 ?
  3. s2一共创建了几个对象?

而且,我想让大家想一想,如果让你们来介绍String,会如何介绍呢?

  • String 类型不属于Java的8种基本数据类型,类不可被继承
  • String是一个不可变对象,API方法返回的其实是一个新的String对象

  • 底层通过char类型的数组来存储

好,那么接下来我们就好好的剖析下这个String类型

从源码看String

整体结构

首先,我们来看看String的结构图

Java基础-重新认识字符串

我们来看看实现源码:

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;

    public String() {
        this.value = "".value;
    }
    // 其他的构造方法
}

我们可以看到,整个String类被final修饰,我们都知道,被final修饰过的类或者对象

  • 不可被继承

我们通过查看其构造方法可以发现,同样被final修饰的char数组存储着我们定义的字符串,看下图也可以看得出字符串的一个存储结构

Java基础-重新认识字符串

所以说我们通过chatAt()方法的下标能够得到指定的字符,原因就在于字符串是通过char数组来储存的。

同样的,char数组被final修饰,权限是private,而且没有提供对外设置的方法

  • String是一个不可变的对象

但是我们要明白一点,这里的不可变指的是:底层存储char数组在内存中地址引用不可变,但是其本身的内容我们是可以通过一些手段来改变的,比如:反射

如果我们以后也想实现一个不可变的类,就可以参考String来实现

具体方法

我们在看源码的时候,我们要重点看一看作者在实现一些方法时的思路,有什么地方的想法是我们可以借鉴的,好用在我们以后的代码设计中(逼格高)

作为重写方法出现频率最高的两个方法,我们就看String是如何实现的

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

为什么在计算的时候,采用的是 31

很简单,我们都知道,计算机的底层数据都是0和1,而31的二进制数值正好是 11111,在计算上可以进行移位操作,效率较高

2. equals()

在Java开发中,我们判断两者是否相等的时候,使用的两种方式

  • ==
  • equals

而我们在比较字符串的时候会推荐使用equals,下面是String中的实现方式

public boolean equals(Object anObject) {
    if (this == anObject) {
        /**如果是当前对象,就直接返回true*/
        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;
            /**循环字符数组进行item的验证*/
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

方法实现上还是比较简单的。对比的char数组每个值是否相等。

如果以后有这样的需求:判断两者是否相等?那么我们就可以参考上面的实现,从底层结构出发,我们就可以通过底层结构快速想到贴合实际的思路。

所以说,看源码重点是学习这种思路

那有人就会问了,既然equals比较的是具体内容,那么==比较的是什么呢?

其实,如果对比项是基本数据类型, 那么对比的就是基本数据类型的值是否相等。比如 5 == 5 = true

还有对比的一点:对比的是内存空间地址是否相等。

这里应该怎么说呢?下面说

3. intern()

/* 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.
*/
public native String intern();

简单一点来说,如果定义的字符串在常量池中定义过,并且通过equals()对比相等的话,那么就直接返回字符串在常量池中的内存地址给变量,看下图

Java基础-重新认识字符串

从实际案例分析String

回到最初我给大家列出的那个面试题:

String s1 = "abc";
String s3 = "abc";

String s2 = new String("abc");

System.out.println(s1 == s3);   true
System.out.println(s1 == s2);   false

s2 = s2.intern();
System.out.println(s1 == s2);   true

大家亲自尝试下,看看我输出的对不对

s1 == s3

讲解这一点之前,需要先跟大家说明下String字符串在内存空间中的一个存放

Java基础-重新认识字符串

String定义的字符串会存放到一个叫做常量池的地方,这个常量池在JDK1.7之后放在了堆空间中。

首先,s1="abc",会在常量池中开辟一块空间存放字符串abc,然后将abc的引用地址指向s1。

接下来是s3="abc",这里和之前有区别:如果常量池中存在当前字符串,那么就直接将当前字符串的引用地址再指向定义的对象。如果不存在,就先存放字符串然后再指向引用地址

也就是说,s1和s3虽然定义了两个变量,但是在内存空间中它们指向的地址都是一样的,所以说s1==s3

s1 != s2

Java基础-重新认识字符串

s1还是之前的s1

s2是通过new来定义出来的变量,这样在堆空间中会开辟一块新内存,构造方法传入'abc'字符串,常量池中的abc字符串的引用地址会先指向堆空间开辟的内存空间,然后new出来的地址再指向s2,这和直接从常量池中引用的地址肯定是不一样的。

所以s1 != s2

但是,如果调用了intern()之后,s2的空间地址会直接指向常量池中字符串的地址,和s1的空间地址就是一样的,所以s1和s2也就相等了

这里也从侧面印证出了==还会对比内存空间中的引用地址是否相等

s2 创建了几个对象

String s2 = new String("abc");

这里就不用多说了吧,看上面的图就知道了,创建了2个对象

可修改字符串

上面我们说到,String是不可变对象,在进行字符串操作时每次都会产生新的对象,这样存在一些缺点:

  • 操作效率低下,每次操作生成新的对象,然后变量的引用地址也要跟着变化
  • 内存浪费严重

针对这些问题,Java为我们提供了另外两个新的操作字符串的类,

  • StringBuffer
  • StringBuilder

从功能和API方法上讲,这两个类没有任何区别,都是用来操作字符串的类并且可以多次修改,并不会产生新的对象。

那为什么还会有两个不同的类呢? 我们通过源码来看下其中的区别

特性

结构

首先,两者都继承自 AbstractStringBuilder

Java基础-重新认识字符串

Java基础-重新认识字符串

而且,通过查看其父类源码,我们可以发现一点:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

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

StringBufferStringBuilder在底层存储结构上,和String没有区别,而且,两类同样被final修饰,

  • 同样说明了该类不可被继承

唯一不同是:

  • 我们可以指定该char数组的初始长度,比如
StringBuffer stringBuffer = new StringBuffer(20);
StringBuilder stringBuilder = new StringBuilder(20);

还有一点:相信我们很多人都这样写过:

/**
无参方法
*/
StringBuffer stringBuffer = new StringBuffer();
StringBuilder stringBuilder = new StringBuilder();
/**
默认初始值方法
*/
StringBuffer stringBuffer = new StringBuffer("abc");
StringBuilder stringBuilder = new StringBuilder("abc");

直接看源码,通过构造方法查看两者区别:

  • 如果是无参构造方法的话,那么char数组的初始长度为:16
  • 如果添加了默认初始值的构造方法的话,那么char数组的初始长度为:当前字符串的长度 + 16

线程安全性

我们抽出其中常用的一个方法来看看:append()

StringBuffer 的 append()

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

StringBuilder 的 append()

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

通过查看两者的方法,很明显的一个区别:

  • StringBuffer 的方法存在 synchronized, 而且如果我们翻一下StringBuffer的源码的话,我们会发现其所有的方法都存在这个关键字

对线程有了解的童鞋都知道这个关键字的意义:同步锁,对应方式是同步方法

  • 表示当前方法是线程安全的方法
  • StringBuilder方法进行对比的话,那么在执行效率上就略低一些

所以说,如果我们在多线程环境下需要对字符串进行操作的话,那么优先推荐采用StringBuffer,其他方面的话,对效率没要求两者都行,否则的话,就推荐采用StringBuilder

关于append(),这里要额外说一点:

我们前面说过,StringBufferStringBuilder构造方法是会传递数组的初始长度,那么我们来看看append方法是如何进行长度扩容的(没错,不止ArrayList会扩容):

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

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

针对数组扩容,很简单的逻辑:

  • 判断当前长度和字符串长度相加是否大于最初设定的初始长度
  • 如果没有超过,那么赋值就行了
  • 如果超过,复制一个新的数组出来,然后在赋值

完结

到这里,我们关于字符串的内容也就完结了,关于具体的API方法,推荐直接查看官方文档:

StringBuffer文档介绍

StringBuilder文档介绍

String文档介绍

扫码领视频副本.gif

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号