【Java】ラムダ式、メソッド参照

2022年6月1日

本稿は、ラムダ式とメソッド参照について説明します。

Java8からは新しく関数型プログラミングの要素が利用できるようになりました。

関数というのは、今までさんざん使用してきた、メソッドのことだと思ってもらって構いません。

これを利用することで、プログラミングが楽になったり実行効率がよくなる場合もあります。

ラムダ式やメソッド参照はStreamAPIとともに使用されることが多いです。

StreamAPIについてはこちらで、機能はこちらの記事で紹介しています。

関数型インターフェース

関数型インターフェースとは抽象メソッドが一つだけ定義されたインターフェースのことです。

@FunctionalInterfaceと記述しておくことでそのインターフェースに一つの抽象メソッドだけ定義することを強制できます。

@FunctionalInterface
interface Chara{
	void move();
}

ラムダ式の使用

ラムダ式とは、その場でインターフェースの処理を実装するための機能です。

匿名クラスと似ていますね。

ただし、匿名クラスよりももっと限定的な関数型インターフェースのみに使用できるという点で異なります。

匿名クラスが理解できた人なら容易に理解できると思います。

定義方法は

(引数) -> {実装内容};

のみです。

引数や戻り値は関数型インターフェースの抽象メソッドのパターンと一致しなければなりません。

今回の例であれば、move(int num)が抽象メソッドなので、(int num)としています。

引数は、型さえ合えばいいのでnumの変数名は別の名前でも構いません。

実装内容が1行(;1かいまでの処理)の時は「return」や「{}」を省略できます。

また、引数の型は型推論可能で、省略可能です。

それでは、シンプルな例を見てみましょう。

public class Test {
	public static void main(String[] args){
		Chara ch = (int num) -> {
			System.out.println(num);
		};
		ch.move(100);
	}
}
@FunctionalInterface
interface Chara{
	void move(int num);
}
実行結果

100

ラムダ式の記述によりmoveメソッドが実装されたCharaインターフェースオブジェクトを取得できます。

あとはmoveメソッドを呼び出すだけですね。

上記を匿名クラスで書き直したものも参考として紹介しておきます。

public class Test {
	public static void main(String[] args){
		Chara ch = new Chara(){
			public void move(int num){
				System.out.println(num);
			}
		};
		ch.move(100);
	}
}
@FunctionalInterface
interface Chara{
	void move(int num);
}
実行結果

100

ラムダ式の省略形がこちらです。

public class Test {
	public static void main(String[] args){
		Chara ch = num -> System.out.println(num);
		ch.move(100);
	}
}
@FunctionalInterface
interface Chara{
	void move(int num);
}
実行結果

100

非常にすっきりしてますね。

ラムダ式はStreamAPIを利用して使用されることが多いですが、ここで説明している基本がわかれば、StreamAPIも理解できるはずです。

メソッド参照

メソッド参照とは、既に存在しているメソッドを関数型インターフェースオブジェクトとして扱ってしまおうというものです。

結果的にはラムダ式や匿名クラスのようにその場に実装されたデータを用意する行為とほとんど変わりません。

メソッド参照は、インスタンスメソッドを参照する場合は「変数::メソッド名」

staticメソッドを参照する場合は「クラス::メソッド名」と記述します。

こちらも同じく、参照するメソッドの引数や戻り値は関数型インターフェースの抽象メソッドのパターンと一致しなければなりません。

今回は引数intに合致するrefメソッドを参照することにしました。

public class Test {
	public static void main(String[] args){
		Chara ch = Test::ref;
		ch.move(100);
	}
	
	static void ref(int num){
		System.out.println(num);
	}
}
@FunctionalInterface
interface Chara{
	void move(int num);
}
実行結果

100


メソッドだけでなく、コンストラクタも同じように扱うことができます。

コンストラクタの場合は、戻り値が参照クラスコンストラクタのインスタンスかvoidの関数インターフェースに適用できます。

コンストラクタの場合は「クラス::new」と定義します。

それではサンプルをどうぞ。

public class Test{
	public static void main(String[] args) {
		Chara ch = Test::new;
		ch.move();
	}
	
