BlazeMaple BlazeMaple
首页
  • 基础知识

    • Java的基本数据类型
    • Java中的常用类String
    • Java中的异常
    • Java中的注解
    • Java中的反射机制
    • Java中的泛型
    • Java为什么是值传递
  • 集合框架

    • Java集合核心知识总结
    • HashMap的7种遍历方式
    • 源码分析
  • Java新特性

    • Java8新特性
  • IO流

    • Java基础IO总结
    • Java IO中的设计模式
    • Java IO模型
    • IO多路复用详解
  • 并发编程

    • 并发编程基础总结
  • JVM

    • JVM基础总结
  • MySQL

    • MySQL核心知识小结
    • MySQL 45讲
  • Redis

    • Redis核心入门知识简记
  • Spring
  • SpringCloud Alibaba
  • 开发工具

    • Git详解
    • Maven详解
    • Docker详解
    • Linux常用命令
  • 在线工具

    • json (opens new window)
    • base64编解码 (opens new window)
    • 时间戳转换 (opens new window)
    • unicode转换 (opens new window)
    • 正则表达式 (opens new window)
    • md5加密 (opens new window)
    • 二维码 (opens new window)
    • 文本比对 (opens new window)
  • 学习资源

    • 计算机经典电子书PDF
    • hot120
GitHub (opens new window)
首页
  • 基础知识

    • Java的基本数据类型
    • Java中的常用类String
    • Java中的异常
    • Java中的注解
    • Java中的反射机制
    • Java中的泛型
    • Java为什么是值传递
  • 集合框架

    • Java集合核心知识总结
    • HashMap的7种遍历方式
    • 源码分析
  • Java新特性

    • Java8新特性
  • IO流

    • Java基础IO总结
    • Java IO中的设计模式
    • Java IO模型
    • IO多路复用详解
  • 并发编程

    • 并发编程基础总结
  • JVM

    • JVM基础总结
  • MySQL

    • MySQL核心知识小结
    • MySQL 45讲
  • Redis

    • Redis核心入门知识简记
  • Spring
  • SpringCloud Alibaba
  • 开发工具

    • Git详解
    • Maven详解
    • Docker详解
    • Linux常用命令
  • 在线工具

    • json (opens new window)
    • base64编解码 (opens new window)
    • 时间戳转换 (opens new window)
    • unicode转换 (opens new window)
    • 正则表达式 (opens new window)
    • md5加密 (opens new window)
    • 二维码 (opens new window)
    • 文本比对 (opens new window)
  • 学习资源

    • 计算机经典电子书PDF
    • hot120
