Java调用Scala的一个真实案例

Scala调用Java,完全没有问题;但是反过来总有那么一点不爽,或者必须要修改。这里是项目中一个真实的案例,拿出来分享一下。

1. 类型转换

1
2
3
4
5
object AsnLoader {
  ...
  def batchLoadFile(files: List[String]): Map[String, AsnModule] = files.foldLeft(Map[String, AsnModule]())((x, y) => x ++ load(y))
  ...
}

这里的List是Scala里的类,从Java里面调这个方法是不行的,所以需要弄一个wrapper方法:

1
2
3
import collection.JavaConversions._
...
def batchLoadFile(files: java.util.List[String]): java.util.Map[String, AsnModule] = batchLoadFile(asScalaBuffer(files).toList)

虽然asScalaBuffer本身就是个implicit,但是这里仍需要显示调用一下,否则编译器会认为batchLoadFile(file: java.util.List[String])更符合调用匹配规则,结果就变成了无限递归……

Java里面这样调用:

1
AsnLoader.batchLoadFile(list);

这样看起来解决问题了,但是Eclipse的Scala插件会报一个莫名其妙的函数签名错误,其实根本就没错,直接maven或者用Intellij都是对的,Eclipse插件太烂或者太老。无奈只好把函数名也一起改了:batchLoadFileJ,这样就可以了。如果还不是很明白,去看看JavaConversions里面的implicit。

把Java的Map转换成Scala的immutale map稍微曲折了些,先转换成mutable map,再调用toMap方法转换成immutable map。

2. 构造方法

1
2
3
4
5
class DefaultTransformer(private val asnModule: AsnModule, private val dependencies: Map[String, AsnModule] = Map.empty,
                         private val repository: Map[String, AsnModule] = Map.empty,
                         private val messageHandler: MessageHandler = ConsoleMessageHandler(false)) {
  ...
}

这里面的Map都是Scala的Map,所以从Java里不能直接调用,而Scala的构造方法又比较特别,重载的话只能减少参数个数而不能改变类型。这时候就需要companion object上场了。

1
2
3
4
5
6
object DefaultTransformer {
  def apply(asnModule: AsnModule,
                 dependencies: java.util.Map[String, AsnModule],
                 repository: java.util.Map[String, AsnModule],
                 messageHandler: MessageHandler) = new DefaultTransformer(asnModule, dependencies, repository, messageHandler)
}

Java这样调用:

1
Transformer transformer = DefaultTransformer.apply(...);

3. Scala的bug

嗯,XML在什么语言里都是让人恶心的东西,即使Scala已经尽力做好了:https://issues.scala-lang.org/browse/SI-4865

JRuby和Java的互操作

我试图让新的BL框架支持所有流行的JVM语言。Scala很简单,天衣无缝,但是Ruby和Python就比较周折了。不谈Python,先看看Ruby,确切地说是JRuby。

jrubyc可以把一个Ruby的class编译成Java的,看看下面的例子:

Bar.java

1
2
3
4
5
package com.honnix.foo;
 
interface Bar {
    int getValue();
}

BarImpl.rb

1
2
3
4
5
6
7
8
9
10
11
java_package 'com.honnix.foo'
require 'java'
 
class BarImpl
  include Java::com.honnix.foo.Bar
 
  java_signature 'int getValue()'
    def get()
      1
    end
end

> javac Bar.java
> jrubyc --javac -cp com.honnix.foo BarImpl.rb

这样基本上就差不多了。

不过看看生产的Java代码,就知道这样玩儿的效率恐怕成问题。其实就是先build出来一个Ruby代码的巨大字符串,然后给交给Ruby引擎去跑,最后返回结果。

不管怎么说,只是这是一个方案,接下来就是要考虑怎么把Java的Document转成Ruby喜欢的格式,再转回来。

Java的范型方法

Java的范型一直让我头疼,也一直没仔细研究,总之感觉不好用。这两天折腾了一下,把范型方法整明白了。

一个简单的例子:

1
2
3
4
private static <T> T test(T a) {
    Person<T> person = new Person<T>(a);
    return person.getName();
}

调用的时候这样:

1
test(1);

是不是认为编译器编译的时候会自动将T转换成Integer?我们来看看javap的结果:

1
invokestatic	#22; //Method test:(Ljava/lang/Object;)Ljava/lang/Object;

看到什么了?没有Integer,只有Object!

如果这样设计test():

1
2
3
4
private static <T extends Number> T test(T a) {
    Person<T> person = new Person<T>(a);
    return person.getName();
}

结果就是:

1
invokestatic	#23; //Method test:(Ljava/lang/Number;)Ljava/lang/Number;

所以是不是可以得出结论,javac根本就不看调用的时候传入什么类型的东西,只看方法定义?

