文章导览
String类
String 类代表字符串。Java 程序中的所有字符串字面值(如 “abc” )都作为此类的实例实现。字符串是常量;它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享
一、String的创建
- 创建字符串的2(3+1)种方式
1、public String():创建一个空白字符串,不含有任何内容
2、public String(char[] array):根据字符数组的内容,来创建对应的字符串
3、public String(byte[] array):根据字节数组的内容,来创建对应的字符串
4、String str = “”;
public class Demo01String {
public static void main(String[] args) {
//使用空参构造
String str1 = new String();//小括号留空,说明字符串什么内容都没有
System.out.println("第一个字符串:" + str1);
//根据字符数组创建字符串
char[] charArray = {'A', 'B', 'C'};
String str2 = new String(charArray);
System.out.println("第二个字符串:" + str2);
//根据字节数组来创建字符串
byte[] byteArray = {97, 98, 99};
String str3 = new String(byteArray);
System.out.println("第三个字符串:" + str3);
//直接创建
String str4 = "Hello";
System.out.println("第四个字符串:" + str4);
}
}
- 2种创建方式的区别
直接赋值的方式创建对象是在方法区的常量池当中
String str4 = "Hello";
通过构造方法创建字符串的对象是在堆内存
String str1 = new String("Hello");
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String("hello");
String str3 = str2;
String str4 = "hello";
/*
String类对象==比较的是地址而不是内容
*/
System.out.println(str1 == str2);//false
System.out.println(str1 == str3);//false
System.out.println(str3 == str2);//true
System.out.println(str1 == str4);//true
}
总结:两种创建方式的区别:
1、直接赋值:会开辟一块堆内存空间,并且自动入池(常量池),不会产生垃圾。
2、构造方法赋值:会开辟两块堆内存空间,其中一块堆内存会变成垃圾被系统回收,而且不能够自动入池,需要通过String类下的方法intren()方法手动入池。(在开发过程中不会采用这种方式进行字符串的创建)
二、String类的常用方法
- 与获取相关的方法
public int length() 获取字符串当中含有的字符个数,拿到字符串长度
public String concat(String str) 将当前字符串和参数字符串拼接返回新的字符串
public char charAt() 获取指定索引位置的单个字符
public int indexOf(String str) 查找参数字符串在本字符串当中首次出现的索引位置,如果没有返回-1
public static void main(String[] args) {
//获取字符串的长度
int length = "internationalization".length();
System.out.println("字符串的长度:" + length);
System.out.println("=====================");
//拼接字符串
String str1 = "Hello";
String str2 = "World";
String str3 = str1.concat(str2);
System.out.println(str3); //HelloWorld
System.out.println("=====================");
//获取指定索引位置的单个字符
System.out.println(str3.charAt(5)); //W
System.out.println("=====================");
//查找参数字符串在本字符串当中首次出现的索引位置
System.out.println(str3.indexOf("llo")); //2
System.out.println(str3.indexOf("abc")); //-1
}
- 与截取相关的方法
String substring(int beginIndex)
返回一个新的字符串,它是此字符串的一个子字符串。
String substring(int beginIndex, int endIndex)
返回一个新字符串,它是此字符串的一个子字符串[beginIndex,endIndex)
public static void main(String[] args) {
String str1 = "HelloWorld";
String str2 = str1.substring(5);
System.out.println(str1); //HelloWorld
System.out.println(str2); //World
System.out.println("========================");
String str3 = str1.substring(4, 7);
System.out.println(str3); //oWo
//下面这种写法,字符串的内容仍然是没有改变的
//strA种保存的是地址值 本来地址值为0x666 后来变成了0x999
String strA = "Hello";
System.out.println(strA);
strA = "Java";
System.out.println(strA);
}
- 与转换相关的方法
char[] toCharArray() 将此字符串转换为一个新的字符数组。
byte[] getBytes() 使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中
String replace(CharSequence target, CharSequence replacement) 使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
备注:CharSequences意思就是说可以接受字符串类型
public static void main(String[] args) {
char[] chars = "Hello".toCharArray();
System.out.println(chars[0]); //H
System.out.println(chars.length); //5
System.out.println("===============");
byte[] bytes = "abc".getBytes();
for (int i = 0; i < bytes.length; i++) {
System.out.println(bytes[i]);
}
System.out.println("================");
String str1 = "How do you do?";
String str2 = str1.replace("o", "*");
System.out.println(str1); //How do you do?
System.out.println(str2); //H*w d* y*u d*?
}
- 与分割相关的方法
String[] split(String regex)
根据给定正则表达式的匹配拆分此字符串。
注意事项:
split方法的参数其实是一个正则表达式
. 必须写成 \.
public static void main(String[] args) {
String str1 = "aaa,bbb,ccc";
String[] split = str1.split(",");
System.out.println(split[1]);
System.out.println("================");
String str2 = "XXX.YYY.ZZZ";
//System.out.println(str2.split("."));
String[] array = str2.split("\\.");
System.out.println(array.length); //0
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
- ==与equals
==是进行对象的地址值比较,如果确实需要字符串的内容比较,可以使用两个方法:
1、boolean equals(Object anObject) 将此字符串与指定的对象比较
注意事项:
1、任何对象都能用Object接收
2、equals方法具有对称性,也就是a.equals(b)和b.equals(a)效果一样
3、如果比较双方一个常量一个变量,推荐写法:“abc”.equals(str1);
如果字符串str是null,那么str.equals(“abc”)会出现空指针异常。
三、String的不可变性
Strings are constant; their values cannot be changed after theyare created. 这是官方文档的一句注释
在Java8中,Strng内部使用了char数组存储数据。value数组被声明为final,这意味着value数组初始化之后不能再引用其他数组了。并且String内部也没有改变value数组的方法,因此可以保证String不变。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
不可变带来了那些好处:
1、可以缓存hash值: 因为String的hash值经常被用到,如果String被用作HashMap的Key。不可变的特性可以使得hash值也不可变,因此只需要进行一次计算。
2、字符串常量池的需要: 如果一个String对象已经被创建过了,那么就可以从字符串常量池中取得引用,只有String不可变,才能使用字符串常量池。
3、线程安全: String不可变性天生具备线程安全,可以在多个线程中安全地使用。
4、安全性: String不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果String可变,那么在网络连接过程中,String被改变,改变的String哪一方以为现在连接的是其他主机,而实际情况却不是。
四、字符串常量池
-
常量池表(Constant_Pool table)
Class文件中存储所有常量(包括字符串)的table。这是Class文件中的内容,还不是运行时的内容,不要理解它是一个池子,其实就是Class文件中的字节码指令。 -
运行时常量池(Runtime Constant Pool)
JVM内存中方法区的一部分,这是运行时的内容
这部分内容(绝大部分,除了Class中常量池内容,还包括动态生成并加入这里的内容)是随着JVM运行的时候,从常量池转化而来,每个Class对应一个运行时常量池。 -
字符串常量池(String Pool)
这部分也在方法区当中,但是与Runtime Constant Pool不是一个概念,String Pool是JMV实例全局共享的,全局只有一个。
JVM规范:要求进入这里的String实例叫“被驻留的interned string”。各个JVM可以用不同的实现。HotSpot只是设置了一个哈希表StringTable来引用堆中的字符串实例,被引用就是被驻留。
关于String Pool
String Pool中保存着所有字符串字面量(lieral strings),这些字面量在编译时期就确定。不仅如此,还可以使用String类中的方法intern()在运行过程中将字符串添加到String Pool中。
当一个字符串调用intern()方法时,如果String Pool中已经存在了一个字符串和该字符串相等(使用equals方法进行确定),那么就会返回String Pool中字符串的引用;否则,就会在String Pool中添加一个新的字符串,并返回这个字符串的引用。
在Java7之前,String Pool被放在运行时常量池中,它属于永久代。而在Java7,String Pool被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
五、String、StringBuilder、StringBuffer
1、可变性
- String不可变
- StringBuilder&StringBuffer可变
2、线程安全
- String不可变,线程安全
- StringBuilder不是线程安全的
- StringBuffer是线程安全的,内部使用了synchronized进行同步
六、"+"连接符
- "+“连接符的实现原理
Java语言为“+”连接符以及对象转换为字符提供了特殊的支持,字符串对象可以使用”+"连接其他对象。其中字符串连接是通过StringBuilder(或StringBuffer)类及其append方法实现的,对象转换为字符串是通过toString方法实现的,该方法由Object类定义,并且可以被Java的所有类继承。
反编译验证:
/**
* 测试代码
*/
public class Test {
public static void main(String[] args) {
int i = 10;
String s = "abc";
System.out.println(s + i);
}
}
/**
* 反编译后
*/
public class Test {
public static void main(String args[]) { //删除了默认构造函数和字节码
byte byte0 = 10;
String s = "abc";
System.out.println((new StringBuilder()).append(s).append(byte0).toString());
}
}
由上可以看出,Java中使用"+"连接字符串对象时,会创建一个StringBuilder()对象,并调用append()方法将数据拼接,最后调用toString()方法返回拼接好的字符串。由于append()方法的各种重载形式会调用String.valueOf方法,所以我们可以认为:
//以下两者是等价的
s = i + ""
s = String.valueOf(i);
//以下两者也是等价的
s = "abc" + i;
s = new StringBuilder("abc").append(i).toString();
- "+“连接符的效率
使用”+"时,JVM会隐式创建StringBuilder对象,这种方式在大部分情况下并不会造成效率的损失,不过在大量循环拼接字符串时则注意:肯定会造成效率的损失(大量的StringBuilder对象在堆内存创建),此时应该在循环外边创建一个StringBuilder对象,然后调用append方法手动拼接。
/**
* 循环中使用StringBuilder代替“+”连接符
*/
StringBuilder sb = new StringBuilder("abc");
for (int i = 0; i < 1000; i++) {
sb.append("abc");
}
sb.toString();
如果当"+"两端均为编译器确定的字符串常量时,编译器会进行相应的优化,直接将两个字符串常量拼接好。
System.out.println("Hello" + "World");
/**
* 反编译后
*/
System.out.println("HelloWorld");
/**
* 编译期确定
* 对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。
* 所以此时的"a" + s1和"a" + "b"效果是一样的。故结果为true。
*/
String s0 = "ab";
final String s1 = "b";
String s2 = "a" + s1;
System.out.println((s0 == s2)); //result = true
/**
* 编译期无法确定
* 这里面虽然将s1用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定
* 因此s0和s2指向的不是同一个对象,故上面程序的结果为false。
*/
String s0 = "ab";
final String s1 = getS1();
String s2 = "a" + s1;
System.out.println((s0 == s2)); //result = false
public String getS1() {
return "b";
}
综上,“+”连接符对于直接相加的字符串常量效率很高,因为在编译期间便确定了它的值,也就是说形如"I"+“love”+“java”; 的字符串相加,在编译期间便被优化成了"Ilovejava"。对于间接相加(即包含字符串引用,且编译期无法确定值的),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。