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

Posted

技术标签:

【中文标题】创建一个可变的 java.lang.String【英文标题】:Create a mutable java.lang.String 【发布时间】:2012-06-24 03:58:28 【问题描述】:

众所周知,Java Strings 是不可变的。从一开始,不可变字符串就是对 java 的一个很好的补充。不变性允许快速访问和大量优化,与 C 风格的字符串相比,出错率显着降低,并有助于实施安全模型。

可以在不使用 hack 的情况下创建一个可变的,即

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

但是在纯 Java 中是否有可能,以便可以随时修改字符串?问题是如何

【问题讨论】:

java 没有可调整大小的数组。所有数组length 在实例化后都是最终的且不可变的。 (length 不是一个字段) 您的意思与StringBuilder 不同,这是模拟可变性的推荐方式? 您已经断言存在一种方法可以做到这一点。你知道这是事实吗?这是某种谜题吗? 这可能适合codegolf.stackexchange.com/faq,但我觉得这里不是主题。太糟糕了cannot close while the bounty is active。 @Arjan,您可以随时标记问题或进行编辑。关闭很少是一个好的选择 【参考方案1】:

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

public class MutableStringTest 


    // http://***.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(),但我现在不在乎 :)

【讨论】:

可爱,这是正确答案!由于使用 new String(byte[], offset, len, Charset) 的这个“功能”完全破坏了 b/c 字节 [] 被完全复制 - 即使用 1MB 缓冲区并创建小字符串会破坏任何性能。 好消息,如果在复制返回的char[] 时出现System.getSecurityManager(),这不是安全漏洞。 @Spaeth,它非常可变,对象本身确实会改变其状态 也许有办法使用外部字符列表而不是内部字符数组? 为什么这个答案被否决了?有人不喜欢可变字符串的想法吗? ;-)【参考方案2】:

@mhaller 很好地回答了这个问题。我会说所谓的谜题非常简单,只需查看 String one 的可用 c-tors 就应该能够找出 how 部分,一个

演练

感兴趣的 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;

c-tor 通过传递 Charset 而不是图表集名称来避免查找 chartsetName->charset,提供了据称快速的方法来将 byte[] 转换为 String。 它还允许传递任意 Charset 对象来创建字符串。 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 开发人员会像任何其他字符串构造一样复制数组(例如public String(char value[]))。但是有一个例外 - 如果没有安装 SecurityManager,则不会复制 char[]。

    //Trim the given char array to the given length
    //
    private static char[] safeTrim(char[] ca, int len, Charset cs) 
        if (len == ca.length 
                && (System.getSecurityManager() == null
                || cs.getClass().getClassLoader0() == null))
            return ca;
        else
            return Arrays.copyOf(ca, len);
    

因此,如果没有 SecurityManager,那么绝对有可能拥有一个被字符串引用的可修改 CharBuffer/char[]。

现在一切看起来都很好 - 除了byte[] 也被复制(上面的粗体字)。这是 Java 开发人员在哪里变得懒惰并大错特错。

副本对于防止流氓字符集(上面的示例)能够更改源字节[]是必要的。但是,想象一下大约 512KB byte[] 缓冲区包含少量字符串的情况。试图创建一个小而少的图表 - new String(buf, position, position+32,charset) 导致大量 512KB 字节 [] 副本。如果缓冲区为 1KB 左右,则永远不会真正注意到影响。但是,对于大缓冲区,性能影响确实很大。简单的解决方法是复制相关部分。

...或者java.nio 的设计者考虑过引入只读缓冲区。只需调用 ByteBuffer.asReadOnlyBuffer() 就足够了(如果 Charset.getClassLoader()!=null)* 有时,即使是在 java.lang 工作的人也会完全搞错。

*Class.getClassLoader() 为引导类返回 null,即 JVM 本身附带的类。

【讨论】:

此文字由 Bestsss 通过编辑问题添加。感动,因为它真的是一个答案。【参考方案3】:

我会说 StringBuilder(或用于多线程使用的 StringBuffer)。是的,最后你会得到一个不可变的字符串。但这就是要走的路。

例如,在循环中附加字符串的最佳方法是使用 StringBuilder。当你使用“fu”+变量+“ba”时,Java本身使用StringBuilder。

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

append(blub).append(5).appen("dfgdfg").toString();

【讨论】:

无论如何这不是字符串,充其量是 CharSequence。 String 是 CharSequence(这就是 String 实现 Charsequence^^ 的原因)。 没有字符串是 final 类。 CharSequence 是一个接口。基于同样的理由,两者都扩展了(间接地用于 StringBiuilder/Buffer)java.lang.Object。这个问题正好是关于java.lang.String 这仍然会生成一个字符串,但是 StringBuilder 实现了 CharSequence。所以你可以经常使用 StringBuilder 代替字符串,给你一个 Mutable CharSequence 可以避免 GC 等(我有时喜欢快速打印很多字符串,不希望 GC 成为性能问题)跨度> 【参考方案4】:
// 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(); 
     


【讨论】:

我猜问题中关于 java.lang.reflect 的部分已经逃过了你的视线。该代码在 JDK 7+ 上也会失败【参考方案5】:

不要重新发明***。 Apache commons 提供了这一点。

MutableObject<String> mutableString = new MutableObject<>();

【讨论】:

为什么new 两次?! 糟糕,这是一个错字。【参考方案6】:

交换javajavac 的引导类路径的更简单方法

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

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

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) 创建使用字符串私有字段的测试类

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) 创建空目录target 并编译测试类

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

7) 运行它

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

输出是:

A
X

P.S 这仅适用于修改后的 rt.jar,使用此选项覆盖 rt.jar 违反了 jre 许可证。

【讨论】:

以上是关于创建一个可变的 java.lang.String的主要内容,如果未能解决你的问题,请参考以下文章

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

如何创建一个接受可变数量参数的 Java 方法?

为可变金额和澳元创建一个 PayPal 按钮

为包含可变长度序列的数组的输出标签创建分类 numpy 数组

SAS创建一个可变频率的频率

如何创建一个可变的通用lambda?