1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

mutable(可変な)オブジェクトとimmutable(不変な)オブジェクト

Posted at

Java Silver SE11の学習をしていて、「Stringオブジェクトは不変なオブジェクトである」の意味がよくわからなかったので、覚書ついでに。

mutableとimmutableの違い

そもそもmutable(可変)とimmutable(不変)の違いとはなんぞやから。
簡単に言うと、一度セットした値を後から変更できるオブジェクトがmutableオブジェクト。
一度セットした値を後から変更できないオブジェクトがimmutableオブジェクト。そのままですね。

immutableオブジェクトの定義方法は下記らしいです。

  • すべてのフィールドをprivateで修飾する
  • オブジェクト内部の状態を変更可能なメソッド(setterなど)を提供しない
  • クラスをfinalで宣言し、メソッドがオーバーライドされないことを保証する(サブクラスから変更させない)
  • 内部に可変オブジェクトを保持している場合、そのオブジェクトを外部に提供(getterなど)しない

次項より、immutableなクラス:Hogeを作成することでそれぞれの詳細を確認します。
(その他のクラスも記載しますが、これらはimmutableでないものとします)

すべてのフィールドをprivateで修飾する

これはまぁ、わかりやすいですよね。publicで修飾してたら変え放題ですものね。

Test.java
public class Test {
    public static void main(String args[]){
        Hoge hoge = new Hoge("ダイゴウジ", "ガイ" ,18);
    }
}

class Hoge{
    private String name;
    private int age;
    private Foo foo;

    Hoge(){}
    Hoge(String lastName, String firstName, int age){
        foo = new Foo(lastName, firstName);
        this.name = lastName + firstName;
        this.age = age;
    }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }

    public Foo getFoo() { return foo; }
    public void setFoo(Foo foo) { this.foo = foo; }

    public void dummy(){}
}

class Foo{
    private String lastName;
    private String firstName;
    Foo(String lastName, String firstName){ this.lastName = lastName; this.firstName = firstName; }

    public void setLastName(String lastName) { this.lastName = lastName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    public String getLastName() { return lastName; }
    public String getFirstName() { return firstName; }
}

クラスHogeのフィールド、name、age、fooはアクセス修飾子がprivateになっています。はい。

オブジェクト内部の状態を変更可能なメソッド(setterなど)を提供しない

これもわかりやすいですね。せっかくprivateで修飾しても、メソッド経由で変更可能だったら何も意味ないです。

Test.java
public class Test {
    public static void main(String args[]){
        Hoge hoge = new Hoge("ダイゴウジ", "ガイ" ,18);
    }
}

class Hoge{
    private String name;
    private int age;
    private Foo foo;

    Hoge(){}
    Hoge(String lastName, String firstName, int age){
        foo = new Foo(lastName, firstName);
        this.name = lastName + firstName;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public Foo getFoo() { return foo; }

    public void dummy(){}
}

class Foo{
    private String lastName;
    private String firstName;
    Foo(String lastName, String firstName){ this.lastName = lastName; this.firstName = firstName; }

    public void setLastName(String lastName) { this.lastName = lastName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    public String getLastName() { return lastName; }
    public String getFirstName() { return firstName; }
}

メソッド経由での変更ができなくなりました。

クラスをfinalで宣言し、メソッドがオーバーライドされないことを保証する(サブクラスから変更させない)

クラス:Hogeを継承したHogeSubを作ってみます。

Test.java
public class Test {
    public static void main(String args[]){
        HogeSub hogesub = new HogeSub("ダイゴウジ", "ガイ", 18);
        System.out.println("lastName:" + hogesub.getFoo().getLastName() + " firstName:" + hogesub.getFoo().getFirstName());
        System.out.println("-----------------");
        hogesub.setLastName("山田");
        hogesub.setFirstName("二郎");
        hogesub.dummy();
        System.out.println("lastName:" + hogesub.getFoo().getLastName() + " firstName:" + hogesub.getFoo().getFirstName());
    }
}

class Hoge{
    private String name;
    private int age;
    private Foo foo;

    Hoge(){}
    Hoge(String lastName, String firstName, int age){
        foo = new Foo(lastName, firstName);
        this.name = lastName + firstName;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public Foo getFoo() { return foo; }
    public void dummy(){}
}

class HogeSub extends Hoge {
    private String lastName;
    private String firstName;

    HogeSub(String lastName, String firstName, int age) {
        super(lastName, firstName, age);
        this.lastName = lastName;
        this.firstName = firstName;
    }

