【Java SE】String类(下)

简介: 在我们前面也对字符串进行了简单的使用,在Java当中,String是字符串类型,本质上也是一个类,这个类中提供了很多的方法,我们后续会学习到,现在先来简单看一下String类常见的构造方法

2.4 字符串替换

将一个字符串的内容替换成新的内容,但是还是会返回一个新的对象不会修改原有的对象,至于为什么后面会讲解:

public static void strReplace() {
        String str = "hello world";
        //替换所有的指定内容,不会修改原字符串
        System.out.println(str.replaceAll("l", "i"));
        //替换首个指定内容,不会修改原字符串
        System.out.println(str.replaceFirst("l", "i"));
}

2.5 字符串拆分

这里我们使用的是 String[ ] split(String regex) 方法

2.5.1 按指定字符拆分

public static void strSplit() {
        String str = "hello world is-is-123";
        String[] ret = str.split(" "); //按照空格拆分
        for (String x : ret) {
            System.out.println(x);
        }
        ret = str.split(" ", 3); //部分拆分
        for (String x : ret) {
            System.out.println(x);
        }
}

这个方法的返回值是一个String类型的数组,所以我们需要拿对应的数组来接收,接着可以遍历这个循环来打印这个数组的内容,如果看不懂这种打印,可以参考我之前的程序逻辑控制文章,第二个部分拆分怎么理解呢?就是我要按照指定的字符把他拆分成几段,如果是一段那这这个字符串就不会发生变化,大家下来可以自己尝试下,有些特殊字符可能需要用到转义才能正确拆分。

比如我们拆分 ip 地址和 路径:

public static void StrIntercepting() {
        //拆分ip地址,特殊字符作为分隔符可能无法正确切分,需要加上转义
        String str1 = "192.168.2.1";
        //因为\本身就是个特殊字符,所以需要转义成字面的\才能对.进行转义
        String[] ret = str1.split("\\."); 
        for (String x : ret) {
            System.out.println(x);
        }
        String str3 = "lqg\\work\\code";
        //这里因为\\表示一个\的字面意思,而一个\又是一个特殊字符
        //所以我们还需要\\来表示一个\字面意思,再用它来转义一个\从而才是真正的\
        ret = str3.split("\\\\");
        for (String x : ret) {
            System.out.println(x);
        }
}

2.5.2 多次拆分

    public static void strSplit() {
        //多次拆分
        String str2 = "name=Mike&age=24";
        String[] ret = str2.split("=");
        //第一种方法
        for (String x : ret) {
            String[] tmp = x.split("&");
            for (String s : tmp) {
                System.out.println(s);
            }
        }
        //第二种方法
        for (int i = 0; i < ret.length; i++) {
            String[] tmp = ret[i].split("&");
            for (int j = 0; j < tmp.length; j++) {
                System.out.println(tmp[j]);
            }
        }
    }

这个多次拆分我们可以分析一下,首先 split 方法返回的是一个数组,所以也就是每个数组里面放的是字符串,然后我们想要再次进行拆分就可以访问对应数组下标的字符串进行拆分,这样也就可以实现多次拆分,上面两种方法提供参考。

还有其他的一些方法比如 String trim() 方法,可以去掉字符串开头和结尾的空格字符等等,可以去查阅下帮助手册自行学习,上面已经介绍了一些常用的方法。

3、字符串常量池

3.1 什么是字符串常量池

字符串常量池在 JVM 中是StringTable类,实际上是一个固定大小的HashTable也就是哈希表(一种高效用来查找的数据结构,后续学习数据结构会详细讲解),在不同JDK版本下的字符串常量池的位置以及默认大小是不同的,但是我们今天是用JDK1.8约等于Java8,字符串常量池的位置在堆中,可以设置大小,有范围限制,默认是1009。

在Java程序中,类似于:1,2,3,3.14,"hello" 等字面常量经常被使用,为了程序的运行速度更快,更节省内存,Java为8中基本数据类型和String类都提供了常量池,我们现在只讲述字符串常量池,至于更详细池的了解会在学习JVM的时候进行讲解。

3.2 从内存的角度理解创建String对象

由于常量池在不同的版本是可能不一样的,目前是在Java8上分析:

我们先来看这样一段代码:

