java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java String类

详解Java中String类的各种用法

作者:zjruiiiiii

Java中定义了String和StringBuffer两个类来封装对字符串的各种操作,存放于java.lang包中,是Java语言的核心类,提供了字符串的比较、查找、截取、大小写转换等操作,无需导入即可直接使用它们。让我们来详细了解它吧

一、创建字符串

创建字符串的方式有三种:

// 方式一
String str = "Hello Bit";
// 方式二
String str2 = new String("Hello Bit");
// 方式三
char[] array = {'a', 'b', 'c'};
String str3 = new String(array);

我们对第一和第二种创建字符串的方法都已经非常熟悉了,那至于为什么第三种能够传入一个字符数组变为字符串,我们可以按住ctrl键点入传入字符数组的String当中看其原码,我们能够发现此时是利用的方法是数组的拷贝,将字符数组的所有字符改为字符串形式。

在这里插入图片描述

二、字符、字节与字符串的转换

1.字符与字符串的转换

因为字符串等同于是一个个字符的集合,因此要想字符转为字符串则要调用String的构造方法并传入一个字符数组。例如:

char[] val = {'a','b','c'};
String str = new String(val);
System.out.println(str);

当然,我们也可以选择字符数组从哪个下标开始到哪个下标结束的字符转换为字符串的形式。

例如:

char[] val = {'a','b','c','d','e'};
String str1 = new String(val,0,3);
System.out.println(str1);
//打印结果为abc
System.out.println("=========");
String str2 = new String(val,1,3);
System.out.println(str2);
//打印结果为bcd

因为字符串是字符的集合,因此可以字符串可以转换为一个字符或者一个字符数组。括号内的比如0、1是偏移量,偏移量是从0开始的,因此从偏移量为0的位置处往后取3个字符构成一个字符串。

如果字符串要转换为单个字符,代码如下:

String str = "abc";
System.out.println(str.charAt(1));
//打印结果为b

如果字符串要转换为字符数组,代码如下:

char[] val = str.toCharArray();
System.out.println(Arrays.toString(val));
//打印结果:[a, b, c]

2.字节与字符串的转换

Java中的将字节转为字符串需要将字节数组转为字符串。

字节数组转换为字符串:

byte[] bytes = {97,98,99,100};
String str1 = new String(bytes,0,3);
System.out.println(str1);
//打印结果为abc
System.out.println("========");
String str2 = new String(bytes,1,3);
System.out.println(str2);
//打印结果为bcd

字符串转换为字节数组:

String str1 = "abc";
byte[] bytes1 = str1.getBytes();
System.out.println(Arrays.toString(bytes1));
//打印结果为:[97, 98, 99]

三、字符串的比较

有许多初学者会认为,“ == ”与equals比较的方式是相同的。其实有很大的区别。
对于两个字符串用“ == ”比较,比较的是变量的引用。而String的equals方法比较的是两个字符串的内容。但此时又有个疑问:为什么每个定义字符串常量的是一个引用呢?这样就牵扯到了字符串常量池。

1.字符串常量池

对于“池”这个概念,可能大家还是比较陌生的。比如数据连接池、线程池等等。那这些池的作用的干嘛的呢?是用来提高存储效率的。顾名思义字符串常量池是用来存储字符串常量的。字符串常量池中规定只要有了一个字符串常量就不再存储相同的字符串了。从JDK1.8开始字符串常量池是在堆里的。它本质上是一个哈希表(StringTable)是一个数组。存储字符串常量是,会根据一个映射关系进行存储,这个映射关系需要设计一个哈希函数。(因为字符串常量池是有关于JVM的,需要看其原码才能真正了解字符串常量池是如何操作的,此处不深究其原理也不会影响我们判断引用是否相同)。

字符串常量池中当存储一个字符串常量时会在根据哈希函数计算的某一个位置处产生一个结点,结点是由哈希值、String结点的地址、存储该数组位置处的下一个结点的地址组成的(这在JVM的原码中才能真正了解)。而每一个String结点是由字符型数组value与哈希值hash(默认为0)构成的 (下图所示)。点入String看其原码时就能够会发现这两个变量。此时观察到value数组被final修饰则说明该数组里的字符是不能够被改变的,这就是字符串是一个常量的原因,并且该字符串会转换为字符形式存放在字符数组当中。

在这里插入图片描述

