侧边栏壁纸
博主头像
Dioxide-CN博主等级

茶边话旧,看几许星迢露冕,从淮海南来。

  • 累计撰写 50 篇文章
  • 累计创建 49 个标签
  • 累计收到 21 条评论

目 录CONTENT

文章目录

Java基础知识:String包装类

Dioxide-CN
2022-04-22 / 0 评论 / 6 点赞 / 167 阅读 / 15,382 字
温馨提示:
本文最后更新于 2022-08-02,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

String包装类

String

字符串,就是一串连续的字符。在C语言中就是使用char的数组来表示字符串。

char string[] = {"D","i","o","x","i","d","e","\0"};
char string[] = "Dioxide";

而在 Java 中,为遵循 一切皆对象 的概念,将 char 数组进行了一次封装,进而用 String 类型来表达字符串。

//Java8源码
public final class String{
	private final char value[];
}

在源码中 value[]final 关键字修饰且为 private 私有成员变量,其设计原理即为 String 的不可变性。

不可变性

一个对象创建后,如果可以修改其对象属性,则说明这个对象是可变的,反之则是不可变的。

//Person是可变的
Person p = new Person(18);
p.setAge = 20;

//String是不可变的
String s = "Dioxide.CN";
s = "Dioxide";
System.out.println(s);
栈 Stack堆 Dioxide.CN
引用变更栈 Stack堆 Dioxide.CN堆 Dioxide

对象的不可变性,其实就是指对象本身的属性、数据不会发生改变。将 String 变量重新赋值 不等同于 改变 String 对象本身的属性。而是创建了一个新的 String 对象,将新对象的 引用 赋值给了 String 对象,之前的 String 对象是不会受到影响的。

String 的不可变性不仅是因为其被 final 关键字修饰,最根本的原因是被 private 权限修饰符所修饰。被 final 修饰只能代表它不能指向新的数组,不代表数组本身的数据不会被修改。

private 修饰的 String 并没有暴露和提供任何修改字符数组的方法。很多字符串操作都是返回的新的 String 对象,绝对不会影响原数据。

获取其底层字符数组时都是复制一个新的字符数组进行返回,原数组也不会收到影响。

//Java8源码
public final class String {
	private final char value[];

	public char[] toCharArray() {
		char result[] = new char[value.length];
		System.arraycopy(value, 0, result, 0, value.length);
		return result;
	}
}

并且,String 还被 final 修饰为不可继承类,从而杜绝了子类覆盖父类的可能。

设计原理及好处

