この記事は富士通システムズウェブテクノロジーの社内技術コミュニティで、「イノベーション推進コミュニティ」
略して「いのべこ」が企画する、いのべこ夏休みアドベントカレンダー 2020の14日目の記事です。
本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。
ここまでお約束
はじめに
この記事は、Scalaを始めたJavaプログラマが、Scalaをコンパイルして生成されたclassファイルを逆コンパイルし、
Java的な観点からScalaを理解したいなと思いついて始めたものです。
基本的な流れは以下の通りで進めていきたいと思います。
- docs.scala-lang.org/jaの「TOUR OF SCALA」を見ながら、scalaコードを写経する
- jd-guiというJava Decompilerを使用してclassファイルを逆コンパイルする
- 逆コンパイルしたソースコードを眺めて、あーでもないこーでもないとコメントをつける
以上になります。
それでは、Scalaの世界に少しだけ触れてみましょう。
今回は、TOUR OF SCALAの中から「基本」編を用いてScalaの世界に触れてみたいと思います。(一部発展課題があります)
クラス
scalaにおけるクラスは、Javaとほとんど同じに見えますが、コンストラクタのパラメータに違いがあります。
また、オーバーロードについてはどのように実装するのか、サンプルをもとに確認してみましょう。
サンプルコード
class Greeter(prefix: String, suffix: String) {
def greet(name: String): Unit =
println(prefix + name + suffix)
}
デコンパイル後
package com.github.fishibashi.scaladecompile;
import scala.Predef$;
import scala.reflect.ScalaSignature;
@ScalaSignature(bytes = "\006\005A2A!\002\004\001\037!Aa\003\001B\001B\003%q\003\003\005#\001\t\005\t\025!\003\030\021\025\031\003\001\"\001%\021\025I\003\001\"\001+\005\0359%/Z3uKJT!a\002\005\002\035M\034\027\r\\1eK\016|W\016]5mK*\021\021BC\001\013M&\034\b.\0332bg\"L'BA\006\r\003\0319\027\016\0365vE*\tQ\"A\002d_6\034\001a\005\002\001!A\021\021\003F\007\002%)\t1#A\003tG\006d\027-\003\002\026%\t1\021I\\=SK\032\fa\001\035:fM&D\bC\001\r \035\tIR\004\005\002\033%5\t1D\003\002\035\035\0051AH]8pizJ!A\b\n\002\rA\023X\rZ3g\023\t\001\023E\001\004TiJLgn\032\006\003=I\taa];gM&D\030A\002\037j]&$h\bF\002&O!\002\"A\n\001\016\003\031AQAF\002A\002]AQAI\002A\002]\tQa\032:fKR$\"a\013\030\021\005Ea\023BA\027\023\005\021)f.\033;\t\013=\"\001\031A\f\002\t9\fW.\032")
public class Greeter {
private final String prefix;
private final String suffix;
public Greeter(String prefix, String suffix) {}
public void greet(String name) {
Predef$.MODULE$.println((new StringBuilder(0)).append(this.prefix).append(name).append(this.suffix).toString());
}
}
ScalaSignature
コンパイルされた時のメタ情報を格納しているらしい。
sealedについての実装方法を確認した記事がqiitaにありました。
https://qiita.com/ocadaruma/items/93818bd1a5318e71f0d8
知りたいことかどうかはわかりませんが、なるほどそのためにあるんですね。。。
コンストラクタの引数
引数のprefix: String, suffix: String
が、private final
なフィールドとして定義されました。
※finalになるのは、コンストラクタの引数がデフォルトでval
(変更不可)だからでしょう。
試しに、var
なパラメータとしてコンストラクタパラメータを書いてみると以下のようになります。
class Greeter(var prefix: String, var suffix: String) {
def greet(name: String): Unit =
println(prefix + name + suffix)
}
public class Greeter {
private String prefix;
private String suffix;
public String prefix() {
return this.prefix;
}
public void prefix_$eq(String x$1) {
this.prefix = x$1;
}
public String suffix() {
return this.suffix;
}
public void suffix_$eq(String x$1) {
this.suffix = x$1;
}
public Greeter(String prefix, String suffix) {}
public void greet(String name) {
Predef$.MODULE$.println((new StringBuilder(0)).append(prefix()).append(name).append(suffix()).toString());
}
}
final
がなくなり、getterメソッド(prefix(), suffix())と、setterメソッド($eq)が生成されていることがわかります。
scalaのコードから呼ぶときは、普通にprefixでgetもsetもできそうですが、Javaコードから呼ぶ場合はフィールド名_$eq
になりそうです。
ややこしいですね・・・
greetメソッド
greetメソッドでは、+
メソッド(scalaでは、+
もメソッドとして扱われる!)で文字列を連結していました。ですが、逆コンパイルした結果を見ると、StringBuilderを使用し、文字列を連結しています。なんと!かしこい!
Javaのコードで文字列を結合して処理をして居ようものなら、「StringBuilderを使ってください 」とレビュー表に書かれること間違いなしですが、Scalaの場合はその心配がありません。
試しにJavaで書き直してデコンパイルした結果が以下の通りです。
// コンパイル前コード
//public class JavaGreeter {
// private final String prefix;
//
// private final String suffix;
//
// public JavaGreeter(String prefix, String suffix) {
// this.prefix = prefix;
// this.suffix = suffix;
// }
//
// public String greet(String name) {
// return prefix + name + suffix;
// }
//}
import scala.Predef$;
public class JavaGreeter {
private final String prefix;
private final String suffix;
public JavaGreeter(String prefix, String suffix) {
this.prefix = prefix;
this.suffix = suffix;
}
public void greet(String name) {
Predef$.MODULE$.println(this.prefix + this.prefix + name);
}
}
単純に文字列を結合するようになっているようです。
おそらく、Scalaでは演算子もすべて関数として扱われます。Stringの+メソッド自体がおそらくStringBuilderを使う動きになっているのではないかと想像しています(もしかしたら、scalacがStringBuilderで解釈するようになっているのかもしれませんが・・・)
なお、リテラルを使用した場合でも、同じようにStringBuilderを使ってくれるみたいです。
def greet(name: String): Unit =
println(s"${prefix} ${name} ${suffix}")
/**
public void greet(String name) {
Predef$.MODULE$.println((new StringBuilder(2)).append(prefix()).append(" ").append(name).append(" ").append(suffix()).toString());
}
*/
オブジェクト
次はobject
です。Javaのjava.lang.Object
ではないです。
Scalaのobject
は、JavaでいうところのSingletonパターンに該当するでしょうか。
以下のような構文になります。
サンプルコード
object IdFactory {
private var counter = 0
def create(): Int = {
counter += 1
counter
}
}
デコンパイル後
@ScalaSignature(bytes = "\006\0051:Qa\002\005\t\002E1Qa\005\005\t\002QAQaG\001\005\002qAq!H\001A\002\023%a\004C\004#\003\001\007I\021B\022\t\r%\n\001\025)\003 \021\025Q\023\001\"\001,\003%IEMR1di>\024\030P\003\002\n\025\005q1oY1mC\022,7m\\7qS2,'BA\006\r\003)1\027n\0355jE\006\034\b.\033\006\003\0339\taaZ5uQV\024'\"A\b\002\007\r|Wn\001\001\021\005I\tQ\"\001\005\003\023%#g)Y2u_JL8CA\001\026!\t1\022$D\001\030\025\005A\022!B:dC2\f\027B\001\016\030\005\031\te.\037*fM\0061A(\0338jiz\"\022!E\001\bG>,h\016^3s+\005y\002C\001\f!\023\t\tsCA\002J]R\f1bY8v]R,'o\030\023fcR\021Ae\n\t\003-\025J!AJ\f\003\tUs\027\016\036\005\bQ\021\t\t\0211\001 \003\rAH%M\001\tG>,h\016^3sA\00511M]3bi\026$\022a\b")
public final class IdFactory {
public static int create() {
return IdFactory$.MODULE$.create();
}
}
public final class IdFactory$ {
public static final IdFactory$ MODULE$ = new IdFactory$();
private static int counter = 0;
private int counter() {
return counter;
}
private void counter_$eq(int x$1) {
counter = x$1;
}
public int create() {
counter_$eq(counter() + 1);
return counter();
}
}
なんと!匿名クラスが出来上がりました。
おそらくですが、SingletonオブジェクトであるIdFactory$
と、それを呼び出すIdFactory
クラスという構成です。分離されてしまう理由があるのでしょうか。勉強を進めていくうえで何かあると思って今回はこれで終わりです。
公式ドキュメントにも、後で詳しく取り扱いますとあるので、期待です
trait
traitとは何ぞや。
interfaceのようなものなんですが、フィールドも持ててデフォルト実装もできて、はたまた複数のトレイトを継承(mixin)することができるらしいです。
ここでは、traitの作成と、継承したクラスを作成しどのようなクラスが作成されるのかを見てみましょう。
サンプルコード
trait Greeter {
def greet(name: String): Unit =
println("Hello, " + name + "!")
}
class DefaultGreeter extends Greeter
class CustomizableGreeter(prefix: String, postfix: String) extends Greeter {
override def greet(name: String): Unit = {
println(prefix + name + postfix)
}
}
デコンパイル後
今回は全部で3つのクラスファイルが出来上がりました。
Javaの場合は、1つのjavaソースコードに複数のpublicクラスを作ることができませんので、複数に分割されました。
@ScalaSignature(bytes = "\006\005!2qa\001\003\021\002\007\005Q\002C\003\025\001\021\005Q\003C\003\032\001\021\005!DA\004He\026,G/\032:\013\005\0251\021AD:dC2\fG-Z2p[BLG.\032\006\003\017!\t!BZ5tQ&\024\027m\0355j\025\tI!\"\001\004hSRDWO\031\006\002\027\005\0311m\\7\004\001M\021\001A\004\t\003\037Ii\021\001\005\006\002#\005)1oY1mC&\0211\003\005\002\007\003:L(+\0324\002\r\021Jg.\033;%)\0051\002CA\b\030\023\tA\002C\001\003V]&$\030!B4sK\026$HC\001\f\034\021\025a\"\0011\001\036\003\021q\027-\\3\021\005y)cBA\020$!\t\001\003#D\001\"\025\t\021C\"\001\004=e>|GOP\005\003IA\ta\001\025:fI\0264\027B\001\024(\005\031\031FO]5oO*\021A\005\005")
public interface Greeter {
static void $init$(Greeter $this) {}
default void greet(String name) {
Predef$.MODULE$.println((new StringBuilder(8)).append("Hello, ").append(name).append("!").toString());
}
}
traitはinterfaceになりました
@ScalaSignature(bytes = "\006\005i1AAA\002\001\031!)q\003\001C\0011\tqA)\0324bk2$xI]3fi\026\024(B\001\003\006\0039\0318-\0317bI\026\034w.\0349jY\026T!AB\004\002\025\031L7\017[5cCND\027N\003\002\t\023\0051q-\033;ik\nT\021AC\001\004G>l7\001A\n\004\0015\031\002C\001\b\022\033\005y!\"\001\t\002\013M\034\027\r\\1\n\005Iy!AB!osJ+g\r\005\002\025+5\t1!\003\002\027\007\t9qI]3fi\026\024\030A\002\037j]&$h\bF\001\032!\t!\002\001")
public class DefaultGreeter implements Greeter {
public void greet(String name) {
Greeter.greet$(this, name);
}
public DefaultGreeter() {
Greeter.$init$(this);
}
}
インタフェースのデフォルト実装を呼び出す形になっています。
@ScalaSignature(bytes = "\006\005M2A!\002\004\001\037!A!\004\001B\001B\003%1\004\003\005'\001\t\005\t\025!\003\034\021\0259\003\001\"\001)\021\025a\003\001\"\021.\005M\031Uo\035;p[&T\030M\0317f\017J,W\r^3s\025\t9\001\"\001\btG\006d\027\rZ3d_6\004\030\016\\3\013\005%Q\021A\0034jg\"L'-Y:iS*\0211\002D\001\007O&$\b.\0362\013\0035\t1aY8n\007\001\0312\001\001\t\027!\t\tB#D\001\023\025\005\031\022!B:dC2\f\027BA\013\023\005\031\te.\037*fMB\021q\003G\007\002\r%\021\021D\002\002\b\017J,W\r^3s\003\031\001(/\0324jqB\021Ad\t\b\003;\005\002\"A\b\n\016\003}Q!\001\t\b\002\rq\022xn\034;?\023\t\021##\001\004Qe\026$WMZ\005\003I\025\022aa\025;sS:<'B\001\022\023\003\035\001xn\035;gSb\fa\001P5oSRtDcA\025+WA\021q\003\001\005\0065\r\001\ra\007\005\006M\r\001\raG\001\006OJ,W\r\036\013\003]E\002\"!E\030\n\005A\022\"\001B+oSRDQA\r\003A\002m\tAA\\1nK\002")
public class CustomizableGreeter implements Greeter {
private final String prefix;
private final String postfix;
public CustomizableGreeter(String prefix, String postfix) {
Greeter.$init$(this);
}
public void greet(String name) {
Predef$.MODULE$.println((new StringBuilder(0)).append(this.prefix).append(name).append(this.postfix).toString());
}
}
implementsした実装をoverrideしてるだけのようです。まぁまぁ想定内ですね。
mixin
Scalaではmixinを用いて複数のクラスを合成することができます。
Javaでは複数のクラスやInterfaceを継承することはできず、単一の親クラスを継承することができます。
mixinを用いたサンプルコード
abstract class BaseMixinSample(val prefix: String, val suffix: String) {
def greet(name: String): Unit = println(prefix + name + suffix)
}
trait TraitA {
val a: String
def A(): Unit = println("A")
}
trait TraitB {
val b: String
def B(): Unit = println("B")
}
class DefaultMixinSample() extends BaseMixinSample("Hello", "!") with TraitA with TraitB {
override def greet(name: String): Unit = super.greet(name)
override val a: String = "Value is A"
override val b: String = "Value is B"
}
デコンパイル後
import scala.Predef$;
import scala.reflect.ScalaSignature;
@ScalaSignature(bytes = "\006\005U2Qa\002\005\002\002EA\001\002\007\001\003\006\004%\t!\007\005\tK\001\021\t\021)A\0055!Aa\005\001BC\002\023\005\021\004\003\005(\001\t\005\t\025!\003\033\021\025A\003\001\"\001*\021\025q\003\001\"\0010\005=\021\025m]3NSbLgnU1na2,'BA\005\013\0039\0318-\0317bI\026\034w.\0349jY\026T!a\003\007\002\025\031L7\017[5cCND\027N\003\002\016\035\0051q-\033;ik\nT\021aD\001\004G>l7\001A\n\003\001I\001\"a\005\f\016\003QQ\021!F\001\006g\016\fG.Y\005\003/Q\021a!\0218z%\0264\027A\0029sK\032L\0070F\001\033!\tY\"E\004\002\035AA\021Q\004F\007\002=)\021q\004E\001\007yI|w\016\036 \n\005\005\"\022A\002)sK\022,g-\003\002$I\t11\013\036:j]\036T!!\t\013\002\017A\024XMZ5yA\00511/\0364gSb\fqa];gM&D\b%\001\004=S:LGO\020\013\004U1j\003CA\026\001\033\005A\001\"\002\r\006\001\004Q\002\"\002\024\006\001\004Q\022!B4sK\026$HC\001\0314!\t\031\022'\003\0023)\t!QK\\5u\021\025!d\0011\001\033\003\021q\027-\\3")
public abstract class BaseMixinSample {
private final String prefix;
private final String suffix;
public String prefix() {
return this.prefix;
}
public String suffix() {
return this.suffix;
}
public BaseMixinSample(String prefix, String suffix) {}
public void greet(String name) {
Predef$.MODULE$.println((new StringBuilder(0)).append(prefix()).append(name).append(suffix()).toString());
}
}
public class DefaultMixinSample extends BaseMixinSample implements TraitA, TraitB {
public void B() {
TraitB.B$(this);
}
public void A() {
TraitA.A$(this);
}
public DefaultMixinSample() {
super("Hello", "!");
TraitA.$init$(this);
TraitB.$init$(this);
}
public void greet(String name) {
super.greet(name);
}
public static void test() {
DefaultMixinSample$.MODULE$.test();
}
}
public interface TraitA {
static void $init$(TraitA $this) {}
default void A() {
Predef$.MODULE$.println("A");
}
String a();
}
public interface TraitB {
static void $init$(TraitB $this) {}
default void B() {
Predef$.MODULE$.println("B");
}
String b();
}
どうやら、traitにフィールドを定義すると、暗黙的にフィールド名と同等のメソッドがインタフェースに生まれるようです。
で、そのフィールドを取得するメソッドの実態を合成先のクラスで定義してあげることにより、traitを実現しているようです。
最後に
記事の執筆にあたり、Scalaの仕組みについて理解をすることが目的でしたが、JavaプログラマがScalaってちょっといいかも!と思ってもらえるきっかけになったらなーと思うようになりました。
結構歴史のある言語ですが、Javaプログラマからすると目新しい機能や見ていてほしくなる機能がたくさんあって非常に魅力的な言語だと思います。
今後も学習を進めていくうえで同じようにまたJava視点からScalaを見つめなおす機会が作れたらいいなと思っています。