一:String的介绍:
1.概述:String 类代表字符串
2.特点:
a.Java 程序中的所有字符串字面值(如 "abc" )都作为此类的实例(对象)实现
凡是带双引号的,都是String的对象
String s = "abc"
"abc"就是对象;String就是对象的数据类型;s就是对象名
b.字符串是常量,它们的值在创建之后不能更改(底层是一个被final修饰的byte数组)
String s = "hello"
s+="world" -> 会产生新对象
c.String 对象是不可变的,所以可以共享
String s1 = "abc"
String s2 = "abc"
二:String的实现原理:
1.jdk8的时候:String底层是一个被final修饰的char数组-> private final char[] value;
2.jdk9开始到之后:底层是一个被final修饰的byte数组-> private final byte[] value;
一个char类型占2个字节
一个byte类型占1个字节 -> 省内存空间
其实后面的JDK更新大部分都是在内存的存储方面做研究了
整体的代码思维就出现了一个lamba表达式
上面String的介绍中也有说过:
字符串定义完之后,数组就创建好了,被final一修饰,数组的地址值直接定死
正是应该这个特性,String不能被修改,所以后面才出现了StringBuilder
三:String的创建:
1.String() -> 利用String的无参构造创建String对象
2.String(String original) -> 根据字符串创建String对象
3.String(char[] value) -> 根据char数组创建String对象
4.String(byte[] bytes) -> 通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String
a.平台:操作系统
b.操作系统默认字符集:GBK
GBK:一个中文占2个字节
UTF-8:一个中文占3个字节
而且,中文对应的字节一般都是负数
代码在idea中写的,idea启动的时候,会自动加一个启动参数,此启动参数为UTF-8
-Dfile.encoding=UTF-8
5.简化形式:
String 变量名 = ""
其实String的创建只需了解即可,最常用的还是最后一种简化形式
四:String 面试题:
我觉得这一块可以多写点内容,通过一些题目来加深对字符串的理解,这种方式会好很多
第一段代码:
public class Demo04String {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
System.out.println(s1==s2);//true
System.out.println(s1==s3);//false
System.out.println(s2==s3);//false
}
}
上面这张图是第一段代码的图示
整体的流程是:
首先s1 = "abc" 先在堆中创建了这么一个 "abc"的对象,然后等我们再去创建s2的时候,这个时候
Java会首先检查字符串池中是否存在相同内容的字符串,如果存在,则返回该字符串的引用,如果不存在,则在字符串池中创建一个新的字符串对象。
所以上面的s1==s2是true,地址值相同
然后s3出现了,为什么s3会返回false呢,是因为s3又重新创建了一个对象,重新创建了一个新的u对象,地址值肯定不同,不过如果你用String的equals方法去比,发现比出来的值还是相同的。
因为String的equals方法比的是字符串的内容。
第二段代码:
问1:String s = new String("abc")共有几个对象? 2个
一个new本身 一个是"abc"
问2:String s = new String("abc")共创建了几个对象? 1个或者2个
就看abc有没有提前创建出来了
同理这也是上面那个代码的图示。
这个问题涉及到Java中String类的特性。在Java中,字符串是不可变的,这意味着每次对字符串进行更改时,实际上是创建了一个新的字符串对象。让我们来解析一下这个问题:
当执行 String s = new String("abc"); 时,会发生以下几件事情:
在堆内存中创建一个新的String对象,内容为"abc"。
变量s指向这个新创建的String对象。
此时会发生字符串池(String Pool)的情况:
字符串池是Java中的一个特殊存储区域,用于存储字符串常量。
当使用双引号创建字符串时,Java会首先检查字符串池中是否存在相同内容的字符串,如果存在,则返回该字符串的引用,如果不存在,则在字符串池中创建一个新的字符串对象。
因此,在这个例子中,"abc"这个字符串在字符串池中已经存在,当执行 new String("abc")时,会创建一个新的String对象,但是"abc"这个字符串常量在字符串池中并没有新创建,而是直接引用已存在的"abc"。因此,总共有两个String对象:一个是通过new创建的,另一个是字符串池中的"abc"。
第三段代码:
public class Demo05String {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";
String s4 = "hello"+"world";
String s5 = s1+"world";
String s6 = s1+s2;
System.out.println(s3==s4);//true
System.out.println(s3==s5);//false
System.out.println(s3==s6);//false
}
}
在Java中,对于字符串常量的拼接操作会被编译器优化,最终会被优化为一个字符串常量。这个优化过程被称为编译期优化或者编译时优化(Compile-time Optimization)。让我们来解释一下这个现象:
-
当执行
String s3 = "helloworld";
时,会在字符串池中创建一个字符串常量"helloworld",变量s3指向这个字符串常量。 -
当执行
String s4 = "hello" + "world";
时,编译器会将两个字符串常量"hello"和"world"在编译时直接拼接为一个新的字符串常量"helloworld",而不是在运行时进行拼接。这样,变量s4也会指向这个"helloworld"字符串常量。
由于Java编译器对字符串拼接进行了优化,将这两个字符串在编译阶段连接为一个新的字符串常量"helloworld",因此在运行时,s3和s4实际指向相同的字符串常量,这也解释了为什么它们的地址值是相同的。因为它们实际上都指向了字符串常量池中的同一个"helloworld"字符串常量。
所以总结起来就是:
1.字符串拼接,如果等号右边是字符串字面值拼接,不会产生新对象
2.字符串拼接,如果等号右边有变量参数拼接,会产生新字符串对象
五:String的常用方法:
我觉着直接硬讲方法非常难懂,还是秉承着做题来记方法:
直接看这一篇,做完里面的算法题,很多方法肯定直接能记住
六:StringBuilder
1.概述:一个可变的字符序列,此类提供了一个与StringBuffer兼容的一套API,但是不保证同步(线程不安全,效率高)
2.作用:主要是字符串拼接
我们从两个角度来讲一下这个StringBuilder
1:String、StringBuffer、StringBuilder的区别:
2:为什么要用StringBuilder(包括源码分析和常用方法)
第一个方面:String、StringBuffer、StringBuilder的区别:
String、StringBuffer、StringBuilder的区别:
String、StringBuffer和StringBuilder是Java中用来处理字符串的类,它们之间的主要区别包括:
1. 不可变性:
- String是不可变的,一旦被创建,其值不能被修改。任何对String对象的操作都会返回一个新的String对象。
- StringBuffer和StringBuilder是可变的,可以通过方法修改其值。StringBuffer是线程安全的,而StringBuilder不是线程安全的。
2. 线程安全性:
- String是不可变的,因此在多线程环境下是安全的。
- StringBuffer是线程安全的,可以在多线程环境下安全使用。
- StringBuilder不是线程安全的,因此在多线程环境下可能会有并发访问问题。
3. 性能:
- 由于String是不可变的,每次对String对象进行操作都会创建一个新的对象,可能会导致性能下降。
- StringBuffer是线程安全的,但由于它的方法都是同步的,可能会带来额外的性能开销。
- StringBuilder是非线程安全的,但在单线程环境下比StringBuffer更高效,因为不需要进行同步操作。
综上所述,选择使用String、StringBuffer或StringBuilder取决于具体的应用场景和需求:如果需要频繁修改字符串,并且在多线程环境中操作,建议使用StringBuffer;在单线程环境中,推荐使用StringBuilder以获得更好的性能;如果字符串不需要被修改,可以使用String类来确保不可变性。
关于这个多线程环境的理解:
1. 多线程环境:
在一个服务器程序中,有多个客户端同时向服务器发送请求并进行处理。这时服务器程序中可能会存在多个线程同时访问某个共享的数据结构,比如字符串。如果使用非线程安全的类来处理这个共享的字符串,可能会导致并发访问问题,造成数据混乱或者程序崩溃。在这种情况下,应该使用线程安全的类比如StringBuffer来保证数据操作的安全性。
(如果学过操作系统里面的这个临界区的概念可能会更好理解)
2. 单线程环境:
在一个简单的命令行程序中,只有一个程序在顺序执行各个步骤,没有多个线程同时访问同一个数据结构的情况。在这种情况下,使用非线程安全的类比如StringBuilder来处理字符串是没有问题的,因为不涉及多线程并发访问的情况,不会出现数据安全性问题。
总的来说,多线程环境是指多个线程同时并发执行程序,可能访问共享数据;而单线程环境是指只有一个线程在执行程序,不涉及多线程并发访问。根据实际的应用场景和需求,选择合适的字符串处理类来确保程序的正常运行和数据安全。
第二个方面:为什么要用StringBuilder(包括源码分析和常用方法):
以问题引入的方式来讲为什么要用StringBuilder:
问题:
a.刚讲完String,String也能做字符串拼接,直接用+即可,但是为啥还要用StringBuilder去拼接呢?
b.原因:
String每拼接一次(+号左右两边有变量参与),就会产生新的字符串对象,就会在堆内存中开辟新的空间,如果拼接次数多了,会占用内存,效率比较底
StringBuilder,底层自带一个缓冲区(没有被final修饰的byte数组)拼接字符串之后都会在此缓冲区中保存,在拼接的过程中,不会随意产生新对象,节省内存
小疑问:
这里大家可能会有一个疑问(反正我有):
既然我们要想修改这个字符串,我们还要去专门定义一个StringBuilder类
为什么我们不直接把String定义成可修改的呢?
问题解答:
其实本质还是因为线程安全的问题:
-
线程安全:由于String对象是不可变的,可以在多个线程之间共享而不需要担心线程安全问题。因为不可变的对象是线程安全的。
-
缓存Hashcode:由于String的不可变性,可以使用Hashcode来作为String对象的缓存,提高性能。(这里的Hashcode是我从网上查的,我自己这一块还不理解,等以后碰到了再回来讲细致一点)
-
安全性:String作为参数传递时,不用担心内容被修改,可以保证传递的参数不会被更改。
但同时String线程安全,每次修改都需要重新创建一个对象,这不免也让人想到这会带来一定的性能开销,相较于此:而StringBuilder和StringBuffer则可以避免这种开销。
StringBuilder的特点(从源码中分析):
底层自带缓冲区,此缓冲区是没有被final修饰的byte数组,默认长度为16:
这里打一个断点,这里得选一个这个强制步入,要不然进不去
或者不打断点的话,按住ctrl进去也行
进来之后,我们就会发现StringBuilder继承了一个抽象类:AbstractStringBuilder
并且默认传入的这个capacity是16,看这个单词的意思也可以大概猜到这个是容量的意思
我们再跟进去
我们就会发现,这个AbstractStringBuilder有一个构造方法创建一个容量为16的byte数组
那个COMPACT_STRINGS是一个变量:
我们可以发现,这个是一段静态代码块,限于构造方法执行
然后还有一个变量coder ,这个其实就是指定一下这个编码方法
StringBuilder的扩容特点:
1:如果超出了数组长度,数组会自动扩容创建一个新长度的新数组,将老数组的元素复制到新数组,然后将新数组的地址值重新赋值给老数组
2:默认每次扩容老数组的2倍+2
如果一次性添加的数据超出了默认的扩容数组长度(2倍+2),比如存了36个字符,超出了第一次扩容的34,就按照实际数据个数为准,就是以36扩容
我们还是在对这个数组的扩容进行打一个断点:
进入这个StringBuilder代码中,和上面一样,调用父类AbstractStringBuilder的append方法
当我们进入到这个append方法的时候,我们看到这个ensureCapacityInternal方法,顾名思义,这个大概就是确定扩容长度的方法,我们再进去
这里的value是那个byte数组
找到这里就差不多了,这newLength封装的真严实
我们来仔细读一下这段代码:
oldLength就是当前的byte数组的长度
minGrowth就是需要扩容的最小长度
preGrowth就是默认的扩容大小
这个默认的扩容大小什么意思呢
根据我上面的写的特点:默认每次扩容老数组的2倍+2(16+16+2)刚好是老数组的2倍+2
但是,代码会去判断这个默认大小的扩容够不够大
int prefLength = oldLength + Math.max(minGrowth, prefGrowth);
这一段肯定很好理解,取最大值,如果说这个时候数组不需要扩容那么大,就取默认的扩容大小即可,如果不够,就去这个时候的minGrowth。
至此源码分析完毕。
七:StringBuilder的使用:
这一段的使用和上面的String使用一样
用算法题来熟悉