GitHub (opens new window)
  • 基础知识

    • Java的基本数据类型
    • 聊一聊Java中的常用类String
      • 聊一聊Java中的异常
      • 聊一聊Java中的注解
      • 聊一聊Java中的反射机制
      • 聊一聊Java中的泛型
      • 聊一聊Java为什么是值传递
    • 集合框架

      • Java集合核心知识总结
      • HashMap 的 7 种遍历方式
      • 源码分析

        • ArrayList源码分析
        • LinkedList源码分析
        • HashMap源码分析
        • ConcurrentHashMap源码分析
        • CopyOnWriteArrayList 源码分析
        • LinkedHashMap 源码分析
        • ArrayBlockingQueue 源码分析
        • PriorityQueue 源码分析
        • DelayQueue 源码分析
    • Java新特性

      • Java8新特性
    • Java基础
    • 基础知识
    BlazeMaple
    2023-10-30
    目录

    聊一聊Java中的常用类String

    # String、StringBuffer、StringBuilder 的区别?

    # 从可变性分析

    1. String不可变。
    2. StringBuffer、StringBuilder都继承自AbstractStringBuilder ,两者的底层的数组value并没有使用private和final修饰,所以是可变的。AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法

    AbstractStringBuilder 源码:

    abstract class AbstractStringBuilder implements Appendable, CharSequence {
        char[] value;
        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
    10
    11
    12
    13
    14

    # 从线程安全性分析

    1. String类是常量线程安全。
    2. StringBuilder 线程不安全。
    3. StringBuffer线程安全。

    StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

    # 从性能分析

    1. String是常量每次添加字符串都会将引用指向新的字符串。
    2. StringBuilder 非线程安全所以性能上相较于StringBuffer会快10%-15%

    对于三者使用的总结:

    1. 操作少量的数据: 适用 String
    2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
    3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

    # 为什么String是不可变的?

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

    补充:Java 9 为何要将 String 的底层实现由 char[] 改成了 byte[] ?

    新版的 String 其实支持两个编码方案:Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符

    # 字符串拼接用“+” 还是 StringBuilder?

    public class StringTest {
        @Test
        public void addTest() {
            String s1 = "hello";
            String s2 = "world";
            String s3 = "guy";
            String s4 = s1 + s2 + s3;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
        ALOAD 1
        INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
        ALOAD 2
        INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
        ALOAD 3
        INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
        INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
        ASTORE 4
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    查看其字节码可以看到JVM为了避免大量常量创建,会将其进行优化,改用StringBuilder进行拼接后toString

    不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。

    String[] arr = {"he", "llo", "world"};
    String s = "";
    for (int i = 0; i < arr.length; i++) {
        s += arr[i];
    }
    System.out.println(s);
    
    1
    2
    3
    4
    5
    6

    StringBuilder 对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder 对象

    如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了。

    String[] arr = {"he", "llo", "world"};
    StringBuilder s = new StringBuilder();
    for (String value : arr) {
        s.append(value);
    }
    System.out.println(s);
    
    1
    2
    3
    4
    5
    6

    不过,使用 “+” 进行字符串拼接会产生大量的临时对象的问题在 JDK9 中得到了解决。在 JDK9 当中,字符串相加 “+” 改为了用动态方法 makeConcatWithConstants() 来实现,而不是大量的 StringBuilder 了。这也意味着 JDK 9 之后,你可以放心使用“+” 进行字符串拼接。

    # String的equals() 和 Object的equals() 有何区别?

    String对equals进行了重写,String比较的是字符串的值是否一致

    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;
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    而Object比较的则是两者的引用地址是否一致

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

    # 字符串常量池是什么,它有什么用?

    字符串常量池是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。JVM篇中会详细说明字符串常量池。

    # String s1 = new String("abc")分析

    1、如果字符串常量池中不存在字符串对象“abc”的引用,那么它将首先在字符串常量池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。

    2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。

    在这里插入图片描述

    # String的intern()方法有什么作用?

    String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:

    • 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
    • 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
    // 在堆中创建字符串对象”Java“
    // 将字符串对象”Java“的引用保存在字符串常量池中
    String s1 = "Java";
    // 直接返回字符串常量池中字符串对象”Java“对应的引用
    String s2 = s1.intern();
    // 会在堆中在单独创建一个字符串对象
    String s3 = new String("Java");
    // 直接返回字符串常量池中字符串对象”Java“对应的引用
    String s4 = s3.intern();
    // s1 和 s2 指向的是堆中的同一个对象
    System.out.println(s1 == s2); // true
    // s3 和 s4 指向的是堆中不同的对象
    System.out.println(s3 == s4); // false
    // s1 和 s4 指向的是堆中的同一个对象
    System.out.println(s1 == s4); //true
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    #

    # String 类型的变量和常量做“+”运算时发生了什么?

    1、字符串不加 final 关键字拼接的情况

    String str1 = "str";
    String str2 = "ing";
    String str3 = "str" + "ing";
    String str4 = str1 + str2;
    String str5 = "string";
    System.out.println(str3 == str4);//false
    System.out.println(str3 == str5);//true
    System.out.println(str4 == str5);//false
    
    1
    2
    3
    4
    5
    6
    7
    8

    对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化:在编译过程中,Javac 编译器会进行一个叫做 常量折叠(Constant Folding) 的代码优化。常量折叠就是将常量表达式计算求值,并用求得的值来替换表达式,然后放到常量表中的一种机制。

    对于 String str3 = "str" + "ing"; 编译器会给你优化成 String str3 = "string"; 。

    并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:

    • 基本数据类型( byte、boolean、short、char、int、float、long、double)以及字符串常量。
    • final 修饰的基本数据类型和字符串变量
    • 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )

    引用的值在程序编译期是无法确定的,编译器无法对其进行优化。

    对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。

    2、使用final 关键字修饰后

    final String str1 = "str";
    final String str2 = "ing";
    // 下面两个表达式其实是等价的
    String c = "str" + "ing";// 常量池中的对象
    String d = str1 + str2; // 常量池中的对象
    System.out.println(c == d);// true
    
    1
    2
    3
    4
    5
    6

    被 final 关键字修饰之后的 String 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。

    如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。比如:

    final String str1 = "str";
    final String str2 = getStr();
    String c = "str" + "ing";// 常量池中的对象
    String d = str1 + str2; // 在堆上创建的新的对象
    System.out.println(c == d);// false
    public static String getStr() {
          return "ing";
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

    # String有长度限制吗?

    String有长度限制,分为编译期和运行期

    **编译期:**编译期需要用CONSTANT_utf8_info结构用于表示字符串常量的值,而这个结构是有长度限制的,为65535

    **运行期:**String的length参数是int类型的,所以,String定义的时候,支持的最大长度就是int的最大范围值,最大值为2^31-1

    帮助我们改善此页面! (opens new window)
    上次更新: 2024/08/13, 09:07:12
    Java的基本数据类型
    聊一聊Java中的异常

    ← Java的基本数据类型 聊一聊Java中的异常→

    最近更新
    01
    SpringCloud Alibaba实战
    08-22
    02
    SpringCloud Alibaba核心知识
    08-22
    03
    两数之和
    08-08
    更多文章>
    Theme by Vdoing | Copyright © 2023-2024 BlazeMaple
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式