当我们学习了Java中的继承和多态后,现在我们就可以来学习一个非常重要的东西:String字符串,以及还有StringBuilder和StringBuffer两兄弟。我们直接发车了!!!
前期文章
前言- IDEA如何配置?让你敲代码更轻松!
初识Java语言(一)- 基本数据类型及运算符
初识Java语言(二)- 方法以及递归
初识Java语言(三)- 数组
初识Java语言(四)-类和对象
初识Java语言(五)- 包和继承
初识Java语言(六)-多态、抽象类以及接口
一、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);
4、 String(byte[] bytes, Charset charset),这个构造方法,会将字节数组,按照charset的编码方式,进行编码
byte[] bytes = {'a', 'b', 'c', 'd'};
String str = new String(bytes, "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);
上面的五种构造方法,就是在平时比较常见的,也比较简单。接下来,将来说一说,String字符串,在内存中,是如何进行存储的,以及字符串是如何进行判断相不相等的。
-
String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);
答案毋庸置疑,是true。 那到底是为何相等呢?我们来看内存的情况: 在堆中,还有字符串常量池的概念,但是在具体的内存划分时,是没有这个常量池的,这个常量池,是用哈希表写的。 那到底什么是字符串常量池??? 说的简单一点,这一块区域,就是专门用于存储常量字符串的,每次新建一个字符串时,会在字符串常量池查询,看池中是否已经有了相同的字符串,如果已经有了,那么JVM就会将已经有的字符串的地址进行返回,不会再次在池中放入一模一样的字符串。这样做的目的就是节省空间。 就像上图所示,当str1在新建字符串时,“hello”,在池中没有,那么就放入这个字符串,并将地址赋值给str1,接下来在str2时,发现池中有一个一模一样的字符串,那么就直接将池中的字符串的地址进行返回。所以str1和str2两个字符串是指向同一块内存空间的。所以就是true。 -
String str1 = "hello";
String str2 = new String("hello");
System.out.println(str1 == str2);
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);
在这里,我们需要知道,此时1、2行的3个字符串,都是叫字符串字面值常量,在Java中,常量是会在编译的时候,就会直接计算完成,也就是说编译完成后,str2的值就是“hello”,然后在运行时,再去进行分配空间时,就会回到上面我们第一个问题那里,即就是str1和str2都是指向同一块内存空间的。所以最后的答案就是true。 -
String str1 = "hello";
String str2 = new String("hel") + "lo";
System.out.println(str1 == str2);
此时这个问题,和上面的问题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);
答案很显然是false。 很显然,str1指向常量池的字符串,str2指向的是堆上的String类的对象。二者的内存地址并不相等。 -
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
第一行的代码,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);
这道题就和上面这道题很相似了,只是intern方法的先后顺序不一样而已。当执行到第3行代码的时候,字符串常量池中已经有了s4变量所指向的“11”字符串,此时s3指向的字符串还是在堆上的,没在常量池里面,现在才去调用intern方法,常量池已经有了“11”字符串了,所以不用再放入进去了。此时s3还是指向堆上的String类的对象,s4还是指向常量池的字符串。所以二者的内存地址并不相等,也就是false了。 -
String str1 = "hello";
String str2 = str1;
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));
-
equalsIgnoreCase方法。这个方法比较高级,它会忽略大小写的区别 String str1 = "hello";
String str2 = "HELLO";
System.out.println(str1.equalsIgnoreCase(str2));
-
compareTo方法,这个方法比较的就是字典序,类似于C语言的strcmp方法 String str1 = "hello";
String str2 = "helloo";
System.out.println(str1.compareTo(str2));
字符串替换
-
replace方法,用于替换字符串里面的一些字符 String str1 = "hellohellohello";
String str2 = str1.replace('h', 'H');
-
replaceFirst方法,将第一次出现的字符串进行替换 String str1 = "hellohellohello";
String str2 = str1.replaceFirst("ll", "LL");
字符串查找
-
contains方法,用于判断一个字符串,是否是包含另外一个字符串的
String str1 = "hello world";
System.out.println(str1.contains("world"));
- indexOf方法,用于返回一个子串,在主串中的起始位置,也就是大家熟知的KMP算法实现的
String str1 = "hello KMP";
System.out.println(str1.indexOf("KMP"));
-
startsWith方法,判断主串中,是否是以这个子串开头的(前缀) String str1 = "hello world";
System.out.println(str1.startsWith("hello"));
-
endsWith方法,判断主串中,是否以这个子串结尾的 String str1 = "hello world";
System.out.println(str1.endsWith("world"));
字符串截取
split方法,用于将一个字符串,以某个字符进行分割,返回的是一个字符串数组
这个方法,我在刷题的时候用的挺多的,配合缓冲输入流,读取一行数据,然后进行分割。
String str1 = "I love you";
String[] res = str1.split(" ");
除此之外,还有一个split方法,限制了分割后的数组个数
String str1 = "I love you";
String[] res = str1.split(" ", 2);
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);
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.toString();
-
reverse方法。我记得有一道面试题,就是问如何将一个字符串进行逆序。此时我们就可以将字符串转换为StringBuilder类,然后调用reverse方法. String str = "hello world";
StringBuilder sb = new StringBuilder(str);
str = sb.reverse().toString();
-
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());
-
delete方法。这个方法用于删除当前StringBuilder中的字符串,这个方法有两个参数,第一个是起始位置的偏移量,第二个是结束位置的偏移量。 StringBuilder sb = new StringBuilder("hello world");
sb.delete(1, 4);
System.out.println(sb.toString());
-
insert方法。插入新的参数。有两个参数,第一个参数就是偏移量,第二个参数就是插入的内容。 StringBuilder sb = new StringBuilder("I you");
sb.insert(2, "love ");
System.out.println(sb.toString());
上面的所有代码,在StringBuffer中也是适用的。StringBuilder和StringBuffer,就像同门师兄弟一样,学的每一招功夫,都是相似的。
那么他们二者之间就没有区别吗? 肯定是有的,我们分别来看一下二者底层的源码:
我们可以看到源码StringBuffer类中的每一个方法,都是被synchronized 修饰的,简答点理解,就像一把锁,可以保证线程安全。所以说StringBuffer是线程安全的。而StringBuilder的每个方法,没有这个关键字,所以说它是线程不安全的。
还有一个问题就是:string转StringBuilder,或者StringBuilder转String。前者转换,只能通过调用StringBuilder的构造方法,或者先new一个StringBuilder对象,然后调用append方法添加。后者的话,就调用StringBuilder的toString方法就行。
好啦,上述所有,就是本期的所有内容。本期更新就到此结束啦!!!我们下期见!!!
|