先来举一个比较简单的例子来理解字符串常量池的内存布局。
代码如下:
代码一:

String str1 = "hello";
String str2 = "hello";
System.out.println(str1==str2);
//打印结果为true

内存布局如下:

在这里插入图片描述

代码二:

String str1 = "hello";
String str2 = new String("hello");
System.out.println(str1==str2);
//打印结果为false

在这里插入图片描述

手动入池:
我们根据上图知道了代码二中的运行结果是false的,因为str2指向的是new String产生的String对象,而不是存储“hello”的String对象的地址。如果写为下面这个代码,结果会是如何呢?

String str1 = "hello";
String str2 = new String("hello").intern();
System.out.println(str1==str2);
//运行结果为true

为什么最后的结果为true呢?此时调用了String类当中的intern方法,称为手动入池,它能够将str2的指向不再指向new出来的String对象,而是指向了字符串常量池当中已经存储有“hello”字符数组的String对象。

代码三:

String str1 = "hello";
String str2 = "he"+"llo";
System.out.println(str1==str2);
//打印结果为true

此代码有关字符串的拼接。其实“he”与“llo”在编译时期就已经编译为“hello”了。如果要看编译时期str2是什么字符串,则此时我们先点击Build选项,点入Build Project选项则进行编译(图1)。可以在该类文件的路径(含有.class文件)底下(图2+图3),按住shift键加右键点击powershell窗口,输入反编译指令javap -c 类名则能看到编译时str2是否是已经拼接好的hello。

图1:

在这里插入图片描述

图2:在该类的窗口处点击鼠标右键

在这里插入图片描述

图三:退回上一个文件夹,点入out->prodection->字节码文件所在的该文件夹名->按住shift点击鼠标右键点入powershell窗口->输入javap -c 类名

在这里插入图片描述

代码四:

String str1 = "11";
String str2 = new String("1")+new String("1");
System.out.println(str1==str2);
//运行结果为false

字符串的拼接会产生一个StringBuffer的类型,通过StringBuffer调用toString方法也转变为String类,此时拼接完后字符串“11”存储在value中,但是不会存储到字符串常量池当中。

通过反编译我们看到的确拼接产生StringBuffer,并且StringBuffer调用toString方法产生一个String类的对象存储“11”。由图1、图2可以完全了解。

图1:

在这里插入图片描述


图2:

在这里插入图片描述


图3:

在这里插入图片描述

2.字符串内容比较

对于字符串比较,我们不能直接用“==”,而有三种方法能够对字符串有不同的比较方式。

比较字符串内容:直接调用String类的equals方法,将字符串放入括号当中比较。
比较字符串内容(不分字母大小写):调用String类的equalsIgnoreCase方法。

String str1 = "hello" ; 
String str2 = "Hello" ; 
System.out.println(str1.equals(str2)); // false 
System.out.println(str1.equalsIgnoreCase(str2)); // true

比较字符串中的大小:调用String类当中的compareTo方法。本来String类当中是没有compareTo方法,只不过String类实现了Comparable接口,并且重写了compareTo方法。

在这里插入图片描述

它是一个字符一个字符进行比较的。如果str1大于str2则返回str1该字符减去str2该字符的值。例如:

代码1:

String str1 = "abc";
String str2 = "bcd";
System.out.println(str1.compareTo(str2));
//运行结果为:-1

因为b的ASCII码值比a的ASCII码值大1,则直接返回-1。(如果是字符不相同则返回它们的ASCII码差值)

代码2:

String str1 = "bcdef";
String str2 = "bcd";
System.out.println(str1.compareTo(str2));
//运行结果为2

因为在str2比较结束前与str1的字符值是相同的。因此最后的结果是str1的长度减去str2的长度。

下面是String类的compareTo方法的实现。

在这里插入图片描述

四、字符串查找

1.判断一个子串是否存在于主串中:调用String类的contains方法,返回值为boolean。

String str = "abbabcacc";
boolean flg = str.contains("abc");
System.out.println(flg);
//打印结果为true

2.从头开始查找一个子串,并返回第一个子串开始的索引位置,如果没有,则返回-1。也可以传入一个索引,代表是从哪个索引位置开始寻找,调用String类中的indexOf方法。

String str = "abbabcacc";
int index = str.indexOf("abc");
System.out.println(index);
//打印结果为3

