(一)字符串构造
我们如何创建一个String类型的对象?有三种:
String s1=new String("hello"); //直接new一个String对象
String s2="hello"; //使用常量串构造
final char[] chars = {'h','e','l','l','o'};
String s3=new String(chars); //使用字符数组进行构造
注:String是引用类型,内部存的不是字符串本身,而是一个value[]和hash(value是一个字符数组)
每个对象都会在栈和堆内分配不同的空间
(二)字符串有关常用方法
1.String对象的比较
我们字符串比较,如果直接用==比较,那么我们比较的时字符串的地址,所以如果我们想比较String里面的值,我们应该要用equals这个方法
这里具体内存分配是什么样为什么不相等,一会我们在字符串常量池中讲
boolean equals(Object anObject)
这个方法按照字典序进行比较
字典序:字符大小顺序 我们都知道这个equals是父类Object中的方法,我们String类重写了在这个方法,我们看一下他的原代码
我们发现,这里传来一个Object类型的对象,然后我们这个对象先判断地址是否相等,如果相等那就说明指向同一个对象,我们可以直接返回true,然后我们看下面的。检测anObject是否为String类型的对象,如果是的话就继续比较,否则因为是&&就会直接返回false
接下来,我们看下面这两个,coder是匹配hashcode值的,另一个默认为true,所以我们可以理解,这个就是来判断hashcode值是否相等的,如果相等那么我们就继续,不相等就返回false
然后最后一步,我们点进去看一下
我们就从第一个字符到最后一个字符,一个个比较,如果都相等才返回true,否则返回false;
其实还有很多比较相等的方法,我们就不详细说了,我们一般用到的也就是重写的这个equals方法了
那么我们上面说完了字符串如何比较相等,那我们下面来讲如何比较字符串大小
这里我们使用的是
int compareTo(String s)这个方法 也是按照字典序进行比较
我们来看一下原码,然后来总结一下
那我们看这个源码,我们就知道是从哪里比较的字符串,所以我们直接点到这几个compareTo方法中,我发现差不多都长这个样子
那好,我们就根据这个来总结一下 :首先他会计算我们两个字符串的长度并把他们变成字符数组,然后用最短的长度进入for循环,我们一旦发现有字符不相等了,就会返回他们的差值,如果一直相等就会返回他们的长度差值
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = new String("ac");
String s3 = new String("abc");
String s4 = new String("abcdef");
System.out.println(s1.compareTo(s2)); // 不同输出字符差值-1
System.out.println(s1.compareTo(s3)); // 相同输出 0
System.out.println(s1.compareTo(s4)); // 前k个字符完全相同,输出长度差值 -3
}
那这里我们还有一个忽略大小写比较的方法,他给封装了一下
老样子我们再点进去看看
我发现,他就是把他都转成大写然后进行了比较,其实本质上没什么太大区别
2.字符串查找
我们这里就简单用个表格汇总下字符串查找的方法,因为实在是太多了,如果一个个看源码要好久,而且他们的原码也没有差别很大
3.字符串转化
1) 数值和字符串转化
我们可以把数组转为字符串,把数字转为字符串以及对象(这个对象要重写toString方法因为我们会调用他重写的toString方法)
2)大小写转换
public static void main(String[] args) { String s1 = "hello"; String s2 = "HELLO"; // 小写转大写 System.out.println(s1.toUpperCase()); // 大写转小写 System.out.println(s2.toLowerCase()); }
3)字符串转数组
使用toCharArray()
public static void main(String[] args) {
String s = "hello"; // 字符串转数组
char[] ch = s.toCharArray();
for (int i = 0; i < ch.length; i++) {
System.out.print(ch[i]);
}
System.out.println(); // 数组转字符串
String s2 = new String(ch); System.out.println(s2);
}
我们都知道value是一个字符串数组,那么我们字符串转数组就很简单了
4.字符串替换
使用一个指定新的字符串替换掉已有字符串数据
5.字符串拆分
我们可以使用这两个方法
第一个就是按照特定的字符串进行拆分,然后返回,第二个就是按照特定字符串拆成多少组,如果拆够了就不拆了
注:这里有一些特殊字符作为分隔符是可能无法正确拆分的,所以我们需要加上转义
就比如我们拆分ip地址
就需要写成这样
那我们来总结下注意事项:
1. 字符"|","*","+"都得加上转义字符,前面加上 "\\" .
2. 而如果是 "\" ,那么就得写成 "\\\\" .
3. 如果一个字符串中有多个分隔符,可以用"|"作为连字符.
6.字符串截取
使用substring方法,可以指定范围
我们来继续看源码,这个check...是用来检查我们传入范围对不对的,我们不用管,然后我们直接进入newString方法
我们发现这个新new出来的字符串是左闭右开的
那这里还有个很重要的事,我们上面的方法,都不是基于原字符串进行修改的,都是通过新new出来一个String对象,然后修改引用实现的,因为我们的String是不可变的(为什么我们会在下文讲到)
7.字符串的不可变性
String是一种不可变对象,字符串中的内容不可改变,字符串不可被修改,这也就是为什么我们内部方法也都是创建新对象并且修改引用。那接下来我们看看是为什么?
因为:
String类在设计时就说了,是不可以改变的
图中我们发现都是被final修饰的,即String即不可以被继承,value也被修饰被final修饰,表明value自身的值不能改变,即不能引用其它字符数组,但是其引用空间中 的内容可以修改。
网上有很多人说:String不可变是因为内部保存字符的数组被final修饰了,因此不可以改变(实际上并不是这样的,因为源码中没说)
我发现,这些final并不能本质上让String不可修改,所以我觉得有一些别的原因,我就去网上找了一些
在许多编程语言中,如Java、Python、C#等, string 类型被设计为不可修改,主要有以下几个原因:
内存管理与效率
- 字符串常量池:在Java等语言中,不可变的字符串可以方便地实现字符串常量池。相同内容的字符串字面量在内存中只有一份,多个引用可以指向同一个字符串对象,节省了大量的内存空间,提高了内存使用效率和程序性能。
- 避免频繁复制:当字符串作为参数传递或赋值给其他变量时,如果字符串是可变的,可能会导致在传递和赋值过程中频繁地复制整个字符串内容,而不可变字符串只需要传递引用,减少了不必要的内存复制操作,提高了程序的运行效率。
线程安全
- 不可变对象天生线程安全:在多线程环境下,多个线程可以同时访问不可变的字符串对象而无需担心数据被修改,从而避免了因并发修改导致的数据不一致和同步问题,大大降低了多线程编程的复杂性和风险,提高了程序的并发性能和稳定性。
缓存与哈希码
- 缓存哈希码:由于字符串不可变,其哈希码在对象创建时就可以计算并缓存起来,每次需要获取哈希码时直接返回缓存的值,提高了获取哈希码的效率,这对于在数据结构中使用字符串作为键(如哈希表)时非常重要,可以快速地进行查找和比较操作。
- 作为键的可靠性:不可变的字符串作为键在哈希表等数据结构中使用时,保证了键的唯一性和稳定性,不会因为键的内容被修改而导致在哈希表中找不到对应的键值对或出现错误的查找结果,确保了数据结构的正确性和一致性。
安全与不可篡改
- 防止意外修改:不可变的字符串可以防止在程序的其他部分意外地修改字符串内容,从而避免了一些潜在的错误和安全漏洞,使得程序的行为更加可预测和稳定,尤其是在处理一些敏感信息或重要数据时,如密码、配置文件中的关键参数等,不可变字符串可以提供更好的安全性和可靠性。
那这些都仅供参考,但是我们可以指定String设计成不可变的,有很多原因并不是因为String类本身也不是因为内部value被final修饰不可修改
8.字符串修改
那我们上面说String类型不可修改,最好是创建新对象,然后改变引用,那这个的效率是不高的,所以我们可以借助StringBuilder和StringBuffer
(三)StringBuilder和StringBuffer
由于String的不可更改特性,为了方便字符串的修改,Java中又提供StringBuilder和StringBuffer类。这两个类大 部分功能是相同的
还有些方法与String类型是一样的,就不多介绍了
我们这里来讲一下String,StringBuilder和StringBuffer三者的区别
1.String的内容不可修改,StringBuilder和StringBuffer的内容是可以修改的
2.StringBuilder线程不安全,StringBuffer是线程安全的,而String因为不可以修改,也是线程安全的
(四)字符串常量池
首先我们来看一个代码
那为什么会是这个结果?
在Java程序中,我们有道常量经常频繁使用,所以为了让程序运行更快,更节省内存,Java就给我们8种基本数据类型和String类都提供了常量池
其实还有很多池:内存池,线程池,数据库连接池等等。 目的都是为了一定程度上提升效率
1.字符串常量池(StringTable)
字符串常量池实际上就是一个固定大小的HashTable(一种高效查找的数据结构)
2.再谈String对象创建
我们上面只是简单的画了个内存中的图,并没有涉及到字符串常量池,那这一次我们把字符串常量池加上,来看看字符串在内存中,是怎样分布的
先看这个,我们字符串常量直接赋值的,都会存放到常量中,首先我们会先判断字符串常量池中有没有,如果没有就会放到字符串常量池中,然后在指向引用,如果有了,就直接指向引用
然后有这样一段代码,我们会先去常量池中找有没有这个字符串,如果没有我们就在线程池中创建一个这样的字符串,然后在堆上申请一块内存,指向这个字符串,但是本身他并没有入池,只是在这个池中放了一个这样的字符串。如果有,那就在堆上申请一块内存,直接指向这个字符串,
结论:new出来的对象都是唯一的,但是我们可以使用intern方法,让字符串对象入池