向 Java 开发者介绍 Scala

  • 2011-09-25
  • 本文字数:6544 字

    阅读完需:约 21 分钟

Scala 结合了面向对象编程与函数编程思想,使用一种能够完全兼容 Java、可以运行在 Java 虚拟机上的、简洁的语法。对于函数编程风格的支持,尤其是对于 Lambda 表达式的支持,能够有助于减少必须要编写的逻辑无关固定代码,也许让它可以更简单的关注要面对的任务本身,而相对的 Java 中对 Lamdba 表达式的支持要到预定于 2012 年发布的 JavaSE8 才会实现。本文就是对于 Scala 介绍。

作为第一步,先安装好最新的 Scala 发布包 T ypesafe stack ,打开命令行窗口,键入“scala”:这会启动 REPL(读入 - 运算 输出 循环)交互式编码环境。然后你就可以写下你的第一行 Scala 代码:

scala> val columbus : Int = 1492
columbus: Int = 1492 我们刚刚声明了一个类型为 Int 的变量,初始值为 1492,就像我们在 Java 里用语句 Int columbus = 1492; 所做的一样。除了把类型放在变量之后这样一种反向的声明方式之外,Scala 在这里所表现出的不同是使用“val”来显性地把变量声明为不可变。如果我们想要修改这个变量:


scala> columbus = 1500
<console>:8: error: reassignment to val
   columbus = 1500
          ^

请注意错误消息精确地指出了错误位于行的哪个位置。再尝试声明这个变量,但这一次用“var”,让其可变更。这样编译器拥有足够的智能来推断出 1492 是一个整数,你也就不需要指定类型了:

scala> var columbus = 1492
columbus: Int = 1492

scala> columbus = 1500  columbus: Int = 1500

接下来,我们来定义一个类:


scala> case class Employee( name:String="guest", age:Int=30, company:String =  "DevCode" )
defined class Employee 

我们定义了一个类,名为 Employee,有三个不可变更的字段:name、age 和 company,各自有自己的缺省值。关键字“case”相当于 Java 里的 switch 语句,只不过要更为灵活。它说明该类具有模式匹配的额外机制,以及其他一些特性,包括用来创建实例的工厂方法(不需要使用“new”关键字来构造),同样的也不需要创建缺省的 getter 方法。与 Java 中不同的是,变量缺省下的访问控制是 public(而不是 protected),而 Scala 为公开变量创建一个 getter 方法,并命名为变量名。如果你愿意,你也可以把字段定义成可变且 / 或私有(private)的,只需要在参数之前使用“var”(例如:case class Person(private var name:String))。

我们再来用不同方式创建一些实例,看看其他的特性,像是命名参数和缺省参数(从 Scala2.8 开始引入):


scala> val guest = Employee()
guest: Employee = Employee(guest,30,DevCode)

scala> val guestAge = guest.age // (age 变量的缺省 getter 方法)  guestAge: Int = 300
scala> val anna = Employee("Anna")  anna: Employee = Employee(Anna,30,DevCode)
scala> val thomas = Employee("Thomas",41)  thomas: Employee = Employee(Thomas,41,DevCode)
scala> val luke = Employee("Luke", company="LucasArt")  luke: Employee = Employee(Luke,30,LucasArt)
scala> val yoda = luke.copy("Yoda", age=800)  yoda: Employee = Employee(Yoda,800,LucasArt)

不过,下面的写法


scala> val darth = Employee("Darth", "DevCode")
<console>:9: error: type mismatch;
found : java.lang.String("DevCode")
required: Int
Error occurred in an application involving default arguments.
     val darth = Employee("Darth", "DevCode")
                     ^

是行不通的(可不是因为 Darth 不是 DevCode 的雇员!),这是由于构造函数在这个位置需要 age 作为参数,因为函数参数没有显性地进行命名。

现在我们再来看集合,这才是真正让人兴奋的地方。

有了泛型(Java5 以上),Java 可以遍历一个——比方说整数型列表,用下面这样的代码:


List<Integer> numbers = new arrayList<Integer>();
   numbers.add(1);
   numbers.add(2);
   numbers.add(3);
   for(Integer n:numbers) {
       System.out.println("Number "+n);
}

运行的结果是

Number 1
Number 2
Number 3

Scala 对于可变集合和不可变集合进行了系统性地区别处理,不过鼓励使用不可变集合,也因此在缺省情况下创建不可变集合。这些集合是通过模拟的方式实现添加、更新和删除操作,在这些操作中,不是修改集合,而是返回新的集合。

与前面的 Java 代码等价的 Scala 代码可能像下面这样:


scala> val numbers = List(1,2,3)
numbers: List[Int] = List(1, 2, 3)

scala> for (n <- numbers) println("Number "+n)  Number 1  Number 2  Number 3

这里的“for”循环语法结构非常接近于 Java 的命令式编程风格。在 Scala(以及 Java 虚拟机上其他很多语言如:Groovy、JRuby 或 JPython)里还有另外一种方式来实现上面的逻辑。这种方式使用一种更加偏向函数编程的风格,引入了 Lambda 表达式(有时也称为闭包——closure)。简单地说,Lambda 表达式就是你可以拿来当作参数传递的函数。这些函数使用参数作为输入(在我们的例子中就是“n”整型变量),返回语句作为函数体的最终语句。他们的形式如下