3.从尾处开始寻找,查看主串中有无传入的子串,若有则返回索引值,没有则返回-1。调用String类的lastIndexOf,并且也可以传入索引代表从哪个索引值从尾处寻找到头处。调用String类的lastIndexOf
代码1:

String str = "abbabcacc";
int index = str.lastIndexOf("ac");
System.out.println(index);
//打印结果为6

当我们要找的子串刚好被“切断”时,它仍然会取到后面的字符返回子串开始的索引值,但是后面的字符的索引值不能取到。

代码2:

String str = "abbabcacc";
int index = str.lastIndexOf("ac",6);
System.out.println(index);
//打印结果为6

4.判断一个字符串是否以指定子串开头,调用String类中的startsWith方法。也可以传入索引值说明从指定位置开始判断是否以指定子串开头。

String str = "abbabcacc";
boolean flg = str.startsWith("abb");
System.out.println(flg);
//打印结果为true
String str = "abbabcacc";
boolean flg = str.startsWith("abb",3);
System.out.println(flg);
//打印结果为false

5.判断一个字符串是否以指定子串结尾。调用String类当中的endsWith方法。

String str = "abbabcacc";
boolean flg = str.endsWith("acc");
System.out.println(flg);
//打印结果为true

五、字符串替换

1.替换字符串中的所有的指定内容。调用String类当中的repalceAll方法。

String str = "helloworld" ; 
System.out.println(str.replaceAll("l", "_"));
//打印结果为he__owor_d

也可以选择替换字符串中的首个内容。调用String类中的repalceFirst方法。

System.out.println(str.replaceFirst("l", "_"));
//打印结果为he_loworld

由于字符串是不可变对象, 替换不修改当前字符串, 而是产生一个新的字符串。

六、字符串拆分

指定字符串在主串的基础上能分为几个组就等于分为几个String类数组。因此可以通过foreach循环来遍历拆分后的数组的内容。调用String类的split方法。

String str = "hello world hello bit" ; 
String[] result = str.split(" ") ; // 按照空格拆分
for(String s: result) { 
 System.out.println(s); 
}
//打印结果为

hello
world
hello
bit

split方法还能够传入一个limit参数,代表拆分后最多分为几个数组。如果拆分后数组的个数小于这个limit值则按原来拆分的数组的个数拆分,否则数组的个数不能够超过limit值

String str = "hello world hello bit" ; 
String[] result = str.split(" ",2) ; 
for(String s: result) { 
 System.out.println(s); 
}
//打印结果为

hello
world hello bit

当然,对于字符串的拆分可以嵌套拆分,即先拆分为两部分,再根据另一个字符串再拆分。

String str = "name=zhangsan&age=18";
String[] strings = str.split("&");
for (String s:strings) {
 String[] ss = s.split("=");
 for (String s1:ss) {
   System.out.println(s1);
 }
}
//打印结果为

name
zhangsan
age
18

对于字符串的拆分还有几种特殊情况,当遇到需要拆分的为转义字符时,传入指定的字符串则需要传多两个斜杠。例如:

String str = "192.168.1.1";
String[] strings = str.split("\\.");
for (String s:strings) {
     System.out.println(s);
}
//打印结果为

192
168
1
1

因此需要注意的是:字符"|","*","+“都得加上转义字符,前面加上”\\"。

对于字符串的拆分还可以根据多个指定的字符串进行拆分,指定的字符串之间用‘|'分隔。

String str = "Java30 12&21#hello";
String[] strings = str.split(" |&|#");
for (String s:strings) {
     System.out.println(s);
}
//打印结果为

Java30
12
21
hello

七、字符串截取

对于一个字符串的截取,传入一个索引值代表是从哪个索引开始截取。传入两个索引值则代表截取的范围。调用String类中的substring方法。例如:

String str = "helloworld" ;
System.out.println(str.substring(5));
System.out.println(str.substring(0, 5));
//打印结果为

world
hello

注意:

对于以上字符串操作的方法,我们可以查看其原码能够更好地了解该方法是如何进行操作的。

八、String类中其它的常用方法

1.String类的trim方法。这个方法是用来去掉字符串中左右两边空格,而字符串中间的空格是不会去掉的。
代码:

String str = "  abc  def  ";
String s = str.trim();
System.out.println(s);
//打印结果为

abc def

2.String类中的toUpperCase和toLowerCase方法。toUpperCase是用来将字符串中的小写字母转变为大写字母,而不是字母的不进行处理。toLowerCase方法是用来将字符串中的大写字母转变为写写字母,而不是字母的也不进行处理。