Error: Parse error on line 1:
flowchart LR 	stack([堆]) -.- pool(
--------------^
Expecting 'NEWLINE', got 'ALPHA'

首先,只有 String 不可变了,字符串常量池才能发挥作用。
用字面量创建字符串时,字符串常量池会返回已有对象的引用。如果字符串可变,那引用的值就可以随时修改 ,并能随时影响到其他的引用,从而数据会发生各种错误,这样就会导致常量池不存在复用性

String s1 = "str";
String s2 = "str";
System.out.println(s1 == s2); //true
//如果String会改变,那么s1改变时s2也会跟着改变
System.out.println(s2);

String 不可变可以保证其哈希码也不可变,因此计算一次哈希码后即可将其储存,再用到时就无需计算哈希码了,性能更高。

//Java8源码
public final class String{
	private final char value[];
	//默认值为0
	private int hash;

	private int hashCode() {
		int h = hash;
		if(h == 0 && value.length > 0) {
			char val[] = value;
			for(int i = 0; i < value.length; i++) {
				h = 31 * h + val[i]
			}
			//计算一次后可将哈希码储存
			hash = h;
		}
		return h;
	}
}

得益于 String 的哈希码不会变,所以能够放心地使用和哈希计算相关的对象(如:HashMap、HashSet)。

String s = "Hello World"
HashSet<String> set = new HashSet<>();
set.add(s);
//假设可变,则此时set中的"Hello World"就找不到了
s.value = "Dioxide_CN";

如果 String 的哈希码会改变则会影响到这些对象的哈希计算,从而导致预期之外的效果。

最后一个最重要的原因就是,不可变对象都是 线程安全 的,即当前线程使用的对象不会被其他线程修改。

安全安全线程1<span>s1 = "str"</span>线程2

StringBuilder

弊端: String 对象频繁拼接时会产生大量新的 String 对象。

String s = "报数:";
for(int i = 0; i < 10; i++) {
	s = s + " " + i; //大量创建新的String对象
}
//报数: 0 1 2 3 4 5 6 7 8 9
System.out.println(s);

为此 Java 推出了 StringBuilder 可变的字符串类型:

//抽象类
abstract class AbstractStringBuilder {
	//AbstractStringBuilder底层与String类似
	char[] value;
}
public final class StringBuilder extends AbstractStringBuilder {
}

新的 StringBuilder 和老的 AbstractStringBuilder 都提供了许多方法来修改字符串。但是其修改的数据都是本身,返回的数据也是一个自身 StringBuilder 对象,这样是为了后续更好地链式调用方法。如:

str.appen("1").append("23").append("456");

其实使用 String 拼接字符串时,其底层会自动创建 StringBuilder 对象并调用其 append() 方法完成操作。
所以频繁操作 String 对象时,应当优先使用 StringBuilder 。

StringBuilder的缺点

StringBuilder 是一个可变对象,那么其自身自然是 线程不安全的

安全安全不安全不安全线程1String线程2StringBuilder

为了解决 StringBuilder 的线程不安全的问题,Java 推出了 StringBuffer 来解决线程问题。

StringBuffer

同样的 StringBuffer 也继承了 AbstractStringBuilder 所以同样也能修改字符串。
而其与 StringBuilder 的不同点在于,StringBuffer 的方法中都使用了 synchronized 关键字来保障线程安全

public final class StringBuffer extends AbstractStringBuilder {
	@Override
	public synchronized StringBuffer append(String str) {
		//逻辑段
		return this;
	}
	
	@Override
	public synchronized StringBuffer insert(int offset, String str) {
		//逻辑段
		return this;
	}
	
	//其他方法
}
安全安全不安全不安全安全安全线程1String线程2StringBuilderStringBuffer

正因为 StringBuffer 每次操作 String 时都会加锁,从而导致了它的性能低于 StringBuilder。

类型 特点 适用场景
String 不可变,线程安全 操作少量数据或不需要操作数据
StringBuilder 可变,线程不安全 需要频繁操作数据且不考虑线程安全
StringBuffer 可变,线程安全,性能低 需要频繁操作数据且考虑线程安全

StringJoiner

String[] names = {"A","B","C","D"};
StringBuilder sb = new StringBuilder();
for(String name : names) {
	sb.append(name).append(", ");
}
System.out.println(sb); // A, B, C, D, 

上述案例的需求为:需要将数据集进行不断拼接。

但是使用 StringBuilder 进行拼接所带来的问题就是在最终拼接出来的结果末尾会多出一个 ", " 为了解决这一问题,可以在拼接完成的最后删除多余的字符,或者在拼接时进行边界判断:

String[] names = {"A","B","C","D"};
StringBuilder sb = new StringBuilder();
for(String name : names) {
	sb.append(name).append(", ");
}
sb.delete(sb.length() - 2, sb.length()); //删除
System.out.println(sb); // A, B, C, D, 

若字符串同时又需要在开头和结尾添加括号,则还需要在开头结尾再进行一次拼接:

String[] names = {"A","B","C","D"};
StringBuilder sb = new StringBuilder("["); //头
for(int i = 0; i < names.length; i++) {
	sb.append(name[i]);
	if(i != names.length - 1) {
		sb.append(", ");
	}
}
sb.appen("]"); //尾
System.out.println(sb); // A, B, C, D, 

这进一步导致了代码的冗余程度。为此,Java 8 推出了 StringJoiner 来简化这些操作:

String[] names = {"A","B","C","D"};
StringJoiner sjr = new StringJoiner(", ");
for(String name : names) {
	sjr.add(name);
}
System.out.println(sb); // A, B, C, D

在构造 StringJoiner 时需要传入分隔符,并在遍历过程中将 String 加入 StringJoiner 对象中。

同样的,可在实例化对象时,指定开头和结尾:

String[] names = {"A","B","C","D"};
StringJoiner sjr = new StringJoiner(", ","[","]");
for(String name : names) {
	sjr.add(name);
}
System.out.println(sb); // [A, B, C, D]

在 Java 的标准库中,同样也用到了 StringJoiner,比如 String 对象的静态方法 join() ,以及 Stream 流中常用的 joining() 操作:

String[] names = {"A","B","C","D"};
String.join(", ", names);
Arrays.stream(names).collect(Collectors.joining(", "));
6

评论区