Scala设计哲学
Scala语言的名称来自于“可伸展的语言”。之所以这样命名,是因为他被设计成随着使用者的需求而成长。你可以把Scala应用在很大范围的编程任务上,从写个小脚本到建立个大系统。
Scala是很容易进入的语言。它跑在标准的Java平台上,可以与所有的Java库实现无缝交互。
从技术层面上来说,Scala是一种把面向对象和函数式编程理念加入到静态类型语言中的混血儿。Scala的许多不同的方面都展现了面向对象和函数式编程的熔合。Scala的函数式编程使得它便于快速地从简单的碎片开始建立一些有趣的东西。它的面向对象特性又使它便于构造大型系统并使它们适应于新的需求。
函数式编程
很长一段时间,函数式语言处于边缘地带,在学府里流行,但没有广泛应用于业界。然而,最近几年对函数式语言和技术的热情持续高涨。函数式编程有两种理念做指导,第一种理念是函数是第一类值,第二个主要理念是程序的操作符应该把输入值映射到输出值而不是就地修改数据。
函数式编程理念
在函数式语言中,函数也是值,与,比如说,整数或字串,在同一个地位。你可以把函数当作参数传递给其他函数,当作结果从函数中返回或保存在变量里。你也可以在函数里定义其他函数,就好像在函数里定义整数一样。还可以定义匿名函数,把函数作为第一类值为操作符上的抽象和创建新控制结构提供了便利的方法。相反,在多数传统语言中,函数不是值。确实有函数值的语言则又常常把它们贬为二类地位。
熟悉Java的程序员都应该听说过immutable这个单词,Java的String类就是immutable的。函数式编程第二种理念的方式是方法不应有任何副作用:side effect。它们唯一的与所在环境交流的方式应该是获得参数和返回结果。
区别于传统编程方式,就有了指令式编程与函数式编程两个概念,指令式编程用可变数据和有副作用的方法调用编程。但是Scala通常可以在你需要的时候轻松避免它们,因为有好的函数式编程方式做替代。
immutable
这里引用http://www.cnblogs.com/zdwillie/p/3499110.html,作者对mutable与immutable两个概念介绍的非常清楚
先举个例子,例如我们想实现一个字符串类string,在初始化的时候,我们用new string(“hello”)给它赋一个初值”hello”。后来在使用过程中,我们发现其值需要做改变为“world”。那该如何做呢?一种方式是直接在当前对象中修过内部成员,提供一个string.setValue函数;另外一种方式是创建一个新的字符串对象new string(“world”),而保证之前的字符串对象不变。这里前者就是mutable object,因为在其初始化之后可变;后者是immutable object,因为一旦初始化后,其内部状态就不会发生任何变化,如果需要变化,就必须从头创建一个新的对象。而这相比较,各有什么优劣呢?
- Immutable object表示不可变的对象。或者说,对象一旦创建起来,就只能被访问,不能被修改。程序中经常使用的数据类都可以作为为immutable object。例如整数,字符串,颜色,时间,实体类等。Immuable类不能也不需要提供setter方法,只提供getter类方法或者其他不修改内部状态的方法。而mutable object,必须提供setter方法以修改内部状态。当内部变量比较多而且相互关联时,设计好这些setter并非一件易事。
- Immutable object可以确保线程安全。多线程访问同一个对象,可能会因为一个正在读另一个正在写,或者两个都在写而导致并发问题。而对于immutable object,因为只有在一个线程创建时可以初始化其内部变量,所以后续多线程并发使用时,都只是不可变的操作,不会改变内部状态。所以可以不加任何锁的随意访问;而对于mutable object,因为有写操作,所以如果访问,必须小心的加锁。
- Immutable object不需要保护性拷贝(deffensive copy)。对象可以在函数之间传来传去。如果一个对象可以在多处被修改,那使用者很可能无法知晓对象目前的状态。所以在参数传递的过程中,鼓励采用拷贝一份再传出去的办法来保证对象只在一个地方被修改。对于immutable object,因为它在初始化之后不可修改,所以根本不用关心这个被不知道的类不小心或者恶意修改的问题;而对于mutable object,必须采用保护性拷贝以保证修改可控。例如mutable string用来表示中文名“张三”,它调用了一个打印拼音名字的函数,但该函数不小心直接调用了name.setValue(“zhangsan”),那调用者现在看到的字符串的内容也是“zhangsan”了,这并不是我们希望的。
- Immuable object不需要拷贝构造函数和clone。拷贝构造函数和clone的目的就是创建一个一样的类。而之所以要创建一样的类,是因为需要对两个类分别作修改。所以对于immutable object,因为不能修改,所以也就不需要拷贝构造函数和clone。mutable object则很可能依赖于这类拷贝机制jinxing保护性拷贝等工作。
- Immuable object更容易class invariant(类不变量)。只需要在构造的时候指定关心类不变量,后续不可修改,所以就不用关心了;单独与mutable object,则在内部成员比较多的情况下,一个setter只设置了部分成员,而导致成员之间的正确关系被打破,从而使对象处于一个不稳定的状态。例如假设mutable string有一个content表示内容,一个length表示长度。如果setValue只修改了content而忘记修改length,则会导致奇怪问题。
- Immuable object只需要计算一次hashCode。因为immuable object的内部成员不发生变化,所以可以只计算一次,后续直接使用就可以了。而mutable object,必须每次计算。
- Immutable object可能会有性能问题。例如一个对象有多个内部变量。我们只想修改一个。此时如果使用mutable object,就只修改一个就行了;但如果immutable,则必须从头初始化所有的变量。还有一种情况是对于多阶段的操作,例如多个字符串相加,如果使用immutable object,每两个相加都需要创建新的对象;但如果使用mutable object,则只需要不断的设置新的value就行了。
考虑到这些因素,在实现过程中,如果有的选择,而且不会产生性能问题,我们可以尽量使用immutable object。如果真有性能问题,那经常采用的策略是提供immutable object,同时再提供一个辅助的mutable object。在需要高性能计算时,使用mutable object进行计算,然后把结果在转换成immutable object。一个典型的例子就是String和StringBuilder。
那如何实现一个immutable object?要做到下面几点
- 不提供setter
- 设置类为final,使其不被子类化。
- 设置所有成员变量(域)为final和private,防止直接修改。
- 如果某些成员变量是immuable的,那这些变量一定不能被外界访问到。如果要访问,要使用保护性拷贝来复制一份传给外界。
函数式编程优势
- 不可变性(immutability)
- 并行操作性(parallelism)
不可变数据结构是函数式语言的一块基石,函数式语言天生的并行操作性:“函数式语言已经在简化并行开发中证明了它的作用, 这得益于它既不用共享内存,也不会产生副作用(side effect)的函数。”可以说,函数式编程在这两个方面上的优势是毫无疑问的。
事实上,这两个优势是同进同退的,因为良好的不可变性的使用才能确保不会成为并行操作时性能的瓶颈。而这个优势的重要性,前面的答案中已经很明显的给出来了:如今的CPU在增高频率上已经遇到了瓶颈,多核CPU成为趋势,因此能够充分发挥多核性能的应用程序将越来越重要。
Scala特性
兼容性
Scala程序会被编译为JVM的字节码。它们的执行期性能通常与Java程序一致。Scala代码可以调用Java方法,访问Java字段,继承自Java类和实现Java接口。这些都不需要特别的语法,显式接口描述,或粘接代码。实际上,几乎所有Scala代码都极度依赖于Java库,而经常无须在程序员意识到这点。
简洁性
先看Java经典pojo的定义
class MyClass {
private int index;
private String name;
public MyClass(int index, String name) {
this.index = index;
this.name = name;
}
}
Java中包含大量这样的pojo类,尤其是JavaEE中。一直觉得这些代码都是冗余的,因为不论哪种面向对象语言都会声明字段、构造函数以及相应的getter,setter。虽然这些代码都可以使用IDE自动生成,但是还是繁琐,下面是Scala的实现方式。
class MyClass(index: Int, name: String)
再来看Java的声明
ArrayList<String> list = new ArrayList<String>();
是不是感觉很重复,前后都是一样的类型为什么还需要重复声明。当然,Jdk 7以后第二个语句可以写成这种形式,多了钻石操作符
ArrayList<String> list = new ArrayList<>();
这样的确好了不少,如果用Scala大约是这样声明的
val x: HashMap[Int, String] = new HashMap[Int, String]()
当然,不需要这么复杂,可以这样定义
val x = new HashMap[Int, String]()
val x: Map[Int, String] = new HashMap()
类似的例子还有很多,Scala程序一般都很短。Scala程序员曾报告说与Java比起来代码行数可以减少到1/10。这有可能是个极限的例子,但Scala确实非常简洁。
高层级
我们先看一个例子,Java中判断一个字符串是否包含大写字母,是返回true,否则返回false,传统指令式编程是这样实现的。
boolean nameHasUpperCase = false;
for (int i = 0; i < name.length(); ++i) {
if (Character.isUpperCase(name.charAt(i))) {
nameHasUpperCase = true;
break;
}
}
当然,Scala指令式方式也可以模仿实现,但Scala提供了更好的实现方式
val nameHasUpperCase = name.exists(_.isUpperCase)
后面会说到,exists方法是一个高阶函数,Scala提供了很多这样的高阶函数简化代码。
静态的
Scala类似于Java,C++等,是一种静态的语言。与Python等动态语言不一样,我们这里不讨论静态语音与动态语言谁好谁坏,事实上各有所长,各有所短。很难下定论说动态语言一定优于静态语言。
从Java到Scala
这里简单介绍一下Java程序员学习Scala应具备的思维转变,具体内容后续章节会深入介绍,这里是个人的几点心得体会。
var与val
val类似于Java里的final变量,一旦初始化了,val就不能再赋值了。与之对应的,var如同Java里面的非final变量。var可以在它生命周期中被多次赋值。
val msg = "Hello, world!"
val msg2: java.lang.String = "Hello again, world!"
var greeting = "Hello, world!"
greeting = "Leave me alone, world!"
对于传统编程语言,我们似乎大量使用var进行变量声明,很多计算过程中的临时变量都是var声明的。事实上,,如果代码包含了任何var变量,那它大概就是指令式的风格。如果代码根本就没有var,就是说仅仅包含val,那它大概是函数式的风格。当热,Scala这两种方式都支持,但对于函数式编程来说,最好少使用var,val能带来不可变对象和没有副作用的方法。
比如,现在要输出整个数组的内容,以换行为单位,传统的实现方式是这样的。
def printArgs(args: Array[String]): Unit = {
var i = 0
while (i < args.length) {
println(args(i))
}
}
现在,我们完全可以用foreach循环来实现
def printArgs(args: Array[String]): Unit = {
for (arg <- args)
println(arg)
}
当然,Scala提供了更好的方式
def printArgs(args: Array[String]): Unit = {
args.foreach(println)
}
代码变得非常简洁,但是,这还不是纯函数式的,因为它有副作用,其副作用是打印到标准输出流。
def formatArgs(args: Array[String]) = args.mkString("\n")
println(formatArgs(args))
while与for
现在谈谈while与for两个循环,在C++、Java中两者是等价的,很多时候可以相互转化。但是,在函数式语言中,二者差别很大。while结构被称为“循环”,不是表达式,因为它们不产生有意义的结果,很多纯函数式语言一般舍弃while。同时,Scala的if,for,switch,try等都成为表达式,因为这些语句都能产生值,所以良好的函数式编程方式是少用while,多用for。
当然,Scala的for语句非常强大,你绝对会爱不释手。
for (i <- 1 to 4)
println("Iteration " + i)
val filesHere = (new java.io.File(".")).listFiles
for (file <- filesHere if file.getName.endsWith(".scala"))
println(file)
def grep(pattern: String) =
for {
file <- filesHere
if file.getName.endsWith(".scala")
line <- fileLines(file)
trimmed = line.trim
if trimmed.matches(pattern)
} println(file + ": " + trimmed)
grep(".*gcd.*")
可见,for表达式中可以包含很多子句
函数式
函数是一个值
首先,我们来看一个Scala函数的标准形式
def max(x: Int, y: Int): Int = {
if (x > y) x
else y
}
函数的定义从def开始,函数名是max,括号中x、y是参数,冒号后的Int是函数的返回类型,等号后的大括号是真正的函数体。可以发现Scala的函数不需要return语句,当然,上面max定义的太复杂了,返回类型可以自动推断
def max2(x: Int, y: Int) = if (x > y) x else y
再来看一个例子,使用了匿名函数
var increase = (x: Int) => x + 1
increase(10)
当然,匿名函数中也可以包含其他语句
increase = (x: Int) => {
println("We")
println("are")
println("here!")
x + 1
}
表达式产生值
Scala中if,for,try,match等表达式都能够产生值,这也是和Java不同的地方。
println(if (!args.isEmpty) args(0) else "default.txt")
def urlFor(path: String) =
try {
new URL(path)
} catch {
case e: MalformedURLException =>
new URL("http://www.scalalang.org")
}
val firstArg = if (!args.isEmpty) args(0) else ""
val friend =
firstArg match {
case "salt" => "pepper"
case "chips" => "salsa"
case "eggs" => "bacon"
case _ => "huh?"
}
println(friend)
函数与变量
在C++、Java等传统语言中,函数与变量是两个不同的概念,地位也不相同。但在Scala里二者是同等的,或者说函数是第一公民。所以看到以下代码也自然不要感到困惑。
class Person(age: Int, name: String) {
def ag = age
def na = name
}
这是随便写的一个例子,例子中ag与na让人很困惑,这是函数还是变量。不用说,有def定义的肯定是函数(var与val定义变量),Scala中函数、类如果不接参数都可以省略括号。
另一方面,Scala里禁止在同一个类里用同样的名称定义字段和方法,而在Java里这样做被允许。例如,下面的Java类能够很好地编译:
class CompilesFine {
private int f = 0;
public int f() {
return 1;
}
}
但在Scala中这是编译不通过的。通常情况下,Scala仅为定义准备了两个命名空间,而Java有四个。Java的四个命名空间是字段,方法,类型和包。与之相对,Scala的两个命名空间是:值(字段,方法,包还有单例对象),类型(类和特质名)。Scala把字段和方法放进同一个命名空间的理由很清楚,因为这样你就可以使用val重载无参数的方法,这种你在Java里做不到的事情。
本地函数
函数调用是一个非常常见的问题,比如如下的例子,processFile调用processLine。按照Java的实现方式可以把processLine声明为private,但如果这类方法过多会造成类中方法冗余。
import scala.io.Source
object LongLines {
def processFile(filename: String, width: Int) {
val source = Source.fromFile(filename)
for (line <- source.getLines)
processLine(filename, width, line)
}
private def processLine(filename: String, width: Int, line: String) {
if (line.length > width)
println(filename + ": " + line.trim)
}
}
Scala支持本地方法,即在方法中调用方法
def processFile(filename: String, width: Int) {
def processLine(filename:String, width:Int, line:String) {
if (line.length > width) print(filename+": "+line)
}
val source = Source.fromFile(filename)
for (line <- source.getLines) {
processLine(filename, width, line)
}
}
函数作为参数传递
首先我们考虑一个需求,这个需求非常常见。现有filesMatching方法中希望加入一个判断条件(method),这个判断条件可能会发生更改,最好的实现方式如下。判断条件更改的同时filesMatching也能够正确的改变。
def filesMatching(query: String, method) =
for (file <- filesHere; if file.getName.method(query))
yield file
当然,上面的代码是理想状况,一些动态语言可能能够实现,但Scala是不支持这种方式的。但是,虽然你不能把方法名当作值传递,但你可以通过传递为你调用方法的函数值达到同样的效果。
def filesMatching(query: String,
matcher: (String, String) => Boolean) = {
for (file <- filesHere; if matcher(file.getName, query))
yield file
}
现在,你可以进行如下调用。
def filesEnding(query: String) =
filesMatching(query, _.endsWith(_))
def filesContaining(query: String) =
filesMatching(query, _.contains(_))
def filesRegex(query: String) =
filesMatching(query, _.matches(_))
代码是不是很不容易理解,这里牵扯到函数闭包、偏应用函数多方面内容,后面会继续讨论。
这个例子看不懂没关系,主要为了说明Scala的函数可以作为值进行传递。基于这个设计,我们可以实现很多功能,比如Java中连接数据库、读写文件都需要手动关闭连接,现在,我们可以自己编写一个公共方法,你再也不需要自己关闭这些连接了。
def withPrintWriter(file: File, op: PrintWriter => Unit) {
val writer = new PrintWriter(file) try {
op(writer)
} finally {
writer.close()
}
}
调用这个方法,即使忘记关闭io也没关系了,因为这个方法最后会帮你关闭。
withPrintWriter(
new File("date.txt"),
writer => writer.println(new java.util.Date)
)
方法是实现了,是不是总感觉哪里怪怪的?没错,withPrintWriter传入这样两个参数很不优雅,有没有更好的方法呢?有的,Scala支持curry操作,只需要对withPrintWriter方法稍作修改即可。
def withPrintWriter(file: File)(op: PrintWriter => Unit) {
这里把withPrintWriter方法的两个参数拆开了,原理后面会继续讨论。现在,调用withPrintWriter方法变得非常的优雅了。
val file = new File("date.txt")
withPrintWriter(file) {
writer => writer.println(new java.util.Date)
}
高阶函数
Scala标准库实现了很多的高阶函数。当你把函数值用做参数时,算法的非通用部分就是它代表的某些其它算法。在这种函数的每一次调用中,你都可以把不同的函数值作为参数传入,于是被调用函数将在每次选用参数的时候调用传入的函数值。这种高阶函数:higher-order function,带其它函数做参数的函数,给了你额外的机会去组织和简化代码。
现在考虑实现一个例子,判断一个列表是否包含奇数,传统的实现方式是这样的。
def containsOdd(nums: List[Int]): Boolean = {
var exists = false
for (num <- nums)
if (num % 2 == 1)
exists = true
exists
}
这是典型指令式编程写法,Scala为我们提供了高阶函数,实现方式如下。
def containsOdd(nums: List[Int]) = nums.exists(_ % 2 == 1)
类似的高阶函数有很多,foreach,filesMatching,map,flatMap,filter等等。
面向对象
最后介绍一下Java向Scala转变过程中面向对象方面的差异。二者很多地方相同,这里不细说,仅仅介绍二者之间的差异部分。
最开始介绍了传统Java类与Scala类的区别,比如MyClass类的定义方式如下。
class MyClass {
private int index;
private String name;
public MyClass(int index, String name) {
this.index = index;
this.name = name;
}
}
class MyClass(index: Int, name: String)
不仅如此,Scala支持参数化字段。比如ArrayElement继承Element类,ArrayElement类需要Array[String]作为构造函数,同时希望外界能够访问这个Array[String],这时,可以通过如下方式实现。
class ArrayElement(
val contents: Array[String]
) extends Element
如何调用父类构造函数,Java通过super关键字,Scala可以使用如下方式实现。
class LineElement(s: String) extends ArrayElement(Array(s)) {}
Java每个类都是Object的子类,Scala也有这种约定。Scala每个类都都是Any子类,其中引用类是AnyRef子类,值类是AnyVal子类。与Java不同的是,Scala中Null和Nothing是每个类的子类,其中Null是引用类子类,Nothing是任何其他类子类。
比如,Scala标准库中有一个error方法,定义如下。
def error(message:String): Nothing = throw new RuntimeException(message)
于是我们可以这样实现两个数相除。
def divide(x:Int, y:Int): Int =
if(y != 0) x / y
else error("can't divide by zero")
这一章到此结束,讲解了Scala的设计哲学以及Java程序员学习Scala应该具备的思维转变过程。文章后半部分有些代码难以理解,这是自然的,后面会继续对这些代码进行讲解。本文内容来源于《Programming Scala》中文版,即《Scala编程》。