1.🥗String类简介

image.png

我们在前面的博客系统地过了一遍Java的语法 在我们写代码的时候,String总是充斥着前前后后。 但你会不会经常力不从心, “这个字符串怎么转换不成功啊” “*这个字符串到底是常量还是对象啊” “这字符串内存结构到底是什么啊” “为啥我的字符串数组莫名其妙显示空指针异常啊” ”字符串常量和字符串对象应该怎么比较啊“ ”这字符串数据不都是good嘛,怎么比一比又是false了*“ ”字符串怎么转化成int啊啊“ ”String类为啥是final修饰的捏?“ ”String?StringBuffer??StringBuilder???“ A:”什么?!String不是基本数据类型??它还有什么是我不知道的?“ B:”楼上的,你猜一猜String能被继承嘛?“ A:”…..*

哈哈,知识点看起来又多又杂,但我们好好缕一缕,它还是很好理解的。
在C语言中我们涉及到过字符串,但是在C语言中要表示字符串只能用字符数组或者字符指针,可以使用标准库提供的字符串系列函数完成大部分操作,但是这种将数据和操作数据方法分离开的方法不符合面向对象的思想,而字符串的应用又非常广泛,从上面的对话中就可以发现,哈哈哈。
因此Java语言专门提供了String类
我们不论是在写代码的时候,还是在校招的笔试当中,字符串都是常客。
面试也会经常问到关于String的问题

面试官:来来小伙子,简单背一下String类的源码呢? 小伙子:…我*,(直接丢offer而去)

那么我们就带着对它的期待往下看

2.🍗 String常用方法和性质

熟悉我的朋友应该也熟悉我的一图流,哈哈哈
String常用方法和性质.png

2.1 🥩 字符串的构造

String类提供给我们的构造方式很多,最常见的有以下三种

  1. //使用常量串来构造
  2. String s1 = "hello Gremmie";
  3. System.out.println(s1);
  1. //直接new出String对象
  2. String s2 = new String("hello 葛玉礼");
  3. System.out.println(s2);
  1. //使用字符数组进行构造
  2. char[] array = {'h','e','l','l','o',' ','玉','米'};
  3. String s3 = new String(array);
  4. System.out.println(s3);

🍲 [注意]:

  1. String是引用类型,内部其实并不存储字符串本身,在String类的实现源码中,String类实例变量是这样的👇 ```java public final class String implements java.io.Serializable, Comparable, CharSequence,

    1. Constable, ConstantDesc {

    @Stable private final byte[] value; //字符串实际上就存储在这个用final修饰的byte数组中

    private final byte coder;

    /* Cache the hash code for the string / private int hash; // Default to 0

  1. ```java
  2. /**
  3. * @author Gremmie102
  4. * @date 2022/4/21 15:49
  5. * @purpose : 比较字符串引用
  6. */
  7. public class StringTestDemo1 {
  8. public static void main(String[] args) {
  9. //s1和s2引用的是不同的对象,s1和s3引用的是同一个对象
  10. String s1 = new String("hello");
  11. String s2 = new String("world");
  12. String s3 = s1;
  13. System.out.println(s1.length());
  14. //获取字符串长度--输出5
  15. System.out.println(s1.isEmpty());
  16. //判断字符串长度是否为0,即是否为空
  17. }
  18. }
  1. 在Java中用双引号””引起来的也是String类型对象

    //打印”hello”字符串(String对象)的长度 System.out.println(“hello”.length);

2.2 🍙String对象的比较

字符串的比较是我们常用的操作,比如:字符串排序

  1. ==比较是否引用同一个对象

这里要注意的是,对于内置类型(基础数据),==比较的是变量中的值,对于引用类型==比较的是引用中的地址

  1. public static void main(String[] args) {
  2. int a = 10;
  3. int b = 20;
  4. int c = 10;
  5. //对于基本类型变量,==比较两个变量中存储的值是否相同
  6. System.out.println(a==b);//false
  7. System.out.println(a==c);//true
  8. //对于引用型变量,==比较两个引用变量引用的是否为同一个对象
  9. String s1 = new String("hello");
  10. String s2 = new String("hello");
  11. String s3 = new String("world");
  12. String s4 = s1;
  13. System.out.println(s1==s2);//false
  14. System.out.println(s2==s3);//false
  15. System.out.println(s1==s4);//true
  16. }
  1. boolean equals(Object object)

