当我们学习了Java中的继承和多态后,现在我们就可以来学习一个非常重要的东西:String字符串,以及还有StringBuilder和StringBuffer两兄弟。我们直接发车了!!!
前期文章
前言- IDEA如何配置?让你敲代码更轻松!
初识Java语言(一)- 基本数据类型及运算符
初识Java语言(二)- 方法以及递归
初识Java语言(三)- 数组
初识Java语言(四)-类和对象
初识Java语言(五)- 包和继承
初识Java语言(六)-多态、抽象类以及接口
文章目录
- 一、String
- String类的常用方法
- 字符串比较
- 字符串替换
- 字符串查找
- 字符串截取
- 其他方法
- 二、StringBuilder与StringBuffer
一、String
常见构造字符串的方式:
-
声明String类型的变量,后面直接初始化
String str = "hello world";
-
还有一种就是new一个String类型的对象
String str = new String("hello world"); //将字符串放入括号里
以上两种是最为常见的字符串的构造方式,当然还有另外几种,我们来看一下帮助手册的!!!
3、 String(byte[] bytes), 这个构造方法,将一个字节数组转换为字符串
byte[] bytes = {'a', 'b', 'c', 'd'};
String str = new String(bytes); //这样就能够得到“abcd”字符串
4、 String(byte[] bytes, Charset charset),这个构造方法,会将字节数组,按照charset的编码方式,进行编码
byte[] bytes = {'a', 'b', 'c', 'd'};
String str = new String(bytes, "utf-8"); //以utf-8的编码方式,进行编码
5、String(byte[] bytes, int offset, int length),根据偏移量处开始进行转换,转换length个字节的数据
byte[] bytes = {'a', 'b', 'c', 'd', 'e'};
String str = new String(bytes, 1, 3); //从偏移量为1的字符开始,一直转换3个。即就是“bcd”
//切记,输入的偏移量和长度,要在bytes数组的范围内,不然会报异常
上面的五种构造方法,就是在平时比较常见的,也比较简单。接下来,将来说一说,String字符串,在内存中,是如何进行存储的,以及字符串是如何进行判断相不相等的。
-
String str1 = "hello"; String str2 = "hello"; System.out.println(str1 == str2); //true 还是false?
答案毋庸置疑,是true。 那到底是为何相等呢?我们来看内存的情况:
在堆中,还有字符串常量池的概念,但是在具体的内存划分时,是没有这个常量池的,这个常量池,是用哈希表写的。
那到底什么是字符串常量池???
说的简单一点,这一块区域,就是专门用于存储常量字符串的,每次新建一个字符串时,会在字符串常量池查询,看池中是否已经有了相同的字符串,如果已经有了,那么JVM就会将已经有的字符串的地址进行返回,不会再次在池中放入一模一样的字符串。这样做的目的就是节省空间。
就像上图所示,当str1在新建字符串时,“hello”,在池中没有,那么就放入这个字符串,并将地址赋值给str1,接下来在str2时,发现池中有一个一模一样的字符串,那么就直接将池中的字符串的地址进行返回。所以str1和str2两个字符串是指向同一块内存空间的。所以就是true。
-
String str1 = "hello"; String str2 = new String("hello"); System.out.println(str1 == str2); //true还是false?
str1这样的字符串,叫字符串字面值常量。str2呢,是new了一个对象,既然是new的,肯定是在堆上开辟了一块内存空间的。具体的看下图:
如上图所示,str2,会在堆中先new一个String类,然后这个String类的对象里面有一个value的成员变量,用来存储“hello”的地址,而这个字符串呢,最终是会存储在常量池的,然而此时的常量池是有“hello”的,所以value变量,指向的就是已经存在的“hello”字符串。
由图可知,str1直接指向的“hello”字符串,str2直接指向的是一个String类型的对象,直接进行判断地址,肯定也就是false了。
-
String str1 = "hello"; String str2 = "hel" + "lo"; System.out.println(str1 == str2); //true还是false
在这里,我们需要知道,此时1、2行的3个字符串,都是叫字符串字面值常量,在Java中,常量是会在编译的时候,就会直接计算完成,也就是说编译完成后,str2的值就是“hello”,然后在运行时,再去进行分配空间时,就会回到上面我们第一个问题那里,即就是str1和str2都是指向同一块内存空间的。所以最后的答案就是true。
-
String str1 = "hello"; String str2 = new String("hel") + "lo"; System.out.println(str1 == str2); //true还是false?
此时这个问题,和上面的问题3很相似,答案肯定是false。
此时JVM编译完成后,str2的值还是没有变的,因为等号右边有一个变量(new String()),此时编译器在编译的时候,并不知道这个变量里面存储的是什么内容,只能在代码执行到这一步的时候,才知道这个内容是什么。如下图:
如图,str1还是指向常量池的字符串,而str2是由另外的一个String类的对象加上一个“lo”,所以会在堆上开辟另外一块内存空间,存储这个相加的结果,即就是str2指向的是一个String类的对象。所以答案就是false。
-
String str1 = "hello"; String str2 = new String("hel") + new String("lo"); System.out.println(str1 == str2); // true还是false
答案很显然是false。
很显然,str1指向常量池的字符串,str2指向的是堆上的String类的对象。二者的内存地址并不相等。
-
String s3 = new String("1") + new String("1"); s3.intern(); //手动的,将字符串放入字符串常量池 String s4 = "11"; System.out.println(s3 == s4); //true还是false
第一行的代码,s3肯定是指向堆上的String类的对象的,即就是说此时s3的值是在堆上的字符串“11”。然后执行第2行的代码,手动的将堆上的“11”放入字符串常量池,此时就分为两种情况讨论:1、此时的常量池并没有“11”这个字符串,那么就会将堆上的“11”的地址,放到字符串常量池(JDK1.7之后);2、此时的常量池已经有了“11”这个字符串,那么就不会再将“11”手动放入常量池了,说简单点就是啥事也不干。
执行到第3行代码时,此时常量池中,是有“11”这个字符串的,所以就无需再放入进去,拿已经存在的字符串的地址即可。如下图:
在JDK1.6时,intern方法,是会在字符串常量池直接新建一个字符串存入进去,而在JDK1.7之后,就没有新建字符串了,而是直接将堆上的字符串的地址放入常量池即可。
所以此题所后输出的就是true。
-
String s3 = new String("1") + new String("1"); String s4 = "11"; s3.intern(); System.out.println(s3 == s4); //true还是false
这道题就和上面这道题很相似了,只是intern方法的先后顺序不一样而已。当执行到第3行代码的时候,字符串常量池中已经有了s4变量所指向的“11”字符串,此时s3指向的字符串还是在堆上的,没在常量池里面,现在才去调用intern方法,常量池已经有了“11”字符串了,所以不用再放入进去了。此时s3还是指向堆上的String类的对象,s4还是指向常量池的字符串。所以二者的内存地址并不相等,也就是false了。
-
String str1 = "hello"; String str2 = str1; //此时修改str2的值 str2 = "world"; System,out.println(str1); System.out.println(str2);
当我们修改str2的值后,str1的值会发生改变吗???
答案肯定是不会的。这跟C语言的指针不一样,指针的话,我可以通过地址去改变内存里面的值。在Java中的引用,是做不到的。这里只是重新建了一个字符串“world”,放入字符串常量池,然后这个字符串的地址赋值给了str2,所以str1并没有发生任何的改变。 这一点非常重要。
String类的常用方法
字符串比较
-
equals方法。
比较的是字符串里面的内容是否相等,也是平时使用的最大的比较方法。
String str1 = "hello"; String str2 = "hello"; Ststem.out.println(str1.equals(str2)); //切记,equals方法的调用方,不能是null,不然会报异常。即str1不能是null
-
equalsIgnoreCase方法。这个方法比较高级,它会忽略大小写的区别
String str1 = "hello"; String str2 = "HELLO"; System.out.println(str1.equalsIgnoreCase(str2)); //此时还是true
-
compareTo方法,这个方法比较的就是字典序,类似于C语言的strcmp方法
String str1 = "hello"; String str2 = "helloo"; System.out.println(str1.compareTo(str2)); //返回的小于0的数 //这个方法,会将两个字符串的每一个字符进行比较,在比较的过程中,如果调用方的某一个字符小于另一方的字符,那么就返回负数。 //如果调用方的字符大于另一方的字符,返回正数 //如果两个字符串的长度相等,且每个字符都相等,那么就返回0
字符串替换
-
replace方法,用于替换字符串里面的一些字符
String str1 = "hellohellohello"; String str2 = str1.replace('h', 'H'); //将小写h换成大写H //切记,这里不会影响到str1字符串本身,因为Java中的字符串是不可变的, //这里只会建立一个新的字符串,进行改动的。
-
replaceFirst方法,将第一次出现的字符串进行替换
String str1 = "hellohellohello"; String str2 = str1.replaceFirst("ll", "LL"); //将第一次出现“ll”字符串的替换
字符串查找
-
contains方法,用于判断一个字符串,是否是包含另外一个字符串的
String str1 = "hello world";
System.out.println(str1.contains("world")); //判断str1是否有world子串
- indexOf方法,用于返回一个子串,在主串中的起始位置,也就是大家熟知的KMP算法实现的
String str1 = "hello KMP";
System.out.println(str1.indexOf("KMP")); //返回值就是下标6
-
startsWith方法,判断主串中,是否是以这个子串开头的(前缀)
String str1 = "hello world"; System.out.println(str1.startsWith("hello")); //判断是否以hello开头
-
endsWith方法,判断主串中,是否以这个子串结尾的
String str1 = "hello world"; System.out.println(str1.endsWith("world")); //判断主串是不是以world结尾的
字符串截取
split方法,用于将一个字符串,以某个字符进行分割,返回的是一个字符串数组
这个方法,我在刷题的时候用的挺多的,配合缓冲输入流,读取一行数据,然后进行分割。
String str1 = "I love you";
String[] res = str1.split(" "); //以空格进行分割
除此之外,还有一个split方法,限制了分割后的数组个数
String str1 = "I love you";
String[] res = str1.split(" ", 2); //以空格分割,分割为两个数组
//即以上代码分割后的数组中,只有两个数据:I 和 love you,两部分
split方法,还有一个用法,比如给定一个字符串,我要以多个字符进行分割,假设给定字符串为I love*you
,如何将空格和*号一起分割呢。如下:
String str = "I love*you";
//前面一个空格,后面一个*号,中间用|隔开,就能实现多个字符的分割
String[] res = str.split(" |*");
当然split方法,在分割ip地址时,也是需要注意一个点,那就是ip地址的小数点分割符,需要先用转义字符代替,如下:
String str = "192.168.1.1";
String[] res = str.split("\\."); //用两个斜线,先进行转义
其他方法
- isEmpty方法,用于判断字符串是否为空串。切记此处的空串,指的是字符串里什么都没有,不是null
- intern方法,手动将字符串放入常量池
- trim方法,去掉字符串的首尾的空格
- toUpperCase方法,将字符串的小写字符转换为大写
- toLowerCase方法,将字符串的大写字符转换为小写
等等……,这里我就不列举了。
二、StringBuilder与StringBuffer
在上文中,我们已经了解了String类的简单使用,对于这个类的使用,可能熟读了上文中的内存分配之后,会觉得,String类在拼接字符串的时候,会建立出很多对象,比如有以下代码:
String str = "hello";
for (int i = 0; i < 100; i++) {
str = str + i;
}
System.out.println(str);
上述代码中,str字符串一直在拼接新的字符串,组合成新的字符串。根据上文中的内存分配图,我们可以脑补出大致的内存时如何浪费的,每次拼接,都需要在堆上新建一个对象,然后拼接。这样的方式实在是太浪费空间了。
所以后来就有了StringBuilder和StringBuffer两个字符串相关的类。
我们先来看一下String、StringBuilder和StringBuffer三者之间的区别!
- StringBuffer和StringBuilder非常的相似,均代表可变的字符序列,而且方法都是一样的
- String是不可变字符串
- StringBuffer是可变字符串,执行效率低,但线程安全,适用于多线程
- StringBuilder是可变字符串,执行效率高,但线程不安全,适用于单线程
三者之间的继承关系如下图:
说了那么多,有人可能会问,到底该怎么使用这两个类呢?我们这就来讲。
一样的,还是先从构造方法说着走,有无参构造,也有有参构造。最常用的就是下面这两种:
String str = "hello";
StringBuilder sb = new StringBuilder(); //无参构构造方法
sb.append(str); //通过这个方法,可以将“hello”添加到这个StringBuilder中
StringBuilder sb2 = new StringBuilder(str); //也可以直接在构造方法里,传入字符串
上面这两种方法,是最常用的。StringBuffer也是如此。
String str = "world";
StringBuffer sb = new StringBuffer(); //无参构造
sb.append(str);
StringBuffer sb2 = new StringBuffer(str); //有参构造
我们来讨论一个面试题
//以下代码,是如何进行拼接字符串的?具体流程?
String str1 = "hello ";
String str2 = str2 + "world";
我们通过反编译,来看一下这段代码具体执行了哪些操作。
所以,根据上图,我们可以看出,看似并没有用到StringBuilder,实则在JVM为了优化,所以将StringBuilder加入到了其中,通过StringBuilder的append方法进行添加字符串,然后再转换为字符串即可。
所以现在,我们回过头来看上文中的这一段代码,是否会觉得,很浪费时间和空间呢?
String str = "hello";
for (int i = 0; i < 100; i++) {
str = str + i;
}
System.out.println(str);
每次进行一轮循环,JVM都需要new一个StringBuilder类,每次循环都是这样的。所以这样写代码,就很low。以后我们在写代码的时候,就要避免这样的写法,我们只需手动的在循环外面new一个StringBuilder类,然后循环里面调用append方法即可。
现在我们来说一说这StringBuilder类的一下常用方法;本质是,这个类的很多方法,String类中也是有的,我们只需要知道另外几个不知道的方法:
-
toString方法,将StringBuilder类的对象,转换为字符串类型
StringBuilder sb = new StringBuilder("hello world"); //String res = sb; //error,这样是不行的 String res = sb.toString(); //必须调用这个类的toString方法
-
reverse方法。我记得有一道面试题,就是问如何将一个字符串进行逆序。此时我们就可以将字符串转换为StringBuilder类,然后调用reverse方法.
String str = "hello world"; StringBuilder sb = new StringBuilder(str); str = sb.reverse().toString(); //StringBuilder,可以进行链式调用 //就如上,刚调用完reverse方法,后面可以直接进行调用toString方法。 //因为它的返回值就是StringBuilder本身的对象
-
append方法。用于在添加字符串的,切记这个方法,可以添加字符、数值、字符串等等。
String str = "hello world"; StringBuilder sb = new StringBuilder(); sb.append(str).append("good morning");//同样也是可以进行链式调用的
-
length方法。用于计算当前这个StringBuilder中的字符串,有多少个字符
StringBuilder sb = new StringBuilder("hello world"); System.out.println(sb.length()); //11
-
delete方法。这个方法用于删除当前StringBuilder中的字符串,这个方法有两个参数,第一个是起始位置的偏移量,第二个是结束位置的偏移量。
StringBuilder sb = new StringBuilder("hello world"); sb.delete(1, 4); //从偏移量为1位置开始,一直到4位置,切记是左闭右开区间[1,4) System.out.println(sb.toString()); //输出:ho world
-
insert方法。插入新的参数。有两个参数,第一个参数就是偏移量,第二个参数就是插入的内容。
StringBuilder sb = new StringBuilder("I you"); sb.insert(2, "love "); //在偏移量为2的位置,开始插入 System.out.println(sb.toString()); //输出的结果:I love you
上面的所有代码,在StringBuffer中也是适用的。StringBuilder和StringBuffer,就像同门师兄弟一样,学的每一招功夫,都是相似的。
那么他们二者之间就没有区别吗? 肯定是有的,我们分别来看一下二者底层的源码:
我们可以看到源码StringBuffer类中的每一个方法,都是被synchronized
修饰的,简答点理解,就像一把锁,可以保证线程安全。所以说StringBuffer是线程安全的。而StringBuilder的每个方法,没有这个关键字,所以说它是线程不安全的。
还有一个问题就是:string转StringBuilder,或者StringBuilder转String。前者转换,只能通过调用StringBuilder的构造方法,或者先new一个StringBuilder对象,然后调用append方法添加。后者的话,就调用StringBuilder的toString方法就行。
好啦,上述所有,就是本期的所有内容。本期更新就到此结束啦!!!我们下期见!!!