​在此之前有无数次下定决心要把JDK的源码大致看一遍,但是每次还没点开就已被一个超链接或者其他事情吸引直接跳开了。直到最近突然意识到,因为对源码的了解不深导致踩了许多莫名其妙的坑,所以再次下定决心要把常用的类全部看一遍。。。

一. 声明和成员变量(不可变性)

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
}

String的大部分操作都是围绕value这个字符数组定义的。同时String为了并发和一些安全性的考虑被设计成了不可变的类型,表明一旦被初始化完成后就是不可改变的。

在这里可能有人会疑惑,比如:

public static void main(String[] args) {
String s1 = "abc";
s1 = "bcd";
System.out.println(s1);
}
// 打印: bcd

其中“abc”被初始化完成之后即为不可改变的,s1只是stack里面的引用变量,随后s1将引用指向了“bcd”这个不可变字符,所以最后打印出来的是“bcd”。

而为了实现String的不可变性:

  1. String被声明为final类型:表明String不能被继承,即不能通过继承的方式改变其中的value值。
  2. value被声明为final类型:这个final并不能表示value这个字符数组的值不可变,只是确定了value这个字符数组在内存中的位置是不可变的。
  3. 在随后的方法介绍中可以看到,String并没有提供修改value[]值得方法,并且所有的方法都不是直接返回value[],而是copy value[]中的值或者新建一个String对象。所以在通常意义上String是不可变的,但是却不是绝对意义上的不可变,比如:
方法一:上面的第二点也说了,final char[] value,只定义了value所指向的内存地址不变,其中的值是可以变的,所以我们可以通过反射直接修改value的值
private void test03_reflection() throws Exception {
String s = "abc";
System.out.println("s = " + s);
Field valueFieldOfString = String.class.getDeclaredField("value");
valueFieldOfString.setAccessible(true);
char[] value = (char[]) valueFieldOfString.get(s);
value[1] = 'o';
System.out.println("s = " + s);
}
// 最终打印: aoc 方法二:可以直接使用unsafe类进行修改
// unsafe类不能直接new,构造方法里面有对调用者进行校验,但是我们同样可以通过反射获取
public static Unsafe getUnsafe() throws Exception {
Field theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeInstance.setAccessible(true);
return (Unsafe) theUnsafeInstance.get(Unsafe.class);
} // 通过unsafe类替换value[]
public void test04_unsafe() throws Exception {
String s = "abc";
System.out.println("s = " + s);
Field f = s.getClass().getDeclaredField("value");
f.setAccessible(true);
long offset; Unsafe unsafe = getUnsafe();
char[] nstr = new char[]{'a', 'o', 'c'};
offset = unsafe.objectFieldOffset(f);
Object o = unsafe.getObject(s, offset);
unsafe.compareAndSwapObject(s, offset, o, nstr);
System.out.println("s = " + s);
}
// 最终打印: aoc // 通过unsafe类,定位value[]的内存位置修改值
public void test05_unsafe() throws Exception {
String s = "abc";
System.out.println("s = " + s);
Field f = s.getClass().getDeclaredField("value");
f.setAccessible(true); long offset;
Unsafe unsafe = getUnsafe();
offset = unsafe.arrayBaseOffset(char[].class) + 2;
char[] arr = (char[]) f.get(s);
unsafe.putChar(arr, offset, 'o');
System.out.println("s = " + s);
}
// 最终打印: aoc

二、构造函数

public String()
public String(String original)
public String(char value[])
public String(char value[], int offset, int count)
public String(int[] codePoints, int offset, int count)
public String(byte ascii[], int hibyte, int offset, int count)
public String(byte ascii[], int hibyte)
public String(byte bytes[], int offset, int length, String charsetName)
public String(byte bytes[], int offset, int length, Charset charset)
public String(byte bytes[], Charset charset)
public String(byte bytes[], int offset, int length)
public String(byte bytes[])
public String(StringBuffer buffer)
public String(StringBuilder builder) String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}

以上15个构造方法除了最后一个,都是将传入的参数copy到value中,并生成hash。这也是符合string的不可变原则。而最后一个则是用于string和包内部产生的string对象,他没有复制value数组,而是持有引用,共享value数组。这是为了加快中间过程string的产生,而最后得到的string都是持有自己独立的value,所以string任然是不可变的。

三、常用方法

这个再次强调,String方法的所有返回值,都是new的一个新对象,以保证不可变性

1. String.equalsString.hashCode

public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

equals首先比较是否指向同一个内存地址,在比较是不是String类,再是长度最后内容注意比较。

public 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;
}

