迁移到Scala时,对抗Java形成的习惯

13

当Java开发人员迁移到Scala时,最常见的错误是什么?

所谓错误,指的是编写的代码不符合Scala精神的情况,例如在更适合使用类似于map的函数时使用循环、过度使用异常等。

另外一个错误是使用自己编写的getter/setter而不是Scala自动生成的方法。


4
他们使用四个空格进行缩进。 - huynhjl
4
@Kevin,现在无法将问题标记为“社区维基”。必须引起管理员的注意。 - Daniel C. Sobral
2
@Daniel:关于“社区状态”的问题,请参见http://meta.stackexchange.com/questions/67039/what-can-we-do-to-make-community-wiki-better/67192#67192。 - VonC
1
我完全同意Daniel的观点 - 对于那些从Java转向Scala的人来说,这显然是一个非常有兴趣的问题。 - oxbow_lakes
@Kevin 你不是版主。版主的名字旁边有这个闪亮的钻石符号。 - NullUserException
显示剩余5条评论
7个回答

9

8

一个显而易见的方法是不利用scala允许的嵌套作用域,以及延迟副作用(或者认识到scala中的所有内容都是表达式):

public InputStream foo(int i) {
   final String s = String.valueOf(i);
   boolean b = s.length() > 3;
   File dir;
   if (b) {
       dir = new File("C:/tmp");
   } else {
       dir = new File("/tmp");
   }
   if (!dir.exists()) dir.mkdirs();
   return new FileInputStream(new File(dir, "hello.txt"));
}

可以转换为:

def foo(i : Int) : InputStream = {
   val s = i.toString
   val b = s.length > 3
   val dir = 
     if (b) {
       new File("C:/tmp")
     } else {
       new File("/tmp")
     }
   if (!dir.exists) dir.mkdirs()
   new FileInputStream(new File(dir, "hello.txt"))
}

但这可以有很大的改进空间。它可以是:

def foo(i : Int) = {
   def dir = {
     def ensuring(d : File) = { if (!d.exists) require(d.mkdirs); d }
     def b = { 
       def s = i.toString
       s.length > 3
     }
     ensuring(new File(if (b) "C:/tmp" else "/tmp"));
   }
   new FileInputStream(dir, "hello.txt")
}

后面的例子没有将任何变量“导出”到其所需的范围之外。实际上,它根本没有声明任何变量。这意味着以后更容易重构。当然,这种方法确实会导致类文件极度膨胀!


6
你只保留了一行代码,但失去了很多可读性。 - ryeguy
1
@ryeguy:这只是一个人为制造的例子,所以最小化代码行数并不是我的目的。你关于可读性的说法可能是正确的,但是当你将这样的方法“扩展”到>20行时,Java版本更易读的论点就开始变得站不住脚了。你最终会得到一个笨拙的混乱,其中方法的组件都混在一起;很难重构,因为你无法看到哪里使用了什么。此外,如果你遵循一个标准模式(所有内部定义在顶部,主体在底部),可读性并不一定会受到影响。方法体可以是一行。 - oxbow_lakes

7

以下是我最喜欢的两个例子:

  1. 花了一段时间我才意识到Option有多么有用。从Java中继承过来的一个常见错误是使用null来表示某个字段/变量有时没有值。请注意,您可以在Option上使用'map'和'foreach'来编写更安全的代码。

  2. 学习如何在Scala集合上使用'map'、'foreach'、'dropWhile'、'foldLeft'等其他方便的方法,以节省编写在Java中无处不在的循环结构的时间。我现在认为这些结构冗长,笨拙且难以阅读。


不要忘记 collect,这是我最喜欢的之一。 - Jus12
是啊,我在想我们是否能够在不破坏Java集成的情况下使null过时。 - Michael Lorton
@Malvolio "border-methods"(即使Scala方法可以从Java调用,但许多方法并不适合从Java调用,因此我认为Java-interop必须是一个有意识的暴露层)。 - user166390

3
一个常见的错误是过度使用Java中不存在的功能,一旦你“理解”了它。例如,新手倾向于过度使用模式匹配(*), 显式递归,隐式转换,(伪)运算符重载等。另一个错误是滥用在Java中看起来类似但实际上不同的功能,比如for-comprehensions甚至if(它更像Java的三元运算符?:)。
(*)有一个关于Option模式匹配的很棒的备忘单:http://blog.tmorris.net/scalaoption-cheat-sheet/

我倾向于过度使用Trait,有时会导致我难以理解自己的代码。 - Jus12
如果能说明什么情况下算是过度使用这些功能,那么这个答案会更有帮助。 - Robin Green
Scala中“for”的不同特性是无法过分强调的。 - TechNeilogy

1

使用数组。

这是基础知识,很容易被发现和修复,但当它咬住你的屁股时,最初会减慢你的速度。

Scala有一个Array对象,而在Java中,这是一个内置的工件。这意味着在Scala中初始化和访问数组元素实际上是方法调用:

//Java
//Initialise
String [] javaArr = {"a", "b"};
//Access
String blah1 = javaArr[1];  //blah1 contains "b"

//Scala
//Initialise
val scalaArr = Array("c", "d")  //Note this is a method call against the Array Singleton
//Access
val blah2 = scalaArr(1)  //blah2 contains "d"

1
这并不是一个坏习惯,而是从Java迁移到Scala时的“减速带”。有很长一段时间,我一直在写scalaArr[1],只有在编译/运行时才能捕获错误(主要使用REPL)。由于我大部分开发工作仍然是Java,所以我没有真正内化这个问题。 - GKelly

1

到目前为止,我还没有采用惰性值和流。

一开始,一个常见的错误(编译器会发现)是在for循环中忘记分号:

 for (a <- al;
      b <- bl
      if (a < b)) // ...

以及在哪里放置yield:

 for (a <- al) yield {
     val x = foo (a).map (b).filter (c)
     if (x.cond ()) 9 else 14 
 }

而不是

 for (a <- al) {
     val x = foo (a).map (b).filter (c)
     if (x.cond ()) yield 9 else yield 14  // why don't ya yield!
 }

忘记方法的等号:

 def yoyo (aka : Aka) : Zirp { // ups!
     aka.floskel ("foo")
 }

1

使用if语句。通常可以通过使用if表达式或使用filter来重构代码。

过多使用vars而不是vals。

与其他人所说的一样,不要使用循环,而要使用列表推导函数,如map、filter、foldLeft等。如果没有你需要的函数(仔细查看应该会有可用的函数),则使用尾递归。

我保持函数式编程的精神,而不是使用setter,使我的对象不可变。因此,我会像这样返回一个新对象:

class MyClass(val x: Int) {
    def setX(newx: Int) = new MyClass(newx)
}

我尽可能多地使用列表来工作。此外,在生成列表时,应该使用for/yield表达式,而不是使用循环。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接