Java Silver SE11の学習をしていて、「Stringオブジェクトは不変なオブジェクトである」の意味がよくわからなかったので、覚書ついでに。
mutableとimmutableの違い
そもそもmutable(可変)とimmutable(不変)の違いとはなんぞやから。
簡単に言うと、一度セットした値を後から変更できるオブジェクトがmutableオブジェクト。
一度セットした値を後から変更できないオブジェクトがimmutableオブジェクト。そのままですね。
immutableオブジェクトの定義方法は下記らしいです。
- すべてのフィールドをprivateで修飾する
- オブジェクト内部の状態を変更可能なメソッド(setterなど)を提供しない
- クラスをfinalで宣言し、メソッドがオーバーライドされないことを保証する(サブクラスから変更させない)
- 内部に可変オブジェクトを保持している場合、そのオブジェクトを外部に提供(getterなど)しない
次項より、immutableなクラス:Hogeを作成することでそれぞれの詳細を確認します。
(その他のクラスも記載しますが、これらはimmutableでないものとします)
すべてのフィールドをprivateで修飾する
これはまぁ、わかりやすいですよね。publicで修飾してたら変え放題ですものね。
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で修飾しても、メソッド経由で変更可能だったら何も意味ないです。
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を作ってみます。
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で宣言します。
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も消します。
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型変数の内容変えられるやん
※この項は推測です。誤りがあればご指摘ください。
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の値(参照先)が変わっているだけであって、インスタンスの値は変わっていないという点です。
デバッガで追ってみます。以下のようにブレークポイントを設定します。
strの参照先が変わっていることが確認できます。
Stringクラスは特殊で、new演算子を使わずとも、代入演算子でインスタンスの生成が可能です。
(str = "すとりんぐ2";
と str = new String("すとりんぐ2");
はどちらも同じ処理ということ)
つまり、String型変数に代入演算子で文字列を設定するということは、都度インスタンスを生成していることと同義です。
(厳密には、本当に都度都度生成しているとは限りませんが)
使い方によっては、JVM上のメモリをどんどん食いつぶすことになりかねません。
こういった問題を解消するために、SringBuilderクラスが作られた…のだと、推測します。
最後に
やはりといいますか、(推測交じりですが)アウトプットすることで理解が深まると感じました。
Silver/SE11の取得を目指して引き続き頑張ります。