hashcode使用的数学公式:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

String 经常会用作 hashMap 的 key,所以我们希望尽量减少 String 的 hash 冲突。(冲突是指 hash 值相同,从而导致 hashMap 的 node 链表过长,所以我们通常希望计算的 hash 值尽可能的分散,从而提高查询效率),而这里选择31是因为,如果乘数是偶数,并且结果益处,那么信息就会是丢失(与2相乘相当于移位操作)31是一个奇素数,并且31有个很好的特性(目前大多数虚拟机都支持的优化):

31 * i == (i << 5) - i

2. String.intern

/**
* Returns a canonical representation for the string object. * <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the * <cite>The Java&trade; Language Specification</cite>.
* @return a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.
*/
public native String intern();

可以看到intern这是一个native方法,主要用来查询常量池的字符串,注释中也写了:

  • 如果常量池中中存在当前字符串,就会直接返回此字符串(此时当前字符串和常量池中的字符串一定不相同);
  • 如果常量池中没有,就将当前字符串加入常量池后再返回(此时和常量池的实现相关)。
  • 这里有关常量池的设计,其实是享元模式。

将字符串加入常量池的两种方式:

  • 编译期生成的各种字面量和符号引用
将javap反编译测试:
String s1 = "abc";
public static void main(String[] args) {
String s2 = "123";
} javap -v **.class:
Constant pool:
...
#24 = Utf8 abc
#25 = NameAndType #7:#8 // s1:Ljava/lang/String;
#26 = Utf8 123
...
  • 运行期间通过intern方法将常量放入常量池
// 将javap反编译测试:
String s1 = "abc";
public static void main(String[] args) {
String s2 = (new String("1") + new String("2")).intern();
} javap -v **.class:
Constant pool:
...
#2 = String #32 // abc - "abc" 常量的引用对象
#7 = String #36 // 1 -
#10 = String #39 // 2 -
#15 = Utf8 s1 - 引用变量
#28 = Utf8 s2 -
#32 = Utf8 abc - 变量
#36 = Utf8 1 -
#39 = Utf8 2 -
... 常量池中没有"12"变量,但是在运行的时候,会动态添加进去,后面可以用==测试

JDk版本实验,先简单讲一下常量池在不同JDK中的区别:

  • 在 JDK6 以及以前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 是会直接产生java.lang.OutOfMemoryError: PermGen space错误的。
  • 在 JDK7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域了。为什么要移动,Perm 区域太小是一个主要原因。
  • 在 JDK8 则直接使用 Meta 区代替了 Perm 区,并且可以动态调整 Mata 区的大小。

测试:

public void test06_intern() {
String s1 = new String("1");
s1.intern();
String s2 = "1";
System.out.println(s1 == s2); String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
JDK6: false false
JDK7、8:false true

分析:

  • 对于 JDK6:

如上图所示 JDK6 中的常量池是放在 Perm 区中的,Perm 区和正常的 JAVA Heap 区域是完全分开的。上面说过如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。所以拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用String.intern方法也是没有任何关系的。所以但会的都是false。

  • 对于 JDK7:

public void test06_intern() {
String s1 = new String("1"); //生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象
s1.intern(); // 生成一个 s2的引用指向常量池中的“1”对象。
String s2 = "1"; // 常量池中已经有“1”这个对象了,所以直接返回。
System.out.println(s1 == s2); // 最后比较s1、s2都指向同一个对象,所以是true /**
* 生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的对象。
* 中间还有2个匿名的`new String("1")`我们不去讨论它们。此时s3引用对象内容是"11",但此时常量池中是没有 “11”对象的。
*/
String s3 = new String("1") + new String("1"); /**
* 将 s3中的“11”字符串放入 String 常量池中,因为此时常量池中不存在“11”字符串,因此常规做法是跟 jdk6 图中表示的那样。
* 在常量池中生成一个 "11" 的对象,关键点是 jdk7 中常量池不在 Perm 区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。
* 这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。
*/
s3.intern(); String s4 = "11"; // "11"是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。
System.out.println(s3 == s4); // 所以最后比较是true。
}
  • 这里如果我们修改一下s3.intern();这句的顺序

  String s3 = new String("1") + new String("1");
String s4 = "11"; // 声明 s4 的时候常量池中是不存在“11”对象的,执行完毕后,“11“对象是 s4 声明产生的新对象。
s3.intern(); // 常量池中“11”对象已经存在了,因此 s3 和 s4 的引用是不同的。
System.out.println(s3 == s4); // 所以最终结果是false
  • 还有一些详细的性能测试可以查看

http://java-performance.info/string-intern-in-java-6-7-8/

http://java-performance.info/string-intern-java-6-7-8-multithreaded-access/

3. 其他常用方法

String除此之外还提供了很多其他方法,主要都是操作CharSequence的,另外这里大致讲一下UTF-16编码。

Unicode(统一码、万国码、单一码)是为了解决传统的字符编码方案的局限而产生的。

  • 在存贮英文和一些常见字符的时候用到的区域叫做 BMP(Basic Multilingual Plane)基本多文本平面,这个区域占两个字节;
  • 当超出 BMP 范围的时候,使用的是辅助平面(Supplementary Planes)中的码位,在UTF-16中被编码为一对16比特长的码元(即32位,4字节),称作代理对;Unicode标准现在称高位代理为前导代理(lead surrogates),称低位代理为后尾代理(trail surrogates)。

四、StringBuilder和StringBuffer

由于String对象是不可变的,所以进行字符串拼接的时候就可以使用StringBuilderStringBuffer两个类。他们和String的关系如下:

从图中可以看到StringBuilderStringBuffer也是实现的CharSequence接口,同时他们实现了Appendable接口,具有对字符串动态操作的能力。

从他们父类AbstractStringBuilder的源码来看:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count; public void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > 0)
ensureCapacityInternal(minimumCapacity);
} public AbstractStringBuilder append(***) {
....
} public AbstractStringBuilder insert(***) {
....
}
}
  • 他们同样持有一个字符数组char[] value,并且每次在对字符串进行操作的时候需要首先对数组容量进行确定,不足的时候需要扩容。
  • 他们每个对字符串进行操作的方法,都会返回自身,所以我们可以使用链式编程的方式进行操作。另外现在还有一种通过泛型类定义链式操作的方式。
