信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
ゼロイチ Java 関数型インターフェース
インターフェースとは何かから始め、抽象メソッド・実装・関数型インターフェース・標準FIまで段階的に完全解説。Java Silver対応。
一言結論
関数型インターフェースとは『抽象メソッドが1つだけのインターフェース』。ラムダ式の型として機能し、default・static・Objectのメソッドはカウントしない。
インターフェースとは何か
プログラミングに限らず、「インターフェース」という言葉は「外側から見た使い方のルール」を意味する。
電源コンセントを思い浮かべてほしい。日本の家庭用コンセントは「2つの穴、幅が特定のサイズ」というルールで統一されている。このルールさえ守れば、どのメーカーのプラグでも差し込める。コンセントの中身(配線の素材や絶縁体の種類)は関係ない。使う側は「差し込み方のルール」だけ知っていればいい。
Javaのインターフェースもまったく同じ発想だ。
インターフェース=「このメソッドを必ず持ちなさい」というルールを定義したもの
コードで書くとこうなる:
interface Printable {
void print(); // ルール:「printというメソッドを用意すること」
}
interface キーワードで定義する。クラスに似た見た目だが、クラスではない。インターフェースはインスタンス化できない(new Printable() はできない)。あくまでもルールの定義だ。
抽象メソッドとは何か
インターフェースの中に書いたメソッド void print() には、処理の中身 { } がない。
interface Printable {
void print(); // ← 中身がない。宣言だけ
}
普通のメソッドには中身がある:
// 普通のメソッド(中身あり)
void print() {
System.out.println("印刷する");
}
インターフェースのメソッドは「こういうメソッドを作りなさい」という設計図・命令であり、中身は書かない。この「中身のないメソッドの宣言」を 抽象メソッド(abstract method) という。
「抽象」は「具体的な処理がなく、概念だけがある」という意味だ。抽象絵画が「リンゴそのもの」ではなく「リンゴのイメージ」であるのと同じ感覚だ。
インターフェースに書くメソッドには abstract キーワードを書いてもよいが、省略できる。省略した場合も自動的に public abstract として扱われる:
interface Printable {
void print(); // これと
public abstract void print(); // これは同じ意味
}
試験では両方の書き方が登場する。どちらも同じ「抽象メソッド」だと覚えておこう。
インターフェースを実装する
インターフェースのルールを守ってクラスを作ることを 「実装する」 という。implements キーワードを使う。
// インターフェース(ルール)
interface Printable {
void print();
}
// Printableを実装したクラス
class PdfPrinter implements Printable {
@Override
public void print() {
System.out.println("PDFとして印刷する"); // 中身を自分で書く
}
}
class TextPrinter implements Printable {
@Override
public void print() {
System.out.println("テキストとして印刷する"); // 別の中身
}
}
implements Printable と書くことで「Printableのルールを守ります」という契約を結ぶ。もし print() を実装しなければコンパイルエラーになる。
@Override アノテーションは「インターフェースまたはスーパークラスのメソッドをオーバーライドしている」という明示だ。付けなくてもコンパイルは通るが、付けることでスペルミスなどをコンパイラが検出してくれる。
実際に使うとき、変数の型をインターフェースにできる点が重要だ:
Printable p1 = new PdfPrinter();
Printable p2 = new TextPrinter();
p1.print(); // → PDFとして印刷する
p2.print(); // → テキストとして印刷する
p1 も p2 も型は Printable。でも呼び出すと、それぞれ別の処理が動く。これを ポリモーフィズム(多態性) という。「同じ型・同じメソッド名なのに、中身が違う」という Java の根幹の概念だ。
なぜ「処理を渡す」が必要になるか
インターフェースとその実装の仕組みはわかった。では次の問題だ。
プログラムを書いていると「ちょっとした処理を引数として渡したい」場面が頻繁に登場する。
たとえばリストをソートしたいとする。ソートのアルゴリズム(バブルソートか、クイックソートか)はJavaが知っている。でも「どんな順番で並べるか」は自分で決める必要がある。
List<String> names = Arrays.asList("田中", "鈴木", "佐藤");
// 「アルファベット逆順」というルールを渡したい
names.sort(???);
「文字列を逆順に比較するというロジック」をソートに渡さないといけない。これが「処理を渡す」という操作だ。
他にも:
- フィルタリング:「18歳以上の人だけ」という条件(処理)を渡す
- イベント処理:「ボタンを押したときに何をするか」という処理を登録する
- コールバック:「処理が終わったら次に何をするか」という処理を渡す
このように「処理そのものを引数にしたい」という需要はプログラミング全般で頻繁に発生する。
Java 8 以前:匿名クラスという方法
Java 8 が登場する 2014 年より前、Javaでこれをやるには 匿名クラス を使う必要があった。
まず「普通にクラスを定義する」方法を見てみよう:
// インターフェース
interface Greeter {
void greet(String name);
}
// 実装クラスを別途定義
class FormalGreeter implements Greeter {
@Override
public void greet(String name) {
System.out.println("はじめまして、" + name + "さん。");
}
}
// 使うとき
Greeter g = new FormalGreeter();
g.greet("田中"); // → はじめまして、田中さん。
1回しか使わないのにクラスを1個定義しないといけない。それが嫌なので匿名クラスが使われた:
// 匿名クラス:クラスに名前をつけず、その場で定義して即インスタンス化する
Greeter g = new Greeter() {
@Override
public void greet(String name) {
System.out.println("はじめまして、" + name + "さん。");
}
};
g.greet("田中"); // → はじめまして、田中さん。
クラスファイルを別に作る必要はなくなった。でも問題は残っている。
本当にやりたいのは "はじめまして、" + name + "さん。" という1行だけだ。それなのに:
Greeter g = new Greeter() { ← 構文上必要なボイラープレート
@Override ← 構文上必要なボイラープレート
public void greet(String name) { ← 構文上必要なボイラープレート
System.out.println("はじめまして、" + name + "さん。"); ← やりたいこと
} ← 構文上必要なボイラープレート
}; ← 構文上必要なボイラープレート
本質1行のために、それを囲む5行のボイラープレートが必要になる。コードが複数あれば余計にうるさくなる。
// ソートに渡す比較処理
names.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
これを毎回書くのは苦痛だ。
ラムダ式の登場と関数型インターフェースの必要性
Java 8 でラムダ式が導入され、同じことをこう書けるようになった:
Greeter g = name -> System.out.println("はじめまして、" + name + "さん。");
g.greet("田中"); // → はじめまして、田中さん。
names.sort((a, b) -> a.compareTo(b));
ボイラープレートが消えた。
ここで「関数型インターフェース」が必要になる理由を理解してほしい。
ラムダ式 name -> System.out.println(...) はメソッドの宣言がない。引数名と処理だけだ。コンパイラはこれを見て「どのインターフェースの、どのメソッドの実装か」を判断しなければならない。
このとき、もしインターフェースに抽象メソッドが複数あったら、判断できない:
interface Ambiguous {
void methodA(String s);
void methodB(String s);
}
// どちらの実装なのかコンパイラが判断できない
Ambiguous a = name -> System.out.println(name); // ❌ エラー
抽象メソッドが1つだけなら、迷いなく確定できる:
interface Greeter {
void greet(String name); // これしかない
}
// greet() の実装だと確定できる
Greeter g = name -> System.out.println(name); // ✅ OK
この「抽象メソッドがちょうど1つだけのインターフェース」が 関数型インターフェース(Functional Interface) だ。ラムダ式を代入できる型として機能する。
@FunctionalInterface アノテーション
インターフェースに @FunctionalInterface を付けると、コンパイラが「抽象メソッドが本当に1つか」を強制的にチェックする。
@FunctionalInterface
interface Greeter {
void greet(String name); // 1つ → OK
}
誤って2つ書くとコンパイルエラーになる:
@FunctionalInterface
interface Broken {
void methodA();
void methodB(); // ❌ コンパイルエラー
// エラー: Multiple non-overriding abstract methods found in interface Broken
}
重要:@FunctionalInterface がなくても使える
これは試験で頻出の引っかけだ。
// @FunctionalInterface なし
interface Greeter {
void greet(String name);
}
// ← ラムダ式で代入できる。エラーにならない
Greeter g = name -> System.out.println(name);
g.greet("太郎"); // → 太郎
@FunctionalInterface は「このインターフェースが関数型インターフェースであることを宣言し、コンパイラに守らせる」アノテーションだ。付いていなければチェックされないだけで、実際に抽象メソッドが1つなら問題なく動く。
「@FunctionalInterface がないと関数型インターフェースとして使えない」という選択肢はウソ。
また、@FunctionalInterface が付いていてもラムダ式以外の書き方(匿名クラス、普通のクラス)でも実装できる:
@FunctionalInterface
interface Greeter {
void greet(String name);
}
// 匿名クラスでも問題なく使える
Greeter g = new Greeter() {
@Override
public void greet(String name) {
System.out.println("こんにちは、" + name);
}
};
@FunctionalInterface はラムダ式専用の印ではなく、「抽象メソッドを1つに保つ」という設計の宣言だ。
抽象メソッドのカウントルール
関数型インターフェースの判断で最も引っかかりやすいのが、「何が抽象メソッドとしてカウントされるか」だ。
カウントされないケース1:default メソッド
Java 8 から、インターフェースに default キーワードで中身のあるメソッドを書けるようになった。これは実装済みのメソッドなので抽象メソッドではない。
@FunctionalInterface
interface Greeter {
void greet(String name); // 抽象メソッド:カウントする(1つ)
default void greetFormal(String name) { // default → カウントしない
System.out.println("はじめまして、" + name + "様。");
}
default void greetCasual(String name) { // default → カウントしない
System.out.println("よろしく! " + name);
}
}
default メソッドがいくつあっても、抽象メソッドが1つなら有効な関数型インターフェースだ。
カウントされないケース2:static メソッド
Java 8 から、インターフェースに static メソッドも書けるようになった。静的メソッドはインスタンスに属さないので抽象メソッドとして扱われない。
@FunctionalInterface
interface Greeter {
void greet(String name); // 抽象メソッド(1つ)
static Greeter formal() { // static → カウントしない
return name -> System.out.println("はじめまして、" + name + "様。");
}
}
カウントされないケース3:Object クラスのメソッド
Javaではすべてのクラスは Object クラスを継承している。Object には equals()・toString()・hashCode() などのメソッドが最初から存在する。
インターフェースにこれらのメソッドシグネチャを書いても、どのクラスにも必ず実装が存在するため、「実装を強制する」意味がない。したがって抽象メソッドとしてカウントされない。
@FunctionalInterface
interface Validator {
boolean validate(String s); // 抽象メソッド(1つ)
boolean equals(Object obj); // Object のメソッド → カウントしない
String toString(); // Object のメソッド → カウントしない
int hashCode(); // Object のメソッド → カウントしない
}
これは @FunctionalInterface として有効だ。
カウントルールのまとめ
| メソッドの種類 | カウントするか |
|---|---|
| 普通の抽象メソッド(中身なし) | する |
default メソッド(中身あり) | しない |
static メソッド | しない |
Object クラスのメソッド(equals・toString・hashCode等) | しない |
標準の関数型インターフェース
Java には最初から使える関数型インターフェースが java.util.function パッケージに揃っている。全部覚える必要はないが、Java Silver で頻出のものを押さえておく。
Runnable
java.lang パッケージに存在する(import 不要)。
引数なし・戻り値なし。何かの処理を「後で実行する」ために登録しておくときに使う。スレッドの処理を渡す用途が代表的だ。
// 抽象メソッド:void run()
Runnable r = () -> System.out.println("バックグラウンドで動く処理");
r.run(); // → バックグラウンドで動く処理
// new Thread に渡す使い方
Thread t = new Thread(() -> System.out.println("スレッドで実行!"));
t.start();
Supplier<T>
「供給者」の意味。java.util.function パッケージ。
引数なし・T 型の値を返す。「何かを生成して返す」処理に使う。
// 抽象メソッド:T get()
Supplier<String> s1 = () -> "Hello";
Supplier<Double> s2 = () -> Math.random();
Supplier<List<String>> s3 = () -> new ArrayList<>();
System.out.println(s1.get()); // → Hello
System.out.println(s2.get()); // → 0.73... (乱数)
System.out.println(s3.get()); // → []
値を即座に計算したくない場面(遅延評価)でよく使われる。「必要になったときに初めて実行される処理」を渡しておける。
Consumer<T>
「消費者」の意味。java.util.function パッケージ。
T 型の引数を受け取り・何も返さない(void)。受け取った値を「使い切る」処理に使う。
// 抽象メソッド:void accept(T t)
Consumer<String> print = s -> System.out.println(s);
Consumer<String> shout = s -> System.out.println(s.toUpperCase());
print.accept("hello"); // → hello
shout.accept("hello"); // → HELLO
// リストの全要素に対して実行(forEach はConsumerを受け取る)
List<String> names = Arrays.asList("太郎", "花子", "次郎");
names.forEach(name -> System.out.println("こんにちは、" + name));
andThen() メソッドで Consumer を連結することもできる:
Consumer<String> step1 = s -> System.out.println("Step1: " + s);
Consumer<String> step2 = s -> System.out.println("Step2: " + s.toUpperCase());
Consumer<String> combined = step1.andThen(step2);
combined.accept("test");
// → Step1: test
// → Step2: TEST
Function<T, R>
「関数」の意味。java.util.function パッケージ。
T 型の引数を受け取り・R 型の値に変換して返す。型の変換や加工に使う。
// 抽象メソッド:R apply(T t)
Function<String, Integer> toLength = s -> s.length();
Function<String, String> toUpper = s -> s.toUpperCase();
Function<Integer, String> toStr = n -> "No." + n;
System.out.println(toLength.apply("hello")); // → 5
System.out.println(toUpper.apply("hello")); // → HELLO
System.out.println(toStr.apply(42)); // → No.42
andThen() で複数の変換を連結できる:
Function<String, String> trim = s -> s.trim();
Function<String, String> toUpper = s -> s.toUpperCase();
Function<String, String> process = trim.andThen(toUpper);
System.out.println(process.apply(" hello ")); // → HELLO
Predicate<T>
「述語(条件)」の意味。java.util.function パッケージ。
T 型の引数を受け取り・boolean を返す。条件判定に使う。
// 抽象メソッド:boolean test(T t)
Predicate<Integer> isPositive = n -> n > 0;
Predicate<String> isEmpty = s -> s.isEmpty();
Predicate<String> isLong = s -> s.length() > 10;
System.out.println(isPositive.test(5)); // → true
System.out.println(isPositive.test(-3)); // → false
System.out.println(isEmpty.test("")); // → true
System.out.println(isLong.test("hello")); // → false
and()・or()・negate() で条件を組み合わせられる:
Predicate<String> notEmpty = isEmpty.negate(); // 空でない
Predicate<String> valid = notEmpty.and(isLong); // 空でなく、かつ長い
System.out.println(valid.test("")); // → false(空なのでダメ)
System.out.println(valid.test("hello")); // → false(長くないのでダメ)
System.out.println(valid.test("hello world!")); // → true
Stream API のフィルタリングでよく使われる:
List<Integer> numbers = Arrays.asList(-3, -1, 0, 2, 5, 8);
numbers.stream()
.filter(n -> n > 0) // Predicate を渡している
.forEach(System.out::println); // → 2, 5, 8
BiConsumer・BiFunction・BiPredicate(引数2つバージョン)
Bi はラテン語の「2」。引数が2つのバリエーションだ。
import java.util.function.*;
// BiConsumer<T, U>:引数2つ、戻り値なし
BiConsumer<String, Integer> show = (name, age) ->
System.out.println(name + " は " + age + " 歳");
show.accept("田中", 30); // → 田中 は 30 歳
// BiFunction<T, U, R>:引数2つ、変換して返す
BiFunction<String, String, String> concat = (a, b) -> a + " " + b;
System.out.println(concat.apply("Hello", "World")); // → Hello World
// BiPredicate<T, U>:引数2つ、boolean を返す
BiPredicate<String, Integer> isLongerThan = (s, n) -> s.length() > n;
System.out.println(isLongerThan.test("hello", 3)); // → true
UnaryOperator・BinaryOperator(同じ型変換)
入力と出力が同じ型の Function の特殊バリエーションだ。
import java.util.function.*;
// UnaryOperator<T>:T を受け取り T を返す(Function<T,T> の特殊型)
UnaryOperator<String> addPrefix = s -> ">>>" + s;
System.out.println(addPrefix.apply("hello")); // → >>>hello
// BinaryOperator<T>:T を2つ受け取り T を返す(BiFunction<T,T,T> の特殊型)
BinaryOperator<Integer> max = (a, b) -> a > b ? a : b;
System.out.println(max.apply(3, 7)); // → 7
標準関数型インターフェース 一覧
| インターフェース | 引数 | 戻り値 | 抽象メソッド |
|---|---|---|---|
Runnable | なし | なし | void run() |
Supplier<T> | なし | T | T get() |
Consumer<T> | T | なし | void accept(T) |
BiConsumer<T,U> | T, U | なし | void accept(T, U) |
Function<T,R> | T | R | R apply(T) |
BiFunction<T,U,R> | T, U | R | R apply(T, U) |
UnaryOperator<T> | T | T | T apply(T) |
BinaryOperator<T> | T, T | T | T apply(T, T) |
Predicate<T> | T | boolean | boolean test(T) |
BiPredicate<T,U> | T, U | boolean | boolean test(T, U) |
試験ではどう聞かれるか
パターン1:関数型インターフェースかどうかを判定する
// A
@FunctionalInterface
interface A {
void run();
}
// B
@FunctionalInterface
interface B {
void run();
void stop();
}
// C
@FunctionalInterface
interface C {
void run();
default void pause() { System.out.println("一時停止"); }
}
// D
interface D {
void run();
}
// E
@FunctionalInterface
interface E {
void run();
boolean equals(Object o);
String toString();
}
答え:A・C・D・E が有効な関数型インターフェース(としてラムダ式に使える)
- A:抽象メソッド1つ ✅
- B:
@FunctionalInterfaceが付いているのに抽象メソッド2つ → コンパイルエラー ❌ - C:
defaultはカウント外 → 抽象メソッドは1つ ✅ - D:
@FunctionalInterfaceがないが抽象メソッドが1つ → ラムダ式に使える ✅ - E:
equalsとtoStringは Object のメソッド → カウント外 → 抽象メソッドは1つ ✅
B はコンパイルエラーになる(@FunctionalInterface がついているため検出される)点に注意。
パターン2:ラムダ式を代入できるかどうか
@FunctionalInterface
interface Converter {
int convert(String s);
}
Converter c1 = s -> s.length(); // A
Converter c2 = s -> Integer.parseInt(s); // B
Converter c3 = s -> s; // C
Converter c4 = (String s) -> s.length(); // D
Converter c5 = s -> { return s.length(); }; // E
答え:C だけエラー
- A:
String→int✅ - B:
String→int✅ - C:
String→Stringを返そうとしている。convertの戻り値型intと一致しない ❌ - D:引数の型を明示している。問題なし ✅
- E:波カッコ + return の形。問題なし ✅
パターン3:@FunctionalInterface と匿名クラス
// 問題:このコードはコンパイルできるか?
@FunctionalInterface
interface Processor {
String process(String input);
}
Processor p = new Processor() {
@Override
public String process(String input) {
return input.toUpperCase();
}
};
System.out.println(p.process("hello"));
答え:コンパイルできる。出力は HELLO
@FunctionalInterface がついていても匿名クラスで実装できる。ラムダ式だけが使えるわけではない。
パターン4:どの標準 FI 型を使うか
// 問題:空欄に入る正しい型は何か
_____ f1 = () -> new ArrayList<>();
_____ f2 = list -> list.size();
_____ f3 = s -> s.length() > 5;
_____ f4 = s -> System.out.println(s);
答え:
f1:引数なし、値を返す →Supplier<List>f2:引数あり、値を返す(異なる型に変換) →Function<List, Integer>f3:引数あり、boolean を返す →Predicate<String>f4:引数あり、戻り値なし →Consumer<String>
パターン5:継承した場合
@FunctionalInterface
interface Base {
void execute();
}
interface Extended extends Base {
// execute() を継承している
}
問題:Extended はラムダ式に使えるか?
答え:使える。
Extended は Base から execute() を継承しており、自分では抽象メソッドを追加していない。抽象メソッドはちょうど1つなので、関数型インターフェースとして使える。
Extended e = () -> System.out.println("実行!");
e.execute(); // → 実行!
メリット・デメリット
メリット
コードが短くなる
匿名クラスの定型文が不要になり、処理の本質だけを書ける。
意図が明確になる
@FunctionalInterface が付いていれば「ラムダ式で使う設計」だとひと目でわかる。追加の抽象メソッドを誤って入れてしまう事故を防げる。
Stream API・Optional・CompletableFuture の基盤になっている
list.stream().filter(...).map(...).collect(...) などは全て関数型インターフェースを引数に取っている。Javaの現代的なAPIの土台だ。
既存のインターフェースも活用できる
Runnable・Comparable・Comparator など、Java 8 以前から存在するインターフェースも条件を満たせば関数型インターフェースとして使える。後方互換を壊さずにラムダ式のメリットを享受できる。
デメリット
抽象メソッドが1つという制約がある
後からメソッドを追加しようとすると、ラムダ式で使えなくなってしまう。インターフェースの拡張性を犠牲にする側面がある。
型推論が複雑になりやすい
Function<String, Function<Integer, Boolean>> のようなネストした型は可読性が落ちる。
まとめ
関数型インターフェースの定義
抽象メソッドがちょうど1つだけのインターフェース。これがラムダ式を受け取る「型」として機能する。
@FunctionalInterface の意味
「抽象メソッドを1つに保つ」という設計をコンパイラに宣言するアノテーション。なくても動くが、あると安全。
カウントしないもの
default メソッド・static メソッド・Object クラスのメソッドは抽象メソッドとしてカウントしない。
標準関数型インターフェースの使い分け
- 引数なし・戻り値なし →
Runnable - 引数なし・値を返す →
Supplier<T> - 引数あり・戻り値なし →
Consumer<T> - 引数あり・変換して返す →
Function<T, R> - 引数あり・bool 返す →
Predicate<T>
試験で問われる核心は「抽象メソッドが何個あるか、正確に数えられるか」だ。default・static・Object の3種類を除外して数える習慣をつければ、ほぼすべての問題を正解できる。