0
1

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 1 year has passed since last update.

【Java】Reflection APIでフィールド値を取得してisEmpty()を判定する

Last updated at Posted at 2022-06-09

初めに

本記事では、Javaでオブジェクトが空であるかを判定するisEmpty()メソッドを実装する際に、Reflection APIを用いてフィールド値を取得する方法についてご紹介します。

経緯

DBから取得したデータを入れるEntityクラスにおいてisEmpty()メソッドを実装する際に、「全部のフィールド値を1つずつ手書きで判定するとコードが長くなるな...」と思い、もっとスッキリと書ける方法を調べました。
するとReflection APIという便利なものを見つけたため、使い方の練習と備忘録を兼ねて本記事を執筆しました。

結論

「各フィールドの値がnullかどうか判定する」だけであれば、以下のように書けばOKです。

import
import java.lang.reflect.Field;
reflectを使ったフィールド値の取得とnull判定
	public boolean isEmpty() {
		Field[] fields = CustomerInfo.class.getDeclaredFields(); // Reflection API でフィールド情報を取得

		for(Field f : fields) {
			try {
				if(f.get(this) != null) return false; // 対応するフィールドの値を取得してnull判定

			} catch (IllegalArgumentException | IllegalAccessException e) {
				throw new RuntimeException(e); // この例外は基本的に発生しないはずだけど、念のため書いておく

			}
		}

		return true; // 全てnullならtrue
	}

null以外の値(例えば、String型であれば空文字""など)もtrueにしたい! という場合の書き方については、本記事の後半で説明します。

※注意

  • 実用性は度外視です
  • 業務ロジックなどにこの手法を使用するのはおススメしません。(というより、この手法が有効な場面がほぼない)
  • あくまで「こんな書き方もできるよー」という参考程度にお考え下さい。

準備

本記事では以下のCustomerInfoクラスにisEmpty()メソッドを実装します。簡単のため、getter/setterなどは省略してあります。

CutomerInfo.java
public class CustomerInfo {
	private Integer customerId;
	private String customerName;
	private String rank;

	public CustomerInfo() {

	}

	public CustomerInfo(Integer customerId, String customerName, String rank) {
		this.customerId = customerId;
		this.customerName = customerName;
		this.rank = rank;
	}

	public boolean isEmpty() {
		// ここを実装する
	}
}

isEmpty()の仕様

実装するisEmpty()の仕様は以下の通りです。

  • publicなインスタンスメソッド
  • 引数なし
  • オブジェクトのフィールド値が全てnullのときtrueを返す

実装例

1. if文を使う

if文によるempty判定
	public boolean isEmpty() {
		if(customerId != null) {
			return false;
		}

		if(customerName != null) {
			return false;
		}

		if(rank != null) {
			return false;
		}

		return true;
	}

Reflection APIを使わない場合の実装例その1です。
素直な書き方なので、フィールド値が少ない場合はこれでも十分読みやすいと思います。

2. 条件式をそのまま返す

各フィールド値のnull判定結果をまとめてreturn
public boolean isEmpty() {
		return customerId == null && customerName == null && rank == null;
	}

Reflection APIを使わない場合の実装例その2です。
これもフィールド値が少ない場合は見やすいです。
フィールド値が多いとうっかり判定漏れしていても気づきにくいのでご注意ください。

3. Reflect APIを使う

import
import java.lang.reflect.Field;
reflectを使ったフィールド値の取得とnull判定
	public boolean isEmpty() {
		Field[] fields = CustomerInfo.class.getDeclaredFields(); // Reflection API でフィールド情報を取得

		for(Field f : fields) {
			try {
				if(f.get(this) != null) return false; // 対応するフィールドの値を取得してnull判定

			} catch (IllegalArgumentException | IllegalAccessException e) {
				throw new RuntimeException(e); // 念のため書いておく

			}
		}

		return true; // 全てnullならtrue
	}

本記事のメインである、Reflection APIを使用した実装例です。
どのような処理を行っているか順に説明していきます。

まず、オブジェクトのフィールド情報をReflection APIで取得します。

Reflection APIでフィールド情報を取得
Field[] fields = CustomerInfo.class.getDeclaredFields(); // Reflection API でフィールド情報を取得

CustomerInfo.classでクラス変数を取得し、Class#getDeclaredFieldsでオブジェクトの全フィールドの情報を取得します。
今回はcustomerIdcustomerNamerankの3つのフィールド情報がfieldsに格納されます。
注意すべきなのは、このfiledsに格納される情報はクラス定義から読み取れる静的な情報(つまり型や名前、アクセス修飾子など)であるということです。
よって、オブジェクトの現在のフィールド値を取得するには、このfieldsの情報を使って実際のフィールドにアクセスする必要があります。
これには、Field#getを使用します。