public class A<T extends A> {
public T **(**) {
return t;
}
}
  • StringBuilderStringBuffer的 API 都是互相兼容的,只是StringBuffer的每个方法都用的synchronized进行同步,所以是线程安全的。

1. 操作符重载

每当我们要就行字符串拼接的时候,自然会使用到+,同时++=也是 java 中仅有的两个重载操作符。

public void test07_StringBuffer() {
String s1 = "a" + "b";
s1 += "c";
System.out.println(s1);
} javap -v **.class
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: ldc #37 // String ab
2: astore_1
3: new #16 // class java/lang/StringBuilder
6: dup
7: invokespecial #17 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #19 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #38 // String c
16: invokevirtual #19 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #20 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_1
23: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_1
27: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
LineNumberTable:
line 83: 0
line 84: 3
line 85: 23
line 86: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 this LJDK/Test01_string;
3 28 1 s1 Ljava/lang/String;

从字节码大致可以看出编译器每次碰到”+”的时候,会new一个StringBuilder出来,接着调用append方法,在调用toString方法,生成新字符串,所以在字符串连接的时候,有很多“+”的时候,可以直接使用StringBuffer或者StringBuilder以提高性能;当然如果遇到类似String s = “a” + “b” + “c” + ...类似的连续加号的时候,JVM 会自动优化为一个 StringBuilder。

五、switch对String的支持

String str = "b";
switch (str) {
case "a":
System.out.println(str);
break;
case "b":
System.out.println(str);
break;
default:
System.out.println(str);
break;
}
System.out.println(str); javap -v **:
code:
...
8: invokevirtual #8 // Method java/lang/String.hashCode:()I
11: lookupswitch { // 2
97: 36
98: 50
default: 61
}
...
// 可以看到是首先拿到String的hashcode,在进行switch操作的

总结

本来想就这样结束的,但是总觉得不写点总结什么的就不完整。。。另外文章中还有很多细节没有写完,比如 String 在用于锁对象时,需要使用 intern 来保证是同一把锁。。。

参考链接

https://tech.meituan.com/in_depth_understanding_string_intern.html

