2.3 去重的好处
false true复制代码
输出
String x = new String(new char[]{'a', 'b', 'c'}); // 野生的 String z = x.intern(); // 野生的 x 被复制后加入 StringTable,StringTable 中有了 "abc" String y = "abc"; // 已有,不会产生新的对象,用的是 StringTable 中 "abc" System.out.println(z == x); System.out.println(z == y);复制代码
例子,代码同上面 1.7 相同
xsStringTableintern()如果没有将x引用的对象复制将复制后的对象加入返回 StringTable 对象xsStringTable复制代码
String x = ...; String s = x.intern();复制代码
如果 StringTable 中没有(1.6 JDK 的做法)
true true复制代码
输出
String x = new String(new char[]{'a', 'b', 'c'}); // 野生的 String z = x.intern(); // 野生的 x 加入 StringTable,StringTable 中有了 "abc" String y = "abc"; // 已有,不会产生新的对象,用的是 StringTable 中 "abc" System.out.println(z == x); System.out.println(z == y);复制代码
例子
xsStringTableintern()如果没有将x引用的对象加入返回 StringTable 对象xsStringTable复制代码
String x = ...; String s = x.intern();复制代码
如果 StringTable 中没有(1.7 以上 JDK 的做法)
true false复制代码
输出
String x = new String(new char[]{'a', 'b', 'c'}); // 野生的 String y = "abc"; // 将 "abc" 加入 StringTable String z = x.intern(); // 已有,返回 StringTable 中 "abc",即 y System.out.println(z == y); System.out.println(z == x);复制代码
例子
xsStringTableintern()如果已有返回 StringTable 对象xsStringTable复制代码
总会返回家养的 String 对象
String x = ...; String s = x.intern();复制代码
如果 StringTable 中已有
它会尝试将调用者放入 StringTable
public native String intern();复制代码
字符串提供了 intern 方法来实现去重,让字符串对象有机会受到 StringTable 的管理
野生的字符串也有机会得到教育
子曰:有教无类
- 《 论语· 卫灵公》
2.2 收留野生字符串
当代码运行到一个字面量 "abc" 时,会首先检查 StringTable 中有没有相同的 key,如果没有,创建新字符串对象加入;否则直接返回已有的字符串对象
如何保证家养的字符串对象不重复呢?JDK 使用了 StringTable 来解决,StringTable 是采用 c++ 代码编写的,数据结构上就是一个 hash 表,字符串对象就充当 hash 表中的 key,key 的不重复性,是 hash 表的基本特性
- 字面量方式创建的字符串,会放入 StringTable 中,StringTable 管理的字符串,才具有不重复的特性,这种就像是家养的
- 而 char[],byte[],int[],String,以及 + 方式本质上都是使用 new 来创建,它们都是在堆中创建新的字符串对象,不会考虑字符串重不重复,这种就像是野生的,野生字符串的缺点就是如果存在大量值相同的字符串,对内存占用非常严重
前面我们讲解了 String 的六种创建方式,除了字面量方式创建的字符串是家养的以外,其它方法创建的字符串都是野生的。什么意思呢?
其实字符串也一样,分为家养的和野生的。
京城何日多灯火,让星也羞臊。时有弦月清冷,照我无聊
- 米人《夜辍香山》
2.1 家养与野生
二、字符串之家 - StringTable
JDK 9 当然做的更为专业,可以适配生成不同的参数个数、类型的 MethodHandle,但原理就是这样。
hello,world复制代码
输出
String x = "hello,"; String y = "world"; String s = (String) mh.invoke(x, y);复制代码
最终就可以使用该 MethodHandle 反射完成字符串拼接了
// 生成匿名类所需字节码 byte[] bytes = dump(); // 根据字节码生成匿名类.class Class<?> innerClass = UnsafeAccessor.UNSAFE .defineAnonymousClass(TestString4.class, bytes, null); // 确保匿名类初始化 UnsafeAccessor.UNSAFE.ensureClassInitialized(innerClass); // 找到匿名类中 String concat(String x, String y) MethodHandle mh = MethodHandles.lookup().findStatic( innerClass, "concat", MethodType.methodType(String.class, String.class, String.class) );复制代码
接下来就可以生成匿名类,供 MethodHandler 反射调用
public static String concat(String x, String y) { return new StringBuilder().append(x).append(y).toString(); }复制代码
这么多字节码主要目的仅仅是生成一个匿名类的字节码,其中包括了拼接方法
public static byte[] dump() { ClassWriter cw = new ClassWriter(0); FieldVisitor fv; MethodVisitor mv; AnnotationVisitor av0; cw.visit(52, ACC_PUBLIC + ACC_SUPER, "cn/itcast/string/TestString4", null, "java/lang/Object", null); cw.visitSource("TestString4.java", null); { mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv.visitCode(); Label l0 = new Label(); mv.visitLabel(l0); mv.visitLineNumber(3, l0); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv.visitInsn(RETURN); Label l1 = new Label(); mv.visitLabel(l1); mv.visitLocalVariable("this", "Lcn/itcast/string/TestString4;", null, l0, l1, 0); mv.visitMaxs(1, 1); mv.visitEnd(); } { mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "concat", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", null, null); mv.visitCode(); Label l0 = new Label(); mv.visitLabel(l0); mv.visitLineNumber(9, l0); mv.visitTypeInsn(NEW, "java/lang/StringBuilder"); mv.visitInsn(DUP); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); mv.visitVarInsn(ALOAD, 1); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false); mv.visitInsn(ARETURN); Label l1 = new Label(); mv.visitLabel(l1); mv.visitLocalVariable("x", "Ljava/lang/String;", null, l0, l1, 0); mv.visitLocalVariable("y", "Ljava/lang/String;", null, l0, l1, 1); mv.visitMaxs(2, 2); mv.visitEnd(); } cw.visitEnd(); return cw.toByteArray(); }复制代码
可以使用 asm 生成匿名类字节码
public class UnsafeAccessor { static Unsafe UNSAFE; static { try { Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); UNSAFE = (Unsafe) theUnsafe.get(null); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); } } }复制代码
Unsafe 对象访问类
2. 字节码生成方法
但这样需要自己提供 concat 方法,而且其参数个数都固定死了,能否动态生成这么一个方法呢,答案是肯定的,为了简化生成逻辑,这里我仍然以固定参数为例
hello,world复制代码
输出
String x = "hello,"; String y = "world"; MethodHandle mh = MethodHandles.lookup().findStatic( TestString4.class, "concat", MethodType.methodType(String.class, String.class, String.class) ); String s = (String) mh.invoke(x,y); System.out.println(s);复制代码
用 MethodHandle 反射调用
public static String concat(String x, String y) { return new StringBuilder().append(x).append(y).toString(); }复制代码
提供一个拼接方法
1. 方法手动生成
public static String concat(String x, String y) { return new StringBuilder().append(x).append(y).toString(); }复制代码
其中 + 可以被 invokedynamic 优化为多种实现策略,如果让我自己来实现,我仅会用 StringBuilder 来拼接,因此我希望 x+y 能够被翻译为对下面方法的调用
String x = "hello,"; String y = "world"; String s = x + y;复制代码
先说明一下我的目的
接下来我模拟其中一种策略的实现过程:以字节码指令生成拼接方法为例