1、String 类
1.1 String 字符串
-
字符串是常量,代表不可变的字符序列。
-
常量与不可变的变量不同。虽然都是不能改变。一个是常量,一个还是变量
-
字符串的值在创建之后就不能更改。
-
String 对象的字符内容是存储在一个字符数组 value[] 中的
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** final 的 字符数组 来存储所有字符 */
private final char value[];
。。。
}
String 是一个final 类,意味着不能被继承,没有子类。
实现了 Serializable 、Comparable、CharSequence 三个接口。
Serializable 表示字符串支持序列化,可以进行传输
Comparable :支持比较大小
public interface Comparable<T> {
// 比较大小
public int compareTo(T o);
}
CharSequence :字符序列接口
public interface CharSequence {
// 长度
int length();
char charAt(int index);
CharSequence subSequence(int start, int end);
public String toString();
/**
* @since 1.8
*/
public default IntStream chars() {
class CharIterator implements PrimitiveIterator.OfInt {
int cur = 0;
public boolean hasNext() {
return cur < length();
}
public int nextInt() {
if (hasNext()) {
return charAt(cur++);
} else {
throw new NoSuchElementException();
}
}
@Override
public void forEachRemaining(IntConsumer block) {
for (; cur < length(); cur++) {
block.accept(charAt(cur));
}
}
}
return StreamSupport.intStream(() ->
Spliterators.spliterator(
new CharIterator(),
length(),
Spliterator.ORDERED),
Spliterator.SUBSIZED | Spliterator.SIZED | Spliterator.ORDERED,
false);
}
/**
*/
public default IntStream codePoints() {
class CodePointIterator implements PrimitiveIterator.OfInt {
int cur = 0;
@Override
public void forEachRemaining(IntConsumer block) {
final int length = length();
int i = cur;
try {
while (i < length) {
char c1 = charAt(i++);
if (!Character.isHighSurrogate(c1) || i >= length) {
block.accept(c1);
} else {
char c2 = charAt(i);
if (Character.isLowSurrogate(c2)) {
i++;
block.accept(Character.toCodePoint(c1, c2));
} else {
block.accept(c1);
}
}
}
} finally {
cur = i;
}
}
public boolean hasNext() {
return cur < length();
}
public int nextInt() {
final int length = length();
if (cur >= length) {
throw new NoSuchElementException();
}
char c1 = charAt(cur++);
if (Character.isHighSurrogate(c1) && cur < length) {
char c2 = charAt(cur);
if (Character.isLowSurrogate(c2)) {
cur++;
return Character.toCodePoint(c1, c2);
}
}
return c1;
}
}
return StreamSupport.intStream(() ->
Spliterators.spliteratorUnknownSize(
new CodePointIterator(),
Spliterator.ORDERED),
Spliterator.ORDERED,
false);
}
}
String 内部是用 字符串数组存储数据,这字符串数组也是final的。意味着每一个元素也是不可变的。
/** final 的 字符数组 来存储所有字符 */
private final char value[];
看下面的例子:
public class StringTest {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
s1 = "hello";
System.out.println(s1); // hello
System.out.println(s2); // abc
}
}
String 是一个类 居然像基本数据类型一样,直接写值,而不是new出来。String的这种赋值方式叫做字面量定义
public class StringTest {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2); // true
}
}
比较s1与s2的地址值,你会发现是同一个。
在JVM的内存结构中,字符串是保存在方法区中的字符串常量池的,而不是像其他对象一样,保存在堆空间。
当首次使用 "abc" 给s1时,常量池没有 "abc" 就会创建一个。然后将地址值给 栈空间的s1保存。
当第二次使用 "abc" 给s2时,由于常量池已经有了"abc",就不会再造一个,而是直接将地址值给s2保存。
通过字面量的方式【与new不同】给字符串赋值,此时字符串是放在字符串常量池中,常量池不会存储2个相同的字符串。
public class StringTest {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
s1 = "hello";
System.out.println(s1); // hello
System.out.println(s2); // abc
}
}
上面的代码执行了 s1 = "hello";
如果String是普通类,那么赋值就是通过s1保存的地址值,将这个地址里面保存的abc改为hello。而String是这样进行赋值的:当定义了"hello"时,常量池中没有hello,就创建一个hello,并把地址给s1,修改的是栈中s1保存的地址值。而不是常量池中的"abc"
因为字符串是final的,不可变。这就体现了字符串的不可变性。
s3 = s1 + "world";
当执行上面的代码,进行字符串连接再赋值时,也是先去看看常量池有没有 "helloworld",有就将地址值给s3,没有就创建一个,将地址值给s3。 常量池中原有的 "hello" 不会变,hello的地址值也不会变。
对字符串的任何修改操作,都不会在原值上进行修改。而是看一看常量池有没有,没有就新建。
1.1.1 String对象的创建
// 字面量方式:直接赋值
String str = "hello";
// 对象方式:等价于 this.value = new char[0];
String s1 = new String();
// 对象方式: this.value = original.value;
String s2 = new String("hello");
// 对象方式:this.value = Arrays.copyof(value, value.length);
String s3 = new String(new char[]{'h','e','l','l','o'});
// 对象方式:String(char value[], int offset, int count)
String s4 = new String(new char[]{'h','e','l','l','o'}, 2, 2);
对象方式 创建的String对象 都是在堆里面的。只有字面量方式赋值才是在方法区中的常量池。
看下面的代码:
public class StringTest {
public static void main(String[] args) {
// 方法区-常量池 存储
String s1 = "hello";
String s2 = "hello";
// 堆空间存储
String s3 = new String("hello");
String s4 = new String("hello");
System.out.println(s1 == s2); // true
System.out.println(s2 == s3); // false
System.out.println(s3 == s4); // false
}
}
那是不是只要创建在堆空间了,就与常量池没关系?
不是的,以s3为例,s3这个对象在堆空间,但是堆空间保存的并不是hello这个字符串,而是 常量池中的 hello字符串的地址。就是说, 用new的方式最终还是用的常量池里面的东西,new方式反而多转了1到手。
因为这个原因,字面量的方式定义是效率最高的。
1.1.2 字符串拼接
public class StringTest {
public static void main(String[] args) {
String s1 = "hello";
String s2 = " world";
String s3 = "hello world";
String s4 = "hello" + " world";
String s5 = s1 + " world";
String s6 = "hello" + s2;
String s7 = s1 + s2;
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(s5 == s6); // false
System.out.println(s5 == s7); // false
System.out.println(s6 == s7); // false
}
}
原因是 只要有变量参与拼接,都相当于是new 的方式创建字符串,结果在堆中。
只要常量与常量拼接结果在常量池中。
如果拼接的结果调用intern方法,返回值就在常量池中。
String s8 = s5.intern();
System.out.println(s3==s8); // true
练习:
public class StringTest {
String str = new String("good");
char[] ch = {'t', 'e','s','t'};
public void change(String str, char ch[]){
str = "test ok";
ch[0] = 'b';
}
public static void main(String[] args) {
StringTest ex = new StringTest();
ex.change(ex.str, ex.ch);
System.out.println(ex.str + " and "); // good and
System.out.println(ex.ch); // best
}
}
// ex.ch 肯定是 best。引用数据类型值传递传递的是地址值,会修改地址值里面指向的堆空间里面的值。
// 字符串是不可变量,值传递不会修改原来的值。
上面的解析你肯定不懂。哈哈
分析一下调用change的过程。
已知:成员属性str是使用new方法创建的,存储的是堆空间字符串对象的地址值,假设地址为a。堆空间字符串对象存储的是常量池中 good 的地址值假设地址为b。
如果是字面量赋值,就是直接存储的是常量池中good 的地址值。
字符数组ch我不分析,详情见前面值传递那一部分内容。
当调用change时,方法的形参str 接收 成员属性str的值。注意形参 str 与成员属性str虽然名字一样,但是不是同一个。为了区分,我带上this。 这个过程就是 String str = this.str 形参str 保存的就是this.str原来保存的地址值a。即str 保存了 地址值 a
在方法中,将"test ok"赋值给了 str,此时str变量保存的值由地址值a变成了"test ok"的地址值 假设为c
整个过程只有形参str保存的值在变。 成员属性this.str保存的东西始终没变。所以输出理所当然。
1.1.3 内存结构
上图可以看到 方法区域堆区是2个并列的结构。
堆空间还可以具体划分:
永久区又有点问题,上图是规范。但是事实上,在实际中永久区又不属于堆,永久区就是方法区。
实际上,方法区和堆一样,是各个线程共享的内存区域。用于存储
-
类信息
-
普通常量
-
静态变量
-
编译器编译后的代码等
虽然规范上是属于堆,但是还有一个名字叫做Non-Heap,目的是为了与堆区分。
永久区是方法区的一个实现。JDK1.7 版本将原本放在永久区的字符串常量池移走了,移到堆里面去了。
1.8 又拿回来到永久区,但是永久区改名了,叫元空间,方法区的体现叫元空间了。
即1.7 以前,永久区就是方法区。1.7 版本 永久区与方法区不同了。1.7与1.8 之后又不同
常量池是方法区的一部分,class文件除了有类的版本、字段、方法、接口等描述信息,还有一项信息是常量池。这部分内容在类加载后进入方法区的运行时常量池中存储
1.1.4 常用方法
-
length()方法:返回底层的char数组的长度即字符串的长度。
String str = "hello world";
int length = str.length();
System.out.println(length); // 11
-
charAt(int index) 方法:取指定索引处的字符
String str = "hello world";
int length = str.length();
char c = str.charAt(5);
// 输入的索引不能超过 长度-1,即最大索引。否则报错。
// "hello world"的最大索引是 10
char c1 = str.charAt(20);
-
isEmpty() 判断字符串是否为空,实质是字符数组的长度是否为0
String str = "hello world";
String s1 = "";
boolean empty = str.isEmpty();
System.out.println(empty); // false
System.out.println(s1.isEmpty()); // true
-
toLowerCase() 全部小写/ toUpperCase()全部大写
String s1 = "hello world";
String s2 = "HELLO WORLD";
String s = s1.toUpperCase();
System.out.println(s); // HELLO WORLD
System.out.println(s1); // hello world
System.out.println(s2.toLowerCase(Locale.ROOT)); // hello world
-
trim() 干掉开头、末尾的全部空格,中间的空格保留
String s1 = " hello world ";
System.out.println("==="+s1+"===");
System.out.println("==="+s1.trim()+"===");
// 输出。 加上 === 是为了看得出空格。
=== hello world ===
===hello world===
-
equals() 比较字符串内容相等;equalsIgnoreCase() 忽略大小写比较内容相等。
String s1 = "hello world";
String s2 = "hello world";
String s3 = "hEllo WoRld";
System.out.println(s1.equals(s2)); // true
System.out.println(s1.equals(s3)); // false
System.out.println(s1.equalsIgnoreCase(s3)); // true
-
concat() 字符串拼接,+ 号可以替代这玩意,用的不多
String s1 = "hello world";
String s3 = s1.concat("xxx");
System.out.println(s3); // hello worldxxx
-
compareTo() 比较字符串大小,一个一个字符比较。实际比较的是字符对应的编码大小。相等为返回0,大于返回1,小于返回-1
compareToIgnoreCase() 忽略大小写比较大小。
String s1 = "abc";
String s2 = "abc";
String s3 = "abd";
System.out.println(s1.compareTo(s2)); // 0
System.out.println(s1.compareTo(s3)); // -1
-
substring() 取字符串的子串。有2个重写的方法。
substring(int beginIndex) 取指定索引到字符串末尾的所有字符作为子串 substring(int beginIndex, int endIndex) 取开始索引到结束索引之间的字符作为子串。 [左闭 右开):包含起始索引,不包含结尾索引
String s1 = "hello world";
String substring = s1.substring(2);
String substring2 = s1.substring(2,6);
System.out.println(substring); // llo world
System.out.println(substring2); // llo
-
endsWith() 以什么结尾 startsWith() 以什么开头
startsWith() 还有1个重载方法 startsWith(String prefix, int toffset) ,可以指定从哪个位置开始判断。
String s1 = "hello world";
System.out.println(s1.endsWith("ld")); // true
System.out.println(s1.startsWith("h")); // false
System.out.println(s1.startsWith("ll")); // false
System.out.println(s1.startsWith("ll", 2)); // true 从索引为2开始判断,是否以ll开头
-
contains(CharSequence s) 是否包含某个字符序列,参数是CharSequence接口。String实现了这个接口。所以传入字符串就可。
String s1 = "hello world";
boolean ll = s1.contains("ll");
System.out.println(ll); // true
-
indexof(String str) 返回 输入的字符串首次出现的位置,如果没有就返回-1
lastIndexOf(String str) 返回输入的字符串最后出现的位置,没有就返回-1
String s1 = "hello world";
int ll = s1.indexOf("ll");
System.out.println(ll); // 2
int oo = s1.indexOf("oo");
System.out.println(oo); // -1
int l = s1.lastIndexOf("l");
System.out.println(l); // 9
indexof 和 lastIndexOf 都各有4个重载方法。
各自的前2个是 可以传入 字符或者字符串。后2个方法的第一个参数也是字符或者字符串,第二个参数是指定寻找的起始位置。即从这个位置开始找。
int l1 = s1.lastIndexOf('l'); // 9
int l2 = s1.lastIndexOf("l", 5); // 3
int l3 = s1.indexOf('l'); // 2
int l4 = s1.indexOf("l", 2); // 2
int l5 = s1.indexOf("l", 3); // 3
int l6 = s1.indexOf("l", 4); // 9
-
replace(CharSequence target, CharSequence replacement) 字符串替换
replace(char oldChar, char newChar) 字符替换
String s1 = "hello world";
String replace = s1.replace("l", "aa");
System.out.println(replace); // heaaaao woraad
String s1 = "hello world";
String replace = s1.replace('l', 'a');
System.out.println(replace); // heaao worad
replaceAll(String regex, String replacement) 输入正则表达式,进行字符传替换,全部替换
replaceFirst(String regex, String replacement) 输入正则表达式,进行字符传替换,只替换第一个
String s1 = "234hello world43255";
String s = s1.replaceAll("\\d", "");
String s2 = s1.replaceFirst("\\d", "");
System.out.println(s); // hello world
System.out.println(s2); // 34hello world43255
-
matches(String regex) 输入正则表达式匹配,返回是否匹配成功。
String s1 = "234hello world43255";
String s2 = "0791-8888888";
boolean matches1 = s1.matches("\\d+");
boolean matches2 = s2.matches("0791-\\d{7,8}");
System.out.println(matches1); // false
System.out.println(matches2); // true
-
split(String regex) 输入正则表达式,进行切割,返回数组。
String s1 = "234hello world43255";
String s2 = "0791-8888888-444";
String s3 = "hello|world|java";
String[] hellos = s1.split("hello");
String[] split = s2.split("-");
String[] split1 = s3.split("\\|");
for (int i = 0; i < hellos.length; i++) {
System.out.println(i);
}
for (int i = 0; i < split.length; i++) {
System.out.println(i);
}
for (String s : split1) {
System.out.println(s);
}
1.1.5 与其他类型的转换
-
与基本数据类型、包装类之间的转换。前部分内容已经讲过,此处复习。
1、String 转 基本类型、包装类
String s = "123";
// String 转 包装类 调用 包装类的valueOf方法
Integer integer = Integer.valueOf(s);
// String 转 包装类 调用 包装类的parseXXX方法
int i = Integer.parseInt(s);
2、基本数据类型、包装类 转换为 String
Integer a = 5;
int b = 5;
// 调用 String 的valueOf方法
String s = String.valueOf(a);
String s1 = String.valueOf(b);
// 利用 + 自动类型转换
String s2 = a + "";
String s3 = b + "";
// 调用包装类的 toString 方法
String s4 = a.toString();
-
与 char[] 之间的转换
1、String 转 char[] 数组
String s1 = "123abc";
char [] charArray = s1.toCharArray();
for (char c : charArray) {
System.out.print(c + "\t");
}
// 输出:1 2 3 a b c
2、char[] 转 String
char [] charArray = new char[]{'1', '2', '3', 'a', 'b', 'c'};
// 调用String 的构造器即可
String s = new String(charArray);
System.out.println(s); // 123abc
-
与 byte[] 之间的转换
1、String 转 byte[] 数组。 实际上字符内部都是以byte存储的。字符串转byte就是编码的过程。
String s1 = "abc123";
// 调用 getBytes 方法
byte[] bytes = s1.getBytes();
for (byte aByte : bytes) {
System.out.print(aByte + "\t");
}
// 输出: 97 98 99 49 50 51
getBytes方法有多个重载方法。
可以传入指定的字符编码名称。
String s1 = "中国";
byte[] bytes = s1.getBytes(StandardCharsets.UTF_8);
for (byte aByte : bytes) {
System.out.print(aByte + "\t");
}
// -28 -72 -83 -27 -101 -67
当使用UTF-8编码时,可以看到 一个汉字占用3个字节。中: -28 -72 -83 国: -27 -101 -67
2、byte[] 数组 转 String, 这个过程也称为解码。
byte[] bytes = new byte[]{-28,-72,-83,-27, -101,-67};
// 还是调用String的构造器,也是可以指定编码
String s = new String(bytes);
System.out.println(s); // 中国
// 指定编码
String s = new String(bytes, StandardCharsets.UTF_8);
乱码问题一般就是 编解码使用的编码格式不同,统一编码格式都能解决编码问题。
2、StringBuilder 、StringBuffer 类
StringBuilder 和 StringBuffer 非常类似。均表示可变字符序列,而且提供相关功能的方法也一样。
String:不可变的字符序列,底层使用 final char[]
StringBuffer :可变的字符序列,线程安全的,效率低。底层也是char[] 但不是final
StringBuilder 【JDK5.0新增的】:可变的字符序列,线程不安全,效率高。底层也是char[] 但不是final
源码分析:
//对应String
String str = new String(); // char[] value = new char[0]; 相当于创建了长度为0的char 数组
String str1 = new String("abc"); // char[] value = new char[]{'a','b','c'}
//对于StringBuffer
StringBuffer sb1 = new StringBuffer(); // char[] value = new char[16]; 相当于创建了长度为16的char 数组
System.out.println(str.length()); // 0 这里返回的不是 char[] 数组的长度,而是 append的字符的长度
sb1.append('a'); // value[0] = 'a'
sb1.append('b'); // value[1] = 'b' StringBuffer可以在原来是char数组上添加,而不是新建一个
StringBuffer sb2 = new StringBuffer("abc"); // char[] value = new char["abc".length + 16]; 相当于创建了长度为 你输入的字符串长度加上16 的char 数组
问题来了:如果后面添加的数据超过了16呢?
ans:装不了,就扩容。append之前,先计算确保一下能否添加进去。如果不能添加,就new一个新的数组,容量左移1位,扩容2倍,再加上2,把原来的数据拷贝进去。还有一些特殊情况需要考虑。2倍也装不下,左移超出限制等。
因此,如果需要保存长的可变字符串,使用StringBuilder 、StringBuffer类时,new对象的时候指定容量大小,避免扩容。
2.1 常用方法
buffer与builder的常用方法差不多。以buffer为例。
-
append() 添加数据到末尾,如下图,有13个重载的方法。基本涵盖了各种类型。返回值还是一个StringBuffer。
-
delete(int start, int end) 方法:删除 指定索引内的数据。也是左闭右开区间
StringBuffer s1 = new StringBuffer("abc");
s1.append(1);
s1.append("l");
System.out.println(s1); // abc1l
s1.delete(2,4);
System.out.println(s1); // abl
-
replace(int start, int end, String str) 方法:替换 start、end也是索引,左闭右开 str 是需要替换的内容
StringBuffer s1 = new StringBuffer("abc");
s1.append(1);
s1.append("l");
s1.replace(2,4, "ddd");
System.out.println(s1); // abdddl
-
insert 方法: 有12个重载方法。插入数据。 offset为偏移量,即设置什么时候开始插入,第二个参数是需要插入的内容。
StringBuffer s1 = new StringBuffer("abc");
s1.append(1);
s1.append("l");
s1.insert(2, 'x');
System.out.println(s1); // abxc1l
-
reverse() 方法: 反转字符串
StringBuffer s1 = new StringBuffer("abc1l");
s1.reverse();
System.out.println(s1); // l1cba
-
indexOf 与 String 类一样
-
substring 与 String 类的也一样,这个方法需要返回值。并不会对当前字符串操作。
-
length 返回字符串长度
-
charAt 与String一样
-
setCharAt() 修改指定索引的字符。
2.2 三者效率
效率其实前面已经讲过。但是要注意使用场景。
StringBuilder > StringBuffer > String