各フィールドの値をnull判定
for(Field f : fields) {
	if(f.get(this) != null) return false; // 対応するフィールドの値を取得してnull判定
}

今回はオブジェクトが自分自身のフィールド値を取得するのでf.get(this)で値を取得します。
これでフィールド値を取得できたので、あとはnull判定をするだけ……と見せかけて、もうひとつやることがあります。
例外処理です。
Field#getIllegalArgumentExceptionIllegalAccessExceptionという2つの検査例外を投げうるので、これをcatchしてあげないとコンパイルが通りません。
なので面倒ではありますが、try-catchして適当な例外処理を書いてあげましょう。

例外処理を追加
for(Field f : fields) {
	try {
		if(f.get(this) != null) return false; // 対応するフィールドの値を取得してnull判定
	
	} catch (IllegalArgumentException | IllegalAccessException e) {
		throw new RuntimeException(e); // 念のため書いておく

	}
}

以上で完成です。もう一度全体像を見てみましょう。

reflectを使ったフィールド値の取得とnull判定
	public boolean isEmpty() {
		Field[] fields = CustomerInfo.class.getDeclaredFields(); // Reflection API でフィールド情報を取得

		for(Field f : fields) {
			try {
				if(f.get(this) != null) return false; // 対応するフィールドの値を取得してnull判定

			} catch (IllegalArgumentException | IllegalAccessException e) {
				throw new RuntimeException(e); // 念のため書いておく

			}
		}

		return true; // 全てnullならtrue
	}

try-catchのせいで行数が無駄に増えてますが、メインの処理は数行だけです。
フィールド値が何百種類あったとしても、nullチェックだけならこれでOKです。
Reflection APIって便利!

ちなみに、isEmpty()内で例外処理したくない! という場合は、以下のように値の取得と例外処理を別のメソッドに切り出すことも可能です。

例外処理の分離
	// フィールド値の取得と例外処理はこっち
	private Object getFieldValue(Field field) {
		try {
			return field.get(this);
		} catch (IllegalArgumentException | IllegalAccessException e) {
			throw new RuntimeException(e);
		}
	}

	// 実際の処理はこっち
	public boolean isEmpty() {
		Field[] fields = CustomerInfo.class.getDeclaredFields();

		for(Field f : fields) {
			if(getFieldValue(f) != null) return false;
		}

		return true;
	}

すっきりしましたね。
例外処理を分けることで、isEmpty()の本来の処理を追いやすくなります。

4. Stream APIも使っちゃう

オマケ的な実装です。
例外処理のないfor文なら簡単にStream APIに置き換えることが出来ます。
どうせならもっとスタイリッシュに実装してみましょう。

import
import java.lang.reflect.Field;
import java.util.Objects;
import java.util.stream.Stream;
Stream APIで処理
	// フィールド値の取得と例外処理はこっち
	private Object getFieldValue(Field field) {
		try {
			return field.get(this);
		} catch (IllegalArgumentException | IllegalAccessException e) {
			throw new RuntimeException(e);
		}
	}

	// 実際の処理はこっち
	public boolean isEmpty() {
		Field[] fields = CustomerInfo.class.getDeclaredFields();

		return Stream.of(fields)
				.map(this::getFieldValue)
				.allMatch(Objects::isNull);
	}

return文は可読性のために改行していますが、実際は1行です。
Stream APIに慣れている人はこっちの方が読みやすいかもしれません。

null以外の値もtrueにしたい場合

今回はフィールド値がnullかどうかだけチェックするisEmpty()を実装しましたが、実際にはnullチェック以外も必要になる場合が多いです。
例えば、String型ならnullだけでなく空文字""isEmpty()の条件に含まれるのが一般的です。
本節ではフィールドごとに別々の基準で判定したい場合のisEmpty()の実装方法をご紹介します。

実装方法は以下の3パターンで、それぞれ有効なシチュエーションが異なります。

  1. コンストラクタやsetterでnullに変換する → null値とそれ以外の値を同一視して良い場合
  2. 空値判定用の別メソッドを用意する → 空値の判定基準が型ごとに存在する場合
  3. Reflection APIを使うことを諦める → 空値の判定基準がフィールドごとに存在する場合

1. コンストラクタやsetterでnullに変換する

この方法はnull値とそれ以外の値を同一視して良い場合に有効です。
例えば、String型のnullと空文字""を同一視して良いなら、ConsumerInfoのコンストラクタは以下のように書き換えられます。

