与Java互操作
1. 在Scala里使用Scala类
如果在单独的文件里创建Scala类,就可以轻松地使用它们,就像(无需显式编译)在Scala脚本里使用一样。不过,如果想在编译过的Scala或Java代码里使用Scala类,那就必须编译了。
举例如下:
package com.cn.peng class Person(val firstName:String, val lastName:String) { override def toString():String = firstName + " " + lastName }
package com.cn.peng class Dog(name:String) { override def toString():String = name }
下面是使用上面两个类的脚本:
package com.cn.peng object use { def main(args:Array[String]){ val george = new Person("George", "Washington") val geogesDogs = List(new Dog("Captain"), new Dog("Clode"), new Dog("Forester"), new Dog("Searcher")) printf("%s had several dogs %s...", george,geogesDogs mkString ", ") } }
脚本会产生如下输出:
George Washington had several dogs Captain, Clode, Forester, Searcher...
2. 在Scala里使用Java类
在Scala里可以直接使用Java类。如果要用的Java类是标准JDK的一部分,直接用就是了。如果它不在java.lang里,就要导入类的包。下面用到了java.util和java.lang.reflect包:
package com.cn.peng import java.util.Date import java.lang.reflect._ object UseJDKClasses extends App { println("Today is " + new Date()) val methods = getClass.getMethods() methods.foreach{method:Method => println(method.getName())} }
程序运行结果如下:
Today is Sat Apr 04 16:53:55 CST 2015 main delayedEndpoint$com$cn$peng$UseJDKClasses$1 args delayedInit scala$App$_setter_$executionStart_$eq executionStart scala$App$$_args scala$App$$initCode methods scala$App$$_args_$eq scala$App$_setter_$scala$App$$initCode_$eq wait wait wait equals toString hashCode getClass notify notifyAll
如果想用的Java类是你自己创建的,或是来自第三方,请确保scalac的classpath指向字节码的位置。假定我们有如下的Java文件:
package investments; public enum InvestmentType { SHORT_TERM, BOND, STOCK, REAL_ESTATE, COMMODITIES, COLLECTIBLES, MUTUAL_FUNDS }
package investments; public class Investment { private String investmentName; private InvestmentType investmentType; public Investment(String name, InvestmentType type){ investmentName = name; investmentType = type; } public int yield() { return 0; } }
在Scala代码里使用这些类,同使用Scala类一样。下面是一个在Scala里创建Investment实例的例子:
package com.cn.peng import investments._ object UseInvestment { def main(args: Array[String]){ val investment = new Investment("XYZ Corporation", InvestmentType.STOCK) println(investment.getClass()) } }
运行结果如下:
class investments.Investment
Investment类的yield()方法需要小心使用。如果Java代码有方法或字段的名字(比如trait或yield等)于Scala的关键字冲突,调用它们会导致Scala编译器死掉。比如,下面的代码是不行的:
val theYield1 = investment.yield //ERROR val theYield2 = investment.yield() //ERROR
幸运的是,Scala提供了一个解决方案。把冲突的变量/方法放到反引号里,就可以绕开这个问题。改一下代码就可以让上面的两个调用工作了:
val theYield1 = investment.`yield`
val theYield2 = investment.`yield`()
3. 在Java里使用Scala类
Scala提供了与Java之间完整的双向互操作性。因为Scala能编译成字节码,所以在Java里使用Scala类相当容易。默认情况下,Scala并不遵循JavaBean的约定,要用@scala.reflect.BeanProperty这个注解生成符合JavaBean预定的getter和setter。还可以从Scala类继承Java类,不过,要运行使用了Scala类的Java代码,classpath里需要有scala-library.jar。在本节里,我们会看到Scala的构造在Java端会表现出怎样的不同。
3.1 有普通函数和高阶函数的Scala类
遵循标准Java构造的Scala类相当直白,在Java端使用它们很容易。我们写一个Scala类:
package automobiles class Car(val year:Int) { private[this] var miles : Int = 0 def drive(distance:Int) {miles += distance} override def toString():String = "year: " + year + " miles: " + miles }
下面是个使用这个Scala类的Java类:
package com.cn.peng; import automobiles.Car; public class UseCar { public static void main(String[] args){ Car car = new Car(2014); System.out.println(car); car.drive(10); System.out.println(car); } }
程序运行结果如下:
year: 2014 miles: 0
year: 2014 miles: 10
在Java里使用Scala类相当简单。不过,不是所有的Scala类都那么友善。比如,如果Scala类有方法接收闭包,这些方法在Java里就不可用,因为Java目前尚不支持闭包。下面Equipment类的simulate()方法对Java就是不可用的;不过,我们可以用run()方法:
package com.cn.peng class Equipment { //Not usable from Java def simulate(input:Int)(calculator:Int => Int):Int = { //... calculator(input) } def run(duration:Int){ println("running") //... } }
因此,设计API的时候,如果类主要是给Java用,请在提供高阶函数的同时也提供普通函数,让这个类对Java完全可用。
3.2 同trait一起工作
没有方法实现的trait在字节码层面上就是简单的接口。Scala不支持interface关键字。因此,如果想在Scala里创建接口,就创建一个没有实现的trait。下面是个Scala trait的例子,他也是个接口:
package com.cn.peng trait Writable { def wirte(message: String):Unit }
上面的trait里面有个抽象的方法,混入这个trait的类都应该实现这个方法。在Java端,Writable可以看做与其它接口一样;它对Scala根本没有依赖。所以,可以这样实现(implement)它:
package com.cn.peng; public class AWritableJavaClass implements Writable{ @Override public void wirte(String message) { // TODO Auto-generated method stub } }
不过,如果trait有方法实现,那么Java类就不能实现这个trait/interface,虽然它们可以使用它。因此,在Java里不能实现下面的Printable,但可以持有一个Printable的引用:
package com.cn.peng trait Printable { def print(){} //default print nothing }
如果想让Java类实现trait,就让它纯粹些;换句话说,不要有实现。在这种情况下,任何公共的实现都应该放到抽象基类里,而不是trait里。不过,如果只是想让Java类使用trait,就没有任何限制。
3.3 单例对象和伴生对象
Scala将对象(单例对象或伴生对象)编译成一个“单例类”——这个类的名字末尾有一个特殊$符。这样,下面所示的Object Single,会产生一个类名Single$。不过,Scala处理单例对象和伴生对象有些不同,稍后可以看到。
Scala把单例对象编译到一个单例类(它用的是Java的静态方法)中,此外,还会创建一个普通的类,它把调用传递给单例类。所以,下面这段代码创建了一个单例对象Single,而Scala则创建了两个类:Single$和用来传递调用的类Single。
package com.cn.peng object Single { def greet(){println("Hello from Single")} }
在Java里使用上面的单例对象,就像使用由static方法的Java类一样,如下所示:
package com.cn.peng; public class SingleUser { public static void main(String[] args){ Single.greet(); } }
上面代码的输出如下:
Hello from Single
如果对象是同名类的伴生对象,Scala会创建两个类,一个类表示Scala类(下面例子里的Buddy),另一个类表示伴生对象(下面例子里的Buddy$):
package com.cn.peng class Buddy { def greet(){println("Hello from Buddy class")} } object Buddy { def greet(){println("Hello from Buddy object")} }
访问伴生类可以直接使用类的名字。访问伴生对象需要使用特殊的符号MODULE$,如下例所示:
package com.cn.peng; public class BuddyUser { public static void main(String[] args){ new Buddy().greet(); Buddy$.MODULE$.greet(); } }
输出如下:
Hello from Buddy class Hello from Buddy object
4. 继承类
Scala类可以继承Java类,反之亦然。大多数情况下,这应该够用了。之前也讨论过,如果方法接收闭包作为参数,重写起来就有些麻烦。异常也是这个问题。
Scala没有throws字句。在Scala里,任意方法都可以抛出异常,无需显式声明成方法签名的一部分。不过,如果在Java里重写这样的方法,试图抛出异常,就会陷入麻烦。看个例子,假设Scala定义了Bird:
package com.cn.peng abstract class Bird { def fly(); //... }
还有另一个类Ostrich:
package com.cn.peng class Ostrich extends Bird { def fly(){ throw new NoFlyException } //... }
其中NoFlyException定义如下:
package com.cn.peng class NoFlyException extends Exception{ }
在上面的代码里,Ostrich的fly()抛出异常没有任何问题。不过,如果要在Java里实现一个不能飞的鸟,就会有麻烦,如下所示:
package com.cn.peng; public class Penguin extends Bird{ public void fly() throws NoFlyException { throw new NoFlyException(); } //... }
首先,如果只是抛出异常,Java会报错“unreported exception NoFlyException;must be caught or declared to be thrown." 一旦加上了throws子句,java又会报错”Exception NoFlyException is not compatible with throws clause in Bird.fly()“。
即便Scala很灵活,并不强求一定要指定抛出哪些异常,但是要想在Java里继承这些方法,就要告诉Scala编译器,把这些细节记录在方法签名里。Scala为此提供了一个后门:定义@throws注解。
虽然Scala支持注解,但它却不提供注解的语法。如果想创建自己的注解,就不得不用Java来做。@throws是已经提供好的注解,用以表示方法抛出的受控异常。这样,对我们来说,要在Java里实现Penguin,必须在把Bird改成这样:
package com.cn.peng abstract class Bird { @throws(classOf[NoFlyException]) def fly(); //... }
现在,编制上面的代码,Scala编译器会在字节码里为fly()方法放上必要的签名。经过了这个修改,Java类Penguin就可以正常编译了。