低代码到底是不是行业毒瘤?一线大厂怎么做的?戳此了解>>> 了解详情
写点什么

向 Java 开发者介绍 Scala

2011 年 9 月 26 日

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
{1}

作为第一个变量传递给 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 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2011 年 9 月 26 日 00:008498

评论

发布
暂无评论
发现更多内容

Spring BeanPostProcessor 你不能不知道的事

CoderLi

Java spring 程序员 源码分析 后端

架构第二周-学习总结

J.Smile

极客大学架构师训练营

用故事讲技术:如何学习一门技术,及如何布道、如何输出一门技术?

李艺

学习 写作

架构师训练营 - 第二周学习总结

hellohuan

极客大学架构师训练营

用故事讲技术:关于js apply、call、bind的区别,我们可以将call忘掉,只使用apply就足够了

李艺

Java 前端

架构师第二周课后作业

傻傻的帅

极客大学架构师训练营

第二章总结

大雄

【week02】总结

chengjing

「架构师训练营」第2周作业

傅晶

ioc 极客大学架构师训练营 第二周作业

架构师训练营第二周总结

极客大学架构师训练营

无抽象不架构

菜根老谭

架构 抽象 架构思维 抽象思维

【week02】作业

chengjing

架构师训练营第二周作业

路人

架构师训练营 第二周作业

傅晶

极客大学架构师训练营

原创 | TDD工具集:JUnit、AssertJ和Mockito (二十三)编写测试-并行测试

编程道与术

Java 编程 TDD 单元测试 JUnit

第二周作业

LEAF

第二周总结

Thrine

【架构师第二周】总结

浪浪

原创 | TDD工具集:JUnit、AssertJ和Mockito (二十二)编写测试-超时

编程道与术

Java 编程 TDD 单元测试 JUnit

架构师训练营-week2命题作业

J.Smile

极客大学架构师训练营

「架构师训练营」第2周作业 - 设计原则

guoguo 👻

极客大学架构师训练营

「架构师训练营」第 2 周学习总结

guoguo 👻

极客大学架构师训练营

架构师训练营-W02H-架构设计

BlazeLuLu

极客大学架构师训练营

架构师训练营作业

邵帅

架构师训练营 No.2 周作业

连增申

架构师训练营 第二周 设计原则作业

且听且吟

极客大学架构师训练营

架构师训练营第二周总结

邵帅

Spring源码-自定义标签

云淡风轻

xml spring

Netty4.x的Channel相关类图及分析

娄江国

第二周

陈皮

架构师训练营第二周课后作业

竹森先生

极客大学 极客大学架构师训练营

2021 ThoughtWorks 技术雷达峰会

2021 ThoughtWorks 技术雷达峰会

向Java开发者介绍Scala-InfoQ