マイペースなプログラミング日記

x86エミュレータやFPGA、WebGLにお熱なd-kamiがマイペースに書くブログ

Stringについて知らなかったこと

Stringは新しい文字列ができる度にインスタンスが生成されるというから、内部に持っているcharの配列型の変数も毎回newしてるのかと思っていたがsubstringやtrim、splitなどでは元の文字列が持っているcharの配列が共有されるらしい。


それを確かめるために、リフレクションを使ったプログラムを書いてみた。Stringが持っているcharの配列は当然privateになってるはずなので、ClassクラスのgetDeclaredFieldsでprivateなフィールド全てを取得して表示させてみた。

import java.lang.reflect.Field;

public class StringTest{
    public static void main(String[] args)
            throws ClassNotFoundException, IllegalAccessException{

        Class classString = Class.forName("java.lang.String");
        Field[] fields = classString.getDeclaredFields();

        String source = "String Test";
        String dest   = source.substring(7, 11);

        System.out.println("### sourceの内容: " + source + " ###");

        for(Field field : fields){
            field.setAccessible(true);
            System.out.println(field.getName() + ": " + field.get(source));
        }

        System.out.println();

        System.out.println("### destの内容: " + dest + " ###");

        for(Field field : fields){
            field.setAccessible(true);
            System.out.println(field.getName() + ": " + field.get(dest));
        }
    }
}

このプログラムを動かした結果は


### sourceの内容: String Test ###
value: [C@19821f
offset: 0
count: 11
hash: 0
serialVersionUID: -6849794470754667710
serialPersistentFields: [Ljava.io.ObjectStreamField;@42e816
CASE_INSENSITIVE_ORDER: java.lang.String$CaseInsensitiveComparator@9304b1

### destの内容: Test ###
value: [C@19821f
offset: 7
count: 4
hash: 0
serialVersionUID: -6849794470754667710
serialPersistentFields: [Ljava.io.ObjectStreamField;@42e816
CASE_INSENSITIVE_ORDER: java.lang.String$CaseInsensitiveComparator@9304b1

こんな感じになった。Stringが持つcharの配列型は名前からしてvalueだろう。ハッシュ値らしきものが表示されていて、値が同じなので同じインスタンスだとは思う。そしてoffset番目からcount数までをこのStringオブジェクトの文字列としているのだろうとは思う。しかし、これではvalueが本当に一致しているかはわかりづらい。なのでvalueをcharの配列だと信じて新しいプログラムを作る。今回はvalueだけ取得して、その内容を表示してみた

import java.lang.reflect.Field;

public class StringTest2{
    public static void main(String[] args)
            throws ClassNotFoundException, IllegalAccessException,
                   NoSuchFieldException{

        Class classString = Class.forName("java.lang.String");
        Field fieldValue = classString.getDeclaredField("value");
        fieldValue.setAccessible(true);

        String source = "String Test";
        String dest   = source.substring(7, 11);

        System.out.println("### sourceの内容: " + source + " ###");
        System.out.print("value: ");

        char[] sourceChars = (char[])fieldValue.get(source);
        for(char c : sourceChars)
            System.out.print(c);

        System.out.println("\n");

        System.out.println("### destの内容: " + dest + " ###");
        System.out.print("value: ");

        char[] destChars = (char[])fieldValue.get(dest);
        for(char c : destChars)
            System.out.print(c);

        System.out.println();
    }
}

そしてその結果は


### sourceの内容: String Test ###
value: String Test

### destの内容: Test ###
value: String Test

とvalueの中身は一致した。destの方はTestという文字列なのにも関わらず、内部のvalueはString Testを持っている。trimやsplitでも試したけど元の文字列とvalueの中身は同じだった。これでは元の文字列への参照がなくなっても、substringなどで取り出した文字列への参照があればvalueは解放されないまま、無駄にメモリを使うことになるのかな?結構重要かも