コンストラクタで空値をnullに変換
	boolean isEmptyStr(String str) {
		return str == null || str.isEmpty();
	}

	public CustomerInfo(Integer customerId, String customerName, String rank) {
		this.customerId = customerId;
		this.customerName = isEmptyStr(customerName) ? null : customerName;
		this.rank = isEmptyStr(rank) ? null : rank;
	}

空文字""をコンストラクタの引数に与えられた場合は、nullに変換してフィールドに代入します。
これにより、isEmpty()側ではnull判定だけすればOKになります。
isEmpty()の実装自体には手を加えなくて良いのが最大のメリットですね。

2. 空値判定用のメソッドを用意する

この方法はそれぞれの空値を同一視できない場合で、かつ空値の判定基準が型毎に決まっている場合に有効です。
例えば、String型のnullと空文字""と空白文字" "は全部空値とみなすけど、データとしてはそれぞれ別のものとして扱いたい! という場合です。
そのような場合は、以下のように型毎の空値判定メソッドを用意して対処します。

空値判定用メソッドを使用する
	// 例外処理はこっち
	private Object getFieldValue(Field field) {
		try {
			return field.get(this);
		} catch (IllegalArgumentException | IllegalAccessException e) {
			throw new RuntimeException(e);
		}
	}
	
	// 空値判定はこっち
	private boolean isEmptyValue(Object value) {
		if(value instanceof String) {
			return value == null || ((String)value).isBlank(); // null・空値・空白を判定
		}
		return value == null; // その他の型はnullチェックのみ行う
	}
	
	// 実際の処理はこっち
	public boolean isEmpty() {
		Field[] fields = CustomerInfo.class.getDeclaredFields();
		
		return Stream.of(fields)
				.map(this::getFieldValue)
				.allMatch(this::isEmptyValue); // ここを変更
	}

isEmptyValue()にString型用の空値判定処理を実装し、isEmpty()でそれを呼び出します。
消したかったはずの条件分岐が復活してしまっているので、ちょっとイケてない感はありますが……
フィールド値の種類に対して型の種類が少ない場合であれば、まだ有効かなと思います。

3. Reflection APIを使うことを諦める

上記の方法でも対処しきれない場合、すなわち、同じ型でもフィールド毎に異なった空値の判定基準があるといった場合は、Reflection API を使うことを諦めましょう。
以下のように空値判定用メソッドを拡張して無理やり実装することも不可能ではないですが……

本末転倒な実装
	// 例外処理はこっち
	private Object getFieldValue(Field field) {
		try {
			return field.get(this);
		} catch (IllegalArgumentException | IllegalAccessException e) {
			throw new RuntimeException(e);
		}
	}

	// 空値判定はこっち
	private boolean isEmptyValue(Field field) {
		switch(field.getName()) {
		case "customerId" :
			return customerId == null || customerId.intValue() == 0;
			
		case "customerName" :
			return customerName == null || customerName.isBlank();
			
		case "rank" :
			return rank == null || rank.isEmpty() || rank.equals("NO_RANK");
		}
		throw new RuntimeException("ありえないエラー");
	}

	// 実際の処理はこっち
	public boolean isEmpty() {
		Field[] fields = CustomerInfo.class.getDeclaredFields();

		return Stream.of(fields).allMatch(this::isEmptyValue);
	}

isEmptyValue()の中で、本来消したかったはずの条件分岐が完全復活してますね……
こんな書き方をするくらいなら、以下のように書いた方が100倍マシです。

素直が一番
	public boolean isEmpty() {
		boolean customerIdIsEmpty = customerId != null && customerId.intValue() != 0;
		if(customerIdIsEmpty) return false;
		
		boolean customerNameIsEmpty = customerName != null && !customerName.isBlank();
		if(customerNameIsEmpty) return false;
		
		boolean rankIsEmpty = rank != null && !rank.isEmpty() && !rank.equals("NO_RANK");
		if(rankIsEmpty) return false;
		
		return true;
	}

フィールド毎に個別の判定をする必要がある場合は、Reflection APIを使っても条件分岐を減らすことができません。
意地を貼らずに素直なif文で記述しましょう。
そもそもこんな面倒な判定を大量にしなくちゃいけない場合はまずクラスやDBの設計から見直した方が良いと思います。

まとめ

本記事ではReflection APIによるオブジェクトのフィールド値の取得方法と、それを用いたisEmpty()の実装方法をご紹介しました。
Reflection APIの本来の使用用途とは異なりますが、ちょっとしたテストクラスに使用する分には便利かもしれませんね。

ただし、冒頭にも述べた通り実用性は度外視であるため、業務ロジックなどに使うのはおすすめしません。
どうしても使いたい場合は自己責任でお願いします。

それではまた!

参考文献

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?