众所周知,JavaString
是不可变的。从一开始,不可变字符串就是对 java 的一个很好的补充。不变性允许快速访问和大量优化,与 C 风格的字符串相比,出错率显着降低,并有助于实施安全模型。
可以在不使用 hack 的情况下创建一个可变的,即
java.lang.reflect
sun.misc.Unsafe
- 引导类加载器中的类
- JNI(或 JNA,因为它需要 JNI)
但是在纯 Java 中是否有可能,以便可以随时修改字符串?问题是如何?
使用 Charset 构造函数创建一个java.lang.String
,可以注入你自己的 Charset,它会带来你自己的CharsetDecoder
. 在decodeLoop 方法中CharsetDecoder
获取对对象的引用。CharBuffer
CharBuffer 包装了原始 String 对象的 char[]。由于 CharsetDecoder 具有对它的引用,因此您可以使用 CharBuffer 更改底层 char[],因此您有一个可变字符串。
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 one 的可用 c-tors 就应该能够找出如何部分,a
演练
感兴趣的 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 而不是图表集名称来提供所谓的快速转换byte[]
为 String 的方法,以避免查找 chartsetName->charset。它还允许传递任意 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 开发人员复制数组,就像任何其他 String 构造(例如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 本身附带的类。
我会说 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();
// 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(); }
}
}
不要重新发明轮子。Apache commons提供了这一点。
MutableObject<String> mutableString = new MutableObject<>();
交换和的引导类路径的更简单java
方法javac
1)转到jdk安装并复制到单独的文件夹rt.jar
和src.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
PS这仅适用于修改rt.jar
并使用此选项来覆盖rt.jar
违反jre
许可证。