functionName { input =>
   body
}

scala> numbers.foreach { n:Int =>     // 按回车键继续下一行 
    | println("Number "+n)  
    | }  Number 1  Number 2  Number 3

上面的例子中,函数体只有一条语句(println……),返回的是单位(Unit,也就是“空结果”),也就是大致相当于 Java 中的 void,不过有一点不同的是——void 是不返回任何结果的。

除了只打印出我们的数值列表以外,应该说我们更想做的是处理和变换这些元素,这时我们需要调用方法来生成结果列表,以便后面接着使用。让我们尝试一些例子:


scala> val reversedList = numbers.reverse
reversedList: List[Int] = List(3, 2, 1)

scala> val numbersLessThan3 = numbers.filter { n => n < 3 }  numbersLessThan3: List[Int] = List(1, 2)
scala> val oddNumbers = numbers.filterNot { n => n % 2 == 0 }  oddNumbers: List[Int] = List(1, 3)
scala> val higherNumbers = numbers.map { n => n + 10 }  higherNumbers: List[Int] = List(11, 12, 13)

最后的这一个变换“map”非常有用,它对列表的每一个元素应用闭包,结果是一个同样大小的、包含了每个变换后元素的列表。

我们在这里还想介绍最后的一个方法,就是“foldLeft”方法,它把状态从一个元素传播到另一个元素。比如说,要算出一个列表里所有元素的和,你需要累加它们,并在切换元素的时候保存中间的计数:


scala> val sumOfNumbers = numbers.foldLeft(0) { (total,element) =>
    | total + element
    | }
sumOfNumbers: Int = 6

作为第一个变量传递给 foldLeft 的值 0 是初始值(也就是说在把函数用到第一个列表元素的时候 total=0)。(total,element) 代表了一个 Tuple2,在 Scala 里这是一个二元组(就像要表示三维空间坐标,经常要用到 Tuple3(x,y,z) 等等)。注意在合计时,Scala 的编程接口实际上提供了一个“sum”方法,这样上一条语句就可以写成:


scala> val sumOfNumbers = numbers.sum
sumOfNumbers: Int = 6

还有许多其他的类似的集合变换方法,你可以参照 scaladoc API 。你也可以把这些方法组合起来(例如:numbers.reverse.filter……),让代码更加简洁,不过这样会影响可读性。

最后,{ n => n + 10 }还可以简单地写成 (_ + 10),也就是说如果输入参数只是用于你调用的方法,则不需要声明它;在我们的例子里,“n”被称为匿名变量,因为你可以把它用任何形式来代替,比如说“x”或者“number”,而下划线则表示一处需要用你的列表的每个元素来填补的空白。(与“_”的功能类似,Groovy 保留了关键字“it”,而 Python 则使用的是“self”)。


scala> val higherNumbers = numbers.map(_+10)
higherNumbers: List[Int] = List(11, 12, 13) 

在介绍了对整数的基本处理后,我们可以迈入下一个阶段,看看复杂对象集合的变换,例如使用我们上面所定义的 Employee 类:


scala> val allEmployees = List(luke,anna,guest,yoda,thomas)
allEmployees: List[Employee] = List(Employee(Luke,30,LucasArt),  Employee(Anna,30,DevCode), Employee(guest,30,DevCode),  Employee(Yoda,800,LucasArt), Employee(Thomas,41,DevCode))

从这个五个元素的列表里,我们可以应用一个条件来过滤出应用匿名方法后返回值为 True 的雇员,这样就得到了——比方说属于 DevCode 的雇员:


scala> val devcodeEmployees = allEmployees.filter { _.company == "DevCode" }
devcodeEmployees: List[Employee] = List(Employee(Anna,30,DevCode),  Employee(guest,30,DevCode), Employee(Thomas,41,DevCode))

scala> val oldEmployees = allEmployees.filter(_.age > 100).map(_.name)  oldEmployees: List[String] = List(Yoda)

假设我们手头的 allEmployees 集合是我们使用 SQL 查询获得的结果集,查询语句可能类似于“SELECT * FROM employees WHERE company = ‘DevCode’ ”。现在我们可以把 List[Employee] 变换到以 company 名称作为键、属于该公司的所有员工的列表作为值的 Map 类型,这样就可以把雇员按 company 来排序:

scala> val sortedEmployees = allEmployees.groupBy(_.company)
sortedEmployees: scala.collection.immutable.Map[String,List[Employee]] = Map(DevCode - > List(Employee(Anna,30,DevCode), Employee(guest,30,DevCode),  Employee(Thomas,41,DevCode)), LucasArt -> List(Employee(Luke,30,LucasArt),  Employee(Yoda,800,LucasArt)))

每一个列表已经作为一个值存入了(键——值)哈希表,为了示范如何进一步处理这些列表,可以设想我们需要计算每个公司的雇员平均年龄。

这具体意味着我们必须要计算每个列表的每个雇员的的“age”字段的和,然后除以该列表中雇员的数量。让我们先计算一下 DevCode:


