一、String
(1)如何获取String对象?
1、直接赋值:String s = “123”;
2、使用new关键字结合不同的构造方法创建。
构造方法:
public String() | 创建一个空字符串对象 |
public String(String original) | 创建一个值为original的字符串对象 |
public String(char[] value) | 创建一个值为value的字符串对象 |
public String(byte[] value) | 创建一个值为value的字符串对象 |
关于空字符串:
类似于空集的概念,空有一个集合但里面什么都没装。
代码例子:
//直接赋值
String s1 = "abc";
//使用new关键字
//1.空参的构造方法
String s2 = new String();
//2.传字符串
String s3 = new String("abc");
//3.传字符数组
char[] value1 = {'a', 'b', 'c'};
String s4 = new String(value1);
//4.传byte数组
byte[] value2 = {97, 98, 99};
String s5 = new String(value2);
(2)String类中的方法
由于字符串对象一旦创建便不可再改变,所以下面这些方法都是返回一个新的字符串。
1、拼接:+,例如下面这个代码,
int num = 1;
System.out.println("这个数字是" + num);
在输出语句中经常将字符串和变量使用+进行拼接形成一个新的字符串。
2、比较:s1.equals(s2),比较s1和s2中存的内容是否相等。
3、截取:substring(6, 14),用来截取字符串,包左不包右。
4、替换:replace("TMD", "***"),对子字符串进行替换。
5、indexOf("world"):返回指定子字符串在原字符串第一次出现位置的索引。
(3)应用场景
二、StringBuilder
相当于一个容器,里面的内容是可变的。
1、如何创建一个StringBuilder对象?
new一个。
构造方法:
public StringBuilder() | 创建一个StringBuilder对象 |
public StringBuilder(String str) | 创建一个StringBuilder对象并将str添加到容器中 |
2、StringBuilder中的方法
(1)append(int/String/...):重载方法,添加数据,可以是任意数据类型;
(2)reverse():反转容器中的数据;
(3)toString():将StringBuilder对象转换为String对象;
(4)length():返回字符串的长度。
关于append方法的重载:
下面是具体代码:
//创建一个StringBuilder对象
StringBuilder sb = new StringBuilder();
//添加数据
sb.append(10).append('2');
//反转
sb.reverse();
//长度
int length = sb.length();
//toString:转换成String对象
String string = sb.toString();
3、应用场景
当字符串需要拼接和反转时用StringBuilder比较方便。
链式编程:
sb.append("aaa").append("bbb").toString().length();
三、StringJoiner
1、如何创建一个StringJoiner对象?
new一个。
构造方法:
public StringJoiner(s1) | 创建一个StringJoiner对象并指定分隔符为s1 |
public StringJoiner(String str) | 创建一个StringJoiner对象并指定分隔符为s1,以及字符串的开始符号为s2和结束符号为s3 |
2、StringJoiner中的方法
(1)add(int/String/...):添加数据;
(2)toString():将StringBuilder对象转换为String对象;
(3)length():返回字符串的长度。
3、应用场景
每次添加需要分隔符以及字符串的开始和结束需要符号时。
问题1:Java编译器在遇到字符串字面量时是怎么做的?
1、首先去字符串常量池里查看是否有这个字符串对象;
2、①如果有,直接将其引用赋给变量;
②如果没有,会自动在字符串常量池中创建一个包含其值的字符串对象,再将引用赋给变量。
问题2:Java在使用new关键字创建String对象时做了什么?
下面以这个String s = new String("abc");来解释:
1、首先检查字符串字面量abc在字符串常量池中是否存在;
2、①如果存在,则在堆中new一个字符串对象,存的值为abc;
②如果不存在,先在字符串常量池中创建一个包含其值的字符串对象,然后再在堆中new一个字符串对象,存的值为abc。
问题3:为什么String类的对象一旦创建便不可再变?
final修饰的变量
(1)基本数据类型:其值不可再变化;
(2)引用数据类型:指向的地址不会再变化,但地址中存的内容是可能会被改变的。
下面介绍一下JDK8及以前的String类源码:
private final char value[];
可以看到定义了一个字符数组value,用来存字符串。修饰符为final,value所记录的地址值不会再发生变化,private修饰符使外界无法访问到value所记录的地址值,而且在本类中也没有提供相应的方法去访问value所记录的地址值和修改value所指向的地址中存储的数据。
问题4:为什么String类中的方法都返回一个新的字符串?
下面看一下substring方法的源码,可以看到new String,所以是新创建了一个字符串。
public String substring(int beginIndex) {
// 检查起始索引是否小于 0,如果是,则抛出 StringIndexOutOfBoundsException 异常
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
// 计算子字符串的长度
int subLen = value.length - beginIndex;
// 检查子字符串长度是否为负数,如果是,则抛出 StringIndexOutOfBoundsException 异常
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 如果起始索引为 0,则返回原字符串;否则,创建并返回新的字符串
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
问题5:使用+拼接字符串的原理
分为两种情况:
(1)字面量拼接:
String s = "a" + "b";
上面这行代码经过编译生成class文件,在还没有运行的时候,通过字符串优化机制s就已经是拼接之后的结果"ab"了,即
String s = "ab";
所以这时候就会应用问题1中的步骤了,在字符串常量池中进行查找,有则复用,没有则创建。
(2)有变量参与
如下面这行代码:
String s1 = "hello";
String result = s1 + "world";
由于Java是一门解释型语言,在拼接时会变为下面这行代码:
new StringBuilder().append(s1).append("world").toString()
下面来介绍一下StringBuilder中的toString()方法:
public String toString() {
return new String(value, 0, count);
}
可以看到new了一个新的String对象,所以每次使用+进行字符串拼接时会创建两个对象,分别是StringBuilder和String对象。当拼接次数过多时资源浪费比较严重,所以可以使用StringBuilder进行拼接比较好。
问题6:StringBuilder底层原理
1、首先在使用空参构造函数new一个StringBuilder对象时就创建了一个容量为16的数组;
public StringBuilder() {
super(16);
}
调用了AbstractStringBuilder类中带有一个参数的构造函数,可以看到创建一个长度为16的数组。
//定义一个数组,存放byte型数据
byte[] value;
//AbstractStringBuilder类带有一个参数的构造方法
AbstractStringBuilder(int capacity) {
if (COMPACT_STRINGS) {
//创建一个长度为16的数组
value = new byte[capacity];
coder = LATIN1;
} else {
value = StringUTF16.newBytesFor(capacity);
coder = UTF16;
}
}
2、当添加数据时,来看一下append方法:
public StringBuilder append(String str) {
super.append(str);
return this;
}
实际上是调用了AbstractStringBuilder类中的append方法,len表示要添加的字符串的长度,count表示value数组中已经存的字符个数,然后将count + len的值传入ensureCapacityInternal 方法。
public AbstractStringBuilder append(String str) {
if (str == null) {
return appendNull();
}
int len = str.length();
ensureCapacityInternal(count + len);
putStringAt(count, str);
count += len;
return this;
}
先来介绍一下容量和长度:
①容量:最多可以存多少;
②长度:已经存了多少。
下面来看一下ensureCapacityInternal方法:
关注两个参数:minimumCapacity和oldCapacity。
minimumCapacity表示添加所需的最小容量,和数组的长度oldCapacity进行判断,如果minimumCapacity大于oldCapacity,表示放不下需要扩容。
private void ensureCapacityInternal(int minimumCapacity) {
// oldCapacity表示没添加之前的容量
int oldCapacity = value.length >> coder;
//如果添加完之后的容量大于oldCapacity,则需要扩容
if (minimumCapacity - oldCapacity > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity) << coder);
}
}
下面来看一下是如何扩容的: oldLength 表示数组的长度,newLength 表示所需的最小容量,
growth 表示需要数组需要增加的长度。
private int newCapacity(int minCapacity) {
// oldLength 表示数组的长度
int oldLength = value.length;
// newLength 表示所需的最小容量
int newLength = minCapacity << coder;
// growth 表示需要增加的长度
int growth = newLength - oldLength;
int length = ArraysSupport.newLength(oldLength, growth, oldLength + (2 << coder));
if (length == Integer.MAX_VALUE) {
throw new OutOfMemoryError("Required length exceeds implementation limit");
}
return length >> coder;
}
比较growth和oldlength+2哪个大?
如果growth大于oldlength+2,则证明扩容的长度oldlength+2是不够的,此时以growth为准。
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
return prefLength;
} else {
// put code cold in a separate method
return hugeLength(oldLength, minGrowth);
}
}
综上所述底层原理为:
①在创建StringBuilder对象时会初始化一个长度为16的数组;
②当添加字符串时,如果用于存储的数组长度不够时会扩容,扩容之后的数组长度是2*length+2;
如果扩容后的数组依然放不下时则会以实际添加的字符串长度为准。