你完全可以这样写:

1
2
3
4
private static Object test(Object a) {
    Person<Object> person = new Person<Object>(a);
    return person.getName();
}

只是前者看上去比较牛逼罢了,其实效果完全相同。

Mac里面的JDK的源码和javadoc

在Apple还没完全转到OpenJDK之前,他自家的东西还是得用着。不过默认JDK源码和javadoc是不会装的。

到这里下:https://connect.apple.com/,右边找Dowloads->Java。

需要ADC,没有的话自己注册一个,也不花钱。

10.6 update 2之前是叫Developer Document,update 3之后就叫Developer Package了。找需要的下。装好之后在这里:/Library/Java/JavaVirtualMachines/<version>/Contents/Home。同样是src.jar和docs.jar,另外还有苹果自己的一个appledocs.jar。

慢慢地转到maven

开始把之前做的一些项目往maven上转了。先是jfetion,再加上今天做的smsd。

说来惭愧,jfetion做了很久了,但是自己一直都没弄个什么应用出来,一直在用的smsd还是最早的时候用C写的。今天老婆出去逛街,闲来无事,用Scala重写了一遍,已经可以正常工作了,其它的功能慢慢加吧。

基本想法很简单,后台启一个daemon,监视某个文件夹,如果里面有.sms文件,就把里面的内容发送给相应的人,当然必须是飞信上面的好友。

顺便说一下,Snow Leopard的Java默认是64位的,而libfetion没有Mac上64位的版本,所以要么在系统设置里面更改Java的默认设置,要么运行的时候给个参数:java -d32 …

我是如何用Scala定义新异常类型的

Scala的构造方法非常灵活(具体怎么个灵活法这里不多说了),但随之而来的是重载构造方法的麻烦。

例如定义新异常类型。一般来说,自己定义的异常都属于checked异常,大都从Exception继承过来,所以也大都需要定义多个构造方法。如果用Java来定义,没什么好说的,重载就行,但是用Scala的话就有点麻烦。Scala规定所有重载的构造方法都必须调用或间接调用默认构造方法,所以必须使用如下的方法。

1
2
3
4
5
class MyException(message: String, cause: Throwable) extends Exception(message, cause) {
  def this(message: String): = this(message, null)
  def this(cause: Throwable): = this(null, cause)
  def this: = this(null, null)
}

当然,这样是可以工作的,但是仔细看看Throwable的实现就会发现如果传入的cause为null话会导致异常栈的丢失。而且最恶心的是Throwable没有提供相应的setter/getter,我们能做的就是调用构造方法。
所以我就想出了下面的怪招。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
object SpcException {
  def apply(message: String, cause: Throwable): Exception =
    (new SpcException1(message, cause)).asInstanceOf[Exception]
 
  def apply(message: String): Exception =
    (new SpcException2(message)).asInstanceOf[Exception]
 
  def apply(cause: Throwable): Exception =
    (new SpcException3(cause)).asInstanceOf[Exception]
 
  def apply(): Exception =
    (new SpcException4).asInstanceOf[Exception]
}
 
trait SpcException
 
class SpcException1(message: String, cause: Throwable)
    extends Exception(message, cause) with SpcException
 
class SpcException2(message: String)
    extends Exception(message) with SpcException
 
class SpcException3(cause: Throwable)
    extends Exception(cause) with SpcException
 
class SpcException4 extends Exception with SpcException

基本思想是定义一个trait,然后定义四种异常,每种都从该trait扩展并提供不同的默认构造方法,同时定义一个singleton,提供四种不同的apply方法用来构造四种不同的异常。这样就可以解决之前的问题,虽然不怎么好看。

准备发布jfetion了

昨天把build.xml搞好了,现在可以方便地构建整个库,准备发布了,虽然还没有测完全。有些东西实在是不好测,尤其是要一边在虚拟的Windows上用移动官方的fetion登陆,改东西,一边又要在虚拟的Gentoo上写代码、测试,实在是麻烦。先发布一个alpha版本好了。

现在在上班,晚上回去放上来。

Java里面的Reference

之前写过一篇关于Java Reference的文章,只放了一个链接;这个星期要开Java的Workshop,所以仔细研究了一把。

Java的Reference

很惭愧,自己对Java里面的各种refernce一直不是很明白。今天研究JNI的时候看到了这篇文章,讲得比较清晰,拿来引用一下吧:http://weblogs.java.net/blog/enicholas/archive/2006/05/understanding_w.html

鬼知道Eclipse怎么想的

为什么把默认的class输出目录改成bin?这么好的名字被污染了。目录命名空间的污染也是一件让人愤怒的事情,虽然我可以设置默认为classes,但实在想不明白改的初衷是什么!