按照字符大小的顺序比较
String重写了父类Object中的equals方法,Object中equals默认会按照==来比较,String重写equals方法之后,语法规则为: s1.equals(s2)
我们看看源码
image.png
用代码来给大家演示解释一下👇

  1. public boolean equals(Object anObject){
  2. //1.先检测this和anObject是否为同一个对象比较,如果是的化则返回true
  3. if(this == anObject){
  4. return true;
  5. }
  6. //2.检测anObject是否为String类型的对象,如果是的话那就继续比较
  7. //不是的话那就返回false
  8. if (anObject instanceof String){
  9. String anotherString = (String)anObject;
  10. int n = value.length;
  11. //3.判断调用对象的this和anObject两个字符串的长度是否相同
  12. if (n == anotherString.value.length){
  13. char v1[] = value;
  14. char v2[] = anotherString.value;
  15. int i = 0;
  16. //4.按照字典顺序,从前往后逐个字符进行比较
  17. while(n-- != 0){
  18. if (v1[i] != v2[i])
  19. return false;
  20. i++;
  21. }
  22. return true;
  23. }
  24. }
  25. return false;
  26. }

然后我们再写一个main方法来实验一下

  1. public static void main(String[] args) {
  2. String s1 = new String("hello");
  3. String s2 = new String("hello");
  4. String s3 = new String("HELLO");
  5. //s1,s2,s3引用的是三个不同的对象,因此==的比较结果都是false
  6. System.out.println(s1 == s2);//false
  7. System.out.println(s1 == s3);//false
  8. //equals比较:String对象中的逐个字符
  9. //虽然s1和s2引用的不是同一个对象,但是两个对象中放置的内容,所以输出true
  10. //s1与s3引用的不是同一个对象,而且两个对象中内容也不同,因此输出false
  11. System.out.println(s1.equals(s2));//true
  12. System.out.println(s1.equals(s3));//false
  13. }
  1. int compareTo(String s)方法:按照字典序进行比较

与equals不同的是,equals返回的是boolean类型,而compareTo返回的是int类型。
具体比较方法为:

  1. 先按照字典顺序大小比较,如果出现了不等的字符,则直接返回这两个字符的大小差值
  2. 如果前k个字符相等(k为两个字符长度最小值)返回值为两个字符串的长度差值

源码如下👇
image.png
测试代码如下👇

  1. public static void main(String[] args) {
  2. String s1 = new String("abc");
  3. String s2 = new String("ac");
  4. String s3 = new String("abc");
  5. String s4 = new String("abcdef");
  6. System.out.println(s1.compareTo(s2));//不同,输出字符差值-1
  7. System.out.println(s1.compareTo(s3));//相同输出0
  8. System.out.println(s1.compareTo(s4));//前k个字符完全相同,输出长度差值-3
  9. }
  1. int compareToIgnoreCase(String str)方法:与compareTo方式相同,但是忽略大小写比较

源码👇
image.png

  1. public static void main(String[] args) {
  2. String s1 = new String("abc");
  3. String s2 = new String("ac");
  4. String s3 = new String("Abc");
  5. String s4 = new String("abcdef");
  6. System.out.println(s1.compareToIgnoreCase(s2));//不同,输出字符差值-1
  7. System.out.println(s1.compareToIgnoreCase(s3));//相同,输出0
  8. System.out.println(s1.compareToIgnoreCase(s4));//前k个字符完全相同,输出长度差-3
  9. }

2.3 🍱 字符串查找

这是一个超级实用的功能,String类中提供了一些常用的查找方法:

方法 功能
char charAt(int index) 返回index位置上字符,如果index为负数或者越界,抛出 IndexOutOfBoundsException异常
int indexOf(int ch) 返回ch第一次出现的位置,没有返回-1
int indexOf(int ch, int fromIndex) 从fromIndex位置开始找ch第一次出现的位置,没有返回-1
int indexOf(String str) 返回str第一次出现的位置,没有返回-1
int indexOf(String str, int fromIndex) 从fromIndex位置开始找str第一次出现的位置,没有返回-1
int lastIndexOf(int ch) 从后往前找,返回ch第一次出现的位置,没有返回-1
int lastIndexOf(int ch, int fromIndex) 从fromIndex位置开始找,从后往前找ch第一次出现的位置,没有返 回-1
int lastIndexOf(String str) 从后往前找,返回str第一次出现的位置,没有返回-1
int lastIndexOf(String str, int fromIndex) 从fromIndex位置开始找,从后往前找str第一次出现的位置,没有返回-1

