当前位置: 首页 > news >正文

深入理解Java String:不可变性、内存机制与高效操作

文章目录

    • 1.字符串构造
      • 1.1 再识 String
      • 1.2 String 直接创建与对象创建
      • 1.3 编译器优化
    • 2.String 对象直接的比较
    • 3.字符串查找
    • 4.String 方法
      • 4.1 转换
      • 4.2 字符串调整
    • 5.字符串的不可变性
    • 6. 字符串修改
    • 7.StringBuilder 和 StringBuffer
      • 7.1 二者区别
      • 7.2 StringBuilder 和 StringBuffer

1.字符串构造

1.1 再识 String

String 类常用构造方式:

String s1 = "Hello Sirens";					//直接创建
System.out.println(s1);String s2 = new String("Hello Sirens");		//对象创建
System.out.println(s1);char[] array = {'S','i','r','e','n','s'};
String s3 = new String(array);				//对象创建
System.out.println(s3);

注意

1.String 是引用类型,内部并不存储字符串,而是像静态方法一样,通过引用去调用堆区的 value(JDK9 之后 value 由 char[] 类型改为了 byte[] 类型,)

在这里插入图片描述

1.2 String 直接创建与对象创建

String s1 = "Hello";
String s2 = "Sirens";
String s3 = s1;
String s4 = new String("Hello");
String s5 = new String("World");