	//参照用コンストラクタ
	Test(){
		System.out.println("コンスト!!");
	}
}
interface Chara{
	void move();
}
実行結果

コンスト!!

ラムダとStreamAPIのイメージプログラム

せっかくなのでラムダ式とStreamAPIのイメージができそうなプログラムを作成してみました。

StreamAPIを利用してしまうと中の動きがわかりずらいと思いますので、すべて自作のサンプルで簡単な動きを実装しました。

StreamAPIの動きのイメージの参考になればいいのですが・・・ならないだろうな・・・

内容は、魔王様が攻撃したり、ダメージを受けたりする処理を連続で呼び出すだけのプログラムです。

public class Test {
	public static void main(String[] args){
		//skillメソッドで攻撃
		//dmgメソッドで攻撃を受ける
		//recoveryメソッドでHP,MPの回復
		Chara ch = new Chara("魔王様", 1000, 1000)
			.stream()
			.dmg(() -> 100)
			.recovery(e -> e.set(300,200))
			.dmg(() -> 600)
			.skill(() -> 120)
			.skill(() -> 50)
			.dmg(() -> 400)
			.recovery(e -> e.set(100,100))
			.skill(() -> 700)
			.skill(() -> 500)
			.result();
		System.out.println();
		System.out.println("最終ステータス");
		System.out.println("名前:"+ch.name);
		System.out.println("HP:"+ch.hp);
		System.out.println("MP:"+ch.mp);
	}
}
//キャラクタクラス
class Chara{
	String name;
	int hp;
	int mp;
	Chara(String name, int hp, int mp){
		this.name = name;
		this.hp = hp;
		this.mp = mp;
	}
	CharaStream stream(){
		return new CharaStream(this);
	}
}
//Chara型の操作をStreamAPIのように連続で操作するクラス
class CharaStream{
	Chara chara;
	CharaStream(Chara chara){
		this.chara = chara;
	}
	CharaStream recovery(Recovery rec){
		StatusUpdater upd = new StatusUpdater();
		rec.recovery(upd);
		System.out.println("魔王はHPが"+upd.hp+"回復しました。MPが"+upd.mp+"回復しました。");
		chara.hp+=upd.hp;
		chara.mp+=upd.mp;
		return this;
	}
	CharaStream dmg(Dmg dmg){
		int hp = dmg.dmg();
		System.out.println("魔王は"+hp+"のダメージを受けました。");
		chara.hp -= hp;
		if ( chara.hp < 0 ) chara.hp = 0;
		return this;
	}
	CharaStream skill(Skill skill){
		int useMp = skill.skill();
		if ( chara.mp >= useMp ) {
			chara.mp -= useMp;
			if ( useMp > 100 ) {
				System.out.println("すっごいパワーで攻撃しました。");
			}else {
				System.out.println("しょぼーいパワーで攻撃しました。");
			}
		} else {
			System.out.println("魔王はMPがたりない!!");
		}
		return this;
	}
	Chara result(){
		return chara;
	}
}
//ステータスの計算用クラス
class StatusUpdater{
	int hp;
	int mp;
	void set(int hp, int mp){
		this.hp = hp;
		this.mp = mp;
	}
}
//回復処理を実装する関数型インターフェース
//回復量を返すように実装する
@FunctionalInterface
interface Recovery{
	void recovery(StatusUpdater upd);
}
//ダメージ処理を実装する関数型インターフェース
//ダメージ量を返すように実装する
@FunctionalInterface
interface Dmg{
	int dmg();
}
//スキルの実行を行う関数型インターフェース
//消費MPを返すよう実装する
@FunctionalInterface
interface Skill{
	int skill();
}
実行結果

魔王は100のダメージを受けました。
魔王はHPが300回復しました。MPが200回復しました。
魔王は600のダメージを受けました。
すっごいパワーで攻撃しました。
しょぼーいパワーで攻撃しました。
魔王は400のダメージを受けました。
魔王はHPが100回復しました。MPが100回復しました。
すっごいパワーで攻撃しました。
魔王はMPがたりない!!

最終ステータス
名前:魔王様
HP:300
MP:430


関連記事

Java

Posted by nompor