String、StringBUffer、StringBUilder
String类
public final class String
extends Object
implements Serializable, Comparable, CharSequence
String的不可变性
private final char value[];//String底层是char数组
public class Test {
public static void main(String[] args) {
String str1 = "Hello";//字面量的定义,类似基本变量赋值
String str2 = "hiWorld";
str1 = "hi";//重新赋值
System.out.println(str1);
//以上面这种方式赋值都会保存到常量池中,而常量池当中不会存
//相同的两个字符串的,他会先去找常量池里面有没有,有的话就会去复用了
String str3 = "hi";
str3 += "World";
//这里str3是hi,与str1是相同的,他们是共用一块空间,但是str3进行了拼接
System.out.println(str1);//输出str1,结果还是hi,说明并没有在原来空间上进行修改,故是str3是新开辟内存空间
System.out.println(str3);//输出hiWorld,也是在常量池中开辟了一个新的空间,并非在原有空间上进行修改
System.out.println("*********************");
System.out.println(str2 == str3);//因为进行了拼接操作,所以开辟了新的内存空间,所以返回是false
System.out.println("*********************");
String str4 = "abc";
//replace并没有改变char数组的长度,只是改变了其中一个字符的值,即使这样,也要必须重新开辟内存空间
String str5 = str4.replace('a', 'c');
System.out.println(str4);//abc
System.out.println(str5);//cbc
System.out.println(str5==str4);//false
}
}
不可变性
1、当对字符串进行重新赋值时,要重新开辟一块内存空间,不能再原有的字符串上进行修改。(一个char数组hello是5,一个char数组hi是2,不能直接修改原有的长度为五的char数组,因为是final修饰的,需要重新开辟一块空间去存储)
2、当对现有的字符串进行连接操作时,也需要重新制定内存区进行复制,不能对原有的char数组进行修改。
3、当调用String的replace()方法修改制定的字符或字符串时,也必须重新开辟内存空间,不能对原有的char数组进行修改。
String对象创建的几种方式
String str = “hello”;
String str1 = new String(); // 相当于一个this.value = new char[0];
String str2 = new String(String original); //相当于一个this.value = new value[original.length];
String str3 = new String(char[] a); //相当于this.value = Arrays.copyOf(value,value.length)全部复制
String str4 = new String(char[] a,int startIndex,int count);//部分复制,从index开始到index后面count个
String str1 = “abc”; 与String str2 = new String(“abc”);的区别?
package com.api.string;
public class StringTest2 {
public static void main(String[] args) {
//通过字面量的方式定义:此时s1和s2的数据声明在字符串常量池中
String s1 = "Hello";
String s2 = "Hello";
//通过构造方法来赋值,此时s3和s4保存的地址值是数据开辟在堆空间中对应的地址值
String s3 = new String("Hello");
String s4 = new String("Hello");
//构造方法赋值地址指向的是开辟在堆空间中的char数组的地址,
//而char数组里面保存的又是常量池中的对应数据的地址,所以其实用的还是常量池中的数据
System.out.println(s1 == s2);//true
System.out.println(s1 == s3);//false
System.out.println(s1 == s4);//false
System.out.println(s3 == s4);//false
User user1 = new User("Jack", 12);
User user2 = new User("Jack", 12);
User user3 = new User(new String("Rose"), 12);
User user4 = new User(new String("Rose"), 12);
System.out.println(user1 == user2); //false
System.out.println(user1.equals(user2)); //false
System.out.println(user1.name == user2.name); //true ,在User构造方法中采用的是字面量方式去赋值的
System.out.println(user3.name == user4.name); //false, 使用构造方法则会在堆内存中开辟两个内存空间
}
}
String str = new String(“Hello”);这种方式创建对象,在内存中创建了几个对象?
两个。 一个是在堆空间开辟的char数组,另一个是堆空间中char数组对应的常量池中的数据"Hello"。当然了,如果前面已经声明过"Hello",那么直接使用常量池中已有的即可。
String的拼接
public class StringTest {
public static void main(String[] args) {
String s1 = "Java";
String s2 = "EE";
String s3 = "JavaEE";
String s4 = "Java" + "EE"; //通过加号进行字面量连接,与s3是相等的,还是在常量池中找
String s5 = "s1" + "EE";//以下几种都是变量加字面量或者变量加变量
String s6 = "Java" + s2;
String s7 = s1 + s2;
s6.intern();
System.out.println(s3==s6);//false
System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//false
System.out.println(s3 == s7);//false
System.out.println(s4 == s7);//false
System.out.println(s5 == s6);//false
System.out.println(s5 == s7);//false
}
}
结论:
常量与常量的拼接结果在常量池中,且常量池中不会存放相同内容的常量
只要拼接中有一个是变量,那么结果就在堆中,相当于new了
当然也可以对结果调用intern()方法,进行手动入池,返回值就在常量池中
如果用final修饰的变量名加上字面量,则结果还是在常量池当中,因为final修饰的变量则也是常量。
package com.api.string;
public class StringTest3 {
public static void main(String[] args) {
String s1 = "HelloWorld";
String s2 = "Hello";
String s3 = s2 + "World";
System.out.println(s1 == s3);
final String s4 = "Hello"; //final修饰的也是常量,也是字面量,入池的
String s5 = s4 + "World";//相当于两个字面量相加,结果在常量池中
System.out.println(s1 == s5);//true
}
}
String类与基本数据类型与包装类之间的转换
String ----->基本数据类型、包装类:调用包装类的静态方法:parseXxx(str);
基本数据类型、包装类----->String:调用String重载的valueOf()方法,连接符也可以。
String和字符数组的转换
String---->char[];调用String的toCharArray()方法
char[]数组---->String:调用String 的构造器即可
String和字节数组之间的转换
String---->byte[] :调用getBytes()方法,可以在方法中指定字符集,无参则调用默认的字符集来编码。中文UTF-8占三个字节,GBK占2个字节
byte[]---->String:调用String构造方法
两个转换之间字符集一定要保持一致,不然会乱码。
String、StringBuffer、StringBuilder三者的异同
String:不可变的字符序列,底层使用char[]存储
StringBuffer:可变的字符序列,JDK1.0的时候就有了,效率低,线程安全,底层是继承自AbstractStringBuffer的char[],和String一样,但是没有final修饰,是可变的
StringBuilder:可变的字符序列,JDK1.5新增的,效率高,线程不安全,底层是继承自AbstractStringBuffer的char[],和String一样,但是没有final修饰,是可变的
这三个底层都是char[]存储,但是后两个是可变的,长度不是固定的
底层源码分析:
三者实例化底层实现:
String str = new String();//char[] value = new char[0]
String str1 = new String("abc");//char[] value = new char[]{'a','b','c'}
StringBuffer sb1 = new StringBuffer();//char[] value = new char[16]
//在StringBuffer空参调用了父类的方法,底层默认创建了一个长度是16的数组
public StringBuffer() {
super(16);
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
StringBuffer的构造方法
/*
无论怎样添加,变得都是底层数组的长度,所以是可变的
而String底层的数组是死的,长度要想变必须重新创建
*/
StringBuffer sb1 = new StringBuffer();//空参
sb1.append("a");//相当于底层char数组 value[0] = 'a';
sb1.append("b");//相当于底层char数组 value[1] = 'b';
StringBuffer sb2 = new StringBuffer("abc");//有参
//底层相当于 char[] value = new char["abc".length() + 16]
public StringBuffer(String str) {
super(str.length() + 16);// 在默认长度16的基础上加上参数的char数组长度
append(str);
}
问题1:为什么输出length是3而不是19
StringBuffer sb1 = new StringBuffer("abc");
sb1.setCharAt(0,'m');
System.out.println(sb1);
System.out.println(sb1.length());//3,并不是16+3=19
//以下是StringBuffer的length()方法的源码
public synchronized int length() {
return count;//返回的是count!而不是value.length,count表示的是当前value数组中元素的个数
}
问题2:如果添加的数据底层数组装不下了,如何扩容?
首先来看StringBuffer的append()方法,发现调用的是父类的append()方法。请看源码。
public AbstractStringBuilder append(String str) {
if (str == null) //判断是否为空,为空直接返回null
return appendNull();
int len = str.length();//获取要添加数组的长度
ensureCapacityInternal(count + len);//判断是否需要扩容
str.getChars(0, len, value, count);
count += len;
return this;
}
这里我们假设:
StringBuffer sb = new String();
sb.append(“a”);
…
sb.append(“abc”);
在sb.append(“abc”);之前假设我们已经在底层默认长度为16的char数组中添加了15个字符了,接下来要添加"abc",很显然,16已经装不下了。ensureCapacityInternal(count + len); 参数传进去的就是15+3=18,请接着往下看。
每次添加之前都要调用一次ensureCapacityInternal(count + len); 方法,判断是否大于16,如果不大于,则直接添加。否则,请看源码。
private void ensureCapacityInternal(int minimumCapacity) {//18传入
// overflow-conscious code
if (minimumCapacity - value.length > 0) { // 18 > 16 18是大于底层默认数组长度的
value = Arrays.copyOf(value, // 进行数组拷贝返回新的char数组
newCapacity(minimumCapacity));//参数二调用
//了newCapacity(minimumCapacity)方法
}
}
接着调用newCapacity(minimumCapacity)方法;
private int newCapacity(int minCapacity) { //传入18
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;//newCapacity等于原有数组长度16*2+2
if (newCapacity - minCapacity < 0) {//如果扩容后还不够,那直接newCapacity直接就等于传进来的长度
newCapacity = minCapacity;
}
//如果newCapacity不小于等于0并且不大于Integer.MAX_VALUE - 8,返回扩容后的值
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
所以当添加的数据底层数组装不下了,扩容至默认数组长度两倍+2,如果添加的字符串超出这个长度,则直接返回原本字符串长度加上需要添加的字符串长度,然后进行数组拷贝,返回新数组。
扩容举例
public static void main(String[] args) {
StringBuffer sb1 = new StringBuffer();
sb1.append("abcdefghijklmno");
System.out.println(sb1.length());//15
System.out.println(sb1.capacity());//16
sb1.append("aa");
System.out.println(sb1.length());//17
System.out.println(sb1.capacity());//34
}
public static void main(String[] args) {
StringBuffer sb1 = new StringBuffer();
sb1.append("abcdefghijklmnopabcdefghijklmnopaa");
System.out.println(sb1.length());//34
System.out.println(sb1.capacity());//34
sb1.append("aa");
System.out.println(sb1.length());//36
System.out.println(sb1.capacity());//70
}
StringBuffer和StringBuilder基本一致,前者使用的都是同步方法,而后者没有,其他基本相同。
如果在开发当中拼接十分频繁,不要使用String,优先使用StringBuffer和StringBuilder
在开发当中尽量使用代参的构造器:StringBuffered(int capacity) 这样指定容量。能够避免扩容,避免复制,也会提高性能。