Question:
public static void main(String[] args) {
String s = new String("abc");
// 在这中间可以添加N⾏代码,但必须保证s引⽤的指向不变,最终将输出变成abcd
System.out.println(s);
}
其实是想问:如何在不改变s引用的指向的情况下,将输出变成abcd的方法。考虑通过反射。
package com.alau;
import java.lang.reflect.Field;
public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s = new String("abc");
System.out.println(System.identityHashCode(s));
// 在这中间可以添加N⾏代码,但必须保证s引⽤的指向不变,最终将输出变成abcd
Field value = s.getClass().getDeclaredField("value");
value.setAccessible(true);
value.set(s, "abcd".toCharArray());
System.out.println(s);
/*String s = new String("abc");
// 使用反射机制修改String对象的值
try {
// 获取String类中的value字段
Field valueField = String.class.getDeclaredField("value");
// 设置该字段为可访问的
valueField.setAccessible(true);
// 获取s对象中的value属性的值
char[] value = (char[]) valueField.get(s);
// 改变value所引用的数组中的第4个字符
value[3] = 'd';
} catch (Exception e) {
e.printStackTrace();
}
// 输出修改后的字符串
System.out.println(s); // 输出abcd*/
}
}
执行结果:
Exception in thread "main" java.lang.IllegalArgumentException: Can not set final [B field java.lang.String.value to [C
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167)
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:171)
at java.base/jdk.internal.reflect.UnsafeQualifiedObjectFieldAccessorImpl.set(UnsafeQualifiedObjectFieldAccessorImpl.java:83)
at java.base/java.lang.reflect.Field.set(Field.java:780)
at com.alau.Test.main(Test.java:13)
这与预期不符。
进到String类一看,怎么成byte[]了。因为新项目更新了jdk11.所以我环境都是11了。
切回jdk1.8可以正常执行。
D:\dev\tools\jdk1.8.0_131\bin\java.exe -javaagent:D:\dev\tools\idea\ideaIU-2023.1.win\lib\idea_rt.jar=54875:D:\dev\tools\idea\ideaIU-2023.1.win\bin -Dfile.encoding=UTF-8 -classpath D:\dev\tools\jdk1.8.0_131\jre\lib\charsets.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\deploy.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\ext\access-bridge-64.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\ext\cldrdata.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\ext\dnsns.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\ext\jaccess.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\ext\jfxrt.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\ext\localedata.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\ext\nashorn.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\ext\sunec.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\ext\sunjce_provider.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\ext\sunmscapi.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\ext\sunpkcs11.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\ext\zipfs.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\javaws.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\jce.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\jfr.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\jfxswt.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\jsse.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\management-agent.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\plugin.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\resources.jar;D:\dev\tools\jdk1.8.0_131\jre\lib\rt.jar;D:\workspaces\demo\ReflectDemo\ReflectDemo\target\classes com.alau.TestReflect
abcd
Process finished with exit code 0
对比了下jdk11和jdk8的String类
jdk8
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}
jdk11
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value;
/**
* The identifier of the encoding used to encode the bytes in
* {@code value}. The supported values in this implementation are
*
* LATIN1
* UTF16
*
* @implNote This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*/
private final byte coder;//LATIN1或者UTF16
static final boolean COMPACT_STRINGS;
static {
COMPACT_STRINGS = true;
}
...
}
对比发现,jdk8是char[],jdk11是byte[]。这一改动是从jdk9开始,由此牵扯出下一个问题。
Java 9为何要将String的底层实现由char[]改成了byte[]的原因
Java 9的目的是为了节省String占用的内存空间,因为在大多数Java程序的堆里,String对象和char数组占用的空间最大,而绝大多数String对象只包含Latin-1字符,这些字符只需要一个字节就够了,而char类型占用两个字节,导致了内存的浪费。12 [3][3]
Java 9的方法是将String类的内部表示从UTF-16字符数组改成字节数组加一个编码标志字段,根据字符串的内容,使用Latin-1或UTF-16编码,如果是Latin-1编码,每个字符占用一个字节,如果是UTF-16编码,每个字符占用两个字节,这样就可以根据实际情况动态调整内存分配,提高了内存利用率。12 [3][3]
Java 9为什么不使用UTF-8编码呢?因为UTF-8是变长的编码,一个字符可能占用1到4个字节,这样就不利于String类的随机访问方法,例如charAt和subString,需要从头开始遍历每个字符的长度,才能找到指定位置的字符,效率很低。而UTF-16虽然也是变长的编码,但是在Java中,一个字符(char)就是两个字节,占用四个字节的字符用两个char来存储,String类的各种操作都是以char为单位的,所以UTF-16在Java中可以视为一个定长的编码,方便进行随机访问。12 [3][3]Java 9的目的是为了节省String占用的内存空间,因为在大多数Java程序的堆里,String对象和char数组占用的空间最大,而绝大多数String对象只包含Latin-1字符,这些字符只需要一个字节就够了,而char类型占用两个字节,导致了内存的浪费。12 [3][3]
Java 9的方法是将String类的内部表示从UTF-16字符数组改成字节数组加一个编码标志字段,根据字符串的内容,使用Latin-1或UTF-16编码,如果是Latin-1编码,每个字符占用一个字节,如果是UTF-16编码,每个字符占用两个字节,这样就可以根据实际情况动态调整内存分配,提高了内存利用率。12 [3][3]
Java 9为什么不使用UTF-8编码呢?因为UTF-8是变长的编码,一个字符可能占用1到4个字节,这样就不利于String类的随机访问方法,例如charAt和subString,需要从头开始遍历每个字符的长度,才能找到指定位置的字符,效率很低。而UTF-16虽然也是变长的编码,但是在Java中,一个字符(char)就是两个字节,占用四个字节的字符用两个char来存储,String类的各种操作都是以char为单位的,所以UTF-16在Java中可以视为一个定长的编码,方便进行随机访问。12 [3][3]