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 未采用同步处理,属于线程不安全操作。(后期学习会接触多线程,目前了解即可)