この記事は株式会社富士通システムズウェブテクノロジーの社内技術コミュニティで、「イノベーション推進コミュニティ」
略して「いのべこ」が企画する、いのべこ Advent Calendar 2020の16日目の記事です。
本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。
ここまでお約束
はじめに
この記事は、Scalaを始めたJavaプログラマが、Scalaをコンパイルして生成されたclassファイルを逆コンパイルし、
Java的な観点からScalaを理解したいなと思いついて始めたものです。
今回は、Class~Tuple編の内容を主に振り返っていきたいと思います。
Class
何の面白みもない基本的なクラス
class User
import scala.reflect.ScalaSignature;
@ScalaSignature(bytes = "\006\005]1AAA\002\001\031!)1\003\001C\001)\t!Qk]3s\025\t!Q!\001\btG\006d\027\rZ3d_6\004\030\016\\3\013\005\0319\021A\0034jg\"L'-Y:iS*\021\001\"C\001\007O&$\b.\0362\013\003)\t1aY8n\007\001\031\"\001A\007\021\0059\tR\"A\b\013\003A\tQa]2bY\006L!AE\b\003\r\005s\027PU3g\003\031a\024N\\5u}Q\tQ\003\005\002\027\0015\t1\001")
public class User {}
特にいうことはない・・・・
何の面白みもない基本クラス2
class Point(var x: Int, var y: Int) {
def move(dx: Int, dy: Int): Unit = {
x = x + dx
y = y + dy
}
override def toString: String =
s"($x, $y)"
}
@ScalaSignature(bytes = "\006\005\0313AAC\006\001)!A1\004\001BA\002\023\005A\004\003\005!\001\t\005\r\021\"\001\"\021!9\003A!A!B\023i\002\002\003\025\001\005\003\007I\021\001\017\t\021%\002!\0211A\005\002)B\001\002\f\001\003\002\003\006K!\b\005\006[\001!\tA\f\005\006g\001!\t\001\016\005\006s\001!\tE\017\002\006!>Lg\016\036\006\003\0315\tab]2bY\006$WmY8na&dWM\003\002\017\037\005Qa-[:iS\n\f7\017[5\013\005A\t\022AB4ji\",(MC\001\023\003\r\031w.\\\002\001'\t\001Q\003\005\002\02735\tqCC\001\031\003\025\0318-\0317b\023\tQrC\001\004B]f\024VMZ\001\002qV\tQ\004\005\002\027=%\021qd\006\002\004\023:$\030!\002=`I\025\fHC\001\022&!\t12%\003\002%/\t!QK\\5u\021\0351#!!AA\002u\t1\001\037\0232\003\tA\b%A\001z\003\025Ix\fJ3r)\t\0213\006C\004'\013\005\005\t\031A\017\002\005e\004\023A\002\037j]&$h\bF\0020cI\002\"\001\r\001\016\003-AQaG\004A\002uAQ\001K\004A\002u\tA!\\8wKR\031!%N\034\t\013YB\001\031A\017\002\005\021D\b\"\002\035\t\001\004i\022A\0013z\003!!xn\025;sS:<G#A\036\021\005q\032eBA\037B!\tqt#D\001@\025\t\0015#\001\004=e>|GOP\005\003\005^\ta\001\025:fI\0264\027B\001#F\005\031\031FO]5oO*\021!i\006")
public class Point {
private int x;
private int y;
public int x() {
return this.x;
}
public void x_$eq(int x$1) {
this.x = x$1;
}
public int y() {
return this.y;
}
public void y_$eq(int x$1) {
this.y = x$1;
}
public Point(int x, int y) {}
public void move(int dx, int dy) {
x_$eq(x() + dx);
y_$eq(y() + dy);
}
public String toString() {
return (new StringBuilder(4)).append("(").append(x()).append(", ").append(y()).append(")").toString();
}
}
、getter/setter、各プロパティに対する_$eqメソッドが生えてます。
toStringメソッドのoverrideキーワードは消え去ってますね。
デフォルト引数を持つコンストラクタ
class Point(var x: Int = 0, var y: Int = 0)
@ScalaSignature(bytes = "\006\005\0353A!\004\b\001/!Aa\004\001BA\002\023\005q\004\003\005$\001\t\005\r\021\"\001%\021!Q\003A!A!B\023\001\003\002C\026\001\005\003\007I\021A\020\t\0211\002!\0211A\005\0025B\001b\f\001\003\002\003\006K\001\t\005\006a\001!\t!M\004\bm9\t\t\021#\0018\r\035ia\"!A\t\002aBQ\001M\005\005\002eBqAO\005\022\002\023\0051\bC\004G\023E\005I\021A\036\003\013A{\027N\034;\013\005=\001\022AD:dC2\fG-Z2p[BLG.\032\006\003#I\t!BZ5tQ&\024\027m\0355j\025\t\031B#\001\004hSRDWO\031\006\002+\005\0311m\\7\004\001M\021\001\001\007\t\0033qi\021A\007\006\0027\005)1oY1mC&\021QD\007\002\007\003:L(+\0324\002\003a,\022\001\t\t\0033\005J!A\t\016\003\007%sG/A\003y?\022*\027\017\006\002&QA\021\021DJ\005\003Oi\021A!\0268ji\"9\021FAA\001\002\004\001\023a\001=%c\005\021\001\020I\001\002s\006)\021p\030\023fcR\021QE\f\005\bS\025\t\t\0211\001!\003\tI\b%\001\004=S:LGO\020\013\004eQ*\004CA\032\001\033\005q\001b\002\020\b!\003\005\r\001\t\005\bW\035\001\n\0211\001!\003\025\001v.\0338u!\t\031\024b\005\002\n1Q\tq'A\016%Y\026\0348/\0338ji\022:'/Z1uKJ$C-\0324bk2$H%M\013\002y)\022\001%P\026\002}A\021q\bR\007\002\001*\021\021IQ\001\nk:\034\007.Z2lK\022T!a\021\016\002\025\005tgn\034;bi&|g.\003\002F\001\n\tRO\\2iK\016\\W\r\032,be&\fgnY3\0027\021bWm]:j]&$He\032:fCR,'\017\n3fM\006,H\016\036\0233\001")
public class Point {
private int x;
private int y;
public static int $lessinit$greater$default$2() {
return Point$.MODULE$.$lessinit$greater$default$2();
}
public static int $lessinit$greater$default$1() {
return Point$.MODULE$.$lessinit$greater$default$1();
}
public int x() {
return this.x;
}
public void x_$eq(int x$1) {
this.x = x$1;
}
public int y() {
return this.y;
}
public void y_$eq(int x$1) {
this.y = x$1;
}
public Point(int x, int y) {}
}
public final class Point$ {
public static final Point$ MODULE$ = new Point$();
public int $lessinit$greater$default$1() {
return 0;
}
public int $lessinit$greater$default$2() {
return 0;
}
}
なんと、PointクラスとPoint$クラスに分離してしまいました。
デフォルト値のほうは、Point$クラスのstaticメソッドとして実装されています($lessinit$greater$default$?から呼ばないのはなぜなのだろうか・・・)
Javaにはデフォルト引数の概念はなく、オーバーロードやBuilderパターンなどを用いて実装されることが多いです。
にしても、staticな定数を呼び出しているメソッドと値を分離するメリットもよくわからないし、
$lessinit$greater$default$?はいつ呼び出されているのだろうか・・・
試しにjshellに上記のjavaコードを張り付けて実行してみたところ、以下のようなエラーになったのでScalaのRuntime Reflectionが何らかの解決をしてくれているのだろうな・・・となった。
var point = new Point()
| エラー:
| クラス Pointのコンストラクタ Pointは指定された型に適用できません。
| 期待値: int,int
| 検出値: 引数がありません
| 理由: 実引数リストと仮引数リストの長さが異なります
| var point = new Point();
| ^---------^
ちょうど次の章でデフォルト引数に触れる予定だったらしく、「Scalaで定義したデフォルトパラメータはJavaのコードから呼び出される時はオプショナルではありません。」と書かれていた。
名前付き引数
メソッドを呼び出すとき、引数に名前を付けることができます。
def printName(first: String, last: String): Unit = {
println(first + " " + last)
}
// 呼び出し側
printName("John", "Smith") // ① Prints "John Smith"
printName(first = "John", last = "Smith") // ② Prints "John Smith"
printName(last = "Smith", first = "John") // ③ Prints "John Smith"
public void printName(String first, String last) {
Predef$.MODULE$.println((new StringBuilder(1)).append(first).append(" ").append(last).toString());
}
// 呼び出し側
printName("John", "Smith"); // ①
printName("John", "Smith"); // ②
String x$1 = "Smith", x$2 = "John";
printName("John", "Smith"); // ③
なんだかモヤっとする結果になりましたね・・・
Javaには名前付き引数がないので、①, ②の結果については想定通りです。
しかしながらなぜx$1, x$2の引数を用意しておきながら、printNameに代入するときには使用していないのか不可解です。
おそらくJVMの最適化の影響なのかなと思ってますが。。。
とりあえず、一度変数化⇒代入のようにコンパイルされるのかな?という知見が得られたことにします。。。
Tuple
class TupleSample {
def getIngredient: (String, Int) = {
("Sugar", 25)
}
def useIngredient(): Unit = {
val ingredient = getIngredient;
println(ingredient._1)
println(ingredient._2)
}
}
import scala.Predef$;
import scala.Tuple2;
import scala.reflect.ScalaSignature;
import scala.runtime.BoxesRunTime;
@ScalaSignature(bytes = "\006\005E2A\001B\003\001\035!)Q\003\001C\001-!)\021\004\001C\0015!)A\006\001C\001[\tYA+\0369mKN\013W\016\0357f\025\t1q!\001\btG\006d\027\rZ3d_6\004\030\016\\3\013\005!I\021A\0034jg\"L'-Y:iS*\021!bC\001\007O&$\b.\0362\013\0031\t1aY8n\007\001\031\"\001A\b\021\005A\031R\"A\t\013\003I\tQa]2bY\006L!\001F\t\003\r\005s\027PU3g\003\031a\024N\\5u}Q\tq\003\005\002\031\0015\tQ!A\007hKRLen\032:fI&,g\016^\013\0027A!\001\003\b\020*\023\ti\022C\001\004UkBdWM\r\t\003?\031r!\001\t\023\021\005\005\nR\"\001\022\013\005\rj\021A\002\037s_>$h(\003\002&#\0051\001K]3eK\032L!a\n\025\003\rM#(/\0338h\025\t)\023\003\005\002\021U%\0211&\005\002\004\023:$\030!D;tK&swM]3eS\026tG\017F\001/!\t\001r&\003\0021#\t!QK\\5u\001")
public class TupleSample {
public Tuple2<String, Object> getIngredient() {
return new Tuple2("Sugar", BoxesRunTime.boxToInteger(25));
}
public void useIngredient() {
Tuple2<String, Object> ingredient = getIngredient();
Predef$.MODULE$.println(ingredient._1());
Predef$.MODULE$.println(BoxesRunTime.boxToInteger(ingredient._2$mcI$sp()));
}
}
ここで注目すべきは、scala.TupleN
とscala.runtime.BoxesRunTime
でしょうか。。。
scala.TupleN
scala.TupleNはN = 2~22まで定義されたScalaの組み込みTuple型です。
scala.runtime.BoxesRunTime
scalaのコードでTupleの2番目の要素に指定されているInt
はScalaの組み込み型であるscala.Int
です。
Stringはscala.Predef
にaliasとしてjava.lang.String
がStringとして定義されているため、実質ただのStringです。
そこで、JVMの世界で利用するために、オートボクシング(?)と言っていいのかはわかりませんが、
Java側の型に戻してあげる処理をBoxexRunTimeが行っているように見えます。
public static java.lang.Integer boxToInteger(int i) {
return java.lang.Integer.valueOf(i);
}
タプルでのパターンマッチング
パターンマッチングで要素を分解できます。
val (name, quantity) = getIngredient
println(name)
println(quantity)
Tuple2 tuple21;
Tuple2<String, Object> tuple2 = getIngredient();
if (tuple2 != null) {
String str = (String)tuple2._1();
int i = tuple2._2$mcI$sp();
tuple21 = new Tuple2(str, BoxesRunTime.boxToInteger(i));
} else {
throw new MatchError(tuple2);
}
Tuple2 tuple22 = tuple21;
String name = (String)tuple22._1();
int quantity = tuple22._2$mcI$sp();
Predef$.MODULE$.println(name);
Predef$.MODULE$.println(BoxesRunTime.boxToInteger(quantity));
なんだかたった3行だったコードが超面倒なことになりましたね。。。
ポイントはこの辺ですよね
if (tuple2 != null) { // tupleがnullかどうかチェック
String str = (String)tuple2._1();
int i = tuple2._2$mcI$sp();
tuple21 = new Tuple2(str, BoxesRunTime.boxToInteger(i)); // なぜかここで再度Tupleを作成
} else {
throw new MatchError(tuple2); // まぁnullだったらエラーをthrowするのはわかる
}
Tuple2 tuple22 = tuple21; // なぜかここで新しいTupleに代入
String name = (String)tuple22._1();
int quantity = tuple22._2$mcI$sp();
黙ってこんなコードではダメなのか・・・
try {
String name = (String)tuple2._1();
int quantity = tuple2._2$mcI$sp();
} catch (XXXException e) { // ClassCastExceptionとか
throw new MatchError(tuple2);
}
上記のデコンパイル結果のコードのほうが良い理由が知りたい。
for内包表記
for内包表記はscalaではよく使う思います
val numPairs = List((2, 5), (3, -7), (20, 56))
for ((a, b) <- numPairs) {
println(a * b)
}
List numPairs = (List)new .colon.colon(new Tuple2.mcII.sp(2, 5), (List)new .colon.colon(new Tuple2.mcII.sp(3, -7), (List)new .colon.colon(new Tuple2.mcII.sp(20, 56), (List)Nil$.MODULE$)));
numPairs.withFilter(TupleSample::$anonfun$forInclusion$1$adapted).foreach(TupleSample::$anonfun$forInclusion$2$adapted);
これまたすごいコードになってしまいましたね。。。
調べてみたところこんな記事が。。。
https://qtamaki.hatenablog.com/entry/20120125/1327500931
見たところ、内部クラスとかにforの中身を展開して云々のようだけど、gradleのビルド結果を見る限りそのような内部クラスは見つからなかったし、jarにも含まれていなかった。。。
まず、scalaのfor
はmap/flatMap/withFilterのシンタックスシュガーであるというのは有名な話ですが、
numPairs.withFilterで処理している部分、まずここいらなくね?と思うのだが、numPairsに(List)Nil$.MODULE$
が含まれていたりするし、変数a,bの作成などをしているのではないかと思うので、ここでおそらくfilterをかましている?
で、$anonfun$forInclusion$2$adapted
がおそらくforの中に書いていたprintlnの部分か?
なんにせよここまで見ただけではscalaのことがわかったような気になるだけで何もわかってないので、よく調査する必要がある気がする。
最後に
今回はClassとTupleについての記事でしたが、デコンパイルしただけではわからないことが多く、
もう少し深堀が必要な内容となってしまいました。
次回の記事(いつになるかは未定)では、以降のTOUR OF SCALAをやっていくのもいいですが、
Scalaのコンパイラ・ランタイムが何をしてくれているのかについて学んでいこうかなと思っています。
ちょっと中身が薄い気がするけど。