scala> devcodeEmployees
res4: List[Employee] = List(Employee(Anna,30,DevCode), Employee(guest,30,DevCode),  Employee(Thomas,41,DevCode))

scala> val devcodeAges = devcodeEmployees.map(_.age)  devcodeAges: List[Int] = List(30, 30, 41)
scala> val devcodeAverageAge = devcodeAges.sum / devcodeAges.size  devcodeAverageAge: Int = 33

回到我们的 Map (key:String ->value:List[Employee]),下面是个更加一般性的例子。我们现在可以归并并计算每个公司的平均年龄,要做的只是写几行代码:


scala> val averageAgeByCompany = sortedEmployees.map{ case(key,value)=>
    | value(0).copy(name="average",age=(value.map(_.age).sum)/value.size)}
averageAgeByCompany: scala.collection.immutable.Iterable[Employee] =  List(Employee(average,33,DevCode), Employee(average,415,LucasArt))

这里的“case(key,value)”说明了 Scala 提供的模式匹配机制是多么强大。请参考 Scala 的文档来获取更多的信息。

到这里我们的任务就完成了。我们实现的是一个简单的 Map-Reduce 算法。由于每个公司雇员的归并是完全独立于其他公司,这个算法非常直观地实现了并行计算。

在后面的附录里给出了此算法的等价的实现,分为 Java 版本和 Scala 版本。

参考

The typesafe stack.

附录

Map Reduce: Java


public class Employee {

   final String name;
   final Integer age;
   final String company;

   public Employee(String name, Integer age, String company) {
       this.name = name == null ? "guest" : name;
       this.age = age == null ? 30 : age;
       this.company = company == null ? "DevCode" : company;
   }

   public String getName() {
       return name;
   }

   public int getAge() {
       return age;
   }

   public String getCompany() {
       return company;
   }

   @Override
   public String toString() {
       return "Employee [name=" + name + ", age=" + age + ",
              company="
              + company + "]";
   }
}

class Builder {
   String name, company;
   Integer age;

   Builder(String name) {
       this.name = name;

   }

   Employee build() {
       return new Employee(name, age, company);
   }

   Builder age(Integer age) {
       this.age = age;
       return this;
   }

   Builder company(String company) {
       this.company = company;
       return this;
   }
}

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimaps;

public class MapReduce {

   public static final void main(String[] args) {
       Employee guest = new Builder("Guest").build();
       Employee anna = new Builder("Anna").build();
       Employee thomas = new Builder("Thomas").age(41).build();
       Employee luke = new
           Builder("Luke").company("LucasArt").build();
       Employee yoda = new
           Builder("Yoda").age(800).company("LucasArt").build();

       Collection<Employee> employees = new ArrayList<Employee>();
       employees.add(guest);
       employees.add(anna);
       employees.add(thomas);
       employees.add(luke);
       employees.add(yoda);

       ImmutableListMultimap<String, Employee>
           personsGroupByCompany = Multimaps.index(employees, new Function<Employee,String>() {

               public String apply(Employee person) {
                  return person.getCompany();
                }

             });

       ImmutableSet<String> companyNamesFromMap =
           personsGroupByCompany.keySet();

       List<Employee> averageAgeByCompany = new
           ArrayList<Employee>();

       for(String company: companyNamesFromMap) {
            List<Employee> employeesForThisCompany =
               personsGroupByCompany.get(company);
            int sum = 0;
            for(Employee employee: employeesForThisCompany) {
                sum+= employee.getAge();
            }
            averageAgeByCompany.add(new
               Employee("average",sum/employeesForThisCompany.size(),company));
    }
    System.out.println("Result: "+averageAgeByCompany);

   }
}

MapReduce.scala:


case class Employee(name: String = "guest", age: Int = 30, company: String = "DevCode")

   object MapReduce {
       def main(args: Array[String]): Unit = {

       val guest = Employee()
       val anna = Employee("Anna")
       val thomas = Employee("Thomas", 41)
       val luke = Employee("Luke", company = "LucasArt")
       val yoda = luke.copy("Yoda", age = 800)

       val allEmployees = List(luke, anna, guest, yoda, thomas)
       val sortedEmployees = allEmployees.groupBy(_.company)
       val averageAgeByCompany = sortedEmployees.map { case (key, value) =>
           value(0).copy(name = "average", age = (value.map(_.age).sum) / value.size)
     }
       println("Result: "+averageAgeByCompany)
   }
}

关于作者

Thomas Alexandre是 DevCode 的高级咨询顾问,专注于 Java 和 Scala 软件开发。他热爱技术,热衷于分享知识,永远在寻求方法、采用新的开源软件和标准来实现更加有效的编程。在十四年的 Java 开发经验之外,过去几年他集中精力在新的编程语言和 Web 框架上,例如 Groovy/Grails 和 Scala/Lift。Thomas 从法国里尔大学获得了计算机科学博士学位,在卡耐基梅隆大学度过了两年的博士后研究生涯,研究方向是安全和电子商务。

查看英文原文: An Introduction to Scala for Java Developers


给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。