在 Java 中使用 Scala 類別


若 是在JVM平台上,Scala的類別會編譯為.class檔案,基本上在Java程式中,也可以使用這些編譯好的.class檔,不過由於某些Scala 的語法特性,在Java中並不存在,因此若要在Java程式中使用Scala所編譯好的類別,就必須注意一些語法或名稱對應的方式。

當你在Scala中定義一個類別:
class Some

使用scalac編譯後會產生Some.class檔案,若你想要在Java中使用這個Some類別:
public class Main {  // 這是 Java
public static void main(String[] args) {
Some s = new Some();
}
}

你可以編譯Main.java,但在執行時若沒有指定Classpath中包括scala-library.jar的位置,就會出現錯誤:
>javac Main.java

>java Main
Exception in thread "main" java.lang.NoClassDefFoundError: scala/ScalaObject
....

這是因為,Scala中的自定義類別,會實作scala.ScalaObject特徵,也就是編譯出來的.class,其實相當於:
import java.rmi.RemoteException;
import scala.ScalaObject;

public class Some implements ScalaObject {
  public int \$tag() throws RemoteException {
    return ScalaObject.class.\$tag(this);
  }
}

若要能順利執行,則必須指定Classpath包括scala-library.jar的位置:
>java -cp .;%SCALA_HOME%/lib/scala-library.jar Main

在定義類別時,使用val宣告的變數,對應至Java使用final修飾的變數。如果是類別的資料成員,無論是使用val宣告或var宣告的變數,一律對應至private資料成員,在Scala中提供的權限修飾,對應至相對應的存取方法權限。例如:
class Some {
private var m = 123
private val s = "XD"
}

編譯過後的.class會是:
import java.rmi.RemoteException;
import scala.ScalaObject;

public class Some implements ScalaObject {
    private int m;
    private final String s = "XD"

    public Some() {
        super();
        m = 123;
    }

    private void m_\$eq(int x\$1) { m = x\$1; }
    private int m() { return m; }

    private String s() { return s; }

    public int \$tag() throws RemoteException {
        return scala.ScalaObject.class.\$tag(this);
    }
}

所以,如果是這個類別:
class Some {
val s = "XD"
}

在Java中要取得s的值,則必須呼叫方法,例如:
public class Main { // 這是 Java
public static void main(String[] args) {
Some some = new Some();
System.out.println(some.s());
}
}

如果你希望編譯過後的類別具有設值方法(Setter)與取值方法(Getter),則可以使用 @BeanProperty 標註,請參考 標註(Annotation) 的內容。

在Scala中所有東西都是物件,就算是1、0.1等數值也是物件,而在編譯為.class之後,為了效率,會儘量將數值轉為Java的基本資料型態,而在必要的地方使用Integer等包裹類別(Wrapper)。例如:
class Some {
def doIt(a: Int): Int = {
val l = new java.util.ArrayList[Int]
l.add(a)
return l.get(0)
}
}

這個類別在編譯為.class之後,相當於:
import java.rmi.RemoteException;
import java.util.ArrayList;
import scala.ScalaObject;
import scala.runtime.BoxesRunTime;

public class Some implements ScalaObject {
  public int doIt(int a)  {
    ArrayList l = new ArrayList();
    l.add(BoxesRunTime.boxToInteger(a));
    return BoxesRunTime.unboxToInt(l.get(0));
  }

  public int \$tag()  throws RemoteException {
    return ScalaObject.class.\$tag(this);
  }
}

在 參數與傳回值的型態部份轉為int,而必要的地方使用裝箱(Boxing)與拆箱(Unboxing)。在Scala中如果設定參數型態或傳回值型態為 Any、AnyRef或AnyVal,編譯.class後,都對應於java.lang.Object。如果傳回值設定為AnyVal,則傳回的數值會裝 箱,例如:
class Some {
def doSomething(a: AnyVal): AnyVal = 1
}

這個類別在編譯過後,.class的定義基本上如下:
import java.rmi.RemoteException;
import scala.ScalaObject;
import scala.runtime.BoxesRunTime;

public class Some implements ScalaObject {
    public Some() {}

    public Object doSomething(Object a) {
        return BoxesRunTime.boxToInteger(1);
    }

    public int \$tag() throws RemoteException {
        return scala.ScalaObject.class.\$tag(this);
    }
}

Scala的 單 例物件 在Java中,可以使用存取靜態成員的方式來使用,例如:
object Some {
def doSomething = "XD"
}

在Java中可以這麼使用:
public class Main {  // 這是 Java
public static void main(String[] args) {
System.out.println(Some.doSomething()); // XD
}
}

不過別誤以為單例物件轉換為Java之後,單例物件的名稱就是類別名稱,其中的成員直接就是實作為靜態成員!不然會誤入這個陷阱:
class Some

object Some {
def doSomething = "XD"
}

上面定義了伴侶類別與伴侶物件,在編譯過後,在Java中這麼使用是會有錯的:
public class Main {  // 這是 Java
public static void main(String[] args) {
System.out.println(Some.doSomething()); // 編譯錯誤
}
}

