一、String的创建
创建字符串的方式有三种:
// 方式一
String str = "Hello world";
// 方式二
String str2 = new String("Hello world");
// 方式三
char[] array = {'a', 'b', 'c'};
String str3 = new String(array);
我们对第一和第二种创建字符串的方法都已经非常熟悉了,那至于为什么第三种能够传入一个字符数组变为字符串,我们可以按住ctrl键点入传入字符数组的String当中看其原码,我们能够发现此时是利用的方法是数组的拷贝,将字符数组的所有字符改为字符串形式。
二、了解字符串类型
按住Ctrl,点击String,进入String
根据上图,我们发现对于
字符串
来说,有两个属性,一个是char 类型的 value数组
(此时这个数组,只是一个变量【引用类型】,没有给这个数组,分配内存。也没有new)。一个是 哈希码(hash)
。
问题:字符串str,重新赋值会不会影响 str2的输出结果?
我们需要搞懂:字符串常量是不能被改变的
例如: String str = “abcd”; 通过引用 str 去将 字符串"abcd" 修改成 “gbcd”. 答案是做不到的,因为被双引号引起来的是字面值常量,常量是不能被修改的。 例题中,str = “author”; 这句代码是将str重新指向一个新的对象(修改str的指向),而不是将原来的字符串对象修改成author。
接下来再看一道例题:
import java.util.Arrays;
public class Test {
public static void func(String s,char[] array){
s = "author";
array[0] = 'p';
}
public static void main(String[] args) {
String str = "abcd";
char[] chars = {'y','o','u'};
func(str,chars);
System.out.println(str);
System.out.println(Arrays.toString(chars));
}
}
结论:
不是说 转引用 就能改变实参的值。 你要看,到底这个引用干了什么!
三、字符串比较相等(==,equals)
==比较的是对象的身份(比较两个引用中保存的地址是否相同/比较两个引用是否指向同一个对象)
而String的equals方法比较的是两个字符串的内容。但此时又有个疑问:为什么每个定义字符串常量的是一个引用呢?这样就牵扯到了字符串常量池。
字符串常量池
对于“池”这个概念,可能大家还是比较陌生的。比如数据连接池、线程池等等。那这些池的作用的干嘛的呢?是用来提高存储效率的。顾名思义字符串常量池是用来存储字符串常量的。字符串常量池中规定只要有了一个字符串常量就不再存储相同的字符串了。从JDK1.8开始字符串常量池是在堆里的。它本质上是一个哈希表(StringTable)是一个数组。存储字符串常量是,会根据一个映射关系进行存储,这个映射关系需要设计一个哈希函数。(因为字符串常量池是有关于JVM的,需要看其原码才能真正了解字符串常量池是如何操作的,此处不深究其原理也不会影响我们判断引用是否相同)。
字符串常量池中当存储一个字符串常量时会在根据哈希函数计算的某一个位置处产生一个结点,结点是由哈希值、String结点的地址、存储该数组位置处的下一个结点的地址组成的(这在JVM的原码中才能真正了解)。而每一个String结点是由字符型数组value与哈希值hash(默认为0)构成的 (下图所示)。点入String看其原码时就能够会发现这两个变量。此时观察到value数组被final修饰则说明该数组里的字符是不能够被改变的,这就是字符串是一个常量的原因,并且该字符串会转换为字符形式存放在字符数组当中。
1.举例一
public class Test {
public static void main(String[] args) {
String str = "abcef";
String str2 = str;
System.out.println(str);
System.out.println(str2);
}
}
结果:
画图解释:
2.举例二
String str1 = "hello";
String str2 = "hello";
System.out.println(str1==str2);
//打印结果为true
内存布局如下:
3.举例三
String str1 = "hello";
String str2 = new String("hello");
System.out.println(str1==str2);
//打印结果为false
内存布局如下:
4.举例四:
手动入池: 我们根据上图知道了代码二中的运行结果是false的,因为str2指向的是new String产生的String对象,而不是存储“hello”的String对象的地址。如果写为下面这个代码,结果会是如何呢?
String str2 = new String("hello").intern();// .intern()手动入池
String str1 = "hello";
System.out.println(str1==str2);
//运行结果为true
为什么最后的结果为true呢?
此时调用了String类当中的intern方法,称为手动入池,它能够将str2的指向不再指向new出来的String对象,而是指向了字符串常量池当中已经存储有“hello”字符数组的String对象。
5.举例五
String str1 = "hello";
String str2 = "he"+"llo";
// 注意:此时两个字符串都是常量,且在编译的时候就已经确定了是"hello"
// 简单来说像这种 直接拿两个字符串常量来拼接的,在编译时,就默认是拼接好了的,或者说 默认就是一个完整的字符串常量
System.out.println(str1==str2);
//打印结果为true
此代码有关字符串的拼接。其实“he”与“llo”在编译时期就已经编译为“hello”了。如果要看编译时期str2是什么字符串,则此时我们先点击Build选项,点入Build Project选项则进行编译(图1)。可以在该类文件的路径(含有.class文件)底下(图2+图3),按住shift键加右键点击powershell窗口,输入反编译指令javap -c
类名则能看到编译时str2是否是已经拼接好的hello。
6.举例六
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:
内存图:
equals( )
对于字符串比较,我们不能直接用“==”,而有三种方法能够对字符串有不同的比较方式。
比较字符串内容:直接调用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方法的实现
:
五、字符串查找
- 判断一个子串是否存在于主串中:调用String类的contains方法,返回值为boolean。
String str = "abbabcacc";
boolean flg = str.contains("abc");
System.out.println(flg);
//打印结果为true
- 从头开始查找一个子串,并返回第一个子串开始的索引位置,如果没有,则返回-1。也可以传入一个索引,代表是从哪个索引位置开始寻找,调用String类中的indexOf方法。
String str = "abbabcacc";
int index = str.indexOf("abc");
System.out.println(index);
//打印结果为3
- 从尾处开始寻找,查看主串中有无传入的子串,若有则返回索引值,没有则返回-1。调用String类的lastIndexOf,并且也可以传入索引代表从哪个索引值从尾处寻找到头处。调用String类的lastIndexOf 代码1:
String str = "abbabcacc";
int index = str.lastIndexOf("ac");
System.out.println(index);
//打印结果为6
当我们要找的子串刚好被“切断”时,它仍然会取到后面的字符返回子串开始的索引值,但是后面的字符的索引值不能取到。
String str = "abbabcacc";
int index = str.lastIndexOf("ac",6);
System.out.println(index);
//打印结果为6
- 判断一个字符串是否以指定子串开头,调用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
- 判断一个字符串是否以指定子串结尾。调用String类当中的endsWith方法。
String str = "abbabcacc";
boolean flg = str.endsWith("acc");
System.out.println(flg);
//打印结果为true
六、字符串替换
替换字符串中的所有的指定内容。调用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
注意:
索引从0开始 注意
前闭后开区间
的写法, substring(0, 5) 表示包含 0 号下标的字符, 不包含 5 号下标
对于以上字符串操作的方法,我们可以查看其原码能够更好地了解该方法是如何进行操作的。
九、String类中其它的常用方法
- String类的trim方法。这个方法是用来去掉字符串中左右两边空格,而字符串中间的空格是不会去掉的。 代码:
String str = " abc def ";
String s = str.trim();
System.out.println(s);
//打印结果为
abc def
- String类中的toUpperCase和toLowerCase方法。toUpperCase是用来将字符串中的小写字母转变为大写字母,而不是字母的不进行处理。toLowerCase方法是用来将字符串中的大写字母转变为写写字母,而不是字母的也不进行处理。
String str = " hello%$$%@#$%world 哈哈哈 " ;
System.out.println(str.toUpperCase());
System.out.println(str.toLowerCase());
打印结果为:
HELLO%$$%@#$%WORLD 哈哈哈
hello%$$%@#$%world 哈哈哈
-
String类中的concat方法。这个方法是用来连接字符串的,相当于字符串中的拼接,但是连接后的字符串不会入到字符串常量池当中。这里不再演示。
-
String类中的length方法。它是用来求字符串长度的,跟数组不一样,数组中的length是数组的属性,而String中的length是一个方法。 代码:
String str = "abcd";
System.out.println(str.length());
//打印结果为4
- 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类中没有。
结论:
- String的内容不可修改,StringBuffer与StringBuilder的内容可以修改. StringBuffer与StringBuilder大部分功能是相似的
StringBuffer采用同步处理,属于线程安全操作;而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