Javaにおけるインタープリターパターン:Javaアプリケーション向けのカスタムパーサーの構築
インタープリターデザインパターンの意図
インタープリターデザインパターンは、言語の文法表現を定義し、この文法を処理するためのインタープリターを提供するために使用されます。このパターンは、算術式やスクリプト言語など、特定のルールセットまたは文法を解釈して処理する必要があるシナリオで役立ちます。
現実世界の例を用いたインタープリターパターンの詳細な説明
現実世界の例
ユーザーが入力した式を解釈して計算するように設計された電卓アプリケーションを考えてみましょう。このアプリケーションは、Javaのインタープリターパターンを使用して、「5 + 3 * 2」などの算術式を解析して評価します。ここで、インタープリターは式の各部分を、数値と演算を表すオブジェクトに変換します。これらのオブジェクトは定義済みの文法に従い、アプリケーションが算術のルールに基づいて結果を正しく理解して計算できるようにします。式の各要素はプログラム構造のクラスに対応し、入力された算術式に対する解析と評価プロセスを簡素化します。
簡単に言うと
インタープリターデザインパターンは、言語の文法の表現と、その表現を使用して言語の文を解釈するインタープリターを定義します。
Wikipediaによると
コンピュータープログラミングにおいて、インタープリターパターンは、言語の文を評価する方法を指定するデザインパターンです。基本的な考え方は、特殊なコンピューター言語における各記号(終端記号または非終端記号)に対してクラスを持つことです。言語の文の構文木はコンポジットパターンのインスタンスであり、クライアントのために文を評価(解釈)するために使用されます。
Javaにおけるインタープリターパターンのプログラミング例
Javaで基本的な数学を解釈するには、式の階層が必要です。Expression
クラスが基底クラスであり、NumberExpression
などの具体的な実装は文法の特定の部分を処理します。Javaのインタープリターパターンは、算術式をアプリケーションが処理できる構造化された形式に変換することにより、算術式の解析と評価を簡素化します。
public abstract class Expression {
public abstract int interpret();
@Override
public abstract String toString();
}
最も単純な式は、単一の整数を格納するNumberExpression
です。
public class NumberExpression extends Expression {
private final int number;
public NumberExpression(int number) {
this.number = number;
}
public NumberExpression(String s) {
this.number = Integer.parseInt(s);
}
@Override
public int interpret() {
return number;
}
@Override
public String toString() {
return "number";
}
}
より複雑な式には、PlusExpression
、MinusExpression
、MultiplyExpression
などの演算があります。以下に最初の例を示しますが、他の演算も同様です。
public class PlusExpression extends Expression {
private final Expression leftExpression;
private final Expression rightExpression;
public PlusExpression(Expression leftExpression, Expression rightExpression) {
this.leftExpression = leftExpression;
this.rightExpression = rightExpression;
}
@Override
public int interpret() {
return leftExpression.interpret() + rightExpression.interpret();
}
@Override
public String toString() {
return "+";
}
}
これで、簡単な数学を解析するインタープリターパターンを動作させることができます。
@Slf4j
public class App {
public static void main(String[] args) {
// the halfling kids are learning some basic math at school
// define the math string we want to parse
final var tokenString = "4 3 2 - 1 + *";
// the stack holds the parsed expressions
var stack = new Stack<Expression>();
// tokenize the string and go through them one by one
var tokenList = tokenString.split(" ");
for (var s : tokenList) {
if (isOperator(s)) {
// when an operator is encountered we expect that the numbers can be popped from the top of
// the stack
var rightExpression = stack.pop();
var leftExpression = stack.pop();
LOGGER.info("popped from stack left: {} right: {}",
leftExpression.interpret(), rightExpression.interpret());
var operator = getOperatorInstance(s, leftExpression, rightExpression);
LOGGER.info("operator: {}", operator);
var result = operator.interpret();
// the operation result is pushed on top of the stack
var resultExpression = new NumberExpression(result);
stack.push(resultExpression);
LOGGER.info("push result to stack: {}", resultExpression.interpret());
} else {
// numbers are pushed on top of the stack
var i = new NumberExpression(s);
stack.push(i);
LOGGER.info("push to stack: {}", i.interpret());
}
}
// in the end, the final result lies on top of the stack
LOGGER.info("result: {}", stack.pop().interpret());
}
public static boolean isOperator(String s) {
return s.equals("+") || s.equals("-") || s.equals("*");
}
public static Expression getOperatorInstance(String s, Expression left, Expression right) {
return switch (s) {
case "+" -> new PlusExpression(left, right);
case "-" -> new MinusExpression(left, right);
default -> new MultiplyExpression(left, right);
};
}
}
プログラムを実行すると、次のコンソール出力が生成されます。
13:33:15.437 [main] INFO com.iluwatar.interpreter.App -- push to stack: 4
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- push to stack: 3
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- push to stack: 2
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- popped from stack left: 3 right: 2
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- operator: -
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- push result to stack: 1
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- push to stack: 1
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- popped from stack left: 1 right: 1
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- operator: +
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- push result to stack: 2
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- popped from stack left: 4 right: 2
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- operator: *
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- push result to stack: 8
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- result: 8
現実世界の例を用いたインタープリターパターンの詳細な説明

Javaでインタープリターパターンを使用する場面
文を解釈する言語があり、その言語の文を抽象構文木として表現できる場合は、インタープリターデザインパターンを使用します。インタープリターパターンは、以下の場合に最適です。
- 文法が単純な場合。複雑な文法の場合、文法のクラス階層は大きくなり、管理不能になります。このような場合は、パーサージェネレーターなどのツールの方が良い選択肢です。抽象構文木を構築せずに式を解釈できるため、スペースと時間を節約できる可能性があります。
- 効率が重要な懸念事項ではない場合。最も効率的なインタープリターは、通常、構文木を直接解釈することによって実装されるのではなく、最初に別の形式に変換することによって実装されます。たとえば、正規表現は多くの場合、ステートマシンに変換されます。しかし、それでも翻訳機はインタープリターパターンによって実装できるため、パターンはまだ適用可能です。
Javaにおけるインタープリターパターンの現実世界のアプリケーション
- java.util.Pattern
- java.text.Normalizer
- java.text.Formatのすべてのサブクラス
- javax.el.ELResolver
- さまざまなデータベース管理システムのSQLパーサー。
インタープリターパターンのメリットとトレードオフ
メリット
- 文法やデータのクラスを変更せずに、式を解釈する新しい演算を簡単に追加できます。
- 文法を言語に直接実装することで、修正や拡張が容易になります。
トレードオフ
- 大規模な文法では複雑で非効率になる可能性があります。
- 文法の各ルールはクラスになるため、複雑な文法では多数のクラスが生じます。
関連するJavaデザインパターン
- コンポジット:インタープリターパターンが文法をツリー構造として表現するためにコンポジットパターンを利用するなど、多くの場合一緒に使用されます。
- フライウェイト:特に言語の繰り返し要素を扱うインタープリターの場合、メモリ使用量を削減するために状態の共有に役立ちます。