public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";
        String s3 = new String("hello");
        String s4 = new String("hello");
        System.out.println(s1 == s2); // true
        System.out.println(s1 == s3); // false
        System.out.println(s3 == s4); // false
    }

我们尝试着先来简单分析一下这段代码:

首先 s1 是用常量字符串进行构造,所以会先去字符串常量池存放的地址找对应的链表节点里面对应的对象有没有指向 "hello" 这个字符串(简单理解就是看有没有对应的字符串的对象),如果没有,先用一个哈希桶,每个桶中是链表的结构,链表节点里面包含存储着这个字符串对象的地址,和哈希值,以及其他东西,并且字符串常量池中存放着这个节点的地址,而我们知道字符串对象中有两部分,一部分是数组一部分是哈希,所以数组的那部分就存了 "hello" 字符串的地址

当 s2 创建的时候,发现已经有了 "hello" 字符串对象,所以在创建 s2 这个引用的时候,指向的就是 s1 指向的对象,即不用再新创建新对象了

当 s3 创建的时候,我们还是会去字符串常量池中找,发现有对应的字符串对象,但是我们是 new对象,也就是 new 代表着新,所以会新建一个对象,而这个对象是String类型,里面有两个部分,由于已经存在了 "hello" 字符串,所以 s3 引用的对象里面数组那部分存的是这个字符串的地址,本质上是新创建了一个对象,但是对象里面的数组还是指向那个字符串

当 s4 创建的时候,跟 s3 一模一样,这里我就不多说,下面我们就来看真实的内存图:

所以通过上图我们也可以看出,只要是new对象,都是唯一的!

还可以看到,使用常量串字符串创建String类型对象的效率更高,并且不用创建新对象还更节省空间,用户也可以将创建的字符串对象通过 intern 方式添加进字符串常量池中。

3.3 intern 方法

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

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

这串代码我们可以来简单分析一下:首先 s1 是用字符数组构造的,所以value指向的就是堆中把已有的数组拷贝一份,指向新拷贝的数组,即s1对象并不会放在池中,而后面调用了 intern  方法,也就是将 s1 对象的引用放到常量池,而 s2 发现常量池中有对应这个字符串的对象,就会直接引用 s1 对象的地址。

假设没有使用 intern 这个方法,s1引用的对象与s2并不相同,如果使用了则 s1 与 s2 引用的对象相同!

3.4 一道面试题

在JDK1.8中,请解释一下对象实例化的区别(常量池中都不存在当前字符串):

String str = "hello"; :这个只会开辟一块空间,会把字符串保存在常量池中,然后str共享常量池中的String对象,如果有,则直接引用这个对象。

String str = new String("hello"); :这会开辟两块堆内存空间,字符串"hello"保存在字符串常量池中,然后用常量池中的String对象给新开辟 的String对象赋值。。

String str = new String(new char[] {'h', 'e', 'l', 'l', 'o'}); :先在堆中创建一个String对象,然后利用 coypof 将重新开辟数组空间将参数字符串数组中内容拷贝到String对象中。

4、StringBuilder 和 StringBuffer

4.1 为什么String对象不可变?

前面的很多方法确实证明了String对象的不可变,都会产生一个新的对象,都不会在原有的基础上修改,这是为什么呢?我们再次看一下String类中的原码:

我们先来看String类被 final 修饰了,表示这个类不能被继承,跟里面的 value 数组能不能被修改没有关系,接着再来看 value 数组被 final 修饰了!

那么 final 修饰的数组,表示数组引用的地址不能被改变!也就是 value 引用的对象不能改变,但是可以改变引用对象里面的值,也就是可以改变数组存储的内容!

所以字符串真正不能被改变的原因是前面的 priavte 权限修饰符,表示这个成员变量只能在该类中被访问,而且 String 类并没有提供 getValue 和 setValue 方法!也即没有提供能让你操作这个字符串的方法,你在外部压根访问不到这个数组,你如何修改?这才是String对象不可变的根本原因!

如果要修改字符串的内容如何修改呢?借助StringBuilder和StringBuffer类!

4.2 介绍StringBuilder 和 StringBuffer

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