JDK源码分析(1)之 String 相关的更多相关文章

  1. JDK源码分析(一)—— String

    dir 参考文档 JDK源码分析(1)之 String 相关

  2. 【JDK】JDK源码分析-Semaphore

    概述 Semaphore 是并发包中的一个工具类,可理解为信号量.通常可以作为限流器使用,即限制访问某个资源的线程个数,比如用于限制连接池的连接数. 打个通俗的比方,可以把 Semaphore 理解为 ...

  3. 【JDK】JDK源码分析-HashMap(2)

    前文「JDK源码分析-HashMap(1)」分析了 HashMap 的内部结构和主要方法的实现原理.但是,面试中通常还会问到很多其他的问题,本文简要分析下常见的一些问题. 这里再贴一下 HashMap ...

  4. JDK源码分析(三)—— LinkedList

    参考文档 JDK源码分析(4)之 LinkedList 相关

  5. 【JDK】JDK源码分析-LinkedHashMap

    概述 前文「JDK源码分析-HashMap(1)」分析了 HashMap 主要方法的实现原理(其他问题以后分析),本文分析下 LinkedHashMap. 先看一下 LinkedHashMap 的类继 ...

  6. 【JDK】JDK源码分析-HashMap(1)

    概述 HashMap 是 Java 开发中最常用的容器类之一,也是面试的常客.它其实就是前文「数据结构与算法笔记(二)」中「散列表」的实现,处理散列冲突用的是“链表法”,并且在 JDK 1.8 做了优 ...

  7. 【JDK】JDK源码分析-TreeMap(2)

    前文「JDK源码分析-TreeMap(1)」分析了 TreeMap 的一些方法,本文分析其中的增删方法.这也是红黑树插入和删除节点的操作,由于相对复杂,因此单独进行分析. 插入操作 该操作其实就是红黑 ...

  8. 【JDK】JDK源码分析-Vector

    概述 上文「JDK源码分析-ArrayList」主要分析了 ArrayList 的实现原理.本文分析 List 接口的另一个实现类:Vector. Vector 的内部实现与 ArrayList 类似 ...

  9. 【JDK】JDK源码分析-List, Iterator, ListIterator

    List 是最常用的容器之一.之前提到过,分析源码时,优先分析接口的源码,因此这里先从 List 接口分析.List 方法列表如下: 由于上文「JDK源码分析-Collection」已对 Collec ...

  10. 【JDK】JDK源码分析-AbstractQueuedSynchronizer(2)

    概述 前文「JDK源码分析-AbstractQueuedSynchronizer(1)」初步分析了 AQS,其中提到了 Node 节点的「独占模式」和「共享模式」,其实 AQS 也主要是围绕对这两种模 ...

随机推荐

  1. VMware Workstation安装Red hat7.0联网问题总结

    1.在red hat7当中iconfig命令是被取消了的,开发者用ip addr命令取代了ifconfig命令. 当然也是可以用ifconfig命令的 (前提是你安装的linux是可以联网的): 1) ...

  2. ODM、JDM、OEM概念

    OEM (Original Equipment Manufacturer) - 原始设备制造商 委托生产,或者说“代工生产”,其含义是品牌厂商不直接制造产品,而是负责设计和开发新产品,控制销售“渠道” ...

  3. delphi 鼠标拖动

    GetWindowRect(tgph, Rect); //获得窗体大小 setcursorpos(Rect.Left + 487, Rect.Top + 274); delay(100); mouse ...

  4. lvm快照备份数据库(Mysql5.7)

    备份的目的 能够防止由于机械故障以及人为误操作带来的数据丢失,例如将数据库文件保存在了其它地方. 备份的分类 以操作过程中服务的可用性分: 冷备份:cold backup mysql服务关闭,mysq ...

  5. 201771010118 马昕璐《面向对象程序设计java》第十二周学习总结

    第一部分:理论知识学习部分 用户界面:用户与计算机系统(各种程序)交互的接口 图形用户界面:以图形方式呈现的用户界面 AET:Java 的抽象窗口工具箱包含在java.awt包中,它提供了许多用来设计 ...

  6. C# static 变量 和方法

    静态成员属于类所有,无认创建多少实例对象,静态成员在内存中只有一份:实例成员属于类的实例所有,每创建一个实例对象,实例成员都会在内存中分配一块内存区域. 就像图书馆的书,书的数量就是图书馆这个对象的静 ...

  7. 【react】---17新增的生命周期

    一.废除的生命周期 官网文档指出使用这些生命周期的代码会在未来版本的react中更容易产生bug,尤其是对于异步渲染的版本 由于未来采用异步渲染机制,所以即将在17版本中去掉的生命周期钩子函数 com ...

  8. 下载 mysql 数据库 的步骤 完整版

    1. 官网(点这里)上下载 2. 3. 4. 5. 6. 7.

  9. js 阻止事件执行

    三种阻止事件执行的方式 event.preventDefault() event.stopPropagation() return false event.preventDefault() 阻止特定事 ...

  10. 2019PHP面试题最全面归纳总结

    1.请选择以下代码运行的结果: <?php if ('1e3' == '1000') echo 'LOL'; ?> A 无任何输出结果  B   LOL  C 不执行且报错 解析:1e3 ...