読者です 読者をやめる 読者になる 読者になる

(define -ayalog '())

括弧に魅せられて道を外した名前のないプログラマ

immutableなStringとmutableなArrayListな話

immutable??mutable??

ということで、immutable(イミュータブル:不変)とmutable(ミュータブル:変更可能)なオブジェクトの話です。

そもそもimmutable/mutableってなんだろう?というところから書きたいと思います。

|> immutable(不変)オブジェクトとは

オブジェクト(インスタンス)そのものの状態を変更できないこと。
Javaにおける代表格はStringクラスです。事実Stringは自分自身の状態を変更するメソッドを提供していません。
※replaceメソッドなんかは勘違いされやすいですが、自分自身が変化することはないと覚えていれば分かりやすいかと。

不変オブジェクトとして、必要な条件は下記の通り。

  1. オブジェクトの内部状態を変更可能なメソッドを提供しない。(つまり、getterは用意してもsetterやそれに準ずるものは用意しない)
  2. メソッドがオーバーライドされないことを保証する。(サブクラスからの状態変更を防ぐ)
  3. 全てのフィールドが"private final"で定義されること。(finalについては必須ではないが、明示的に不変であることを示すことができる)
  4. 内部に可変オブジェクトを保持している場合、そのオブジェクトを外部に提供しない。また、該当可変オブジェクトの値を内部的に変更しない。

細かいことは良いから、とにかく変更できないのです!!(ぁ

|>mutable(変更可能)オブジェクトとは

これについては特に意識しなければ、Javaにおいてはほとんどがmutableなオブジェクトです。
ArrayListしかり、HashMapしかり。自分で作ったクラスとかも意識しなければ、mutableなオブジェクトです。

簡単な例を出せば、以下の通り。

ArrayList<String> list = new ArrayList<>();
list.add("hoge"); //インスタンスを変更している。
list.remove(0);    //インスタンスを変更している。

こんな感じ。

Stringはimmutable!

|>何故、Stringはimmutableなの?

何故、Stringはimmutableなんでしょうか?その話をしたいと思います。

例えば、下の様なメソッドがあったとします。

public void shout(String str){
    str += "!!";
    System.out.println(str);
}

単純に与えられた文字列の最後に"!!"を付け足して、標準出力へ出力するだけのメソッドです。
もし、このときStringはmutableだったら、どうでしょう?呼び出し元では"!!"がくっついてしまいます。

String str = "Hello";
shout(str); //先のメソッドを実行
System.out.println(str); //…ここでも"Hello!!"が出力されてしまう。予期していない/嬉しくない動作。

メソッドの引数にオブジェクトを渡すとき、それがmutableであればメソッドの中で変更されてしまうことを考えなければいけません。
しかし、StringはJava言語の中でもよく多用されるクラスの1つです。そんなクラスがもしmutableだったら、プログラマにとってはかなりのストレスになるでしょう。

なのでJava言語におけるStringは、プリミティブ型と同じように振る舞えるimmutableである必要があるのです。
ただし、immutableであるということは状態毎にインスタンスを生成するという意味なので、メモリを圧迫する原因にもなりえます。
ここらへんは言語の性質上致し方ないというか、プログラミングの容易さとトレードオフの関係にあると言えます。

|>immutableオブジェクトのJVM仮想メモリ上の動き

JVM仮想メモリには、「スタック領域」と「ヒープ領域」という仮想メモリ領域があるというところからの説明になりますが。

まず、スタック領域とは「ローカル変数領域」とも呼ばれ、主にメソッド処理などで一時的な値の保持に使用されますが、プリミティブ型はこのスタック領域で処理されます。これに対してオブジェクト型の場合、保持するインスタンスはヒープ領域に作成され、スタック領域には保持したインスタンスのアドレスが格納されます。

例えば、次のような変数宣言がある場合。

Integer objInt = new Integer();
int i = 1;

こんなイメージになります。

ということで実際に以下のコードの動作を図示してみたいと思います。

public static void main(String[] args) {
    String str = "Hello"; //--①
    shout(str); //--②
    System.out.println(str);
}

private static void shout(String str){
    str += "!!"; //--③
    System.out.println(str);
}

こんな感じになります。

①〜③を順番に解説すると、以下の通り。

①mainメソッドでstr変数が宣言されて、ヒープ領域のインスタンスを参照している。
②shoutメソッドへmainメソッドのstr変数の参照先をshoutメソッド内のstr変数へ渡している。
③shoutメソッドのstr変数へ、新しい参照先が格納される(Hello!!)。

というわけでimmutableオブジェクトのJVM仮想メモリ内での動きでした。

|>でも"+"で結合してるのに、新しくインスタンスが生成されているって納得できない!!

immutableなのでString自身を変更する方法は提供されていません。+=も実はくっつけて代入してるだけで中身を変えているわけじゃありません。

String str = "Hello";
str += "!!";

は分かりやすく表すと下のようになります。

String str = "Hello";
String xxx = str + "!!";
str = xxx;

ArrayListはmutableオブジェクトです

Stringがimmutableオブジェクトだというのを理解した上で、ArrayListの話をします。

|>こんなときどうなる?

例えばこんなコードがあったとします。

public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    list.add("Hello,");
    list.add("World");
    list.add("!!");
    
    print(list); // >> Hello,World!!
    hoge(list); //hogeメソッドを実行
    print(list); // >> ②
}
private static void hoge(ArrayList<String> list){
    list.set(1, "Hoge");
    print(list); // >> ①
}

ここでprint(list)メソッドは、ArrayListの要素を全て結合して出力するメソッドだとします。
このとき?と?でそれぞれ表示されるのはなんでしょうか??

>Stringはimmutableオブジェクトなので…

最初書いてきた通り、Stringはimmutableオブジェクトなので
①では"Hello,Hoge!!"が出力され、?では"Hello,World!!"が表示されるんじゃないのでしょうか??

答えは①も②も"Hello,Hoge!!"が出力されます。

JVM仮想メモリは次のようなイメージになります。

Stringのインスタンスは確かにimmutableなのですが、ArrayListは先にそれぞれの要素のアドレスを獲得していて、要素がStringのインスタンスのアドレスを持つので、Stringがimmutableであるとかは関係なく要素を書き換えることができます。
つまり、ArrayListはmutableであると言えます。

以上で、immutable/mutableの話はお終いです。

※文中の画像についてはCacooを使用して描きました。