guijiu
@guijiu (taka kuwa)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

[Java] Stringクラスはなぜイミュータブルなのですか?

解決したいこと

Stringクラスはなぜイミュータブルなのですか?

結城浩著「Java言語で学ぶデザインパターン入門【マルチスレッド編】」p.89冒頭に次のように記述があります。

「java.lang.Stringは文字列を表すクラスです。Stringクラスには、文字列の内容を変更するメソッドが用意されていません。つまり、Stringのインスタンスが表している文字列の内容は、決して変化しないことになります。」

ここで疑問が湧きます。例えば、

String s = "abc";

と、String インスタンスsにabcと文字列を与えます。
次に、sに別の文字列を与えます。

s = "DEF";

もちろん、Stringのインスタンス sの文字列は変更できます。「変更できる」ということは、「Stringのインスタンスが表している文字列の内容は、変化している」ように思うのですが、違うのでしょうか?

著者の言わんとすることや、私の理解の足らないところを、ご指摘いただければと思います。

よろしくお願いします。

0

4Answer

「Stringのインスタンスが表している文字列の内容は、変化している」

"abc"のときのインスタンスと、"DEF"のインスタンスは別物です。
"abc"のときのインスタンスが表している文字列が変化した訳ではありませんね。

2Like

Comments

  1. @guijiu

    Questioner

    ご教示いただき、ありがとうございます。

    ご教示の「"abc"のときのインスタンスと、"DEF"のインスタンスは別物」という観点から調べ直し、考え直してみました。

    1.s自体はString型の変数(値を入れる箱)であり、インスタンスではない。
        ーー>"abc"がインスタンスである。
      s="DEF"としたら、これはsと言う箱に"DEF"のインスタンスを入れたことを意味する。

    2.s="abc"の時、t=s.toUpperCase()とのようにメソッドで文字列を操作しても、変数sに入っているインスタンス"abc"自体は変化しない。貴殿が言われるように「インスタンスが表している文字列"abc"が変化した訳ではない。」

    3.例えばs="abc"の時、s+="xyz"と言う式を実行するとJavaコンパイラは、ミュータブルな文字列クラスであるStringBufferを使い、

    StringBuffer temp = new StringBuffer();
    temp.append(s);
    temp.append("xyz");
    s = temp.toString();
    

    を実行する。

    このような理解ででいいでしょうか?

  2. その理解でおおよそ正しいと思います。
    なお、内部実装では、StringBuffer ではなく、StringConcatFactory.makeConcatWithConstants が使われていることを確認しました。

    $ cat Main.java
    import java.util.*;
    
    public class Main {
        public static void main(String[] args) throws Exception {
            String s = "abc";
            s += "xyz";
    //      System.out.println(s);
        }
    }
    
    $ javac Main.java
    
    $ javap -v Main.class
    Classfile /Users/username/Main.class
      Last modified 2024/10/11; size 778 bytes
      SHA-256 checksum 99a30fbcda7276ec6c4d085675d07d26cb92e0dcd543439c6ec157ddaaf2903a
      Compiled from "Main.java"
    public class Main
      minor version: 0
      major version: 61
      flags: (0x0021) ACC_PUBLIC, ACC_SUPER
      this_class: #13                         // Main
      super_class: #2                         // java/lang/Object
      interfaces: 0, fields: 0, methods: 2, attributes: 3
    Constant pool:
       #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
       #2 = Class              #4             // java/lang/Object
       #3 = NameAndType        #5:#6          // "<init>":()V
       #4 = Utf8               java/lang/Object
       #5 = Utf8               <init>
       #6 = Utf8               ()V
       #7 = String             #8             // abc
       #8 = Utf8               abc
       #9 = InvokeDynamic      #0:#10         // #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
      #10 = NameAndType        #11:#12        // makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
      #11 = Utf8               makeConcatWithConstants
      #12 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
      #13 = Class              #14            // Main
      #14 = Utf8               Main
      #15 = Utf8               Code
      #16 = Utf8               LineNumberTable
      #17 = Utf8               main
      #18 = Utf8               ([Ljava/lang/String;)V
      #19 = Utf8               Exceptions
      #20 = Class              #21            // java/lang/Exception
      #21 = Utf8               java/lang/Exception
      #22 = Utf8               SourceFile
      #23 = Utf8               Main.java
      #24 = Utf8               BootstrapMethods
      #25 = MethodHandle       6:#26          // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      #26 = Methodref          #27.#28        // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      #27 = Class              #29            // java/lang/invoke/StringConcatFactory
      #28 = NameAndType        #11:#30        // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      #29 = Utf8               java/lang/invoke/StringConcatFactory
      #30 = Utf8               (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      #31 = String             #32            // \u0001xyz
      #32 = Utf8               \u0001xyz
      #33 = Utf8               InnerClasses
      #34 = Class              #35            // java/lang/invoke/MethodHandles$Lookup
      #35 = Utf8               java/lang/invoke/MethodHandles$Lookup
      #36 = Class              #37            // java/lang/invoke/MethodHandles
      #37 = Utf8               java/lang/invoke/MethodHandles
      #38 = Utf8               Lookup
    {
      public Main();
        descriptor: ()V
        flags: (0x0001) ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 3: 0
    
      public static void main(java.lang.String[]) throws java.lang.Exception;
        descriptor: ([Ljava/lang/String;)V
        flags: (0x0009) ACC_PUBLIC, ACC_STATIC
        Code:
          stack=1, locals=2, args_size=1
             0: ldc           #7                  // String abc
             2: astore_1
             3: aload_1
             4: invokedynamic #9,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
             9: astore_1
            10: return
          LineNumberTable:
            line 5: 0
            line 6: 3
            line 8: 10
        Exceptions:
          throws java.lang.Exception
    }
    SourceFile: "Main.java"
    BootstrapMethods:
      0: #25 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
        Method arguments:
          #31 \u0001xyz
    InnerClasses:
      public static final #38= #34 of #36;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
    

    ご参考まで

  3. @guijiu

    Questioner

    文字列のインスタンスについて、私、そこそこ理解できているようですね。ありがとうございます。

    またs+="xyz"について、StringConcatFactory.makeConcatWithConstantsが使われていることも、初めて知りました。残念ですが、逆コンパイラの記述を読めないのが残念です。容易くスラスラとプログラミングができるようになったら、解読にも挑戦したいのですが。

    StringConcatFactoryは、デザインパターンのFactoryパターンなのでしょうね。(勝手な想像ですが。)

    StringConcatFactory.makeConcatWithConstantsについて、サックと検索してみたら、s+="xyz"のコンパイル方法(結果)についての記事を見つけました。(参考までに)
    https://www.slideshare.net/slideshow/jep280-java-9/82267503
    この記事によると、java8まではStringBufferを使っていたが、Java9ではInvokeDynamicで処理すると書かれていました。(InvokeDynamicについても、私には理解できませんが。)
    分かったことは、Javaのバージョンにより、コンパイルの結果が違うということです。

    この度は、ありとうございました。

質問者さんの言う「イミュータブル」の意味が不明ですが、そこはちょっと置いといて・・・

まず、データ型は理解してますか? そして、String は参照型であることは理解してますか?

さらに、String s = "abc"; というのは、"abc" というインスタンスを作って、そのインスタンスのアドレスを変数 s に代入しているということはわかりますか?

本に書いてあるという、

Stringクラスには、文字列の内容を変更するメソッドが用意されていません。つまり、Stringのインスタンスが表している文字列の内容は、決して変化しないことになります。

というのは、s が指しているインスタンスの中身 "abc" を変更する手段はないと言ってます。

s = "DEF"; というのは、別に新たに "DEF" というインスタンスを作って、そのアドレスを変数 s に代入しています。(変数 s の中身を入れ替えるということ。インスタンス "abc" の中身を変更しているわけではない)

c# の経験があるそうですが、上記のあたりは c# と同じです。

2Like

Comments

  1. @guijiu

    Questioner

    String s = "abc"と定義した時点で、変数sがインスタントだと思い込んでいました。"abc"と"DEF"が別々のインスタンスであることを理解しました。
    ありがとうございました。

  2. 上にも書きましたが、データ型は理解してますか? そして、String は参照型であることは理解してますか?

  3. @guijiu

    Questioner

    intなどデータ型は数値を入れる箱。
    IntegerやStirngなど参照型は、クラスに基づいた設計図を持ったアドレスをしまう箱と理解しています。

生成したオブジェクト(ここではString)がイミュータブルであることと、変数が再代入できることには違いがあります。

String hoge = "abc";
String piyo = hoge;
hoge = "DEF";
System.out.println(hoge); // DEF
System.out.println(piyo); // abc


final String fuga = "Ghi";
// fuga = piyo; // エラー
System.out.println(fuga); // Ghi

上記例でhogeに再代入していますが、piyo変数に格納することでabcという文字列インスタンス自体には変化がないことがわかるかと思います。
再代入不可の変数はfinalで定義します。

2Like

Comments

  1. @guijiu

    Questioner

    String s = "abc"と定義した時点で、変数sがインスタントだと思い込んでいました。"abc"と"DEF"が別々のインスタンスであることを理解しました。
    sに入るのは、実のところ文字列ではなく、Stringの設計図(クラスの定義)と文字列などString型で何を表現するかが書かれたメモリの番地であることを思い出しました。(<ーー私なりの理解です。)
    教えて頂き、ありがとうございました。

もちろん、Stringのインスタンス sの文字列は変更できます。「変更できる」ということは、「Stringのインスタンスが表している文字列の内容は、変化している」ように思うのですが、違うのでしょうか?

変数 s であって インスタンスはそこに入っている参照値に紐づくものではないでしょうか?
再代入によって参照値は変更できますが、参照値の先のStringは不変です。

再代入の可否 と 不変は別の話なので。

もしも Stringが可変(ミュータブル)であるならば 関数の引数に渡した仮引数への操作で実引数に渡したStringも変更できますので。
(※ なお、仮引数への再代入で実引数に反映できるのが参照渡し

1Like

Comments

  1. @guijiu

    Questioner

    「変数 s であって インスタンスはそこに入っている参照値に紐づくもの」
    このことを忘れていました。
    方々のご指摘で、思い出しました。
    ありがとうございました。

Your answer might help someone💌