以上为 String 直接创建对象创建内存示意图(画起来真累

在这里插入图片描述

整个过程:

1️⃣ String s1 = "Hello";

  • JVM 检查字符串常量池:是否已有 "Hello"
  • 如果没有,就在堆里创建一个 String对象#1001,内容为 "Hello"
  • 常量池记录一条映射 "Hello" → #1001
  • 局部变量表中的 s1 存放引用 #1001

2️⃣ String s2 = "Sirens";

  • 常量池查找 "Sirens",没有;
  • 在堆中创建一个新的 String 对象 #1002
  • 常量池记录 "Sirens" → #1002
  • s2 引用 #1002。

3️⃣ String s3 = s1;

  • 这是引用赋值,不是新建。
  • s3 和 s1 引用完全相同的对象。

4️⃣ String s4 = new String("Hello");

  • "Hello" 是常量池字面量,已存在,指向堆中 #1001;
  • new String("Hello") 表示:
    1. 读取常量池引用 #1001;
    2. 在堆中再创建一个新的 String 对象 #2001;
    3. 把 #1001 的字符数组内容复制到 #2001;
    4. s4 引用的是 #2001。

5️⃣ String s5 = new String("World");

  • 常量池中查找 "World",发现没有;
  • 在堆中创建 "World" 的常量池条目 "World" → #1003
  • new String("World") 又在堆中创建一个新的对象 #2002;
  • s5 引用 #2002。

1.3 编译器优化

重点补充:这是我在学校的课上遇到的代码,说到这我就想提提这个

String hello = "你好";
String testOne = "你" + "好";    // [代码1]
System.out.println(hello == testOne); //输出结果是 true
System.out.println("你好" == testOne); //输出结果是 true
System.out.println("你好" == hello); //输出结果是 true
String you = "你";
String hi = "好";
String testTwo = you + hi;    // [代码2]
System.out.println(hello == testTwo); //输出结果是 false
String testThree = you + hi;
System.out.println(testTwo == testThree); //输出结果是 false

首先我们要知道:

常量+常量=常量

常量+变量=变量

变量+变量=变量

我们分成三部分分析:

部分一:

String hello = "你好";
String testOne = "你" + "好";		//编译器优化

在编译时,Java 编译器发现 "你""好" 都是常量字符串,于是它直接在编译阶段就做了优化:

String testOne = "你好";

也就是说——

变量存放内容
hello指向字符串常量池中 "你好"
testOne也指向同一个常量池中的 "你好"

因为字符串常量池中,“你好” 只有一份副本

所以:

hello == testOne   // true
"你好" == testOne   // true
"你好" == hello     // true

它们的“==”都在比较同一个引用地址(常量池中同一个对象)


部分二

String you = "你";
String hi = "好";
String testTwo = you + hi;

这里的关键在于:

youhi变量,不是编译时的常量。

所以,编译器无法在编译期确定 you + hi 的值
只能在运行时StringBuilder 拼接生成新字符串对象。

编译器其实会把上面的代码翻译成:

String testTwo = new StringBuilder().append(you).append(hi).toString();

这时候:

"你""好" 各自仍然在常量池中;

但拼接产生的 "你好" 是在里新创建的对象;

它没有放回常量池。

于是:

hello == testTwo   // false

因为两者引用的地址不同:

hello → 常量池

testTwo → 堆中新对象


部分三

为什么 testTwo 和 testThree 也不相等

String testThree = you + hi;
System.out.println(testTwo == testThree); // false

理由同上,每次拼接:

都会通过 StringBuilder 重新 new 出一个新的字符串对象;

虽然内容相同,但内存地址不同。

因此:

testTwo == testThree   // false

除非你手动调用 .intern()

testTwo.intern() == testThree.intern()   // true

intern() 会把字符串放进常量池里,如果常量池已有相同内容的,就直接返回那份引用。

2.String 对象直接的比较

1.==比较所引用的是否同属一个地址

我们知道,当==两边都为基本数据类型变量时,比较的是两值之间是否相等而不是地址。

但 String 不一样,首先 String 不是基本数据类型,而是引用数据类型,这在我们的 Java SE 基础提及过。

String s1 = new String("hello");
String s2 = new String("hello");
String s3 = new String("world");
String s4 = s1;
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // false
System.out.println(s1 == s4); // true

2.boolean equals(Object anObject)方法去按照字典顺序比较

字典序–字符大小的顺序(类比 ascii)

方法库中 String 类本身重写了父类 Object 中的 equals 方法,String 类重写后 equals 方法由比较地址改为了如下规则

在这里插入图片描述

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

3.int compareTo(String s)方法去按照字典序进行比较

虽然与 equals 同为按照字典序比较,但其返回值不同。equals 返回 boolean 类型,而 compareTo 返回的是 int 类型。

这也和我们之前的文章谈 compareTo 的时候提到过,(s1.compareTo(s2)如果 s1 比 s2 大,那么返回正数;反之。

但也有要点要注意:

String s1 = "abcd";
String s2 = "abe";
String s3 = "ab";
String s4 = "abcd";System.out.println(s1.compareTo(s2));   //此时都为第三位不同,仅比较到第三位
System.out.println(s3.compareTo(s4));   //此时仅比较长度,因为前面都相同

4.int compareToIgnoreCase(String str)方法,与 compareTo 方式相同,但忽略大小写比较

String s1 = new String("abc");
String s2 = new String("ac");
String s3 = new String("ABc");
String s4 = new String("abcdef");
System.out.println(s1.compareToIgnoreCase(s2)); // 不同输出字符差值-1
System.out.println(s1.compareToIgnoreCase(s3)); // 相同输出 0
System.out.println(s1.compareToIgnoreCase(s4)); // 前k个字符完全相同,输出长度差值 -3

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,int fromIndex)返回 str 第一次出现的位置,没有返回 -1
int lastIndexOf(int ch)从 fromindex 位置开始找 str 第一次出现的位置,没有返回-1
int lastIndexOf(int ch,int fromIndex)从后往前找,返回 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
public static void main(String[] args) {String s1 = "Hello SSiirrieeSns";	//S  S  i  i  r  r  i  e  e  S  n  s//0  1  2  3  4  5  6  7  8  9  10 11System.out.println(s1.charAt(2));				//lSystem.out.println(s1.indexOf("r"));			//10System.out.println(s1.indexOf("l",3));			//3System.out.println(s1.lastIndexOf("S"));		//15System.out.println(s1.lastIndexOf("S",13));		//7System.out.println(s1.lastIndexOf(" ",13));		//5}

以上所调用到的方法产生的 String 类型结果都是方法中 return new String

4.String 方法

4.1 转换

数值和字符串转换

String s1 = String.valueOf(1234);
String s2 = String.valueOf(12.34);
String s3 = String.valueOf(true);
String s4 = String.valueOf(new Student("Sirens", 19));

同样其他类型也能转为 String 类型

int data1 = Integer.parseInt("1234");
double data2 = Double.parseDouble("12.34");
//Integer、Double等是Java中的包装类型
方法名描述
String toUpperCase()小写转大写
String toLowerCase()大写转小写
String toCharArray()字符串转数组
format(格式,数值)格式化

4.2 字符串调整

替换

方法名功能
String replaceAll(String regex,String replace)替换所有的指定内容
(匹配此字符串的正则表达式,用来替换每个匹配项的字符串)
String replaceFirst(String regex,String replacement)替换首个内容
(匹配此字符串的正则表达式,用来替换第一个匹配项的字符串)

注意:

由于字符串是不可变对象, 替换不修改当前字符串, 而是产生一个新的字符串.

拆分

方法功能
String[] split(String regex)将字符串全部拆分
(正则表达式分隔符)
String[] split(String regex,int limit)将字符串以指定的格式,拆分为 limit 组
(正则表达式分隔符,分割的份数)

注意

像拆分 IP 地址,有的需要转义字符

1. 字符"|“,”*“,”+"都得加上转义字符,前面加上 “\” .

2. 而如果是 “” ,那么就得写成 “\\” .

3. 如果一个字符串中有多个分隔符,可以用"|"作为连字符.

String site = "https://sirens007.github.io";
String[] s = site.split("\\.|://");
for (int i = 0; i < s.length; i++) {System.out.print(s[i]+" ");
}

拆分

方法描述
String[] substring(int beginIndex)从指定索引截取到结尾
String[] substring(int beginIndex, int endIndex)截取部分内容

注意

1.索引是从 0 开始的

2.Java 中默认都是左闭右开区间,所以如果截取部分内容的话 [beginIndex, endIndex),不取右值


补充方法

方法描述
boolean contains(CharSequence chars)contains() 方法用于判断字符串中是否包含指定的字符或字符串
chars – 要判断的字符或字符串
String trim()返回字符串的副本,忽略前导空白和尾部空白

使用案例:

 String Str = new String("    sirens007.gtihub.io    ");System.out.print("原始值 :" );System.out.println( Str );System.out.print("删除头尾空白 :" );System.out.println( Str.trim() );//执行结果
//原始值 :    sirens007.github.io        
//删除头尾空白 :sirens007.github.io

5.字符串的不可变性

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

在这里插入图片描述

为了求证官方文件,我们打开 jdk 源码可以看到该核心定义

  • class String首先被声明为final,表示不能被继承
  • 其次value数组被声明为private final–表明 value 自身的值不能改变,即不能引用其它字符数组,但是其引用空间中的内容可以修改。

这说明了以内容:

1.外部无法访问;

2.初始化后不可再被修改引用;

3.数组内容虽可变,但外部没有访问入口。

以上说明意味着:

String 对象一旦创建,就固定绑定了内部value[];

修改字符串(如"abc".replace("a","x"))时,实际上是新创建了一个 String 对象。

举例:

可能很多人认为是 final 修饰了 value 导致 value 不能改变,其实不然

那我们就举一个例子找找是谁限制住了不可修改,如下

在这里插入图片描述

我们在 Test5 文件 default final 定义了数组 str,使用 Test3 去访问修改,可知编译器没有报错

在这里插入图片描述

当我们修改为了 private 后

在这里插入图片描述

在这里插入图片描述

显然爆红了,于此我们得到验证应该是 private 限制了你去访问。

二、所有涉及到可能修改字符串内容的操作都是创建一个新对象,改变的是新对象

在这里插入图片描述

我们就拿 String 类里的 replace 方法,可以看到的是只有”结果不变“的情况才返回this

即:当字符串内容没有发生任何变化时,直接返回原对象。

6. 字符串修改

我们要尽量避免直接对 String 类型对象进行修改,因为 String 类是不能修改的,所有的修改都会创建新对象,效率很低下,就比如下面这种变量拼接(常量拼接没有问题,零开销)

String s1 = "hello";
String s2 = " world";
String s3 = s1 + s2;

此时编译器会在字节码层面生成:

new StringBuilder().append(s1).append(s2).toString();
//也就是下面这样
StringBuilder stringbuilder = new StringBuilder();
stringbuilder.append("hello");
stringbuilder.append("world");
String ret = stringbuilder.toString();
System.out.println(ret);

所以在运行时:

创建一个新的 StringBuilder

两次 append()

再调用 toString() 创建新字符串。

这就是为什么 "hello" + " world"s1 + s2虽然语法一样,但性能差距巨大。

我们还可以用代码来粗略比较一下 StringBuffer 和 StringBuilder

public static void main(String[] args) {long start = System.currentTimeMillis();String s = "";for(int i = 0; i < 10000; ++i){s += i;}long end = System.currentTimeMillis();System.out.println(end - start);start = System.currentTimeMillis();StringBuffer sbf = new StringBuffer("");for(int i = 0; i < 10000; ++i){sbf.append(i);}end = System.currentTimeMillis();System.out.println(end - start);start = System.currentTimeMillis();StringBuilder sbd = new StringBuilder();for(int i = 0; i < 10000; ++i){sbd.append(i);}end = System.currentTimeMillis();System.out.println(end - start);}

得到的结果依此是 70 、1、1,可以看出在对 String 进行修改时,效率是非常慢的。因此,如果要修改的话我们尽量使用 StringBuffer 或者 StringBuilder

7.StringBuilder 和 StringBuffer

7.1 二者区别

当对字符串进行修改的时候,需要使用 StringBuffer 和 StringBuilder 类。

和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。

在这里插入图片描述

StringBuilder

public final class StringBuilder extends AbstractStringBuilder
implements java.io.Serializable, CharSequence {public StringBuilder append(String str) {super.append(str);return this;}
}

StringBuffer

public final class StringBuffer extends AbstractStringBuilder
implements java.io.Serializable, CharSequence {public synchronized StringBuffer append(String str) {super.append(str);return this;}
}

唯一的区别就在于:

StringBuffer 的方法前多了一个 synchronized 关键字。

StringBuffer:多线程情况下

StringBuffer 类比公共厕所,

每次只能进去一人;

门口有锁(synchronized同步锁),别人必须等前一个人出来才能进去。

确保每次使用都“安全”,不会出现混乱(数据不一致)

你可以这么理解:

假如你和同事都要上厕所(多个线程操作字符串),
StringBuffer 就像门上了锁的公共厕所:
一个用完另一个再用,虽然慢一点,但不会出事故。

StringBuilder:单线程情况下

没有锁,谁抢到谁用

设计时假设“你家就你一个人”,所以没必要加锁

你也可以这么理解:

如果你自己住(单线程程序),
当然不用给厕所门上锁,加锁反而浪费时间。
所以 StringBuilder 就是单人卫生间,速度飞快,但只适合自己用。

7.2 StringBuilder 和 StringBuffer

方法说明
StringBuffer append(String str)在尾部追加,相当于 String+=,可以追加:booleancharchar[]doublefloatintlongObjectStringStringBuffer变量
char charAt(int index)获取 index位置的字符
int length()获取字符串的长度
int capacity()获取底层保存字符串空间的大小
void ensureCapacity(int minimumCapacity)扩容
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最后一次出现的位置
StringBuffer 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的方式返回

以上是我关于Java的笔记分享,也可以关注关注我的静态网站🥰

感谢你读到这里,这也是我学习路上的一个小小记录。希望以后回头看时,能看到自己的成长~

http://www.dtcms.com/a/546539.html

相关文章:

  • 仓颉GC调优参数:垃圾回收的精密控制艺术
  • 小学生做网站步骤wordpress 调用媒体库
  • 徐州营销型网站制使做网站广告联盟赚钱
  • 常州网站营销推广免费科技软件
  • 如何使用框架来建设网站常州网站排名优化
  • 网站 网络营销价值雷山网站快速排名
  • Spring Cloud 多租户实现(MySQL + MyBatis + MyBatis-Plus 实战)
  • 专注网站建设与制作网站图片的暗纹是怎么做的
  • 给周杰伦做网站网站做百度推广划算吗
  • k8s——实战入门(资源)
  • 茶叶网站建设策划方案u001f代做毕设自己专门网站
  • 找建设网站网站建设搜索优化app推广新闻营销
  • 山东集团网站建设做网站免费的域名
  • 做平面什么网站的素材不侵权北京公司提供注册地址
  • Linux学习笔记--中断子系统
  • Selenium常用方法
  • 哈尔滨网站建设推广服务人力资源网站
  • screen命令指南
  • SAP-ABAP:穿越时空的ABAP基石:深入理解WRITE语句的奥秘与技巧实例详解
  • 做网站产品图片素材前端后端分别是什么意思
  • 做网站销售的昆明百度小程序
  • LeeCode 137. 只出现一次的数字II
  • AOI设备在消费电子领域的检测应用
  • 网站制作 成都土巴兔官网
  • 如何 做网站跳转建设网站企业网上银行
  • 基于需求驱动的自动驾驶感知任务数据集缺口识别与缓解方法
  • 上海文明城市建设网站如何做网站商铺
  • 怎么自己给自己的网站做推广做旅游网站毕设任务书
  • 丹阳网站建设价位网站建设的中期目标
  • html 网站链接cms编码是什么