近些天,无意间看到一篇博文说,String在jdk9进行了不少优化,其中为了提高字符串拼接效率的优化引起了我的注意。
String就是不可变的值对象,一旦创建就不可更改。这也是我们不管是replace方法,还是substring方法,都是返回一个新的对象的重要原因。
新增3个包私有构造器,这样说可能没感觉,我先上源码!
/*
* Package private constructor. Trailing Void argument is there for
* disambiguating it against other (public) constructors.
*
* Stores the char[] value into a byte[] that each byte represents
* the8 low-order bits of the corresponding character, if the char[]
* contains only latin1 character. Or a byte[] that stores all
* characters in their byte sequences defined by the {@code StringUTF16}.
*/
String(char[] value, int off, int len, Void sig) {
if (len == 0) {
this.value = "".value;
this.coder = "".coder;
return;
}
if (COMPACT_STRINGS) {
byte[] val = StringUTF16.compress(value, off, len);
if (val != null) {
this.value = val;
this.coder = LATIN1;
return;
}
}
this.coder = UTF16;
this.value = StringUTF16.toBytes(value, off, len);
}
/*
* Package private constructor. Trailing Void argument is there for
* disambiguating it against other (public) constructors.
*/
String(AbstractStringBuilder asb, Void sig) {
byte[] val = asb.getValue();
int length = asb.length();
if (asb.isLatin1()) {
this.coder = LATIN1;
this.value = Arrays.copyOfRange(val, 0, length);
} else {
if (COMPACT_STRINGS) {
byte[] buf = StringUTF16.compress(val, 0, length);
if (buf != null) {
this.coder = LATIN1;
this.value = buf;
return;
}
}
this.coder = UTF16;
this.value = Arrays.copyOfRange(val, 0, length << 1);
}
}
/*
* Package private constructor which shares value array for speed.
*/
String(byte[] value, byte coder) {
this.value = value;
this.coder = coder;
}
前两个构造器还算正常,但最后一个构造器居然直接使用入参的byte[]作为String的value。这意味着,String有可能是可变的!这可是个大问题。
由于是包私有的,所以这里用反射操作一波,毕竟也就只有setAccessible这个大流氓能干这些鸡鸣狗盗之事了(手动滑稽)
@Test
public void testString1() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<?> aClass = Class.forName("java.lang.String");
Constructor<?> constructor = aClass.getDeclaredConstructor(byte[].class, byte.class);
constructor.setAccessible(true);
byte[] bytes = new byte[6];
String str = (String) constructor.newInstance(bytes, (byte)0);
bytes[0] = 'h';
bytes[1] = 'e';
bytes[2] = 'l';
bytes[3] = 'l';
logger.info("identicalHashCode:{}, str:{}", System.identityHashCode(str), str);
bytes[4] = 'o';
bytes[5] = '.';
logger.info("identicalHashCode:{}, str:{}", System.identityHashCode(str), str);
bytes[5] = '!';
logger.info("identicalHashCode:{}, str:{}", System.identityHashCode(str), str);
}
输出结果:
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.evan.datastructure.LinkedList06 (file:/D:/InfoTechHome/workspace/datastructure/target/test-classes/) to constructor java.lang.String(byte[],byte)
WARNING: Please consider reporting this to the maintainers of com.evan.datastructure.LinkedList06
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
00:33:11.963 [main] INFO com.evan.datastructure.LinkedList06 - identicalHashCode:660017404, str:hell
00:33:11.963 [main] INFO com.evan.datastructure.LinkedList06 - identicalHashCode:660017404, str:hello.
00:33:11.963 [main] INFO com.evan.datastructure.LinkedList06 - identicalHashCode:660017404, str:hello!
Process finished with exit code 0
这个结果有没有超出想象?这是同一个String对象,值却不停地在变化!与此同时,反射调用告警:非法的反射调用,将在未来版本中被拒绝!
但是不必惊讶,并非只有新的这个构造器才能达到这种效果。以反射大流氓的能力,直接操作String中的value属性,也能达到修改String内容的效果!
因此强调一下,Java的封装,大家还是尽量不要破坏它,这可能导致一些不安全问题。例如上面的String骚操作修改了原字符串后,如果直接修改的字面量的对象内容,那么将导致原字面量地址跟新内容的字符地址是一样的,不管是使用==还是equals都无法区分!
为了提高String的操作效率,jdk9进行了一些优化:
很多时候我们的代码都会进行字符串拼接操作,
String a = “A” + 1 + “h”;
String b = a + ‘d’ + “h”;
对于第一种情况,可以在编译期就优化成字面量,从而不需要拼接。但是对于第二种情况,每拼接一次就会创建一个新的String对象。
到这里总算是把事情衔接上了。为了实现上述的优化思路,首当其冲的就是String本身,这也是新增构造器的意义。然后,为了方便操作value数组,引入了StringConcatHelper,专门干拼接的活。但是这还没完,工具算是搞定了,但是怎么知道要申请多大的value数组呢?这必须得计算一下。
思前想后,如果可以把这些+拼接的变量全部拿到,那不就可以了。但是要用什么方式呢?直接调用StringConcatHelper?这可不行,因为为了封装安全,只有java.lang包下的类才能直接访问。而业务类肯定都不在此列。使用反射?那更不行,本来就是为了提高效率做优化的,使用反射不是倒退了?于是,java7中引入的invokedynamic上场了!
相比于invokeinterface和invokevirtual,invokedynamic直接由应用程序来选择目标方法。invokedynamic的参数指向的是启动方法BootstrapMethod,该方法会生成一个CallSite调用点。在调用点中封装了目标方法的方法句柄。
方法句柄-MethodHandle,作为invokedynamic的基石,跟反射有什么区别?
再次回到前文:jdk9的字符串拼接:
现在整个调用就可以这样串起来了:
封装MethodHandle,调用时通过invokestatic调用StringConcatHelper。
通过BootstrapMethod,将封装好的MethodHandle再次封装为CallSite。
最后是invokedynamic指令调用。