LoginSignup
1
0

More than 3 years have passed since last update.

Function と Consumer の相互変換

Last updated at Posted at 2017-03-16

はじめに

Function を引数に取るメソッドに対し、返り値を捨ててよいので副作用だけ実行したいような場合、直接 Consumer 型の変数を渡すことはできません。
何かしらの方法で Function 型に変換する必要があります。

なお、この記事では簡単のため Function と Consumer を例に採っていますが、値を返す関数型インタフェースと返さない関数型インタフェースの間での変換は全て同様です。
他の関数型インタフェースを使う場合は適宜読み替えて下さい。

変換例

以下のようにすれば変換できます。

// Function<T, R> f を Consumer<T> に変換
Consumer<T> c = f::apply;

// Consumer<T> c を Function<T, Void> に変換
Function<T, Void> f = arg -> {
    c.accept(arg);
    return null;
};

なお Consumer 型としてメソッド参照を渡す場合は、値を返す関数型であっても変換は不要です。
上記の変換もその仕様を利用したものです (後述)。

ユーティリティメソッド

Consumer を都度その場で Function に変換するのが面倒な場合は、ユーティリティクラスを作って変換用の static メソッドを用意してもよいでしょう。

public class Functions {
    public static <T> Function<T, Void> of(Consumer<T> consumer) {
        return arg -> {
            consumer.accept(arg);
            return null;
        };
    }
}

// 使用例
Stream.of("1","2","3").map(Functions.of(System.out::println)).forEach(v -> {});

対象が自前の関数型インタフェースの場合は、デフォルトメソッドとして定義した方が使いやすいかも知れません。

@FunctionalInterface
public static interface MyConsumer<T> {
    public abstract void doSomething(T t);
    public default Function<T, Void> asFunction() {
        return arg -> {
            doSomething(arg);
            return null;
        };
        // 前記のユーティリティクラスも使う場合は
        // return Functions.of(this::doSomething);
    }
}

// 使用例
MyConsumer<String> m = System.out::println;
Stream.of("a","b","c").map(m.asFunction()).forEach(v -> {});

ちなみに、この例はあくまでデモンストレーションのためのコードですので、通常はもちろん forEach(System.out::println) を使って下さい。念のため。

余談1 (具体例と設計に関する雑感)

自分はリソースのクローズ漏れを防ぐためによくこんな感じのユーティリティメソッドを書きます。

// 勝手にクローズする Files#lines()
public static <R> R safeLines(Path path, Function<Stream<String>, R> lineProcessor) throws IOException {
    try(Stream<String> stream = Files.lines(path)) {
        return lineProcessor.apply(stream);
    }
}

// 使用例
Path path = Paths.get("example.txt");
Optional<String> head = safeLines(path, Stream::findFirst);
System.out.printf("最初の行: %s%n", head.orElse(""));

こういうメソッドを用意して、そのプロジェクト内でテキストファイルを扱う場合は常にこのメソッドを使用する、というような規約にしておけば、うっかりでクローズ漏れを起こす心配がありません。
しかし、このメソッドの引数は Function ですので Consumer を渡すことはできません。

// 例えばこんな感じのメソッドがあっても
public class StreamUtils {
    public static void printAll(Stream<?> stream) {
        stream.forEach(System.out::println);
    }
}

// これはコンパイルエラーになる
safeLines(path, StreamUtils::printAll);

// こうする必要がある
safeLines(path, stream -> {StreamUtils.printAll(stream); return null;});

元がラムダの場合はまだ諦めも付きますが、メソッド参照で済みそうな場面でブロック付きのラムダを書いて return null; しなければならないのは中々の苦痛です。

テンプレート側を変更できる場合は Consumer を引数に取る同様のメソッド 1 を用意してもよいのですが、もちろん変更できない場合もありますし、テンプレート1つごとにメソッドを2つずつ用意する 2 よりは変換メソッドを1つ用意した方が低コストでしょう。

ちなみに、メソッド参照で渡すメソッドの API を変更できる場合は、返却型を void の代わりに Void にしてしまうという方法もありますが、この書き方が賛同を得られるかどうかはわかりません。

// こうする
public class StreamUtils {
    public static Void printAll(Stream<?> stream) {
        stream.forEach(System.out::println);
        return null;
    }
}

// 通る
safeLines(path, StreamUtils::printAll);

実際のところこちらの方が汎用性は高いのですが、やはり一般的な書き方ではありませんし、結局 return null; を明記しなければいけないのも微妙なところです。

余談2 (変換が不要なケース)

冒頭でも触れたように、渡し方によってはそもそも変換が不要な場合もあります。

// 変数で渡す場合は型が明示されているので常に変換が必要
Function<String, String> f = String::trim;

// もちろんコンパイルエラー: Type mismatch
Consumer<String> c = f;

// キャストも実行時例外になる: ClassCastException
Consumer<String> c = (Consumer<String>) f;

// 通る
Consumer<String> c = f::apply;

// 直接メソッド参照で渡す場合はそのままでよい
Consumer<String> c = String::trim;

詳細は言語仕様の 15.13.2. Type of a Method Reference を参照してください。
ざっくり言うと、受け取る側の結果型が void の場合はメソッド参照側の結果型は何でもよいということです。
そうでない場合は受け取る側もメソッド参照側も void 以外でかつ互換性のある型を返す必要があることになっています。

ちなみにラムダについては 15.12.2.1. Identify Potentially Applicable Methods に記述があり、簡単に言うと

  • ラムダ本体が式の場合は文としても扱える式である
  • ラムダ本体がブロックの場合は値を return していない

いずれかの場合は変換 (というか修正) することなく渡せます。

// コンパイルエラー: Void methods cannot return a value
Consumer<String> c = s -> s;

// 通る
Consumer<String> c = s -> s.trim();

// コンパイルエラー: Void methods cannot return a value
Consumer<String> c = s -> {return s.trim();};

// 通る
Consumer<String> c = s -> {s.trim(); return;};

まあラムダは使い回すたぐいのものではないので、コードコピペでもしない限りこういうエラーを出すことはないでしょうが。


  1. 通常なら同名のメソッドでオーバーロードするところですが、ラムダで呼び出すとしばしばメソッドの呼び出しが曖昧になる (引数の型を明示する必要が出てくる) ため、この場合メソッド名は分けた方が無難です。 

  2. もう一段抽象化していい感じにすればいい感じにできる気もしますが、まあそこまでしなくてもってなりますよね。なりました。 

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