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]