String类介绍
String
类介绍
String
介绍
String
,也就是字符串,就是字符char
的集合。这个时候可能有人会想,既然String
是字符的集合,那为什么还要一个类去描述这个集合呢?
实际上,String
类并不止定义了char
类型的集合,并且提供了很多操作String
的方法。其实其他基本类型也有对应的类,例如int double
就分别有Integer
和Double
,这种类被称为包装类,我们在之后数据结构前置章节详细说明,我们现在先了解即可
String
的创建和初始化
String
有非常多的构造方法,我们先看比较常见的三种
public class Test {
public static void main(String[] args) {
//直接赋值
String s1 = "hehe";
//通过new String
String s2 = new String("hehe");
//通过提供一个字符数组
char[] chars = {'h','e','h','e'};
String s3 = new String(chars);
}
}
其中,第一种方法非常常用,实际上第一种直接赋值的方法就是new String()
写法的简写,就和创建char[]
数组也可以省略new char[]
一样
实际上String
还有非常多的构造方法,剩下的那些我们可以在[帮助手册](Overview (Java Platform SE 8 ) (oracle.com)****)上查询
在帮助手册,通过Ctrl+F
查找String
然后在左下角一直翻,直到找到String
往下看到Constructors
就是它的所有构造方法
String
的不可变性
由于String
是一个类,因此String
类型的变量都是引用变量,也就是说它存储着指向一块空间的地址。但是这块空间存储的是什么呢?以下面代码为例,运用调试
public class Test {
public static void main(String[] args) {
String str = "hehe";
}
}
会发现,它存储了两个值,一个是char[4]
还有一个hash
,有关hash
我们以后讨论,我们这里先看value
那么我们可不可以通过这个引用,访问这个字符数组从而直接改变这个字符串的值呢?
答案是不行,我们看一下String
的源码,会发现里面的char[] value
被private
修饰不让别人访问
并且我们还可以发现,char[] value
还由final
修饰,也就是说这个引用变量的指向无法被改变。另外,这个String
类前面是由final
修饰的,也就是说它无法被继承。
(这里的字符数组写法是C语言写法,不必过于在意,知道它是一个字符数组即可)
总而言之,字符串是无法被修改的(但是这里要注意,不能改变的原因是因为我们没有任何办法可以操作到value
中的值。而不是因为它被final
修饰,就如我们上面说的final
只是限制了它的指向,并没有限制不能改变它其中的值,但是又由于它是private
的,我们书写的类中是不可能访问到value
的,因此才说字符串是无法修改的)
但是此时可能有人会拿着类似下面代码来问,这不是可以修改嘛?
public class Test{
public static void main(String[] args) {
String str1 = "hehe";
str1 = "hehe1";
System.out.println(str1);
}
}
//打印hehe1
回顾我们上面的知识,这里的str1 = "hehe1"
实际上是str1 = new String("hehe1")
,也就是说,这里实际上是将一个新的引用给了str
,改变了str
的指向,而不是修改了原字符串
包括一些将什么+
号连接的字符串赋值给String
变量,实际上都是创建了一个新的String
对象,改变了原来变量的指向,而不是改变了原字符串
String
的比较
String
为引用类型,因此我们如果直接比较都是比较地址
public class Test {
public static void main(String[] args) {
String s1 = new String("hehe");
String s2 = new String("hehe");
String s3 = new String("haha");
System.out.println(s1 == s2);
System.out.println(s1 == s3);
}
}
//全部为false
如果要比较引用类型,则要使用equals()
方法,并且重写它,String
类中已经重写了这个方法,因此我们直接调用即可
public class Test {
public static void main(String[] args) {
String s1 = new String("hehe");
String s2 = new String("hehe");
String s3 = new String("haha");
System.out.println(s1.equals(s2));
System.out.println(s1.equals(s3));
}
}
//依次打印 true false
但是这里有一个特殊情况,这个涉及到的内容有一些超前,因此只是先做了解,看不懂也没关系
在我们直接赋值的时候,如果赋值的是两个相同的字符串,则直接判断引用会相等
public class Test {
public static void main(String[] args) {
String s1 = "hehe";
String s2 = "hehe";
System.out.println(s3 == s4);
}
}
原因是在我们直接赋值的时候,会将字符串放入一块特别的空间,这块空间被称作常量池
放入常量池的字符串都会进行检查,如果发现常量池中已经有一样的字符串了,那么就直接让那个char[] value
直接指向这一块地址,如果没有就直接放入
所以如果我们还进行直接赋值并且值还和之前赋的某个字符串相等,那么它们两个都会指向常量池中的那个字符串
但是new String()
的字符串不会放入常量池中,因此假如有下面代码,则输出的是false
public class Test {
public static void main(String[] args) {
String s1 = new String("hehe");
String s2 = "hehe";
System.out.println(s1 == s2);
}
}
String
类常用方法介绍
compareTo()
compareTo()
用于比较字符串的大小,定义如下
public int compareTo(String anotherString)
方法参数:anotherString
为要比较的字符串
返回值:>0
,当前字符串大于anotherString
=0
,当前字符串等于anotherString
<0
,当前字符串小于anotherString
返回的差值有两种情况:
- 当两个字符串长度相等时有不同,则返回第一个不同位置的字符的
Unicode
码差值 - 当字符串长度不同时,则都有字符的区域根据第一种情况进行比较。假如公共长度区域字符都相等,则直接返回字符串长度差值
所有的差值计算的顺序均为当前字符串 - anotherString
我们通过例子来更细致的了解一下这个方法
public class Test {
public static void main(String[] args) {
String str1 = "abb";
String str2 = "acd";
System.out.println(str1.compareTo(str2));
}
}
//打印 -1
长度相等,返回第一个不同位置的Unicode
码差值,在本例中计算为'b'-'c'
public class Test {
public static void main(String[] args) {
String str1 = "abc";
String str2 = "abbde";
String str3 = "abcde";
System.out.println(str1.compareTo(str2));
System.out.println(str1.compareTo(str3));
}
}
//依次打印 1 -2
长度不同时,则都有字符的区域根据第一种情况进行比较,因此在本例中第一个比较的计算是'c' - 'b'
公共长度区域字符都相等,则直接返回字符串长度差值,因此在本例中第二个比较的计算是str1.length() - str3.length()
另外还有一个比较方法compareToIgnoreCase
,它可以无视字母的大小写进行比较,其他的没有什么区别,因此不再赘述
length()
length()
用于计算字符串长度,但是要与关键字length
区分
length
关键字用于测定数组的长度,而length()
是String
类里面 用于测定字符串长度的方法,但其实内部也是调用了length
关键字用来测定其字符数组的数组长度
下面是length()
的源码以及一个例子
public int length() {
//value为String类定义的一个字符数组,用于存放字符串中的字符
return value.length;
}
public class Test {
public static void main(String[] args) {
String str = "asd";
System.out.println(str.length());
int[] arr = {1, 2, 3, 4, 5, 6};
System.out.println(arr.length);
}
}
//依次打印 3 6
isEmpty()
isEmpty()
用于判断一个字符串是否为空,但是注意:字符串是引用类型,你定义的引用变量为空不代表这个字符串为空,而是这个字符串不存在
//输出什么?
public class Test {
public static void main(String[] args) {
String str = "abc";
str = null;
System.out.println(str.isEmpty());
}
}
答案当然是抛出空指针异常,这里并不是字符串为空,而是你的引用str
为空,我们之前说过,new
的对象如果没有任何引用指向,则它会被回收,那么实际上这里根本就不存在任何字符串了
那么怎么样才算一个空的字符串呢?答案是就只写一个引号
public class Test {
public static void main(String[] args) {
String str = "";
System.out.println(str.isEmpty());
}
}
//输出 true
charAt()
我们上面已经说过,字符串的本质是一个字符数组,那么我们能不能通过下标来访问字符串里面的字符呢?
String
类里面就提供了一个方法charAt()
,我们就可以借助这个方法来通过下标来访问字符串里面的字符
使用方法也很简单,我们将字符串想象为一个字符数组拼接而成的,就输入下标即可
public class Test {
public static void main(String[] args) {
String str = "hehe";
for (int i = 0; i < 4; i++) {
System.out.println(str.charAt(i));
}
}
}
//依次打印 h e h e
indexOf()
这个方法用于在字符串中进行查找,它又多种重载,以下为常见的三种重载方法的定义
int indexOf(int ch);
int indexOf(int ch, int fromIndex);
int indexOf(String str);
int indexOf(String str, int fromIndex);
方法参数:ch
为要查找的目标字符(字符的本质是整型,所以用int
没问题),str
为要查找的目标字符串,fromIndex
是查找起始点
返回值:如果查找到了目标字符,则直接返回目标字符的下标,不再往后查找。如果查找到了字符串,则返回目标字符串首元素的下标,不再往后查找。如果没找到则返回-1
下面通过几个例子来说明如何使用
public class Test {
public static void main(String[] args) {
String str = "aababce";
System.out.println(str.indexOf('a'));
//第一个位置就是a,直接返回首元素下标0
System.out.println(str.indexOf("ab"));
//第二个位置有ab,返回目标字符首元素下标1
System.out.println(str.indexOf('a',3));
//从下标为3的地方开始找a,返回3
System.out.println(str.indexOf("ab",3));
//从下标为3的地方开始找ab,返回3
System.out.println(str.indexOf('d',3));
//找不到d,返回-1
System.out.println(str.indexOf("abcd",3));
//找不到abcd,返回-1
}
}
另外,String
类还提供了一个从后往前找字符及字符串的方法lastIndexOf()
,使用方法和indexOf()
是一样的,因此不再过多赘述
valueOf()
valueOf()
可以将其他类型的数据转换为字符串,我们直接上代码看一下效果
public class Test {
public static void main(String[] args) {
String s1 = String.valueOf(1234);
String s2 = String.valueOf(12.34);
String s3 = String.valueOf(true);
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
}
}
//依次打印 1234 12.34 true
有人可能会说,你这样和我直接打印1234 12.34 true
有什么区别,实际上并没有区别,因为println
本身在你传入的数据不是字符串的时候就会调用valueOf()
,因此这里只是演示
valueOf
也支持转换自定义类型,但是valueOf()
接收自定义类型后会调用toString()
方法,倘若你想要转换自定义类型,则也要重写toString()
方法
class Student{
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Test {
public static void main(String[] args) {
String str = String.valueOf(new Student("zhangsan",18));
System.out.println(str);
}
}
toUpperCase()
将字符串里的小写字母转换为大写字母,同理也有toLowerCase()
,将字符串里的大写字母转换为小写字母
注意:由于String
无法改变,所以这里转换后均是产生了新的字符串而不是在原字符串上直接修改,后面一些修改字符串的方法也是同理
public class Test {
public static void main(String[] args) {
String str = "abcde";
System.out.println(str);
str = str.toUpperCase();
System.out.println(str);
str = str.toLowerCase();
System.out.println(str);
}
}
//依次输出 abcde ABCDE abcde
toCharArray()
将字符串转换为一个字符数组
public class Test {
public static void main(String[] args) {
String str = "hehe";
char[] arr = str.toCharArray();
for (char x : arr) {
System.out.println(x);
}
}
}
//依次打印 h e h e
如果想要将一个字符数组转换为字符串,则借助String
的构造方法,直接传入一个字符数组即可
replace()
将字符串里的元素进行替换,一个只能替换单个字符,另一个可以替换字符串,并且长度不要求相等
String replace(char oldChar, char newChar)
String replace(CharSequence target, CharSequence replacement)
CharSequence
是一个接口,可以理解为字符序列,String
以及下面我们要讲到的StringBuffer
和StringBuilder
都实现了这个接口
oldChar
为要被替换的字符,newChar
为要把oldChar
替换的字符
target
为要被替换的字符序列,replacement
为要把target
替换的字符序列
直接上代码方便理解
public class Test {
public static void main(String[] args) {
String str = "abcabcd";
System.out.println(str.replace('a', 'x'));
System.out.println(str.replace("ab", "xxx"));
}
}
//依次打印 xbcxbcd xxxcxxxcd
实际上还有两个替换方法replaceFirst()
和replaceAll()
String replaceAll(String regex, String replacement)
String replaceFirst(String regex, String replacement)
其中,replaceAll()
和replace(CharSequence target, CharSequence replacement)
的效果相同,而replaceFirst()
是只会替换字符串中第一个出现的目标字符串,但是这两个方法只允许传String
类型
public class Test {
public static void main(String[] args) {
String str = "abcabcd";
System.out.println(str.replaceFirst("a", "xxx"));
System.out.println(str.replaceAll("ab", "xxx"));
}
}
//依次打印 xxxbcabcd xxxcxxxcd
subString()
subString()
用于截取字符串,可以只指定起点,也可以指定起点和终点
String substring(int beginIndex)
String substring(int beginIndex, int endIndex)
注意点:1. 不设置终点的时候默认结尾为终点 2.起点和终点越界会抛出越界异常
public class Test {
public static void main(String[] args) {
String str = "helloworld" ;
System.out.println(str.substring(4));
System.out.println(str.substring(4,8));
}
}
//依次打印 oworld owor
trim()
trim()
会去掉字符串开头和结尾的空白字符(空格, 换行, 制表符等)
直接上代码演示效果
public class Test {
public static void main(String[] args) {
String str = " hello world " ;
System.out.println("["+str+"]");
System.out.println("["+str.trim()+"]");
}
}
//依次打印 [ hello world ] [hello world]
split()
split()
可以帮助我们按照指定的分割符,对字符串进行拆分
String[] split(String regex);
String[] split(String regex, int limit);
regex
为分割符,limit
为分割次数,如果不设定limit
默认将整个字符串按照分割符分割
根据例子说明(这个例子直接说明不好说明,建议自己运行看看效果)
public class Test {
public static void main(String[] args) {
String str = "hello world and bite";
String[] result1 = str.split(" ");
for (String s : result1) {
System.out.println(s);
}
String[] result2 = str.split(" ",2);
for (String s : result2) {
System.out.println(s);
}
}
}
//第一个循环依次输出 hello world and bite,总共输出四次
//第二个循环依次输出 hello world-and-bite,用-表达空格,总共输出两次
由于split
的原理是正则表达式(有关正则表达式这里不详细说明,感兴趣的可以自己搜索一下),所以有一些正则表达式中的特殊字符我们并不能直接分割,例如. + |
等等,对于这些特殊字符,我们需要通过一个\
来将它转义为普通字符
但是由于Java中\
也是一个转义字符,我们就没办法单独的把\
放入正则表达式中,所以要进行两次转义,再加一个\
也就是说,假如我们想要以点号.
为分割符,那么我们应该写成\\.
其中第一个反斜杠\
用来在字符串中转义第二个\
让它变为正常的\
从而和.
结合变成\.
放入正则表达式中。而第二个\
是为了在正则表达式中将\.
转义为.
public class Test {
public static void main(String[] args) {
String str = "hello.world.and.bite";
String[] result = str.split("\\.");
for (String s : result) {
System.out.println(s);
}
}
}
//依次打印 hello world and bite
但是还有一个更加特殊的情况,即以\
为分割符号
我们根据上面的思路分析,首先从正则表达式角度考虑,在正则表达式中,我们肯定要放两个\
,因为\
是特殊字符,\\
才会被识别为一个普通的\
那么我们要怎么写才能往正则表达式里输入\\
呢?
那么从字符串角度考虑,我们单独的\
在字符串中也是特殊字符,所以也要一个\
进行转义,也就是说在字符串中写\\
就可以往正则表达式中输入一个\
,所以我们如果要往正则表达式中输入\\
,则应该在字符串中写\\\\
总结下来就是这张图片
总而言之,如果要以\
为分割符号,则应该写成\\\\
public class Test {
public static void main(String[] args) {
//注意,这里字符串中也要转义后表达的才是一个普通的\
String str = "hello\\world\\and\\bite";
String[] result = str.split("\\\\");
for (String s : result) {
System.out.println(s);
}
}
}
//依次打印 hello world and bite
如果想要有多个分割符,则可以通过|
来连接,或者把它们用中括号括起来
public class Test {
public static void main(String[] args) {
String str = "hello world=and&bite";
String[] result = str.split("=|&| ");
// String[] result = str.split("[=& ]");
for (String s : result) {
System.out.println(s);
}
}
}
//两个代码最后的输出结果相同,依次输出hello world and bite
StringBuilder
和StringBuffer
我们上面讲字符串的不可变性的时候说,即使你用+
连接字符串,也是new
了一个新的对象
可是问题是,这个+
为什么能new
一个新的String
对象?它是怎么做到的?
我们通过反编译看一下下面的代码
public class Test {
public static void main(String[] args) {
String str = "hello";
str += "bit";
System.out.println(str);
}
}
可以发现它这里似乎用到了一个StringBuilder
类以及里面的方法append()
,最后还用了一个toString()
把它转换成字符串
我们可以发现,如果我们尝试对字符串进行修改,实际上会借助StringBuilder
这个类,并且会创建很多的临时变量。所以其实并不推荐直接改变字符串,如果想要改变字符串,我们大可以在刚开始的时候就定义成Java提供的可修改的两个类StringBuilder
或者StringBuffer
那么这个StringBuilder
和StringBuffer
到底是个什么玩意,我们接下来就来了解这两个类
StringBuilder
和StringBuffer
StringBuilder
实际上就可以看作是一个可以改变的字符串,但是它初始化的时候就不能和初始化String
一样省略了
public class Test {
public static void main(String[] args) {
StringBuilder str = new StringBuilder("hello");
System.out.println(str);
}
}
虽然String
类中很多方法在StringBuilder
中也有,但是由于StringBuilder
是可以改变的,所以它又提供了很多改变字符串的方法
例如我们上面在反编译中看到的append()
这个方法允许我们直接改变这个StringBuilder
字符串而不是通过新建一个对象
public class Test {
public static void main(String[] args) {
StringBuilder str = new StringBuilder("hello");
str.append(123);
System.out.println(str);
}
}
//打印 hello123
可以看到,我并没有拿str
的引用去接收append()
的返回值,但是str
还是改变了,这也可以说明StringBuilder
是直接在字符串上改变的而不是new
了一个新的对象
如果我们想要将这个StringBuilder
的字符串变为String
字符串,则直接通过toString()
方法转换即可
public class Test {
public static void main(String[] args) {
StringBuilder str = new StringBuilder("hello");
str.append(123);
String s = str.toString();
System.out.println(s);
}
}
如果想要将String
转换为StringBuilder
,那么就可以通过StringBuilder
的构造方法
public class Test {
public static void main(String[] args) {
String str1 = "hello";
StringBuilder str2 = new StringBuilder(str1);
str2.append("world");
System.out.println(str2);
}
}
实际上StringBuilder
还提供了很多改变字符串的方法,例如删除、插入、替换、反转等等,我们这里直接将其列出,不再做细致的说明
方法 | 说明 |
---|---|
void ensureCapacity(int mininmumCapacity) | 扩容,mininmumCapacity 为新的容量 |
void setCharAt(int index,char ch) | 将下标为index 的位置的字符改为ch |
StringBuilder insert(int offset, String str) | 在下标为offset 处插入字符串(支持基本数据类型,其他数据类型可以通过toString() 转换则也支持) |
StringBuilder deleteCharAt(int index) | 将下标为index 的位置的字符删除 |
StringBuilder delete(int start, int end) | 将下标区间为[start ,end )的字符删除 |
StringBuiler replace(int start, int end, String str) | 将下标区间为[start ,end )的字符替换为str |
StringBuilder reverse() | 反转字符串 |
StringBuffer
在大部分内容上和StringBuilder
没有什么区别,但是在 StringBuffer
中的方法都使用了 synchronized
关键字修饰。那么这个关键字是什么意思呢?
synchronized
直接翻译过来为“同步”的意思,被这个关键字修饰的方法是不能被同时调用的,什么意思呢?比如我们的公共厕所,上面是有门锁的,如果有一个人再用,那么后面的人就要等这个人用完才能用。而没有这个关键字修饰的方法,就相当于一个公开的区域,任何人去都没有限制。
因此假如我们的程序环境是多线程的,那么就可以使用StringBuffer
,但是如果是单线程,则推荐使用StringBuilder
但是这个时候可能又有人疑惑:什么是单线程什么是多线程?单线程就是任务必须依次执行,不能同时进行,而多线程就可以同时执行不同的任务。
依旧是以厕所为例子,如果一个公共厕所里面只有一个隔间,一次只能一个人上厕所,那么就可以看作是单线程的,如果有很多隔间,一次可以有很多人上厕所,就可以看作是多线程的。
String StringBuilder StringBuffer
的区别
String
和另外两者的区别:String
不能修改,另外两个可以修改
StringBuilder
和StringBuffer
的区别:StringBuffer
是采用同步处理的,适合多线程操作,而StringBuilder
是未采用同步处理的,适合单线程操作