はじめに
Javaクラスをロードしてコンストラクタを呼び出し、オブジェクト生成に対応したいと思います。
この記事は「JSON風のオブジェクト定義に対応する」の続きの記事です。
オブジェクト生成対応でやりたいこと
オブジェクト生成対応でやりたいことを確認します。
例えば下のようなプログラムがあります。
loadClass
関数を既定のグローバル関数として新たに導入します。
loadClass
関数は引数に文字列で、Javaの完全修飾クラス名を渡すとそのクラスをロードします。(1行目)
ロードしたものは変数へ代入します。
その変数にnew
を付けて関数のように呼び出すと、インスタンスを生成します。(2行目)
var dateClass = loadClass("java.util.Date")
var date = new dateClass()
println(date.toString())
実装の仕方
実装の仕方について、構文解析(Parser)、インタプリタ(Interpreter)と順に考えていきます。
字句解析(Lexer)の変更はありません。
構文解析(Parser)の実装の仕方
新しく追加された構文はnew
なので、new
の解析方法を考えます。
new
は構文上で1つ目にくるので、他の1つ目のトークンで構文決まるものと同じように、
lead()
メソッドへnew
の構文を解析する処理を実装します。
new
の次にくる構文の要素は関数呼び出しと同じ構文になるので、
new
の構文を解析する処理では、次の要素が関数呼び出しであることを確認します。
インタプリタ(Interpreter)の実装の仕方
インタプリタでは新たに導入するloadClass
関数と、new
構文の実行を実装します。
loadClass
関数はすでにある既定のグローバル関数のprintln
関数と同様に実装します。
new
構文実行の実装は、new
の後ろにくる構文を
関数呼び出しのように扱い、インスタンスを生成するようにします。
Javaで実装してみる
実装にうつります。
構文解析(Parser)、インタプリタ(Interpreter)について、
変更と追加をしたところを順にみていきます。
Parser.java
Parser.javaの実装です。
new
構文の解析を実装します。
1つ目のトークンで構文が決まるものを実装するlead()
メソッドの
// Add
のところへnew
の構文解析を行うnew_()
メソッドの呼び出しを追加しました。
private Token lead(Token token) throws Exception {
if (token.kind.equals("ident") && token.value.equals("function")) {
return func(token);
} else if (token.kind.equals("ident") && token.value.equals("return")) {
token.kind = "ret";
if (!token().kind.equals("eob")) {
token.left = expression(0);
}
return token;
} else if (token.kind.equals("ident") && token.value.equals("if")) {
return if_(token);
} else if (token.kind.equals("ident") && token.value.equals("while")) {
return while_(token);
} else if (token.kind.equals("ident") && token.value.equals("break")) {
token.kind = "brk";
return token;
} else if (token.kind.equals("ident") && token.value.equals("var")) {
return var(token);
// Add
} else if (token.kind.equals("ident") && token.value.equals("new")) {
return new_(token);
} else if (factorKinds.contains(token.kind)) {
return token;
} else if (unaryOperators.contains(token.value)) {
token.kind = "unary";
token.left = expression(70);
return token;
} else if (token.kind.equals("paren") && token.value.equals("(")) {
Token expr = expression(0);
consume(")");
return expr;
} else if (token.kind.equals("bracket") && token.value.equals("[")) {
return newArray(token);
} else if (token.kind.equals("curly") && token.value.equals("{")) {
return newMap(token);
} else {
throw new Exception("The token cannot place there.");
}
}
new
構文解析を行うnew_()
メソッドを追加しました。
引数のtoken
はnew
トークンが渡されてきます。
インタプリタ処理でのキーになる、new
トークンの種類、kind
はnew
にしました。
new
トークンのleft
はexpression
メソッドの戻り値を割り付けます。
その戻り値が関数呼び出しの構文解析になっていることを確認します。
private Token new_(Token token) throws Exception {
token.kind = "new";
token.left = expression(0);
if (!(token.left.value.equals("(") && token.left.kind.equals("paren"))) {
throw new Exception("No constructor invocation.");
}
return token;
}
Parser.javaの変更は以上です。
Interpreter.java
Interpreter.javaの実装です。
loadClass
関数の実体にあたるクラスを追加しました。
invoke
メソッドの引数は、loadClass
関数呼び出しの引数に当たります。
引数の1つ目が完全修飾クラス名の文字列になっている前提で、
Class.forName
メソッドを使ってクラスをロードします。
public static class LoadClass extends Func {
public LoadClass() {
name = "loadClass";
}
@Override
public Object invoke(List<Object> args) throws Exception {
return Class.forName((String) args.get(0));
}
}
init()
メソッドの変更です。
インタプリタを初期化するメソッドです。
// Add
のところに追加したLoadClass
クラスをインスタンス化して、
既定のグローバル関数として呼び出しできるように、
グローバルスコープのfunctions
へ加えます。
public Interpreter init(List<Token> body) {
global = new Scope();
local = global;
// Add
Func loadClass = new LoadClass();
global.functions.put(loadClass.name, loadClass);
Func f = new Println();
global.functions.put(f.name, f);
this.body = body;
return this;
}
expression()
メソッドの変更です。
式を表すトークンの意味(kind)によって分岐する処理です。
// Add
の下へオブジェクト生成のためのnew_()
メソッド呼び出しを追加しました。
public Object expression(Token expr) throws Exception {
if (expr.kind.equals("digit")) {
return digit(expr);
} else if (expr.kind.equals("string")) {
return string(expr);
} else if (expr.kind.equals("ident")) {
return ident(expr);
} else if (expr.kind.equals("blank")) {
return blank(expr);
// Add
} else if (expr.kind.equals("new")) {
return new_(expr);
} else if (expr.kind.equals("newMap")) {
return newMap(expr);
} else if (expr.kind.equals("newArray")) {
return newArray(expr);
} else if (expr.kind.equals("bracket")) {
return accessArrayOrMap(expr);
} else if (expr.kind.equals("func")) {
return func(expr);
} else if (expr.kind.equals("fexpr")) {
return fexpr(expr);
} else if (expr.kind.equals("paren")) {
return invoke(expr);
} else if (expr.kind.equals("sign") && expr.value.equals("=")) {
return assign(expr);
} else if (expr.kind.equals("unary")) {
return unaryCalc(expr);
} else if (expr.kind.equals("sign")) {
return calc(expr);
} else if (expr.kind.equals("dot")) {
return dot(expr);
} else {
throw new Exception("Expression error");
}
}
オブジェクト生成のためのnew_()
メソッドを追加しました。
このメソッドでコンストラクタを呼び出し、生成したオブジェクトを返します。
new_()
メソッド引数のexpr
にはnew
トークンが渡されます。
new
トークンのleft
は関数呼び出し構文になっているトークンが割り付けられています。
関数呼び出し構文になっているnew
トークンのleft
の、さらにleft
は関数名にあたる部分になっています。
new
トークンのleft
のparams
は、関数呼び出し構文の引数にあたる部分になっています。
関数名にあたる部分(expr.left.left
)は、コンストラクタ呼び出しではクラスであるため、
処理の始めではその部分をクラスへ解決します。
コンストラクタ呼び出しを行うには、コンストラクタを表す情報を得る必要があります。
コンストラクタの情報はクラスを表す情報から得ることができます。
クラスへ解決されたc
変数がそのクラスの情報にあたります。
クラス情報からコンストラクタ情報を得ることができますが、
クラスに複数定義されているコンストラクタのうち、
呼び出したいコンストラクタの情報を1つ選ぶ必要があります。
new_
メソッドでは、呼び出したいコンストラクタ情報を1つに選ぶために、
コンストラクタ呼び出しの引数の型情報を使って絞り込みます。
該当するコンストラクタ情報を見つけられないか、1つに絞り込めなかったときはエラーにしています。
絞り込みの仕方は非常に簡便な方法で、Javaコンパイラでの実装とは違っています。
少し詳しい説明をコメント文にしました。
public Object new_(Token expr) throws Exception {
// "("の左側をクラスに解決し、そのクラスのコンストラクタ一覧を取得します
Class<?> c = class_(expression(expr.left.left));
Constructor<?>[] ctors = c.getConstructors();
// "("の右側のコンストラクタ引数を、値へ解決した一覧にします
List<Object> args = new ArrayList<Object>();
for (Token arg : expr.left.params) {
args.add(value(expression(arg)));
}
// 引数から引数の型の一覧を作ります
List<Class<?>> aClasses = argClasses(args);
// 引数の型の一覧が代入可能なシグニチャーになっているコンストラクターのみに絞った一覧にします
List<Constructor<?>> byAssignables = ctorsByAssignable(ctors, aClasses);
// 絞った結果、該当するコンストラクターがなかったらエラー
if (byAssignables.size() == 0) {
throw new Exception("No constructor error");
}
Constructor<?> ctor;
if (byAssignables.size() == 1) {
// 絞った結果、該当するコンストラクターが1つだったら、
// それが呼び出し対象のコンストラクター
ctor = byAssignables.get(0);
} else {
// 絞った結果、該当するコンストラクターが2つ以上だったら、さらに絞り込みます。
// 代入可能なシグニチャーで絞ったコンストラクターの一覧を、
// 引数の型の一覧が完全に一致するシグニチャーになっているコンストラクターのみに絞り込みます。
List<Constructor<?>> byAbsolutes = ctorsByAbsolute(byAssignables, aClasses);
// 絞った結果、該当するコンストラクターが1つにならなかったらエラー
if (byAbsolutes.size() != 1) {
throw new Exception("No constructor error");
}
// 絞った結果、該当するコンストラクターが1つだったら、
// それが呼び出し対象のコンストラクター
ctor = byAbsolutes.get(0);
}
// 1つに絞れたコンストラクターを使って、コンストラクター呼び出しを行う
Object val = ctor.newInstance(args.toArray());
return val;
}
public List<Class<?>> argClasses(List<Object> args) {
List<Class<?>> classes = new ArrayList<Class<?>>();
int psize = args.size();
for (int i = 0; i < psize; ++i) {
Object a = args.get(i);
if (a != null) {
classes.add(a.getClass());
} else {
classes.add(null);
}
}
return classes;
}
public static List<Constructor<?>> ctorsByAssignable(Constructor<?>[] ctors, List<Class<?>> aClasses) {
List<Constructor<?>> candidates = new ArrayList<Constructor<?>>();
int aSize = aClasses.size();
for (Constructor<?> ctor : ctors) {
Class<?>[] pTypes = ctor.getParameterTypes();
if (pTypes.length != aSize) {
continue;
}
Boolean allAssignable = true;
for (int i = 0; i < aSize; ++i) {
Class<?> c = pTypes[i];
Class<?> cc = toBoxClass(c);
Class<?> ac = aClasses.get(i);
if (ac != null) {
Class<?> acc = toBoxClass(ac);
allAssignable &= cc.isAssignableFrom(acc);
}
if (!allAssignable) {
break;
}
}
if (allAssignable) {
candidates.add(ctor);
}
}
return candidates;
}
public static List<Constructor<?>> ctorsByAbsolute(List<Constructor<?>> candidates, List<Class<?>> aClasses) {
List<Constructor<?>> screened = new ArrayList<Constructor<?>>();
int aSize = aClasses.size();
for (int i = 0; i < aSize; ++i) {
Class<?> ac = aClasses.get(i);
if (ac == null) {
return screened;
}
}
for (Constructor<?> ctor : candidates) {
Class<?>[] pTypes = ctor.getParameterTypes();
if (aSize != pTypes.length) {
continue;
}
Boolean allEquals = true;
for (int i = 0; i < aSize; ++i) {
Class<?> c = pTypes[i];
Class<?> ac = aClasses.get(i);
allEquals &= c == ac;
if (!allEquals) {
break;
}
}
if (allEquals) {
screened.add(ctor);
}
}
return screened;
}
引数の値がクラスであることを保証するclass_()
メソッドを追加しました。
上のnew_()
メソッドの始めで使われます。
public Class<?> class_(Object value) throws Exception {
if (value instanceof Class<?>) {
return (Class<?>) value;
} else if (value instanceof Variable) {
Variable v = (Variable) value;
return class_(v.value);
}
throw new Exception("right value error");
}
引数が値であることを保証するメソッドの、value()
メソッドを変更しました。
いままでの実装方針は、引数value
の型が値として妥当か調べていました。
オブジェクトを生成できるようになったので、値の型を全て調べて妥当か判断することができなくなりました。
そこで値として不適切であることをあらわす値の、void_
をスタティックフィールド変数に加えました。
もし引数value
がvoid_
ならば値として不適切とみなす実装方針へ変更しました。
Scope global;
Scope local;
List<Token> body;
// Add
public static Object void_ = new Object();
public Object value(Object value) throws Exception {
if (value == void_) {
throw new Exception("right value error");
} else if (value instanceof Variable) {
Variable v = (Variable) value;
return value(v.value);
}
return value;
}
値として不適切であることをあらわす値のvoid_
追加に対応して、
いままで値を返さないものはnull
を返していたところをvoid_
を返すように変更しました。
下の各メソッドで// Update
とある箇所をvoid_
を返すようにしました。
public Object body(List<Token> body, boolean[] ret, boolean[] brk) throws Exception {
for (Token exprs : body) {
if (exprs.kind.equals("if")) {
Object val = if_(exprs, ret, brk);
if (ret != null && ret[0]) {
return val;
}
} else if (exprs.kind.equals("ret")) {
if (ret == null) {
throw new Exception("Can not return");
}
ret[0] = true;
if (exprs.left == null) {
// Update
return void_;
} else {
return expression(exprs.left);
}
} else if (exprs.kind.equals("while")) {
Object val = while_(exprs, ret);
if (ret != null && ret[0]) {
return val;
}
} else if (exprs.kind.equals("brk")) {
if (brk == null) {
throw new Exception("Can not break");
}
brk[0] = true;
// Update
return void_;
} else if (exprs.kind.equals("var")) {
var(exprs);
} else {
expression(exprs);
}
}
// Update
return void_;
}
public Object ret(Token token) throws Exception {
if (token.left == null) {
// Update
return void_;
}
return expression(token.left);
}
public Object if_(Token token, boolean[] ret, boolean[] brk) throws Exception {
List<Token> block;
if (isTrue(token.left)) {
block = token.block;
} else {
block = token.blockOfElse;
}
if (block != null) {
return body(block, ret, brk);
} else {
// Update
return void_;
}
}
public Object while_(Token token, boolean[] ret) throws Exception {
boolean[] brk = new boolean[1];
Object val;
while (isTrue(token.left)) {
val = body(token.block, ret, brk);
if (ret != null && ret[0]) {
return val;
}
if (brk[0]) {
// Update
return void_;
}
}
// Update
return void_;
}
public Object var(Token token) throws Exception {
for (Token item : token.block) {
String name;
Token expr;
if (item.kind.equals("ident")) {
name = item.value;
expr = null;
} else if (item.kind.equals("sign") && item.value.equals("=")) {
name = item.left.value;
expr = item;
} else {
throw new Exception("var error");
}
if (!local.variables.containsKey(name)) {
newVariable(name);
}
if (expr != null) {
expression(expr);
}
}
// Update
return void_;
}
public static class Println extends Func {
public Println() {
name = "println";
}
@Override
public Object invoke(List<Object> args) throws Exception {
Object arg = args.size() > 0 ? args.get(0) : null;
System.out.println(arg);
// Update
return void_;
}
}
以上の実装を使って下のプログラム
var dateClass = loadClass("java.util.Date")
var date = new dateClass()
println(date.toString())
を実行し、Date
オブジェクトを生成して実行日時を出力します。
public static void main(String[] args) throws Exception {
String text = "";
text += "var dateClass = loadClass(\"java.util.Date\")";
text += "var date = new dateClass()";
text += "println(date.toString())";
List<Token> tokens = new Lexer().init(text).tokenize();
List<Token> blk = new Parser().init(tokens).block();
new Interpreter().init(blk).run();
// --> Sat Jun 17 18:29:13 JST 2017 (実行した日時)
}
実装は以上です。
ありがとうございました。
おわりに
ソースの全文はこちらで公開しています。
Calc
https://github.com/quwahara/Calc/tree/article-19-class-loading/Calc/src/main/java
続きの記事があります。
スタティックメソッド呼び出しに対応する
http://qiita.com/quwahara/items/a8ef47f78b1479a117de