• 【JavaSE】深入理解String及相关面试题


    【JavaSE】深入理解String及相关面试题

    String概览

    String 被声明为 final,因此它不可被继承。

    内部使用 char 数组存储数据,该数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    String字符串的不可变性

    不可变字符串主要是:

    • String 类被 final修饰不能被子类继承,进而避免了子类破坏 String不可变。
    • 保存字符串的数组被final修饰并且是私有的,并且 String 类没有提供和暴露修改这个字符串的方法。

    实际中可变的原因:

    其实并不是改变 String ,是新创建了一个 String 对象指向改变后的值,原本的 String 成为副本字符串对象存留在内存中。

    定义一个字符串

    String s = "abcd";
    
    • 1

    image-20220906222442331

    s 中保存了 string 对象的引用。下面的箭头可以理解为“存储他的引用”。

    使用变量来赋值变量

    String s2 = s;
    
    • 1

    image-20220906222546772

    s2 保存了相同的引用值,因为他们代表同一个对象。

    字符串连接

    s = s.concat("ef");
    
    • 1

    image-20220906222627276

    s 中保存的是一个重新创建出来的 string 对象的引用。

    不可变的好处

    1. 可以缓存 hash 值

    因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。

    2. String Pool 的需要

    如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。

    img

    3. 安全性

    String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。

    4. 线程安全

    String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

    JDK 6 和 JDK 7 中 substring 的原理及区别

    substring(int beginIndex, int endIndex)方法在不同版本的 JDK 中的实现是不同的。

    substring() 的作用

    substring(int beginIndex, int endIndex)方法截取字符串并返回其[beginIndex,endIndex-1]范围内的内容。

    调用 substring()时发生了什么?

    你可能知道,因为 x 是不可变的,当使用 x.substring(1,3)对 x 赋值的时候,它会指向一个全新的字符串:

    image-20220906224114377

    然而,这个图不是完全正确的表示堆中发生的事情。因为在 jdk6 和 jdk7 中调用substring 时发生的事情并不一样。

    JDK 6 中的 substring

    String 是 通 过 字 符 数 组 实现 的 。 在 jdk 6 中 , String 类 包 含 三 个 成 员 变量 :char value[], int offset,int count。他们分别用来存储真正的字符数组,数组的第一个位置索引以及字符串中包含的字符个数。

    当调用 substring 方法的时候,会创建一个新的 string 对象,但是这个 string 的值仍然指向堆中的同一个字符数组。这两个对象中只有 count 和 offset 的值是不同的。

    image-20220906224213117

    下面是证明上说观点的 Java 源码中的关键代码:

    //JDK 6
    String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
    }
    public String substring(int beginIndex, int endIndex) {
    //check boundary
    return new String(offset + beginIndex, endIndex - beginIndex, value);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    JDK 6 中的 substring 导致的问题

    如果你有一个很长很长的字符串,但是当你使用 substring 进行切割的时候你只需要很短的一段。这可能导致性能问题,因为你需要的只是一小段字符序列,但是你却引用了整个字符串(因为这个非常长的字符数组一直在被引用,所以无法被回收,就可能导致内存泄露)。在 JDK 6 中,一般用以下方式来解决该问题,原理其实就是生成一个新的字符串并引用他。

    JDK 7 中的 substring

    上面提到的问题,在 jdk 7 中得到解决。在 jdk 7 中,substring 方法会在堆内存中创建一个新的数组。

    image-20220906224456964

    Java 源码中关于这部分的主要代码如下:

    //JDK 7
    public String(char value[], int offset, int count) {
    //check boundary
    this.value = Arrays.copyOfRange(value, offset, offset + count);
    }
    public String substring(int beginIndex, int endIndex) {
    //check boundary
    int subLen = endIndex - beginIndex;
    return new String(value, beginIndex, subLen);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    以上是 JDK 7 中的 subString 方法,其使用 new String 创建了一个新字符串,避免对老字符串的引用。从而解决了内存泄露问题。

    所以,如果你的生产环境中使用的 JDK 版本小于 1.7,当你使用 String 的 subString方法时一定要注意,避免内存泄露。

    replaceFirst、replaceAll、replace 区别

    replace、replaceAll 和 replaceFirst 是 Java 中常用的替换字符的方法,它们的方法定义是:

    replace(CharSequence target, CharSequence replacement) ,用replacement 替换所有的 target,两个参数都是字符串。

    replaceAll(String regex, String replacement) ,用 replacement 替换所有的regex 匹配项,regex 很明显是个正则表达式,replacement 是字符串。

    replaceFirst(String regex, String replacement) ,基本和 replaceAll 相同,区别是只替换第一个匹配项。

    可以看到,其中 replaceAll 以及 replaceFirst 是和正则表达式有关的,而 replace 和正则表达式无关。

    replaceAll 和 replaceFirst 的区别主要是替换的内容不同,replaceAll 是替换所有匹配的字符,而 replaceFirst()仅替换第一次出现的字符。

    String 对“+”的重载

    1、String s = “a” + “b”,编译器会进行常量折叠(因为两个都是编译期常量,编译期可知),即变成 String s = “ab”

    2、对于能够进行优化的(String s = “a” + 变量 等)用 StringBuilder 的 append()方法替代,最后调用 toString() 方法 (底层就是一个 new String())

    字符串拼接的几种方式和区别

    字符串,是 Java 中最常用的一个数据类型了。字符串拼接是我们在 Java 代码中比较经常要做的事情,就是把多个字符串拼接到一
    起。

    我们都知道,String 是 Java 中一个不可变的类,所以他一旦被实例化就无法被修改。

    但是,既然字符串是不可变的,那么字符串拼接又是怎么回事呢?

    其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。

    下面一段字符串拼接代码:

    String s = "abcd";
    s = s.concat("ef");
    
    • 1
    • 2

    其实最后我们得到的 s 已经是一个新的字符串了。如下图:

    image-20220906225424173

    s 中保存的是一个重新创建出来的 String 对象的引用。

    那么,在 Java 中,到底如何进行字符串拼接呢?字符串拼接有很多种方式,这里简单介绍几种比较常用的。

    使用+拼接字符串

    在 Java 中,拼接字符串最简单的方式就是直接使用符号+来拼接。如:

    String a = "lyh";
    String b = "yby";
    String c = a + "," + b;
    
    • 1
    • 2
    • 3

    Concat

    除了使用+拼接字符串之外,还可以使用 String 类中的方法 concat 方法来拼接字符串。

    String a = "lyh";
    String b = "yby";
    String c = a.concat(b);
    
    • 1
    • 2
    • 3

    StringBuffer || StringBuilder

    关于字符串,Java 中除了定义了一个可以用来定义字符串常量的 String 类以外,还提供了可以用来定义字符串变量的 StringBuffer 类,它的对象是可以扩充和修改的。

    使用 StringBuffer 可以方便的对字符串进行拼接。如:

    StringBuffer a = new StringBuffer("lyh");
    String b = "yby";
    StringBuffer c = a.append(",").append(b);
    
    • 1
    • 2
    • 3

    以上就是比较常用的五种在 Java 种拼接字符串的方式,那么到底哪种更好用呢?为什么 Java 开发手册中不建议在循环体中使用+进行字符串拼接呢?

    image-20220906230204309

    使用+拼接字符串的实现原理

    还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。

    String a = "lyh";
    String b = "yby";
    String c = a + "," + b;
    
    • 1
    • 2
    • 3
    String a = "lyh";
    String b = "yby";
    String c = (new StringBuilder()).append(a).append(",").append(b).toString();
    
    • 1
    • 2
    • 3

    通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将String 转成了 StringBuilder 后,使用其 append 方法进行处理的。

    那 么 也 就 是 说 , J a v a 中 的 + 对 字 符 串 的 拼 接 , 其 实 现 原 理 是 使 用StringBuilder.append。

    concat 是如何实现的

    我们再来看一下 concat 方法的源代码,看一下这个方法又是如何实现的。

    public String concat(String str)
    int otherLen = str.length();
    if (otherLen == 0) {
    return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这段代码首先创建了一个字符数组,长度是已有字符串和待拼接字符串的长度之和,再把两个字符串的值复制到新的字符数组中,并使用这个字符数组创建一个新的 String 对象并返回。

    通过源码我们也可以看到,经过 concat 方法,其实是 new 了一个新的 String,这也就呼应到前面我们说的字符串的不变性问题上了。

    StringBuffer and StringBuilder

    1. 可变性

    • String 不可变
    • StringBuffer 和 StringBuilder 可变

    2. 线程安全

    • String 不可变,因此是线程安全的
    • StringBuilder 不是线程安全的
    • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

    接下来我们看看 StringBuffer 和 StringBuilder 的实现原理。和 String 类类似,StringBuilder 类也封装了一个字符数组,定义如下:

    char[] value;
    
    • 1

    与 String 不同的是,它并不是 final 的,所以他是可以修改的。另外,与 String 不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:

    int count;
    
    • 1

    其 append 源码如下:

    public StringBuilder append(String str) {
    super.append(str);
    return this;
    }
    
    • 1
    • 2
    • 3
    • 4

    该类继承了 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    append 会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。

    StringBuffer 和 StringBuilder 类似,最大的区别就是 StringBuffer 是线程安全的,看一下 StringBuffer 的 append 方法。

    public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    该方法使用 synchronized 进行声明,说明是一个线程安全的方法。而 StringBuilder则不是线程安全的。

    效率比较

    用时从短到长的是:

    StringBuilder < StringBuffer < concat < +

    StringBuffer 在 StringBuilder 的基础上,做了同步处理,所以在耗时上会相对多一些。

    那么问题来了,前面我们分析过,其实使用+拼接字符串的实现原理也是使用的StringBuilder,那为什么结果相差这么多,高达 1000 多倍呢?

    我们再把以下代码反编译下:

    image-20220906231108568

    反编译后代码如下:

    image-20220906231134899

    我们可以看到反编译后的代码,在 for 循环中,每次都是 new 了一个 StringBuilder,然后再把 String 转成 StringBuilder,再进行 append。

    而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费。

    所以, Java 开发手册建议:循环体内,字符串的连接方式,使用 StringBuilder 的append 方法进行扩展。而不要使用+。

    总结

    因此,经过对比,我们发现,直接使用 StringBuilder 的方式是效率最高的。因为StringBuilder 天生就是设计来定义可变字符串和字符串的变化操作的。

    但是,还要强调的是:

    1、如果不是在循环体中进行字符串拼接的话,直接使用+就好了。
    2 、 如 果 在 并 发 场 景 中 进 行 字 符 串 拼 接 的 话 , 要 使 用 StringBuffer来 代 替StringBuilder。

    String.valueOf 和 Integer.toString 的区别

    我们有三种方式将一个 int 类型的变量变成呢过 String 类型,那么他们有什么区别?

    1.int i = 5;
    2.String i1 = "" + i;
    3.String i2 = String.valueOf(i);
    4.String i3 = Integer.toString(i);
    
    • 1
    • 2
    • 3
    • 4

    第三行和第四行没有任何区别,因为 String.valueOf(i)也是调用 Integer.toString(i)来实现的。

    第二行代码其实是 String i1 = (new StringBuilder()).append(i).toString(); 首先创建一个 StringBuilder 对象,然后再调用 append 方法,再调用 toString 方法。

    String.intern()

    使用 String.intern() 可以保证相同内容的字符串变量引用同一的内存对象。

    在 JVM 中,为了减少相同的字符串的重复创建,为了达到节省内存的目的。会单独开辟一块内存,用于保存字符串常量,这个内存区域被叫做字符串常量池。

    当代码中出现双引号形式(字面量)创建字符串对象时,JVM 会先对这个字符串进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回;否则,创建新的字符串对象,然后将这个引用放入字符串常量池,并返回该引用。

    下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用。intern() 首先把 s1 引用的对象放到 String Pool(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。

    String s1 = new String("aaa");
    String s2 = new String("aaa");
    System.out.println(s1 == s2);           // false
    String s3 = s1.intern();
    System.out.println(s1.intern() == s3);  // true
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果是采用 “bbb” 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中。

    String s4 = "bbb";
    String s5 = "bbb";
    System.out.println(s4 == s5);  // true
    
    • 1
    • 2
    • 3

    如何分割一个String?

    split() 方法根据匹配给定的正则表达式来拆分字符串。

    语法:

    public String[] split(String regex, int limit)
    
    • 1

    参数:

    示例:

    当limit等于1时,会把字符串分成长度为1的数组(就是转成数组,不分割)

            String str = "aaa,bbb,ccc,";
            String[] split = str.split(",", 1);
    
    • 1
    • 2
    长度:1
    值:
    aaa,bbb,ccc,
    
    • 1
    • 2
    • 3

    当limit等于2时,会把字符串分成长度为2的数组(从第一个","分割)

            String str = "aaa,bbb,ccc,";
            String[] split = str.split(",", 2);
    
    • 1
    • 2
    长度:2
    值:
    aaa
    bbb,ccc,
    
    • 1
    • 2
    • 3
    • 4

    以此类推,limit为多少,就分割成长度为多少的数组(最大长度等于可分割的长度)…

            String str = "aaa,bbb,ccc,";
            String[] split = str.split(",", 5);
    
    • 1
    • 2
    长度:5
    值:
    [aaa,bbb,ccc,"",""]
    
    • 1
    • 2
    • 3

    有两个特殊的值
    当limit为0的时候,会丢弃后面的空值,split()方法默认使用

        String str = "aaa,bbb,ccc,";
        String[] split = str.split(",", 0);
    
    • 1
    • 2
    长度:3
    值:
    aaa
    bbb
    ccc
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当limit为-1的时候,会保留后面的空字符串(按照可分割的最大长度分割)

            String str = "aaa,bbb,ccc,,";
            String[] split = str.split(",", -1);
    
    • 1
    • 2
    长度:5
    值:
    [aaa,bbb,ccc,"",""]
    
    • 1
    • 2
    • 3

    如何判断两个String是否相等

    有两种方式判断字符串是否相等,使用””或者使用equals方法。当使用””操作符时,不仅比较字符串的值,还会比较引用的内存地址。大多数情况下,我们只需要判断值是否相等,此时用equals方法比较即可。

            String s1 = "abc";
            String s2 = "abc";
            String s3= new String("abc");
            System.out.println("s1 == s2 ? "+(s1==s2)); //true
            System.out.println("s1 == s3 ? "+(s1==s3)); //false
            System.out.println("s1 equals s3 ? "+(s1.equals(s3))); //true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  • 相关阅读:
    电脑硬件——CPU散热器
    Redis系列之初识
    C++打怪升级(一)- 命名空间、缺省形参、重载
    图书管理系统的设计与实现/ssm的图书管理网站
    OAuth2及sa-token框架实践
    常见的RabbitMQ测试点及解决办法
    C语言程序-猜数字for
    洛谷P1202 黑色星期五Friday the Thirteenth
    SSI漏洞之[BJDCTF2020]EasySearch1
    好用的源代码加密软件有哪些?5款源代码防泄密软件推荐
  • 原文地址:https://blog.csdn.net/weixin_63566550/article/details/126755806