String str = " hello%$$%@#$%world 哈哈哈 " ; 
System.out.println(str.toUpperCase()); 
System.out.println(str.toLowerCase());
//打印结果为:

HELLO%$$%@#$%WORLD 哈哈哈
hello%$$%@#$%world 哈哈哈

3.String类中的concat方法。这个方法是用来连接字符串的,相当于字符串中的拼接,但是连接后的字符串不会入到字符串常量池当中。这里不再演示。

4.String类中的length方法。它是用来求字符串长度的,跟数组不一样,数组中的length是数组的属性,而String中的length是一个方法。
代码:

String str = "abcd";
System.out.println(str.length());
//打印结果为4

5.String类中的isEmpty方法。是用来判断字符串是否为空的。
代码:

System.out.println("hello".isEmpty());//false
System.out.println("".isEmpty());//true
System.out.println(new String().isEmpty());//true

九、StringBuffer 和 StringBuilder

对上面String字符串常量池有了了解后,我们知道了String是常量,是不可变的。当拼接时,Java会在编译期间将String类的对象拼接优化为StringBuffer的拼接(不会产生新对象),因此Java中有StringBuffer和StringBuilder中处理字符串,并且它们拼接时不会产生新的对象,而是在原来的字符串基础上拼接。后面我们再将StringBuilder和StringBuffer的区别。
StringBuilder中有一个append方法可以将字符串在原来的基础上拼接。例如当我们有这样的代码时:

String str = "abc";
for (int i = 0; i < 10; i++) {
     str+=i;
}
System.out.println(str);
//打印结果:

abc0123456789

会在常量池中产生很多的临时变量,例abc0会在字符串常量池中产生,abc01又会在字符串常量池中产生等等。如果我们用到StringBuilder的append方法时,可以写为(两种写法最后的结果是相同的,只是StringBuilder处理时不会在字符串常量池中产生临时变量):

StringBuilder stringBuilder = new StringBuilder("abc");
for (int i = 0; i < 10; i++) {
     stringBuilder.append(i);
}
System.out.println(stringBuilder);
//打印结果:

abc0123456789

打印时为什么能够打印StringBuilde类型是因为StringBuilder中重写了父类的toString方法,它能够把StringBuilder类型转变为String类型进行打印。

append方法也可以连着使用。代码如下:

public static void main(String[] args) { 
StringBuffer sb = new StringBuffer(); 
sb.append("Hello").append("World"); 
fun(sb); 
System.out.println(sb); 
}
//打印结果为

HelloWorld

因此:
String变为StringBuffer:利用StringBuffer的构造方法或append()方法。
StringBuffer变为String:调用toString()方法。

1.StringBuilder与StringBuffer的区别

StringBuilder与StringBuffer中的方法都是大致相同的。它们的主要区别就是StringBuilder主要是用于单线程的,而StringBuffer主要是用于多线程的。我们点入StringBuffer类当中按住ctrl+7选择append方法看到如图所示的synchronized英文,则代表是多线程使用的,而StringBuilder类中没有。

在这里插入图片描述

结论:

2.StringBuilder与StringBuffer常用的方法

StringBuilder与StringBuffer常用的方法一般String类当中都是没有的,例如:append方法、delete方法、reserve方法、insert方法等。

reverse方法:

StringBuffer sb = new StringBuffer("helloworld"); 
System.out.println(sb.reverse());
//打印结果为

dlrowolleh

delete方法:

StringBuffer sb = new StringBuffer("helloworld"); 
System.out.println(sb.delete(5, 10));
//打印结果为

hello

insert方法:

StringBuffer sb = new StringBuffer("helloworld"); 
System.out.println(sb.delete(5, 10).insert(0, "你好"));
//打印结果为:

你好hello

十、对字符串引用的理解

许多人认为,如果传一个引用到一个函数中指向了另一个对象,就能够解决“所有问题”,其实不然,下面这个例子能够说明问题。
代码:

    public static void func(String str1,char[] chars) {
    str1="hello";
    chars[0]='g';
    }
    public static void main(String[] args) {
        String str = "abcd";
        char[] chars = {'h','e','l','l','o'};
        func(str,chars);
        System.out.println(str);
        System.out.println(Arrays.toString(chars));
    }
    //打印结果为 

abcd
[g, e, l,