创build一个可变的java.lang.String

Java String是不可改变的常识。 不可变的string是Java自成立以来的重要补充。 不变性允许快速访问和大量的优化,与C风格的string相比,显着减less错误,并有助于实施安全模型。

有可能创build一个可变的不使用黑客,即

  • java.lang.refect
  • sun.misc.Unsafe
  • 引导类加载器中的类
  • JNI(或JNA,因为它需要JNI)

但是,在普通的Java中是否可以这样做,以便随时可以修改string? 问题是如何

使用Charset构造函数创build一个java.lang.String ,可以注入自己的Charset,它带有您自己的CharsetDecoderCharsetDecoder获取对decodeLoop方法中的CharBuffer对象的引用。 CharBuffer包装原始String对象的char []。 由于CharsetDecoder有一个引用,所以你可以使用CharBuffer来改变底层的char [],因此你有一个可变的String。

 public class MutableStringTest { // http://stackoverflow.com/questions/11146255/how-to-create-mutable-java-lang-string#11146288 @Test public void testMutableString() throws Exception { final String s = createModifiableString(); System.out.println(s); modify(s); System.out.println(s); } private final AtomicReference<CharBuffer> cbRef = new AtomicReference<CharBuffer>(); private String createModifiableString() { Charset charset = new Charset("foo", null) { @Override public boolean contains(Charset cs) { return false; } @Override public CharsetDecoder newDecoder() { CharsetDecoder cd = new CharsetDecoder(this, 1.0f, 1.0f) { @Override protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) { cbRef.set(out); while(in.remaining()>0) { out.append((char)in.get()); } return CoderResult.UNDERFLOW; } }; return cd; } @Override public CharsetEncoder newEncoder() { return null; } }; return new String("abc".getBytes(), charset); } private void modify(String s) { CharBuffer charBuffer = cbRef.get(); charBuffer.position(0); charBuffer.put("xyz"); } } 

运行代码打印

 abc zzz 

我不知道如何正确实现decodeLoop(),但我现在不在乎:)

这个问题通过@mhaller得到了很好的回答。 我想说所谓的谜题是相当容易的,只要看看可用的string的一个人应该能够找出如何部分,一个

演练

感兴趣的C-tor在下面,如果你要破解/破解/寻找安全漏洞,总是寻找非最终的任意类。 这里的情况是java.nio.charset.Charset

 //String public String(byte bytes[], int offset, int length, Charset charset) { if (charset == null) throw new NullPointerException("charset"); checkBounds(bytes, offset, length); char[] v = StringCoding.decode(charset, bytes, offset, length); this.offset = 0; this.count = v.length; this.value = v; } 

通过传递字符集而不是字符集名称来避免查找chartsetName-> charset,c-tor提供了将byte[]转换为string的方法。 它也允许传递一个任意的Charset对象来创buildString。 Charset主路由将java.nio.ByteBuffer的内容转换为CharBuffer 。 CharBuffer可能持有对char []的引用,并且可以通过array() ,而CharBuffer也是完全可修改的。

 //StringCoding static char[] decode(Charset cs, byte[] ba, int off, int len) { StringDecoder sd = new StringDecoder(cs, cs.name()); byte[] b = Arrays.copyOf(ba, ba.length); return sd.decode(b, off, len); } //StringDecoder char[] decode(byte[] ba, int off, int len) { int en = scale(len, cd.maxCharsPerByte()); char[] ca = new char[en]; if (len == 0) return ca; cd.reset(); ByteBuffer bb = ByteBuffer.wrap(ba, off, len); CharBuffer cb = CharBuffer.wrap(ca); try { CoderResult cr = cd.decode(bb, cb, true); if (!cr.isUnderflow()) cr.throwException(); cr = cd.flush(cb); if (!cr.isUnderflow()) cr.throwException(); } catch (CharacterCodingException x) { // Substitution is always enabled, // so this shouldn't happen throw new Error(x); } return safeTrim(ca, cb.position(), cs); } 

为了防止改变char[] java开发人员复制数组很像任何其他string构造(例如public String(char value[]) )。 但是,有一个例外 – 如果没有安装SecurityManager,则不会复制char []。

     //修剪给定的字符数组到给定的长度
     //
     private static char [] safeTrim(char [] ca,int len,Charset cs){
        如果(len == ca.length 
                 &&(System.getSecurityManager()== null
                 ||  cs.getClass()。getClassLoader0()== null))
            返回ca;
        其他
            返回Arrays.copyOf(ca,len);
     }

所以如果没有SecurityManager,那么可以通过String来引用可修改的CharBuffer / char []。

一切看起来很好 – 除了byte[]也被复制(上面的粗体)。 这是Java开发者懒惰和大量错误的地方。

副本是必要的,以防止stream氓字符集(上面的例子)能够改变源字节[]。 但是,想象一下包含less数String的大约512KB byte[]缓冲区的情况。 试图创build一个小的,less数图表 – new String(buf, position, position+32,charset)导致大量的512KB字节[]复制。 如果缓冲区大小为1KB左右,影响将永远不会被真正注意到。 然而,使用大缓冲区的情况下,性能的影响是非常巨大的。 简单的修复就是复制相关的部分。

…或者java.nio的devise者想通过引入只读缓冲区来思考。 简单地调用ByteBuffer.asReadOnlyBuffer()就足够了(如果Charset.getClassLoader()!= null)*有时候,即使是使用java.lang也可能完全错误。

* Class.getClassLoader()为引导类返回null,即与JVM本身一起提供的类。

我会说StringBuilder(或multithreading使用StringBuffer)。 是的,最后你得到一个不可变的string。 但是,这是要走的路。

例如,在循环中追加string的最好方法是使用StringBuilder。 当你使用“fu”+variables+“ba”时,Java本身使用StringBuilder。

http://docs.oracle.com/javase/6/docs/api/java/lang/StringBuilder.html

追加(泡壳).append(5).appen( “dfgdfg”)的toString();

 // How to achieve String Mutability import java.lang.reflect.Field; public class MutableString { public static void main(String[] args) { String s = "Hello"; mutate(s); System.out.println(s); } public static void mutate(String s) { try { String t = "Hello world"; Field val = String.class.getDeclaredField("value"); Field count = String.class.getDeclaredField("count"); val.setAccessible(true); count.setAccessible(true); count.setInt (s, t.length ()); val.set (s, val.get(t)); } catch (Exception e) { e.printStackTrace(); } } } 

简单的方法来交换javajavac引导类path

1)转到jdk安装并复制到单独的文件夹rt.jarsrc.zip

2)从源zip中解压String.java并将其内部char数组的私有字段值更改为public

 public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ public final char value[]; 

3)在javac的帮助下编译修改过的String.java:

 javac String.java 

4)将编译的String.class和其他编译的类移动到该目录中的rt.jar

5)创build使用String专用字段的testing类

 package exp; class MutableStringExp { public static void main(String[] args) { String letter = "A"; System.out.println(letter); letter.value[0] = 'X'; System.out.println(letter); } } 

6)创build空dir target并编译testing类

 javac -Xbootclasspath:rt.jar -d target MutableStringExp.java 

7)运行它

 java -Xbootclasspath:rt.jar -cp "target" exp.MutableStringExp 

输出是:

 A X 

PS这只会与修改rt.jar和使用此选项来覆盖rt.jar是违反jre许可证。