为了方便字符串的修改,Java就提供了如上两个类,但是这两个类的大部分操作方式是相同的,下面我们就来简单了解下里面常用的方法,用的时候查一下即可:

  • StringBuff append(String str) :在尾部追加,相当于String的+=,可以追加:boolean、char、char[]、 double、float、int、long、Object、String、StringBuff的变量
  • char charAt(int index):获取index位置的字符
  • void setCharAt(int index, char ch):将index位置的字符设置为ch
  • StringBuff insert(int offset, String str):在offset位置插入:八种基类类型 & String类型 & Object类型数据
  • StringBuffer deleteCharAt(int index):删除index位置字符
  • StringBuffer reverse():反转字符串

这些太多了,就不一一列举,下来可以自己查一下。

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

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

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

4.3 三种字符串类型的区别

String 的内容不可修改,StringBuffer 与 StringBuilder 的内容可以修改。

StringBuffer 与 StringBuilder大部分功能是相似的。

StringBuffer 采用同步处理,属于线程安全操作,而 StringBuilder 未采用同步处理,属于线程不安全操作。(后期学习会接触多线程,目前了解即可)

目录
打赏
0
0
0
0
0
分享
相关文章
Java 密封类:精细化控制继承关系
Java 密封类:精细化控制继承关系
149 83
Java 基础类从入门到精通实操指南
这份指南专注于**Java 17+**的新特性和基础类库的现代化用法,涵盖开发环境配置、数据类型增强(如文本块)、字符串与集合处理进阶、异常改进(如密封类)、IO操作及实战案例。通过具体代码示例,如CSV数据分析工具,帮助开发者掌握高效编程技巧。同时提供性能优化建议和常用第三方库推荐,适合从入门到精通的Java学习者。资源链接:[点此下载](https://pan.quark.cn/s/14fcf913bae6)。
136 35
在Java中将String字符串转换为算术表达式并计算
具体的实现逻辑需要填写在 `Tokenizer`和 `ExpressionParser`类中,这里只提供了大概的框架。在实际实现时 `Tokenizer`应该提供分词逻辑,把输入的字符串转换成Token序列。而 `ExpressionParser`应当通过递归下降的方式依次解析
56 14
|
16天前
|
Java API中Math类功能全景扫描
在实际使用时,这些方法的精确度和性能得到了良好的优化。当处理复杂数学运算或高精度计算时,`Math`类通常是足够的。然而,对于非常精细或特殊的数学运算,可能需要考虑使用 `java.math`包中的 `BigDecimal`类或其他专业的数学库。
51 11
深入理解Java虚拟机--类文件结构
本内容介绍了Java虚拟机与Class文件的关系及其内部结构。Class文件是一种与语言无关的二进制格式,包含JVM指令集、符号表等信息。无论使用何种语言,只要能生成符合规范的Class文件,即可在JVM上运行。文章详细解析了Class文件的组成,包括魔数、版本号、常量池、访问标志、类索引、字段表、方法表和属性表等,并说明其在Java编译与运行过程中的作用。
Java 期末考试救急必备涵盖绝大多数核心考点及五大类经典代码助你过关
本文为Java期末考试复习指南,涵盖基础语法、面向对象编程、异常处理、文件操作、数据库连接五大核心考点,提供详细解析与实用代码示例,助力快速掌握重点,高效备考,轻松应对考试。
32 0
Java并发包下Atomic相关类的使用
本文介绍了 `java.util.concurrent.atomic` 包下的各类原子类及其使用场景,包括基本类型原子类(如 `AtomicInteger`、`AtomicLong`)、数组类型原子类(如 `AtomicIntegerArray`)、引用类型原子类(如 `AtomicReference`)、对象属性修改原子类(如 `AtomicIntegerFieldUpdater`)以及原子操作增强类(如 `LongAdder` 和 `LongAccumulator`)。同时,详细对比了不同原子类在高并发场景下的性能表现,展示了 `LongAdder` 的高效性。
100 31
关于string的‘\0‘与string,vector构造特点,反迭代器与迭代器类等的讨论
你真的了解string的'\0'么?你知道创建一个string a("abcddddddddddddddddddddddddd", 16);这样的string对象要创建多少个对象么?你知道string与vector进行扩容时进行了怎么的操作么?你知道怎么求Vector 最大 最小值 索引 位置么?
37 0
|
2月前
|
【高薪程序员必看】万字长文拆解Java并发编程!(7):不可变类设计指南
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中Java不可变类设计指南,废话不多说让我们直接开始。
52 0
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问