    @Override
    public void dummy() {
        super.getFoo().setFirstName(this.firstName);
        super.getFoo().setLastName(this.lastName);
    }

    public void setLastName(String lastName) { this.lastName = lastName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
}

class Foo{
    private String lastName;
    private String firstName;
    Foo(String lastName, String firstName){ this.lastName = lastName; this.firstName = firstName; }

    public void setLastName(String lastName) { this.lastName = lastName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    public String getLastName() { return lastName; }
    public String getFirstName() { return firstName; }
}

実行結果は次の通りです。

実行結果
lastName:ダイゴウジ firstName:ガイ
-----------------
lastName:山田 firstName:二郎

HogeSubは、Hogeが持っているdummyメソッドをオーバーライドし、fooのフィールドを変更しています。
こういった実装を回避するため、Hogeをfinalで宣言します。

Test.java
public class Test {
    public static void main(String args[]){
        Hoge hoge = new Hoge("ダイゴウジ", "ガイ", 18);
    }
}

final class Hoge{
    private String name;
    private int age;
    private Foo foo;

    Hoge(){}
    Hoge(String lastName, String firstName, int age){
        foo = new Foo(lastName, firstName);
        this.name = lastName + firstName;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public Foo getFoo() { return foo; }
    public void dummy(){}
}

class Foo{
    private String lastName;
    private String firstName;
    Foo(String lastName, String firstName){ this.lastName = lastName; this.firstName = firstName; }

    public void setLastName(String lastName) { this.lastName = lastName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    public String getLastName() { return lastName; }
    public String getFirstName() { return firstName; }
}

オーバーライドによるフィールドの変更ができなくなりました。

内部に可変オブジェクトを保持している場合、そのオブジェクトを外部に提供(getterなど)しない

前段は、フィールド:fooのgetterを経由し、オブジェクトの値を変更していました。
Hoge.getFoo().setXXXName(”xxx”);
このような操作をされてしまうと、不変とは言えませんね。getterも消します。

Test.java
public class Test {
    public static void main(String args[]){
        Hoge hoge = new Hoge("ダイゴウジ", "ガイ", 18);
    }
}

final class Hoge{
    private String name;
    private int age;
    private Foo foo;

    Hoge(){}
    Hoge(String lastName, String firstName, int age){
        foo = new Foo(lastName, firstName);
        this.name = lastName + firstName;
        this.age = age;
    }
    public void dummy(){}
}

class Foo{
    private String lastName;
    private String firstName;
    Foo(String lastName, String firstName){ this.lastName = lastName; this.firstName = firstName; }

    public void setLastName(String lastName) { this.lastName = lastName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    public String getLastName() { return lastName; }
    public String getFirstName() { return firstName; }
}

これで、immutableなクラス:Hogeが完成しました。
完成したHogeは実質何もできませんが、そこはサンプルということでお許しください…。

せやかてString型変数の内容変えられるやん

※この項は推測です。誤りがあればご指摘ください。

Test2.java
public class Test2 {
    public static void main(String args[]){
        String str = "すとりんぐ1";
        System.out.println(str);
        System.out.println("-----------------");
        str = "すとりんぐ2";
        System.out.println(str);
    }
}
実行結果
すとりんぐ1
-----------------
すとりんぐ2

確かにstrの内容は変わっています。
注意が必要なのは、変数strの値(参照先)が変わっているだけであって、インスタンスの値は変わっていないという点です。

デバッガで追ってみます。以下のようにブレークポイントを設定します。
image.png

まずは、一つ目のブレークポイント時の内容から。
image.png

次に、二つ目のブレークポイント時の内容。
image.png

strの参照先が変わっていることが確認できます。

Stringクラスは特殊で、new演算子を使わずとも、代入演算子でインスタンスの生成が可能です。
(str = "すとりんぐ2";str = new String("すとりんぐ2"); はどちらも同じ処理ということ)
つまり、String型変数に代入演算子で文字列を設定するということは、都度インスタンスを生成していることと同義です。
(厳密には、本当に都度都度生成しているとは限りませんが)

使い方によっては、JVM上のメモリをどんどん食いつぶすことになりかねません。
こういった問題を解消するために、SringBuilderクラスが作られた…のだと、推測します。

最後に

やはりといいますか、(推測交じりですが)アウトプットすることで理解が深まると感じました。
Silver/SE11の取得を目指して引き続き頑張ります。

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?