はじめに
はじめまして!
普段は主にJavaのバックエンド開発に携わっており、ここ数年は管理業務も多く
実装をはじめレビューをする機会が増えてきました。
案件を推進する上で勿論ながら品質面は担保マストで「動いているけど保守性が...」
「バグの温床になりそう」などの点は常にケアする必要があります。
ある案件Lをやっていた際に直面した事案から学びになった内容をシェアします!
この記事でわかること
- イミュータブル / ミュータブルなオブジェクトが何か
- 実装時に気を付けたい書き方
- Java以外の言語での表現(オマケ程度)
イミュータブル(なオブジェクト)とは
イミュータブル(immutable)は英語で「不変」「変更不能」という意味があります。
その上でイミュータブルなオブジェクトとは、インスタンス作成後にその状態を変更できないオブジェクトを指します。つまり一度値が設定されると、その値が変更されることなく、また新たに設定し直すこともできません。
このイミュータブルなオブジェクトを使うと、オブジェクトの状態遷移を解析する必要がなくなるため品質向上に繋がります。そのほかにも使用する理由として以下が挙げられます。
1. スレッドセーフ
【スレッドセーフ】
複数のスレッドが同時に同じリソース(変数・オブジェクト)にアクセスしても
データが壊れたり不整合が発生したりしないようにするための仕組み。
イミュータブルなオブジェクトはその状態が変更されないため、複数のスレッドからの同時アクセスがあってもデータ競合が発生しません。つまり安全性が保障されています。
2. キャッシュ効率
【CPUキャッシュライン】
CPUはメモリアクセス時に必要なデータだけでなく周辺バイトも丸ごと読み込むが
この周辺(ブロック)単位のことを指す。一般的にサイズは32, 64, 128バイト。
【キャッシュヒット】
CPUが必要とするデータがキャッシュ内に存在する場合を指し高速アクセス可能。
一方でデータがキャッシュ内に存在せず、RAMまで読みに行く場合は
「キャッシュミス」といいパフォーマンスの低下に繋がる。
イミュータブルなオブジェクトは作成後に変更されないため、連続したメモリアドレスにデータが配置されキャッシュラインに収まりやすいことから、CPUが一度に複数の値をキャッシュへ読み込むことができパフォーマンスの向上に繋がります。
ミュータブル(なオブジェクト)とは
ミュータブル(mutable)は英語で「変わりやすい」「変更可能」という意味があります。
その上でミュータブルなオブジェクトとは、インスタンス作成後もその状態を変更することができるオブジェクトを指します。
このミュータブルなオブジェクトを使うと、一度作成したオブジェクトを再利用することができるのでメモリ効率は良く柔軟な実装が可能ですが、注意すべき点もいくつかあります。
1. 参照の値渡しによる副作用が起きやすい
(=共有参照によるバグが起きやすい)
int[] a = {100, 200, 300};
int[] b = a;
b[0] = 99;
System.out.println(a[0]); // 99(意図せず a まで変わる)
2.キャッシュ効率低下
書き込みが多いとキャッシュラインが頻繁に無効化され、キャッシュミスが起こりやすい。
また書き込みに伴ってロックや同期のコストも増える。
イミュータブル / ミュータブルな型(抜粋)
イミュータブルな型
Java
- int など(プリミティブ型)
- Integer / Long / Double など(ラッパークラス)
- String(文字列)
Python
- int, float など(数値型)
- str(文字列)
- tuple(タプル)
- bool(真偽値)
JavaScript
- number, string, boolean, indefined, symbol など(プリミティブ値)
ミュータブルな型
Java
- Array, int[] など(配列)
- ArrayList など(可変長リスト)
- HashMap, HashSet
- StringBuilder, StringBuffer(可変の文字列)
Python
- list(リスト)
- dict(辞書)
- set(集合)
- bytearray(バイト配列)
JavaScript
- Object, Array, Function, Date, Map など(オブジェクト型)
イミュータブルなクラスの作り方
イミュータブルなクラスを作成するには以下の条件を満たす必要があります。
① クラスはfinalで宣言する。
⇨ オーバーライド等のサブクラスからの変更を防ぐ。
② 全てのフィールドをprivateで宣言する。
③ getter / setter メソッドを提供しない。
⇨ オブジェクトの状態を変更不能(イミュータブル)にする。
⇨ オブジェクト内部に可変オブジェクトが存在した場合に外部への提供を防ぐ。
イミュータブルなオブジェクトの作り方 ①
上記クラスの作り方とも考え方は同じです。
単なる変数の場合は、finalで宣言する。
final int page = customerForm.nowPage;
if (customerForm.isNext()) {
page += 1; // 変更不可(エラーとなる)
}
オブジェクトにfinalを付与した場合、オブジェクトそのものを変更することはできませんが、オブジェクト内部(中身)は変更できてしまいます。
final CustomerDto dto = null;
if (customerForm != null) {
dto = new CustomerDto(customerForm); // 変更不可(エラーとなる)
}
final CustomerDto dto = new CustomerDto(customerForm);
if (customerForm != null && customerForm.isNext()) {
dto.page += 1; // 変更可能(エラーにならない)
}
オブジェクト内部まで変更不可にする場合はクラスのメンバ変数全てfinalにする。
public class CustomerDto {
public final int age; // finalを付ける
public final String username; // finalを付ける
// コンストラクタ
public CustomerDto(final int age, final String username) {
// メンバ変数への代入は全てコンストラクタで実施する
this.age = age;
this.username = username;
}
// getter
public int getAge() {
return this.age;
}
public String getName() {
return this.username;
}
// setterは提供しない
}
final CustomerDto dto = new CustomerDto(customerForm.age, customerForm.username);
dto.age += 1; // 変更不可(エラーとなる)
イミュータブルなオブジェクトの作り方 ②
こちら よりJavaのリストはミュータブルです。
final List<String> pageList = ArrayList<>();
pageList.add("pg01"); // エラーにならない
リストをイミュータブル化し、addなどの更新系メソッドを実行不可とすることができます。
不変コレクション(Collections.unmodifiableList や List.of など)を使用する。
final List<String> pageList = ArrayList<>();
pageList.add("pg01"); // 変更可能(エラーにならない)
final List<String> constPageList = Collections.unmodifiableList(pageList);
constPageList.add("pg02"); // 変更不可(エラーになる)
final List<String> pageList = List.of("pg01", "pg02", "pg03");
pageList.add("pg04"); // 変更不可(エラーになる)
イミュータブルと相性が悪いもの(抜粋)
1. 可変なstaticフィールド
// 【可変】どこからでも書き換えが可能 = 状態が追跡不能になる
public class Config {
public static int count = 0;
}
// 【不変】finalで変更不可にする
public class Config {
public static final int VERSION = 1;
}
2. for文(状態を更新しながら回す)
// ミュータブル
int sum = 0;
for (int n : nums) {
sum += n;
}
// イミュータブル
int sum = Arrays.stream(nums).sum();
3. if文
final String gender = "男性";
if (customerForm.isFemale()) {
gender = "女性"; // 変更不可(エラーとなる)
}
// if文の代替で3項演算子を用いるとよい
final String gender = customerForm.isFemale() ? "女性" : "男性";
オマケ(実際の体験談)
検索機能だとイメージしやすいかと思いますが、条件変更があったりすると入力データは
多様に変化しますよね。input(Form)の値が実質的にイミュータブルになっておらず
バグが発生した際にどこでどう変わったのかを追跡するのがすごく大変でした。
当時は一旦finalを付与してみてエラーになった箇所を1つ1つ洗い出しました。
知っているだけなのと意識して実装しているのは違うな、と改めて感じた瞬間でした。
まとめ
イミュータブルなオブジェクトにするメリットはたくさんあるので、今後実装する機会には
"可能な限り"オブジェクトや変数はイミュータブルにするとよいと思います。
ただし何でもかんでも「final」を付与するとかえって可読性が落ちてしまう場合もあります。
(拡張性が下がる、冗長化する可能性がある、テスト/モック化しにくい、など)
それらをふまえ
- スコープが狭い場合はわざわざ付与しなくてOK(イミュータブルなのが見てわかれば)
- finalが付与されていなくても実質イミュータブルになっていればOK
あたりを意識して実装、またレビューできると品質は上がっていくと思います。
今回はJava中心な内容でしたが書き方は違えど考え方は他の言語にも通ずるかなと。
”経験は生かすもの" 引き続き備忘録もかねアウトプットしていきます!