其實,只要是Scala中使用object定義的物件,在轉換為.class後,其實真正的類別名稱是「單例物件名\$」,該類別中會有個MODULE\$成員參考到this,例如上面的例子會產生Some\$類別,其定義為:
import java.rmi.RemoteException;
import scala.ScalaObject;

public final class Some\$ implements ScalaObject {
    public Some\$() {}

    public String doSomething() {
        return "XD";
    }

    public int \$tag() throws RemoteException {
        return scala.ScalaObject.class.\$tag(this);
    }

    public static final Some\$ MODULE\$ = this;

    static {
        new Some\$();
    }
}

在沒有伴侶類別的情況下,編譯會產生一個Some類別定義如下:
import java.rmi.RemoteException;

public final class Some {
    public static final String doSomething() {
        return Some\$.MODULE\$.doSomething();
    }

    public static final int \$tag() throws RemoteException {
        return Some\$.MODULE\$.\$tag();
    }
}

這也是為何你可以在Java中使用Some.doSometing()的原因。然而在有伴侶類別的情況下,所產生的Some類別定義會是:
import java.rmi.RemoteException;
import scala.ScalaObject;

public class Some implements ScalaObject {
    public Some() {}

    public int \$tag() throws RemoteException {
        return scala.ScalaObject.class.\$tag(this);
    }
}

這也是為什麼,你不可以在Java中使用Some.doSometing()的原因。無論是否有伴侶類別,其實想要在Java中使用Scala單例物件中定義的成員,方式都是:
public class Main {  // 這是 Java
public static void main(String[] args) {
Some\$.MODULE\$.doSomething();
}
}

在Scala中若設定建構式為private或其它權限,例如:
class Some private {}

編譯為.class後,基本上建構式也會被標示為相對應的權限,例如.class的定義會是:
import java.rmi.RemoteException;
import scala.ScalaObject;

public class Some implements ScalaObject{
    private Some() {}

    public int \$tag() throws RemoteException {
        return scala.ScalaObject.class.\$tag(this);
    }
}

但若出現伴侶物件且嘗試建構物件時,編譯過後的.class,private就不是private了。例如:
class Some private {
}

object Some {
def apply = new Some
}

在上例中,Some在編譯過後,其.class的定義會是:
import java.rmi.RemoteException;
import scala.ScalaObject;

public class Some implements ScalaObject {
    public Some() {}

    public int \$tag() throws RemoteException {
        return scala.ScalaObject.class.\$tag(this);
    }
}

在Scala中有特徵(Trait),特徵可以有實作,在Java中要使用Scala中的特徵所編譯過來的.class,最簡單的方式,是就是在Scala中定義沒有任何實作的特徵,如此編譯過去的.class,就是對應至Java的介面(Interface)。例如:
trait Some {
def doIt(a: Any): Any
}

在編譯為.class後,會對應至以下的Java介面:
public interface Some {
    public abstract Object doIt(Object obj);
}

有實作的特徵在Java中沒有直接對應的語法,可以得到如Scala中直接具有實作特徵的好處。真的要使用具實作的特徵編譯出來的.class還是有辦法的,不過只會讓Java中的程式更為複雜,所以並不鼓勵,這在之後的文件還會介紹。

Scala的 型態參數 對應至Java的泛型(Generic)語法,由於Java並不支援 共變性(Covariance)逆變性(Contravariance),在Scala中如果定義型態參數時標註了正變或逆變,編譯之後的.class是不會包含任何正變、逆變資訊的。例如:
class Fruit
class Apple extends Fruit
class Some[+T]
class Util {
val sf: Some[Fruit] = new Some[Apple]
}

編譯之後的.class定義分別如下:
// Some.class
import java.rmi.RemoteException;
import scala.ScalaObject;

public class Some<T> implements ScalaObject {
  public int \$tag() throws RemoteException {
    return ScalaObject.class.\$tag(this);
  }
}

// Util.class
import java.rmi.RemoteException;
import scala.ScalaObject;

public class Util implements ScalaObject {
  private final Some<Fruit> sf;

  public Util() {
    this.sf = new Some();
  }

  public Some<Fruit> sf() { return this.sf; }

  public int \$tag() throws RemoteException {
    return ScalaObject.class.\$tag(this);
  }
}

如果使用了 既存型態(Existential type),編譯後的.class,會對應至Java的型態通配字元語法。例如:
class Util {
val sf: Some[_ <: Fruit] = new Some[Apple]
}

在編譯為.class後,定義如下:
import java.rmi.RemoteException;
import scala.ScalaObject;

public class Util implements ScalaObject {
  private final Some<? extends Fruit> sf;

  public Util() {
    this.sf = new Some();
  }
  public Some<? extends Fruit> sf() { return this.sf; }

  public int \$tag() throws RemoteException {
    return ScalaObject.class.\$tag(this);
  }
}