0
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

逆コンパイルして理解する、Java視点からのScala入門(Class~Tuple編)

この記事は株式会社富士通システムズウェブテクノロジーの社内技術コミュニティで、「イノベーション推進コミュニティ」
略して「いのべこ」が企画する、いのべこ Advent Calendar 2020の16日目の記事です。
本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。

ここまでお約束 :wink:

はじめに

この記事は、Scalaを始めたJavaプログラマが、Scalaをコンパイルして生成されたclassファイルを逆コンパイルし、
Java的な観点からScalaを理解したいなと思いついて始めたものです。

今回は、Class~Tuple編の内容を主に振り返っていきたいと思います。

Class

何の面白みもない基本的なクラス
User.scala
class User
User.java

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
Point.scala
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)"
}
Point.java
@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キーワードは消え去ってますね。

デフォルト引数を持つコンストラクタ

Point.scala
class Point(var x: Int = 0, var y: Int = 0)
Point.java
@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) {}
}
Point$.java

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

TupleSample.java
class TupleSample {
  def getIngredient: (String, Int) = {
    ("Sugar", 25)
  }

  def useIngredient(): Unit = {
    val ingredient = getIngredient;
    println(ingredient._1)
    println(ingredient._2)
  }
}
TupleSample.java
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.TupleNscala.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が行っているように見えます。

BoxesRunTime.java
    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にも含まれていなかった。。。
image.png

まず、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のコンパイラ・ランタイムが何をしてくれているのかについて学んでいこうかなと思っています。

ちょっと中身が薄い気がするけど。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
0
Help us understand the problem. What are the problem?