这里大家要注意的是,上面表格中有 int ch 的参数,是char类型传进来强转换成int值,以此来方便程序进行,并不是说这个字符就是一个数哦

  1. public static void main(String[] args) {
  2. String s = "aaabbbcccaaabbbccc";
  3. System.out.println(s.charAt(3)); // 'b'
  4. System.out.println(s.indexOf('c')); // 6
  5. System.out.println(s.indexOf('c', 10)); // 15
  6. System.out.println(s.indexOf("bbb")); // 3
  7. System.out.println(s.indexOf("bbb", 10)); // 12
  8. System.out.println(s.lastIndexOf('c')); // 17
  9. System.out.println(s.lastIndexOf('c', 10)); // 8
  10. System.out.println(s.lastIndexOf("bbb")); // 12
  11. System.out.println(s.lastIndexOf("bbb", 10)); // 3

2.4 🥡 字符串的转化

我们在做题的时候,字符串的转化也是非常重要的,主要有以下几个

  1. 数值和字符串转化

    1. public static void main(String[] args) {
    2. // 数字转字符串
    3. String s1 = String.valueOf(1234);
    4. String s2 = String.valueOf(12.34);
    5. String s3 = String.valueOf(true);
    6. String s4 = String.valueOf(new Student("Hanmeimei", 18));
    7. System.out.println(s1);
    8. System.out.println(s2);
    9. System.out.println(s3);
    10. System.out.println(s4);
    11. System.out.println("=================================");
    12. // 字符串转数字
    13. // 注意:Integer、Double等是Java中的包装类型,这个后面会讲到
    14. int data1 = Integer.parseInt("1234");
    15. double data2 = Double.parseDouble("12.34");
    16. System.out.println(data1);
    17. System.out.println(data2);
    18. }
  2. 大小写转换

    1. public static void main(String[] args) {
    2. String s1 = "hello";
    3. String s2 = "HELLO";
    4. // 小写转大写
    5. System.out.println(s1.toUpperCase());
    6. // 大写转小写
    7. System.out.println(s2.toLowerCase());
    8. }
  3. 字符串转数组

    1. public static void main(String[] args) {
    2. String s = "hello";
    3. // 字符串转数组
    4. char[] ch = s.toCharArray();
    5. for (int i = 0; i < ch.length; i++) {
    6. System.out.print(ch[i]);
    7. }
    8. System.out.println();
    9. // 数组转字符串
    10. String s2 = new String(ch);
    11. System.out.println(s2);
    12. }

    个人觉得这个方法是非常实用的!!

  4. String格式化输入

    1. public static void main(String[] args) {
    2. String s = String.format("%d-%d-%d", 2019, 9,14);
    3. System.out.println(s);
    4. }

    2.5 🥟 字符串替换

    那么在做一些算法题目时,我们需要替换字符串中的内容。
    好家伙,直接上手利用toCharArray然后几个循环
    没必要!
    我们使用一个指定的新的字符串替换掉已有的字符串数据,可用的方法如下:

方法 功能
String replaceAll(String regex, String replacement) 替换所有的指定内容
String replaceFirst(String regex, String replacement) 替换首个内容
  1. String str = "helloworld" ;
  2. System.out.println(str.replaceAll("l", "_"));
  3. System.out.println(str.replaceFirst("l", "_"));
  4. //输出:
  5. //he__owor_d
  6. //he_loworld

🌭 【注意】:由于字符串是不可变的对象,替换不修改当前字符串,而是产生一个新的字符串

2.6 🫕 字符串的拆分

我们可以利用split将一个完整的字符串按照指定的分隔符划分为若干个子字符串

方法 功能
String[] split(String regex) 将字符串全部拆分
String[] split(String regex,int limit) 将字符串以指定的格式,拆分为limit组

代码1👇实现字符串的拆分

  1. String str = "hello world hello Gremmie" ;
  2. String[] result = str.split(" ") ; // 按照空格拆分
  3. for(String s: result){
  4. System.out.println(s);
  5. }

代码2 👇字符串的部分拆分

  1. public static void main(String[] args) {
  2. String str = "hello world hello Gremmie" ;
  3. String[] result = str.split(" ",2) ;
  4. for(String s: result) {
  5. System.out.println(s);
  6. }
  7. }

运行结果👇
image.png

拆分是特别常用的操作,一定要牢牢记住。 有一些特殊字符作为分隔符的话可能无法正确区分,需要加上转移符号

代码3👇拆分IP地址

  1. public static void main(String[] args) {
  2. String str = "106.14.57.10" ;
  3. String[] result = str.split("\\.") ;
  4. for(String s: result) {
  5. System.out.println(s);
  6. }
  7. }

运行结果👇
image.png

🍫 【注意事项】:

  1. 字符“|”,“*”,“+”都得加上转义字符,前面加上 “\“ .
  2. 而如果是 “\” ,那么就得写成 “\\“ .
  3. 如果一个字符串中有多个分隔符,可以用“|”作为连字符.

代码4👇多次拆分

  1. public static void main(String[] args) {
  2. String str = "name=Gremmie&age=19" ;
  3. String[] result = str.split("&") ;
  4. for (int i = 0; i < result.length; i++) {
  5. String[] temp = result[i].split("=") ;
  6. System.out.println(temp[0]+" = "+temp[1]);
  7. }
  8. }

运行结果👇
image.png

2.7 🥣 字符串的截取

现在我们遇到一种情况,你已经知道了索引,想要截取一段字符串 这个时候,你是不是又想上手循环了? 慢着!substring可以满足你!😋

方法 功能
String substring(int beginIndex) 从指定索引截取到结尾
String substring(int beginIndex, int endIndex) 截取部分的内容

代码👇字符串的截取

  1. public static void main(String[] args) {
  2. String str = "helloGremmie" ;
  3. System.out.println(str.substring(5));//是从下标为5截到最后的
  4. System.out.println(str.substring(0, 5));//下标索引范围左闭右开,[0,5)
  5. }

🍟 注意事项:

  1. 索引要从0开始
  2. 要注意前闭后开的规定,substring(0,5)表示包含0号下标的字符,不包含5号下标

    2.8 🍰 字符串前后的空白删除

    String中的trim()方法用来去掉字符串中的左右两个空格,保留中间的空格

代码示例👇观察trim()方法的使用

  1. public static void main(String[] args) {
  2. String str = " hello Gremmie " ;
  3. System.out.println("["+str+"]");
  4. System.out.println("["+str.trim()+"]");
  5. }

运行结果👇
image.png

trim 会去掉字符串开头和结尾的空白字符(空格, 换行, 制表符等).

2.9🥫字符串常量池

字符串常量池实现的前提条件就是Java中String对象是不可变的,这样可以安全保证多个变量共享同一个对象。 如果Java中的String对象可变的话,一个引用操作改变了对象的值,那么其他的变量也会受到影响,显然这样是不合理的。 在JDK6.0及之前版本,字符串常量池存放在方法区中在JDK7.0版本以后,字符串常量池被移到了堆中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了

2.9.1 🥗 创建对象(一)

我们先来看看下面这段代码,创建String对象的方式是相同的嘛?

  1. public static void main(String[] args) {
  2. String s1 = "hello";
  3. String s2 = "hello";
  4. String s3 = new String("hello");
  5. String s4 = new String("hello");
  6. System.out.println(s1 == s2); // true
  7. System.out.println(s1 == s3); // false
  8. System.out.println(s3 == s4); // false
  9. }

上述程序创建方式类似,为什么s1和s2引用的是同一个对象,而s3和s4不是呢? 在Java程序中,类似于:1, 2, 3,3.14,“hello”等字面类型的常量经常频繁使用,为了使程序的运行速度更快、 更节省内存,Java为8种基本数据类型和String类都提供了常量池。

在系统设计中,我们尝尝会使用到”池”的概念。Eg:数据库连接池,socket连接池,线程池,组件队列。”池”可以节省对象重复创建和初始化所耗费的时间。对那些被系统频繁请求和使用的对象,使用此机制可以提高系统运行性能。 ”池”是一种”以空间换时间”的做法,我们在内存中保存一系列整装待命的对象,供人随时差遣。与系统效率相比,这些对象所占用的内存空间太微不足道了。

image.png
Java中有以下几种池

🍳class文件常量池

我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种 字面量 (Literal)和 符号引用 (Symbolic References),每个class文件都有一个class常量池。 其中 字面量 包括:

  1. 文本字符串
  2. 2.八种基本类型的值
  3. 3.被声明为final的常量等;

符号引用 包括:

  1. 类和方法的全限定名
  2. 字段的名称和描述符
  3. 方法的名称和描述符。

🥯运行时常量池

运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本,是方法区的一部分。不同之处是:它的字面量可以动态的添加(String类的intern()),符号引用可以被解析为直接引用。 JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们下面要说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

2.9.2 🐳 字符串常量池(StringTable)

在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。 在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;在JDK7.0中,StringTable的长度可以通过参数指定。

JDK版本 字符串常量池位置 大小设置
Java6 方法区 固定:1009
Java7 可设置,无大小限制,默认60013
Java8 可设置,有限制,最小1009

总结🍔 :

  1. 单独使用””引号创建的字符串都是常量,编译期就已经确定存储到Constant Pool中
  2. 使用new String(“”)创建的对象会存储到heap中,是运行期新创建的;
  3. 使用只包含常量的字符串连接符如”aa” + “aa”创建的也是常量,编译期就能确定,已经确定存储到String Pool中,String pool中存有“aaaa”;但不会存有“aa”。
  4. 使用包含变量的字符串连接符如”aa” + s1创建的对象是运行期才创建的,存储在heap中;只要s1是变量,不论s1指向池中的字符串对象还是堆中的字符串对象,运行期s1 + “aa”操作实际上是编译器创建了StringBuilder对象进行了append操作后通过toString()返回了一个字符串对象存在heap上。
  5. String s2 = “aa” + s1; String s3 = “aa” + s1; 这种情况,虽然s2,s3都是指向了使用包含变量的字符串连接符如”aa” + s1创建的存在堆上的对象,并且都是s1 + “aa”。但是却指向两个不同的对象,两行代码实际上在堆上new出了两个StringBuilder对象来进行append操作。在Thinking in java一书中285页的例子也可以说明。
  6. 对于final String s2 = “111”。s2是一个用final修饰的变量,在编译期已知,在运行s2+”aa”时直接用常量“111”来代替s2。所以s2+”aa”等效于“111”+ “aa”。在编译期就已经生成的字符串对象“111aa”存放在常量池中。

🍔 注意:

  1. 在JVM中字符串常量池只有一份,是全局共享的
  2. 刚开始字符串常量池是空的,随着程序不断运行,字符串常量池中元素会越来越多
  3. 当类加载时,字节码文件中的常量池也被加载到JVM中,称为运行时常量池,同时会将其中的字符串常量保存在字符串常量池中
  4. 字符创常量池中的内容:一部分来自运行时常量池,一部分来自程序动态添加

2.9.3 🦐 创建对象(二)

创建一个字符串的过程👇

String str = new String(“abc”) 首先定义一个str的String类型的引用并存放在栈中 先在字符串常量池中找到该常量是否存在 如果存在则创建一个引用即可,则在字符串常量池中创建一个内容为”abc”的字符串对象。 执行new操作,在堆中创建一个指定的对象”abc”,这里堆的对象是字符串常量池“abc”对象的一个拷贝对象 让str指向堆中“abc”这个对象(也就是存储这个对象的在堆中的地址)

image.png
image.png

在字节码文件加载的时候,先要将.Class文件中的常量池加载到内存中称为运行时常量池,此时也会将”hello”字符串保存到字符串常量池当中

字符串常量池.png
说明:

  1. 在字节码文件加载的时候,”hello”和”world”就已经创建好了,并保存在字符串常量池当中
  2. 上图是代表直接用字符串常量进行赋值,即:

    1. public static void main(String[] args) {
    2. String s1 = "hello";
    3. String s2 = "hello";
    4. System.out.println(s1 == s2); // true
    5. }
  3. 当使用String s1 = “hello”;创建对象的时候,先要在字符串常量池当中找,如果找到就将该字符串引用赋值给s1

    🥨 intern方法

    intern 是一个native方法(Native方法指:底层使用C++实现的,看不到其实现的源代码),该方法的作用是手动将创建的String对象添加到常量池中。

  1. public static void main(String[] args) {
  2. char[] ch = new char[]{'a', 'b', 'c'};
  3. String s1 = new String(ch); // s1对象并不在常量池中
  4. //s1.intern(); //调用之后,会将s1对象的引用放入到常量池中
  5. String s2 = "abc"; // "abc" 在常量池中存在了,s2创建时直接用常量池中"abc"的引用
  6. System.out.println(s1 == s2);
  7. }
  8. // 输出false
  9. // 用intern之后,就会输出true

不同版本的Java会有不同的Intern来实现

🍜 面试题

请解释String类当中两种对象实例化的区别 JDK1.8中

  1. String str = “hello”

只会开辟一块堆内存空间,保存在字符串常量池中,然后str共享常量池中的String对象

  1. String str = new String(“hello”)

会开辟两块堆内存空间,字符串”hello”保存在字符串常量池中,然后用常量池中的String对象给新开辟的String对象赋值。

  1. String str = new String(new char[]{‘h’, ‘e’, ‘l’, ‘l’, ‘o’})

先在堆上创建一个String对象,然后利用copyof将重新开辟数组空间,将参数字符串数组中内容拷贝到String对象中

2.10 🦑 字符串的不可变性

String是一种不可变对象. 字符串中的内容是不可改变的。

不可变性的体现在: 当对字符串重新赋值时我们需要重新制定一个内存区域,然后才能赋值,不能对原有的value进行赋值(也就是我们不能改变原有的value) 这里其实就是我们每当有一个对象就有一次给value赋值的机会,这次机会用了之后,也就是赋值之后,就不可以再改变了 所以我们在这里说,字符串的值一旦给定就不可以再修改,一旦尝试修改,就会创建一个新的字符串对象,让这个新的字符串去接收你想要成为的值。

  1. String类在设计时就是不可改变的,String类实现描述中已经说明了

image.png

String类中的字符实际保存在内部维护的value字符数组中,该图还可以看出:

  1. String类被final修饰,表明该类不能被继承
  2. value被修饰被final修饰,表明value自身的值不能改变,即不能引用其它字符数组,但是其引用空间中的内容可以修改。
  1. 所有涉及到可能修改字符串内容的操作都是创建一个新对象,改变的是新对象

它是需要新创建一个对象再对其进行修改的,并不是在原有的基础上进行修改

有些人说:字符串不可变是因为其内部保存字符的数组被final修饰了,因此不能改变。 这种说法是错误的,不是因为String类自身,或者其内部value被final修饰而不能被修改。 final修饰类表明该类不想被继承,final修饰引用类型表明该引用变量不能引用其他对象,但是其引用对象中的内容是可以修改的。

  1. public static void main(String[] args) {
  2. final int array[] = {1,2,3,4,5};
  3. array[0] = 100;
  4. System.out.println(Arrays.toString(array));
  5. // array = new int[]{4,5,6}; // 编译报错:Error:(19, 9) java: 无法为最终变量array分配值
  6. }

为什么 String 要涉及成不可变的?(不可变对象的好处是什么?)

  1. 方便实现字符串对象池. 如果 String 可变, 那么对象池就需要考虑写时拷贝的问题了.
  2. 不可变对象是线程安全的.
  3. 不可变对象更方便缓存 hash code, 作为 key 时可以更高效的保存到 HashMap 中.

2.11 🐙 字符串的修改

注意:尽量避免直接对String类型对象进行修改,因为String类是不能修改的,所有的修改都会创建新对象,效率 非常低下。

比如这么做👇

  1. public static void main(String[] args) {
  2. String s = "hello";
  3. s += " world";
  4. System.out.println(s); // 输出:hello world
  5. }

这么做中间创建了很多临时对象,非常浪费资源
那么我们应该怎么做呢?
在对String类进行修改时,效率是非常慢的,因此:尽量避免对String的直接需要,如果要修改建议尽量 使用StringBuffer或者StringBuilder。

3. 🦞 Java StringBuffer 和 StringBuilder 类

当对字符串进行修改的时候,需要使用 StringBuffer 和 StringBuilder 类。
和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。
image.png

在使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,所以如果需要对字符串进行修改推荐使用 StringBuffer。 StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。 由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。

那么StringBuilder在append的时候具体是怎么做的呢?

  1. public static void main(String args[]){
  2. StringBuilder sb = new StringBuilder(10);
  3. sb.append("Runoob..");
  4. System.out.println(sb);
  5. sb.append("!");
  6. System.out.println(sb);
  7. sb.insert(8, "Java");
  8. System.out.println(sb);
  9. sb.delete(5,8);
  10. System.out.println(sb);
  11. }

image.png
image.png
image.png

运行结果为: Runoob.. Runoob..! Runoob..Java! RunooJava!

而如果我们要求线程安全的话,就必须使用StringBuffer类了

  1. public class Test{
  2. public static void main(String args[]){
  3. StringBuffer sBuffer = new StringBuffer("Gremmie:");
  4. sBuffer.append("http://");
  5. sBuffer.append("106.14.57.");
  6. sBuffer.append("10/");
  7. System.out.println(sBuffer);
  8. }
  9. }

运行结果为👇
image.png
那么总结一下StringBuilder常用的一样方法:

方法 说明
StringBuff append(String str) 在尾部追加,相当于String的+=,可以追加:boolean、char、char[]、 double、float、int、long、Object、String、StringBuff的变量
char charAt(int index) 获取index位置的字符
int length() 获取字符串的长度
int capacity() 获取底层保存字符串空间总的大小
void ensureCapacity(int mininmumCapacity) 扩容
void setCharAt(int index, char ch) 将index位置的字符设置为ch
int indexOf(String str) 返回str第一次出现的位置
int indexOf(String str, int fromIndex) 从fromIndex位置开始查找str第一次出现的位置
int lastIndexOf(String str) 返回最后一次出现str的位置
int lastIndexOf(String str, int fromIndex) 从fromIndex位置开始找str最后一次出现的位置
StringBuff insert(int offset, String str) 在offset位置插入:八种基类类型 & String类型 & Object类型数据
StringBuffer deleteCharAt(int index) 删除index位置字符
StringBuffer delete(int start, int end) 删除[start, end)区间内的字符
StringBuffer replace(int start, int end, String str ) 将[start, end)位置的字符替换为str
String substring(int start) 从start开始一直到末尾的字符以String的方式返回
String substring(int start,int end) 将[start, end)范围内的字符以String的方式返回
StringBuffer reverse() 反转字符串
String toString() 将所有字符按照String的方式返回
  1. public static void main(String[] args) {
  2. StringBuilder sb1 = new StringBuilder("hello");
  3. StringBuilder sb2 = sb1;
  4. // 追加:即尾插-->字符、字符串、整形数字
  5. sb1.append(' '); // hello
  6. sb1.append("world"); // hello world
  7. sb1.append(123); // hello world123
  8. System.out.println(sb1); // hello world123
  9. System.out.println(sb1 == sb2); // true
  10. System.out.println(sb1.charAt(0)); // 获取0号位上的字符 h
  11. System.out.println(sb1.length()); // 获取字符串的有效长度14
  12. System.out.println(sb1.capacity()); // 获取底层数组的总大小
  13. sb1.setCharAt(0, 'H'); // 设置任意位置的字符 Hello world123
  14. sb1.insert(0, "Hello world!!!"); // Hello world!!!Hello world123
  15. System.out.println(sb1);
  16. System.out.println(sb1.indexOf("Hello")); // 获取Hello第一次出现的位置
  17. System.out.println(sb1.lastIndexOf("hello")); // 获取hello最后一次出现的位置
  18. sb1.deleteCharAt(0); // 删除首字符
  19. sb1.delete(0,5); // 删除[0, 5)范围内的字符
  20. String str = sb1.substring(0, 5); // 截取[0, 5)区间中的字符以String的方式返回
  21. System.out.println(str);
  22. sb1.reverse(); // 字符串逆转
  23. str = sb1.toString(); // 将StringBuffer以String的方式返回
  24. System.out.println(str);
  25. }

从上述例子可以看出:String和StringBuilder最大的区别在于String的内容无法修改,而StringBuilder的内容可 以修改。频繁修改字符串的情况考虑使用StringBuilder

注意: String和StringBuilder类不能直接转换。
如果要想互相转换,可以采用如下原则 :

  • String变为StringBuilder: 利用StringBuilder的构造方法或append()方法
  • StringBuilder变为String: 调用toString()方法

    🍜 面试题

    String、StringBuffer、StringBuilder的区别

    1. String的内容不可修改,StringBuffer与StringBuilder的内容可以修改.
    2. StringBuffer与StringBuilder大部分功能是相似的
    3. StringBuffer采用同步处理,属于线程安全操作;而StringBuilder未采用同步处理,属于线程不安全操作
  1. 以下总共创建了多少个String对象【前提不考虑常量池之前是否存在】 String str = new String(“ab”); // 会创建多少个对象 String str = new String(“a”) + new String(“b”); // 会创建多少个对象

那么关于String就解释到这里啦
现在已经是👇
image.png
很晚了,整理了6个多小时,但如果有哪里有错的还希望大家评论区指正
希望多多支持!
感谢阅读~