概要

業務ルールや計算式をコードに直接埋め込むのではなく、文法規則として構造化する。Interpreter パターンは、四則演算や論理式の評価、設定ファイルの簡易パーサーなど、小さな言語を扱う場面で有効です。構文木の各ノードが自身の評価ロジックを持ち、再帰的に式全体を計算する仕組みは、コンパイラの教科書的な構成でもあります。ただし文法が複雑になると Expression クラスが膨れるため、適用範囲は限られます。この記事では、四則演算の式ツリーを題材に、終端式・非終端式の構造をクラスとして実装します。Java 17 の sealed interface + record で式の種類を型安全に限定し、Java 21 の switch パターンマッチングで評価ロジックを外出しにする手法も確認します。

使いどころ

帳票テンプレートに埋め込まれた計算式(例: 「単価 * 数量 - 値引額」)を式ツリーに変換して動的に評価する

ワークフローの条件分岐ルール(例: 「金額 > 10000 AND 部門 = 営業」)を論理式として解釈し、承認要否を判定する

設定ファイル中の簡易DSL(独自記法)をパースし、処理パラメータとして評価する

コード例

InterpreterPatternDemo.java
public class InterpreterPatternDemo {

    sealed interface Expression
            permits NumberExpression, AddExpression,
                    SubtractExpression, MultiplyExpression,
                    DivideExpression {
        int interpret();
    }

    record NumberExpression(int value) implements Expression {
        @Override public int interpret() { return value; }
    }

    record AddExpression(Expression left, Expression right)
            implements Expression {
        @Override public int interpret() {
            return left.interpret() + right.interpret();
        }
    }

    record SubtractExpression(Expression left, Expression right)
            implements Expression {
        @Override public int interpret() {
            return left.interpret() - right.interpret();
        }
    }

    record MultiplyExpression(Expression left, Expression right)
            implements Expression {
        @Override public int interpret() {
            return left.interpret() * right.interpret();
        }
    }

    record DivideExpression(Expression left, Expression right)
            implements Expression {
        @Override public int interpret() {
            int divisor = right.interpret();
            if (divisor == 0) {
                throw new ArithmeticException("ゼロ除算");
            }
            return left.interpret() / divisor;
        }
    }

    static String toFormula(Expression expr) {
        if (expr instanceof NumberExpression num) {
            return String.valueOf(num.value());
        } else if (expr instanceof AddExpression add) {
            return "(" + toFormula(add.left()) + " + "
                       + toFormula(add.right()) + ")";
        } else if (expr instanceof SubtractExpression sub) {
            return "(" + toFormula(sub.left()) + " - "
                       + toFormula(sub.right()) + ")";
        } else if (expr instanceof MultiplyExpression mul) {
            return "(" + toFormula(mul.left()) + " * "
                       + toFormula(mul.right()) + ")";
        } else if (expr instanceof DivideExpression div) {
            return "(" + toFormula(div.left()) + " / "
                       + toFormula(div.right()) + ")";
        }
        return "?";
    }

    public static void main(String[] args) {
        // (3 + 5) * 2 - 4 = 12
        var expr = new SubtractExpression(
            new MultiplyExpression(
                new AddExpression(
                    new NumberExpression(3), new NumberExpression(5)),
                new NumberExpression(2)),
            new NumberExpression(4));

        System.out.println("式: " + toFormula(expr));
        System.out.println("結果: " + expr.interpret());

        // 式の再利用: base = 5 + 3 を2倍
        var base = new AddExpression(
            new NumberExpression(5), new NumberExpression(3));
        var doubled = new MultiplyExpression(base,
                                             new NumberExpression(2));
        System.out.println("base = " + toFormula(base)
                         + " = " + base.interpret());
        System.out.println("doubled = " + toFormula(doubled)
                         + " = " + doubled.interpret());
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

sealed interface + record で式の種類を限定でき、instanceof パターンマッチングでキャスト不要になる。

Java 17
// Java 17: sealed interface + record で型安全に
sealed interface Expression
    permits NumberExpression, AddExpression,
            SubtractExpression, MultiplyExpression,
            DivideExpression {
    int interpret();
}
record NumberExpression(int value) implements Expression {
    @Override public int interpret() { return value; }
}
// instanceof パターンマッチングでキャスト不要
if (expr instanceof NumberExpression num) {
    return String.valueOf(num.value());
}

Library Comparison

標準 API(sealed interface + record)四則演算や簡易条件式など、文法規則が少ない場面。外部依存なしで完結する。文法が複雑になると Expression クラスが爆発的に増える。
ANTLRBNF で定義できる本格的な文法のパーサーを生成したい場合。SQL サブセットやDSL の解析に向く。学習コストが高く、ビルド手順も増える。簡易な式評価には過剰。
javax.script(Nashorn / GraalJS)JavaScript の式評価をそのまま流用したい場合。Nashorn は Java 15 で削除済み。GraalJS は別途依存が必要。

注意点

Interpreter パターンは文法規則が10種類を超えると管理しにくくなる。複雑な文法にはパーサージェネレーターやスクリプトエンジンを検討する

式ツリーの深さに上限を設けないと、再帰呼び出しで StackOverflowError が発生する可能性がある

ゼロ除算や型不一致など、評価時の例外処理を各 Expression で確実に行うこと。特に DivideExpression は right.interpret() == 0 のガードが必須

sealed interface の permits リストに新しい式を追加すると、switch 文の網羅チェックでコンパイルエラーになる。これは安全性の面ではメリットだが、拡張頻度が高い場合は設計を見直す

FAQ

Interpreter パターンと Visitor パターンはどう使い分けますか。

Interpreter は各ノードが自身の評価ロジックを持ちます。Visitor は評価ロジックをノードの外に分離し、複数の走査処理を追加しやすくします。式の種類が固定で処理を増やしたい場合は Visitor が有利です。

式の評価結果を int 以外にするにはどうすればよいですか。

interpret の戻り値をジェネリクスや Object にする方法もありますが、型安全性が落ちます。sealed interface で Expression<T> とするか、結果を Value record でラップする方が安全です。

業務で Interpreter パターンを使う場面は多いですか。

直接使う機会は多くありませんが、計算式や条件式の動的評価、テンプレートエンジンの内部構造など、知っておくと設計の引き出しが広がります。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます