前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java基础知识:String包装类

Java基础知识:String包装类

作者头像
DioxideCN
发布2022-08-05 19:34:14
4300
发布2022-08-05 19:34:14
举报

String包装类

String

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

代码语言:javascript
复制
char string[] = {"D","i","o","x","i","d","e","\0"};
char string[] = "Dioxide";

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

代码语言:javascript
复制
//Java8源码
public final class String{
	private final char value[];
}

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

不可变性

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

代码语言:javascript
复制
//Person是可变的
Person p = new Person(18);
p.setAge = 20;

//String是不可变的
String s = "Dioxide.CN";
s = "Dioxide";
System.out.println(s);

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

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

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

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

代码语言:javascript
复制
//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 修饰为不可继承类,从而杜绝了子类覆盖父类的可能。

设计原理及好处

代码语言:javascript
复制
Error: Parse error on line 1:
flowchart LR 	stack([堆]) -.- pool(
--------------^
Expecting 'NEWLINE', got 'ALPHA'

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

代码语言:javascript
复制
String s1 = "str";
String s2 = "str";
System.out.println(s1 == s2); //true
//如果String会改变,那么s1改变时s2也会跟着改变
System.out.println(s2);

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

代码语言:javascript
复制
//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)。

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

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

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

StringBuilder

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

代码语言:javascript
复制
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 可变的字符串类型:

代码语言:javascript
复制
//抽象类
abstract class AbstractStringBuilder {
	//AbstractStringBuilder底层与String类似
	char[] value;
}
public final class StringBuilder extends AbstractStringBuilder {
}

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

代码语言:javascript
复制
str.appen("1").append("23").append("456");

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

StringBuilder的缺点

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

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

StringBuffer

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

代码语言:javascript
复制
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;
	}
	
	//其他方法
}

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

类型

特点

适用场景

String

不可变,线程安全

操作少量数据或不需要操作数据

StringBuilder

可变,线程不安全

需要频繁操作数据且不考虑线程安全

StringBuffer

可变,线程安全,性能低

需要频繁操作数据且考虑线程安全

StringJoiner

代码语言:javascript
复制
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 进行拼接所带来的问题就是在最终拼接出来的结果末尾会多出一个 ", " 为了解决这一问题,可以在拼接完成的最后删除多余的字符,或者在拼接时进行边界判断:

代码语言:javascript
复制
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, 

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

代码语言:javascript
复制
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 来简化这些操作:

代码语言:javascript
复制
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 对象中。

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

代码语言:javascript
复制
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() 操作:

代码语言:javascript
复制
String[] names = {"A","B","C","D"};
String.join(", ", names);
Arrays.stream(names).collect(Collectors.joining(", "));
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • String包装类
    • String
      • 不可变性
      • 设计原理及好处
    • StringBuilder
      • StringBuilder的缺点
    • StringBuffer
      • StringJoiner
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档