文章目录
1、类的定义
String 类
public final class String implements Serializable, Comparable<String>, CharSequence
String 类被声明为 final 的,意味着它不可以被继承。
- 实现了
Serializable
接口使它可以被序列化 - 实现了
Comparable
接口便于字符串之前的比较 - 实现了
CharSequence
接口,该接口是 char 值的一个可读序列,代表一个有序字符集合。
CharSequence 接口
CharSequence 接口中声明了如下几个方法:
public interface CharSequence {
// 获取字符序列长度
int length();
// 获取某个指定位置的字符
char charAt(int index);
// 获取子序列
CharSequence subSequence(int start, int end);
// 将字符序列转换为字符串
public String toString();
}
对于一个抽象类或者是接口类,不能使用 new 来进行赋值,但是可以通过以下的方式来进行实例的创建:
CharSequence cs = "hello"; // 等价于 CharSequence cs = new String("hello");
CharSequence 就是字符序列,而 String 本质上是通过字符数组实现的!
CharSequence 和 String 都能用于定义字符串,但 CharSequence 的值是可读可写序列,而 String 的值是只读序列。
2、成员变量
// 字符数组,用于存储字符串中的字符
private final char[] value;
// 缓存字符串的hash值,默认是0
private int hash;
- final 修饰 value,表示其一旦被赋值,内存地址是绝对无法修改的
- private 修饰 value,外部绝对访问不到,且 String 类也没用提供任何访问方法
String 是不可变类。即类值一旦被初始化,就不能再被改变了,如果被修改,将会是新的类。也就是说 String s = “a”; s = “b”
时并不是对变量 s 进行了修改,而是重新指向了新的字符串对象。
String 为什么要设计成不可变类呢?
- 不可变对象不能被写,避免了引用传值,所以线程安全。
- 方便使用字符串常量池,节省开销
不可变类的设计通常要遵循以下几个原则:
- 将类声明为 final,所以它不能被继承。
- 将所有的成员声明为私有的,这样就不允许直接访问这些成员。
- 对变量不要提供 setter 方法。
- 将所有可变的成员声明为 final,这样只能对它们赋值一次。
- 通过构造器初始化所有成员,进行深拷贝(deep copy)。
- 在 getter 方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝。
常量池
1)class 文件中的常量池
在 class 文件中,除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用。
编写如下代码,并查看其 class 文件内容:
public class Person {
final int age = 10;
String name = "老王";
}
在上图中可以看到,字面量 10 和 “老王” 出现在 Constant pool 列表中。
字面量是用于表达源代码中一个固定值的表示法。数字,字符串等都有字面量表示。
2)运行时常量池
根据《java虚拟机规范》的规定,class 文件的常量池中的信息,将在类加载后进入方法区中的常量池存储。
3、构造方法
1)利用字节数组来生成字符串
byte 是网络传输或存储的序列化形式。在很多传输和存储的过程中需要将 byte[] 数组和 String 进行相互转化。
不带编码
-
String(byte[] bytes)
public String(byte[] bytes) { this(bytes, 0, bytes.length); }
-
String (byte[] bytes, int offset, int length)
public String(byte[] bytes, int offset, int length) { checkBounds(bytes, offset, length); // 将字节数组解码为字符数组 this.value = StringCoding.decode(bytes, offset, length); }
private static void checkBounds(byte[] bytes, int offset, int length) { if (length < 0) throw new StringIndexOutOfBoundsException(length); if (offset < 0) throw new StringIndexOutOfBoundsException(offset); if (offset + length > bytes.length) throw new StringIndexOutOfBoundsException(offset + length); }
如果没有指明解码使用的字符集,StringCoding 的 decode方法首先调用系统的默认编码格式;
如果没有指定编码格式(charsetName),则默认使用ISO-8859-1
编码格式进行编码操作。
带编码
字符集(charset)是一个系统支持的所有抽象字符的集合。常见的字符集有 ascii字符集、Unicode字符集。
字符编码(charsetName)是对字符集的一套编码规则,将具体的字符进行“数字化”,便于计算机理解和处理。例如常用的 UTF-8 字符编码就是对 Unicode 字符集的一种具体编码规范。
- String (byte[] bytes, Charset charset)
- String (byte[] bytes, int offset, int length, Charset charset)
- String (byte[] bytes, String charsetName)
- String (byte[] bytes, int offset, int length, String charsetName)
public String(byte[] bytes, int offset, int length, Charset charset) {
if (charset == null)
throw new NullPointerException("charset");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charset, bytes, offset, length);
}
其他逻辑不变,参数从 Charset 换成 String,即为后两种的实现
可以看到利用字节数组的核心逻辑是:
char[] value = StringCoding.decode(charsetName, bytes, offset, length);
char[] value = StringCoding.decode(charset, bytes, offset, length);
char[] value = StringCoding.decode(bytes, offset, length);
StringCoding 类的 decode 方法如下:
private final static ThreadLocal<SoftReference<StringDecoder>> decoder
= new ThreadLocal<>();
static char[] decode(String charsetName, byte[] ba, int off, int len)
throws UnsupportedEncodingException {
// 从线程级缓存中获取反序列化器
StringDecoder sd = deref(decoder);
String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
// 缓存中没有反序列化器,或者虽然有,但是之前反序列化的字符集与这次不同,则重新生成decoder
if ((sd == null) || !(csn.equals(sd.requestedCharsetName())
|| csn.equals(sd.charsetName()))) {
sd = null;
try {
Charset cs = lookupCharset(csn);
if (cs != null)
sd = new StringDecoder(cs, csn);
} catch (IllegalCharsetNameException x) {
}
if (sd == null)
throw new UnsupportedEncodingException(csn);
// 将decoder放入线程级缓存,以备下次使用
set(decoder, sd);
}
// 完成反序列化
return sd.decode(ba, off, len);
}
// 从缓存中获取反序列器,此处使用了软引用,便于jvm在内存不足时,释放该缓存
private static <T> T deref(ThreadLocal<SoftReference<T>> tl) {
SoftReference<T> sr = tl.get();
if (sr == null)
return null;
return sr.get();
}
// 判断字符集是否支持,并加载字符集处理类
private static Charset lookupCharset(String csn) {
if (Charset.isSupported(csn)) {
try {
return Charset.forName(csn);
} catch (UnsupportedCharsetException x) {
throw new Error(x);
}
}
return null;
}
// 将对象的软引用放入线程级缓存
private static void set(ThreadLocal tl, Object ob) {
tl.set(new SoftReference(ob));
}
就是利用了线程级缓存来缓存 decoder,这样就不必每次都实例化新的 decoder,同时线程级缓存也确保了反序列化的操作是线程安全的。其中 ThreadLocal 和 SoftReference 结合的用法值得借鉴。
2)利用字符数组来生成字符串
-
String(char[] value)
public String(char[] value) { this.value = Arrays.copyOf(value, value.length); }
-
String(char[] value, int offset, int count)
public String(char[] value, int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count <= 0) { if (count < 0) { throw new StringIndexOutOfBoundsException(count); } if (offset <= value.length) { this.value = "".value; return; } } // Note: offset or count might be near -1>>>1. if (offset + count > value