Scala详解
13.6.9 列表转换:toArray, copyToArray
13.7.1 映射与处理:map、flatMap、flatten、foreach
13.7.2 过滤:filter、partition、find、takeWhile、dropWhile、span
14.4 可变(mutable)集合Vs.不可变(immutable)集合
14.5.1 集合转化为数组(Array)、列表(List)
1 快速入门
1.1 分号
分号表示语句的结束;
如果一行只有一条语句时,可以省略,多条时,需要分隔
一般一行结束时,表示表达式结束,除非推断该表达式未结束:
// 末尾的等号表明下一行还有未结束的代码.
def equalsign(s: String) =
println("equalsign: " + s)
// 末尾的花括号表明下一行还有未结束的代码.
def equalsign2(s: String) = {
println("equalsign2: " + s)
}
//末尾的逗号、句号和操作符都可以表明,下一行还有未结束的代码.
def commas(s1: String,
s2: String) = Console.
println("comma: " + s1 +
", " + s2)
多个表达式在同一行时,需要使用分号分隔
1.2 常变量声明
1.2.1 val常量
定义的引用不可变,不能再指向别的对象,相当于Java中的final
Scala中一切皆对象,所以,定义一切都是引用(包括定义的基本类型变量,实质上是对象)
val定义的引用不可变,指不能再指向其他变量,但指向的内容是可以变的:
val定义的常量必须要初始化
val的特性能并发或分布式编程很有好处
1.2.2 var变量
定义的引用可以再次改变(内容就更可以修改了),但定义时也需要初始化
在Java中有原生类型(基础类型),即char、byte、short、int、long、float、double和boolean,这些都有相应的Scala类型(没有基本类型,但好比Java中相应的包装类型),Scala编译成字节码时将这些类型尽可能地转为Java中的原生类型,使你可以得到原生类型的运行效率
用val和var声明变量时必须初始化,但这两个关键字均可以用在构造函数的参数中,这时变量是该类的一个属性,因此显然不必在声明时进行初始化。此时如果用val声明,该属性是不可变的;如果用var声明,则该属性是可变的:
class Person(val name: String, var age: Int)
即姓名不可变,但年龄是变化的
val p = new Person("Dean Wampler", 29)
var和val关键字只标识引用本身是否可以指向另一个不同的对象,它们并未表明其所引用的对象内容是否可变
1.2.3 类型推导
定义时可以省略类型,会根据值来推导出类型
scala> var str = "hello"
str: String = hello
scala> var int = 1
int: Int = 1
定义时也可明确指定类型:
scala> var str2:String = "2"
str2: String = 2
1.2.4 函数编程风格
以前传统Java都是指令式编程风格,如果代码根本就没有var,即仅含有val,那它或许是函数式编程风格,因此向函数式风格转变的方式之一,多使用val,尝试不用任何var编程
指令式编程风格:
def printArgs(args: Array[String]): Unit = {
var i = 0
while (i < args.length) {
println(args(i))
i += 1
}
}
函数式编程风格:
def printArgs(args: Array[String]): Unit = {
for (arg <- args)
println(arg)
}
或者:
def printArgs(args: Array[String]): Unit = {
//如果函数字面量只有一行语句并且只带一个参数,
//则么甚至连指代参数都不需要
args.foreach(println)
}
1.3 Range
数据范围、序列
支持Range的类型包括Int、Long、Float、Double、Char、BigInt和BigDecimal
Range可以包含区间上限,也可以不包含区间上限;步长默认为1,也可以指定一个非1的步长:
1.4 定义函数
函数是一种具有返回值(包括空Unit类型)的方法
函数体中最后一条语句即为返回值。如果函数会根据不同情况返回不同类型值时,函数的返回类型将是不同值的通用(父)类型,或者是可以相互转换的类型(如Char->Int)
如果函数体只一条语句,可以省略花括号:
def max2(x:Int,y:Int)=if(x>y)x else y
scala> max2(3,5)
res2: Int = 5
Unit:返回类型为空,即Java中的void类型。如果函数返回为空,则可以省略
scala> def greet()=println("Hello")
greet: ()Unit
1.5 while、if
打印入口程序的外部传入的参数:
object Test {
def main(args: Array[String]): Unit = {
var i = 0
while (i < args.length) {
if (i != 0) print(" ")
print(args(i))
i += 1
}
}
}
注:Java有++i及i++,但Scala中没有。
与Java一样,while或if后面的布尔表达式必须放在括号里,不能写成诸如 if i < 10 的形式
1.6 foreach、for
object Test {
def main(args: Array[String]): Unit = {
var i = 0;
args.foreach(arg => { if (i != 0) print(" "); print(arg); i += 1 })
}
}
foreach方法参数要求传的是函数字面量(匿名函数),arg为函数字面量的参数,并且值为遍历出来的集合中的每个元素,类型为String,已省略,如不省略,则应为:
args.foreach((arg: String) => { if (i != 0) print(" "); print(arg); i += 1 })
如果函数字面量只有一行语句并且只带一个参数,则么甚至连指代参数都不需要:
args.foreach(println)
也可以使用for循环来代替:
for (arg <- args) println(arg)
1.7 读取文件
object Test {
def main(args: Array[String]): Unit = {
import scala.io.Source
//将文件中所有行读取到List列表中
val lines = Source.fromFile(args(0)).getLines().toList
//找到最长的行:类似冒泡排序,每次拿两个元素进行比较
val longestLine = lines.reduceLeft((a, b) => if (a.length() > b.length()) a else b)
//最长行的长度本身的宽度
val maxWidth = widthOfLength(longestLine)
for (line <- lines) {
val numSpaces = maxWidth - widthOfLength(line)
//让输出的每行宽度右对齐
val padding = " " * numSpaces
println(padding + line.length() + " | " + line)
}
}
def widthOfLength(s: String) = s.length().toString().length()
}
1.8 类、字段和方法
类的方法以 def 定义开始,要注意的 Scala 的方法的参数都是 val 类型,而不是 var 类型,因此在函数体内不可以修改参数的值,比如如果你修改 add 方法如下:
使用class定义类:
class ChecksumAccumulator {}
然后就可以使用 new 来实例化:
val cal = new ChecksumAccumulator
类里面可以放置字段和方法,这都称为成员(member)。
字段,不管是使用val还是var,都是指向对象的变量(即Java中的引用)
方法,使用def进行定义
class ChecksumAccumulator {
var sum = 0
}
object Test {
def main(args: Array[String]): Unit = {
val acc = new ChecksumAccumulator
val csa = new ChecksumAccumulator
}
}
刚实例化时内存的状态如下:
注:在Scala中,由于数字类型(整型与小数)都是final类型的,即不可变,所以在内存中如果是同一数据,则是共享的
由于上面是使用var进行定义的字段,而不是val,所以可以重新赋值:
acc.sum = 3
现在内存状态如下:
由于修改了acc中sum的内容,所以acc.sum指向了3所在的内存。
对象的稳定型就是要保证对象状态的稳定型,即对象中字段值在对象整个生命周期中持续有效。这需要将字段设为private以阻止外界直接对它进行访问与修改,因为私有字段只能被同一类里的方法访问,所以更新字段的代码将被锁定在类里:
class ChecksumAccumulator {
private var sum = 0
def add(b: Byte): Unit = {
sum += b
}
}
与Java 不同的,Scala 的缺省修饰符为 public,也就是如果不带有访问范围的修饰符 public,protected,private,Scala 缺省定义为 public
类的方法以 def 定义开始,要注意的 Scala 的方法的参数都是 val 类型,而不是 var 类型,因此在函数体内不可以修改参数的值,比如如果你修改 add 方法如下:
def add(b: Byte): Unit = {
b = 1 // 编译出错,因为b是val :error: reassignment to val
sum += b
}
如果某个方法的方法体只有一条语句,则可以去掉花括号:
def add(b: Byte): Unit = sum += b
类的方法分两种,一种是有返回值的,一种是不含返回值
如果方法没有返回值(或为Unit),则定义方法时可以去掉结果类型和等号 =,并把方法体放在花括号里:
def add(b: Byte) { sum += b }
定义方法时,如果去掉方法体前面的等号 =,则方法的结果类型就一定是Unit,这不管方法体最后语句是啥,因为编译器可以把任何类型转换为Unit,如结果是String,但返回结果类型声明为Unit,那么String将被转换为Unit并丢弃原值。下面是明确定义返回类型为Unit:
scala> def f(): Unit = "this String gets lost"
f: ()Unit
去掉等号的方法返回值类型也一定是Unit:
scala> def g() { "this String gets lost too" }
g: ()Unit
加上等号时,如果没有确定定义返回类型,则会根据方法体最后语句来推导:
scala> def h() = { "this String gets returned!" }
h: ()java.lang.String
scala> h
res0: java.lang.String = this String gets returned!
Scala 代码无需使用“;”结尾,也不需要使用 return返回值,函数的最后一行的值就作为函数的返回值
1.9 Singleton单例对象
Scala中不能定义静态成员,而是以定义成单例对象(singleton object)来代替,即定义类时,使用的object关键字,而非class关键字,但看上去就像定义class一样:
class ChecksumAccumulator {
private var sum = 0
def add(b: Byte): Unit = {
sum += b
}
def checksum(): Int = {
return ~(sum & 0xFF) + 1
}
}
import scala.collection.mutable.Map
object ChecksumAccumulator {
private val cache = Map[String, Int]()
def calculate(s: String): Int =
if (cache.contains(s))
cache(s)
else {
val acc = new ChecksumAccumulator
for (c <- s)
acc.add(c.toByte)
val cs = acc.checksum()
cache += (s -> cs)
cs
}
}
当单例对象(object)与某个类(class)的名称相同时(上面都为ChecksumAccumulator),它就被称为是这个类的伴生对象。类和它的伴生对象必须定义在一个源文件里。类被称为这个单例对象的伴生类。类和它的伴生对象可以互相访问其私有成员
可以将单例对象当作Java中的静态方法工具类来使用,可以直接通过单例对象的名称来调用:
ChecksumAccumulator.calculate("Every value is an object.")
其实,单例对象就是一个对象,不需要实例化就可以直接通过单例对象名来访问其成员,即单例对象名就相当于变量名,已指向了某个类的实例,只不过该类不是由你来实例化,而是在访问它时由Scala实例化出来的,且在JVM只有一个这个的实例。在编译伴生对象时,会生成一个相应名为ChecksumAccumulator$(在单例对象名后加上美元符号)的类:
类和单例对象的差别:单例对象定义时不带参数(Object关键字后面),而类可以(Class关键字后面可以带括号将类参数包围起来),因为单例对象不是使用new关键字实例化出来的(这也是 Singleton 名字的由来)
单例对象在第一次被访问的时候才会被始化
没有伴生类的单例对象被称为独立对象,一般作为相关功能方法的工具类,或者用作Scala应用的入口程序
1.10 Scala入口程序
在Java中,只要类中有如下签名的main方法,即可作为程序入口程序:
class T {
public static void main(String[] args) {}
}
在Scala中,入口程序不是定义在类class中的,而是定义在单例对象中的,
object T {
def main(args: Array[String]): Unit = {}
}
与Java 类似,Scala 中任何 Singleton对象(使用Object关键字定义),如果包含 main 方法,都可以作为应用的入口
Scala的每个源文件都会自动引入包java.lang和scala包中的成员,和scala包中名为Predef的单例对象的成员,该单例对象中包含了许多有用的方法,例如,当在Scala源文件中写pringln的时候,实际调用了Predef.println,另外当你写assert,实质上是调用Predef.assert
Java的源文件扩展名为.java,而Scala的源文件扩展名为.scala
在Java中,如果源文件中有public的class,则该public类的类名必须与Java源文件名一致,但在Scala中没有这种限制,但一般会将源文件名与类名设为一致
与Java一样,也有对应的编译与运行命令,它们分别是scalac(编译)与scala(运行),ava中的为javac、java,不管是Java还是Scala程序,都会编译成.class的字节码文件
Scala 为 Singleton 对象的 main 定义了一个 App trait 类型
Scala的入口程序还可以继承scala.App特质(Trait,Scala中的Trait像Java中的Interface,但不同的是可以有方法的实现),这样就不用写main方法(因为scala.App特质里实现了main方法),而直接将代码写在花括号里,花括号里的代码会被收集进单例对象的主构造器中,并在类被初始化时执行:
object T extends scala.App {
println("T")
}
缺点:命令参数行args不能再被访问;某些JVM线程会要求main方法不能通过继承得到,必须自己行编写;
1.11 基本类型
Java 支持的基本数据类型,Scala 都有对应的支持,不过 Scala 的数据类型都是对象,而且这些基本类型都可以通过隐式自动转换的形式支持比 Java 基本数据类型更多的方法:比如调用 (-1).abs() ,Scala 发现基本类型 Int 没有提供 abs()方法,但可以发现系统提供了从 Int 类型转换为 RichInt 的隐式自动转换,而 RichInt 具有 abs 方法,那么 Scala 就自动将 1 转换为 RichInt 类型,然后调用 RichInt 的 abs 方法。
Scala 的基本数据类型有: Byte,Short,Int,Long 和 Char (这些成为整数类型)。整数类型加上 Float 和 Double 成为数值类型。此外还有 String 类型,除 String 类型在 java.lang 包中定义,其它的类型都定义在包 scala 中。比如 Int 的全名为 scala.Int。实际上 Scala 运行环境自动会载入包 scala 和 java.lang 中定义的数据类型,你可以使用直接使用 Int,Short,String 而无需再引入包或是使用全称(如scala.xx与java.lang.xx)。
Scala的基本类型与Java对应类型范围完全一样,这样可以让Scala编译器直接把这些类型编译成Java中的原始类型
scala> var hex=0xa //十六进制,整数默认就是Int类型
hex: Int = 10
scala> var hex:Short=0x00ff //若要Short类型,则要明确指定变量类型
hex: Short = 255
scala> var hex=0xaL //赋值时明确指定数据为Long型,否则默认为Int类型
hex: Long = 10
scala> val prog=2147483648L //若超过了Int范围,则后面一定要加上 L ,置换为Long类型
prog: Long = 2147483648
scala> val bt:Byte= 38 //若要Byte类型,则要在定义时明确指定变量的类型为Byte类型
bt: Byte = 38
scala> val big=1.23232 //小数默认就是Double类型
big: Double = 1.23232
scala> val big=1.23232f //如果要定义成Float,则可直接在小数后面加上F
big: Float = 1.23232
scala> val big=1.23232D //虽然默认就是Double,但也可在小数后面加上D
big: Double = 1.23232
scala> val a='A' //类型推导成Char类型
a: Char = A
scala> val f ='\u0041' //也可以使用Unicode编码表示,以 \u 开头,u一定要小写,且\u后面接4位十六进制
f: Char = A
scala> val hello="hello" //类型推导成String类型
hello: String = hello
scala> val longString=""" Welcome to Ultamix 3000. Type "Help" for help.""" //以使用三个引号(""")开头和结尾,这样之间的字符都将看作是最原始的字符,不会被转义
longString: String = " Welcome to Ultamix 3000. Type "Help" for help." //注:开头与结尾的双引号不属于上面字符串的一部分,而是表示控制台上输出的是String
scala> val bool=true
bool: Boolean = true
1.12 字面量
字面量就是直接写在代码里的常量值
1.12.1 整数字面量
十六进制以 0x或0X开头:
scala> val hex = 0x5
hex: Int = 5
scala> val hex2 = 0x00FF
hex2: Int = 255
scala> val magic = 0xcafebabe
magic: Int = -889275714
注:不区分大小写
八进制以0开头
scala> val oct = 035 // (八进制35是十进制29)
oct: Int = 29
scala> val nov = 0777
nov: Int = 511
scala> val dec = 0321
dec: Int = 209
如果是非0开头,即十进制:
scala> val dec2 = 255
dec2: Int = 255
注:不管字面量是几进制,输出时都会转换为十进制
如果整数以L或l结尾,就是Long类型,否则默认就是Int类型:
scala> val prog = 0XCAFEBABEL
prog: Long = 3405691582
scala> val tower = 35L
tower: Long = 35
scala> val of = 31l
of: Long = 31
从上面可以看出,定义Int型时可以省去类型即可,如果是Long类型,定义时也可省略Long类型,此时在数字后面加上L或l即可,但也可以直接定义成Long也可:
scala> var lg:Long = 2
lg: Long = 2
如果要得到Byte或Short类型的变量时,需在定义时指定变量的相应类型:
scala> val little: Short = 367
little: Short = 367
scala> val littler: Byte = 38
littler: Byte = 38
1.12.2 浮点数字面量
scala> val big = 1.2345
big: Double = 1.2345
scala> val bigger = 1.2345e1
bigger: Double = 12.345
scala> val biggerStill = 123E45
biggerStill: Double = 1.23E47
小数默认就是Double类型,如果要是Float,则要以F结尾:
scala> val little = 1.2345F
little: Float = 1.2345
scala> val littleBigger = 3e5f
littleBigger: Float = 300000.0
当然Double类型也可以D结尾,不过是可选的
scala> val anotherDouble = 3e5
anotherDouble: Double = 300000.0
scala> val yetAnother = 3e5D
yetAnother: Double = 300000.0
当然,也要以在定义变量时明确指定类型也可:
scala> var f2 = 1.0
f2: Double = 1.0
scala> var f2:Float = 1
f2: Float = 1.0
1.12.3 字符字面量
使用单引号引起的单个字符
scala> val a = 'A'
a: Char = A
单引号之间除了直接是字符外,也可以是对应编码,编码是八进制或十六进制来表示
如果以八进制表示,则以 \ 开头,且为 '\0 到 '\377' (0377=255):
scala> val c = '\101'
c: Char = A
注:如果以八进制表示,则只能表示一个字节大小的字符,即0~255之间的ASCII码单字节字符,如果要表示大于255的Unicode字符,则只能使用十六进制来表示:
scala> val d = '\u0041'
d: Char = A scala>
val f = '\u0044'
f: Char = D
scala> val c = '\u6c5f'
c: Char = 江
注:以十六进制表示时,需以 \u(小写)开头,即后面跟4位十六进制的编码(两个字节)
实际上,十六进制可以出现在Scala程序的任何地方,如可以用在变量名里:
scala> val B\u0041\u0044 = 1
BAD: Int = 1
转义字符:
scala> val backslash = '\\'
backslash: Char = \
1.12.4 字符串字面量
使用双引号引起来的0个或多个字符
scala> val hello = "hello"
hello: java.lang.String = hello
特殊字符也需转义:
scala> val escapes = "\\\"\'"
escapes: java.lang.String = \"'
如果字符串中需要转义的字符很多时,可以使用三个引号(""")开头和结尾,这样之间的字符都将看作是最原始的字符,不会被转义(当然三个连续的引号除外):
发现第二行前面的空格也会原样输出来,所以第二行前面看起来缩进了,如果要去掉每行前面的空白字符(ASCII编码小于等于32的都会去掉),则把管道符号(|)放在每行前面,然后对字符串调用stripMargin:
1.12.5 Symbol符号字面量
以单引号打头,后面跟一个或多个数字、字母或下划线,但第一个字符不能是数字,这种字面量会转换成预定义类scala.Symbol的实例,如 'cymbal编译器将会调用工厂方法Symbol("cymbal")转化成Symbol实例。
scala> val s = 'aSymbol
s: Symbol = 'aSymbol
scala> s.name
res20: String = aSymbol
符号字面量 'x 是表达式 scala.Symbol("x") 的简写
Java中String的intern()方法:String类内部维护一个字符串池(strings pool),当调用String的intern()方法时,如果字符串池中已经存在该字符串,则直接返回池中字符串引用,如果不存在,则将该字符串添加到池中,并返回该字符串对象的引用。执行过intern()方法的字符串,我们就说这个字符串被拘禁了(interned),即放入了池子。默认情况下,代码中的字符串字面量和字符串常量值都是被拘禁的,例如:
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s1 == s2);//false
System.out.println(s1 == s2.intern());//true
同值字符串的intern()方法返回的引用都相同,例如:
String s2 = new String("abc");
String s3 = new String("abc");
System.out.println(s2 == s3);// false
System.out.println(s2.intern() == s3.intern());// true
String str1 = "abc";
String str2 = "abc";
System.out.println(str1 == str2);//true
String str3 = new String("abc");
System.out.println(str1 == str3);//false
Sysmbol实质上也是一种字符串,其好好处:
1. 节省内存
在Scala中,Symbol类型的对象是被拘禁的(interned,即会被放入池中),任意的同名symbols都指向同一个Symbol对象,避免了因冗余而造成的内存开销:
val s1 = "aSymbol"
val s2 = "aSymbol"
println(s1.eq(s2)) //true :表明s1与s2指向同一对象
val s3 = new String("aSymbol") //由于在编译时就确定,所以还是会放入常量池
println(s1.eq(s3)) //false : 表明s1与s3不是同一对象
println(s1 == s3) //true:虽然不是同一对象,但是它们的内容相同
val s = 'aSymbol
println(s.eq('aSymbol)) //true
println(s.eq(Symbol("aSymbol"))) //true:只要是同名的Symbol,则都是指向同一对象
//即使s与s3的内容相同,但eq比较的是对象地址,所以不等
println(s.name.eq(s3)) //false
println(s.name == s3) //true:但内容相同
println(s.name.eq(s1)) //true : s与s1的的内容都会放入池中,所以指向的是同一对象
注:在Scala中,如果要基于引用地址进行比较,则要使用eq方法,而不是==,这与Java是不一样的
2. 快速比较
由于Symbol类型的对象会自动拘禁的(interned),任意的同名symbols(准确的说是值)都指向同一个Symbol对象,而相同值的字符串并不一定是同一个instance,所以symbols对象之间的比较操作符==速度会很快:因为它只基于地址进行比较,如果发现不是同一Symbols对象,则就认为不相同,不会在对内容进行比较(因为不同名的Symbols的值肯定也不相同)
Symbol类型的应用
Symbol类型一般用于快速比较,例如用于Map类型:Map<Symbol, Data>,根据一个Symbol对象,可以快速查询相应的Data, 而Map<String, Data>的查询效率则低很多。
虽说利用String的intern方法也可以实现Map<String, Data>的键值快速比较,但是由于需要显式地调用intern()方法,在编码时会造成很多的麻烦,而且如果忘了调用intern()方法,还会造成难以寻找的bug。从这个角度看,Scala的Symbol类型会自动进行intern操作(加入到池中),所以简化了编码的复杂度;而Java中除了字符串常量,是不会自动进行intern的,需要对相应对象手动调用interned方法
1.12.6 布尔字面量
scala> val bool = true
bool: Boolean = true
scala> val fool = false
fool: Boolean = false
1.13 操作符和方法
操作符如+加号,实质上是类型中有名为 + 的方法:
scala> val sum = 1 + 2 // Scala调用了(1).+(2)
sum: Int = 3
scala> val sumMore = (1).+(2)
sumMore: Int = 3
实际上Int类型包含了名为 + 的各种不同参数的重载方法,如Int+Long:
scala> val longSum = 1 + 2L // Scala调用了(1).+(2L)
longSum: Long = 3
既然+是名为加号的方法,可以以 1+2 这种操作模式来调用,那么其他方法也是可以如下方式来调用:
scala> val s = "Hello, world!"
s: java.lang.String = Hello, world!
scala> s indexOf 'o' // 调用了s.indexOf(’o’)
res0: Int = 4
如果有多个参数,则要使用括号:
scala> s indexOf ('o', 5) // Scala调用了s.indexOf(’o’, 5)
res1: Int = 8
在 Scala 中任何方法都可以是操作符:Scala里的操作符不是特殊的语法,任何方法都可以是操作符,到底是方法还是操作符取决于你如何使用它。如果写成s.indexOf('o'),indexOf就不是操作符。不过如果写成,s indexOf 'o',那么indexOf就是操作符了
上面看到的是中缀操作符,还是前缀操作符,如 -7里的“-”,后缀操作符如 7 toLong里的“toLong”( 实为 7.toLong )。
前缀操作符与后缀操作都只有一个操作数,是一元(unary)操作符,如-2.0、!found、~0xFF,这些操作符对应的方法是在操作符前加上前缀“unary_”:
scala> -2.0 // Scala调用了(2.0).unary_-
res2: Double = -2.0
scala> (2.0).unary_-
res3: Double = -2.0
可以当作前缀操作符用的标识符只有+、-、!和~。因此,如果你定义了名为unary_!的方法,就可以对值或变量用 !P 这样的前缀操作符方式调用方法。但是如果你定义了名为unary_*的方法,就没办法将其用成前缀操作符了,因为*不是四种可以当作前缀操作符用的标识符之一。
后缀操作符是不用点与括号调用的不带任何参数的方法:
scala> val s = "Hello, world!"
s: java.lang.String = Hello, world!
scala> s.toLowerCase()
res4: java.lang.String = hello, world!
由于不带参数,则可能省略括号
scala> s.toLowerCase
res5: java.lang.String = hello, world!
还可以省去点:
scala> import scala.language.postfixOps//需要导一下这个来激活后缀操作符使用方式,否则会警告
import scala.language.postfixOps
scala> s toLowerCase
res6: java.lang.String = hello, world!
1.14 数学运算
scala> 1.2 + 2.3
res6: Double = 3.5
scala> 3 - 1
res7: Int = 2
scala> 'b' - 'a'
res8: Int = 1
scala> 2L * 3L
res9: Long = 6
scala> 11 / 4
res10: Int = 2
scala> 11 % 4
res11: Int = 3
scala> 11.0f / 4.0f
res12: Float = 2.75
scala> 11.0 % 4.0
res13: Double = 3.0
数字类型还提供了一元的前缀 + 和 - 操作符(方法 unary_+ 和 unary_-)
scala> val neg = 1 + -3
neg: Int = -2
scala> val y = +3
y: Int = 3
scala> -neg
res15: Int = 2
1.15 关系和逻辑操作
scala> 1 > 2
res16: Boolean = false
scala> 1 < 2
res17: Boolean = true
scala> 1.0 <= 1.0
res18: Boolean = true
scala> 3.5f >= 3.6f
res19: Boolean = false
scala> 'a' >= 'A'
res20: Boolean = true
可以使用一元操作符!(unary_!方法)改变Boolean值:
scala> val thisIsBoring = !true
thisIsBoring: Boolean = false
scala> !thisIsBoring
res21: Boolean = true
逻辑与(&&)和逻辑或(||):
scala> val toBe = true
toBe: Boolean = true
scala> val question = toBe || !toBe
question: Boolean = true
scala> val paradox = toBe && !toBe
paradox: Boolean = false
与Java里一样,逻辑与和逻辑或有短路:
scala> def salt() = { println("salt"); false }
salt: ()Boolean
scala> def pepper() = { println("pepper"); true }
pepper: ()Boolean
scala> pepper() && salt()
pepper
salt
res22: Boolean = false
scala> salt() && pepper()
salt
res23: Boolean = false
scala> pepper() || salt()
pepper
res24: Boolean = true
scala> salt() || pepper()
salt
pepper
res25: Boolean = true
1.16 位操作符
按位与运算(&):都为1时才为1,否则为0
按位或运算(|):只要有一个为1 就为1,否则为0
按位异或运算(^):相同位产生0,不同产生1,因此0011 ^ 0101产生0110。
scala> 1 & 2 // 0001 & 0010 = 0000 = 0
res24: Int = 0
scala> 1 | 2 // 0001 | 0010 = 0011 = 3
res25: Int = 3
scala> 1 ˆ 3 // 0001 ^ 0011 = 0010 = 2
res26: Int = 2
scala> ~1 // ~0001 = 1110(负数原码:从最末一位向前除符号位各位取反即可) = 1010 = -2
res27: Int = -2
负数补码:反码+1
左移(<<),右移(>>)和无符号右移(>>>)。左移和无符号右移在移动的时候填入零。右移则在移动时填入左侧整数的最高位(符号位)。
scala> -1 >> 31 //1111 1111 1111 1111 1111 1111 1111 1111 向右移动后,还是 1111 1111 1111 1111 1111 1111 1111 1111,左侧用符号位1填充
res38: Int = -1
scala> -1 >>> 31 //1111 1111 1111 1111 1111 1111 1111 1111 向右无符号移动后,为0000 0000 0000 0000 0000 0000 0000 0001,左侧用0填充
es39: Int = 1
scala> 1 << 2 //0000 0000 0000 0000 0000 0000 0000 0001 向左位移后,为0000 0000 0000 0000 0000 0000 0000 0100,右侧用0填充
res40: Int = 4
1.17 对象相等性
基本类型比较:
scala> 1 == 2
res31: Boolean = false
scala> 1 != 2
res32: Boolean = true
scala> 2 == 2
res33: Boolean = true
这些操作对所有对象都起作用,而不仅仅是基本类型,如列表的比较:
scala> List(1, 2, 3) == List(1, 2, 3)
res34: Boolean = true
scala> List(1, 2, 3) == List(1, 3, 2)
res35: Boolean = false
还可以对不同类型进行比较,如:
scala> 1 == 1.0
res36: Boolean = true
scala> List(1, 2, 3) == "hello"
res37: Boolean = false
甚至可以与null进行比较:
scala> List(1, 2, 3) == null
res38: Boolean = false
== 操作符在比较之前,会先判断左侧的操作符是否为null,不为null时再调用左操作数的equals方法进行比较,比较的结果主要是看这个左操作数的equals方法是怎么实现的。只要比较的两者内容相同且并且equals方法是基于内容编写的,不同对象之间比较也可能为true。x == that判断表达式判断的过程实质如下:
if (x.eq(null))
that eq null
else
x.equals(that)
equals方法是检查值是否相等,而eq方法检查的是引用是否相等。所以如果使用 == 操作符比较两个对象时,如果左操作数是null,那么调用eq判断右操作数也是否为null;如果左操作数不是null的情况则调用equals基于对象内容进行比较
!= 就是 == 计算结果取反
Java中的==即可以比较原始类型,也可以比较引用类型。对于原始类型,Java的==比较值的相等性,与Scala一致,而对于引用类型,Java的==是比较这两个引用是否都指向了同一个对象,不过Scala比较对象地址不是 == 而是使用eq方法,所以Scala中的 == 操作符作用于对象时,会转换调用equals方法来比较对象的内容(在左操作数非null情况下)
AnyRef的equals方法默认调用eq方法实现,也就是说,默认情况下,判断两个变量相等,要求必须指向同一个对象实例
注:在Scala中,如果要基于引用地址进行比较,则要使用eq方法,而不是==,这与Java是不一样的
1.18 操作符优先级
Scala中没有操作符,操作符只是方法的一种表达方式,其优先级是根据作用符的第一个字符来判断的(也有例外,请看后面以等号 = 字符结束的一些操作符),如规定第一个字符*就比+的优先级高。以操作符的第一个字符为依据,优先级如下:
(所有其他的特殊字符)
* / %
+ -
:
= !
< >
&
^
|
(所有字母)
(所有赋值操作)
上面同一行的字符具有同样的优先级
scala> 2 << 2 + 2 // 2
<< (2 + 2)
res41: Int = 32
<<操作符第一个字符为<,根据上表 << 要比 + 优先级低
如果操作符以等号字符( =)结束 , 且操作符并非比较操作符<=, >=, ==,或=,那么这个操作符的优先级与赋值符( =)相同。也就是说,它比任何其他操作符的优先级都低。例如:
x *= y + 1
与下面的相同:
x *= (y + 1)
操作符 *= 以 = 结束,被当作赋值操作符,它的优先级低于+,尽管操作符的第一个字符是*看起来高于+。
任何以“:”字符结尾的方法由它的右操作数调用,并传入左操作数;其他结尾的方法与之相反,它们被左操作数调用,并传入右操作:a * b 变成 a.*(b), a:::b 变成 b.:::(a)。
多个同优先级操作符出现时,如果方法以:结尾,它们就被从右往左进行分组;反之,就从左往右进行分组,如:a ::: b ::: c 会被当作 a :::
(b ::: c),而 a * b * c 被当作(a * b) * c
1.19 基本类型富包装类型
基本类型除了一些常见的算术操作外,还有一些更为丰富的操作,这些操作可以直接使用,更多需要参考API中对应的富包装类:
上述相应的富操作都是由下面相应的富包装类提供的,使用前会先自动进行隐式转换:
1.20 Scala标识符
变量(本地变量、方法参数、成员字段)、或方法的定义名都是标识符。
标识符由字母、数字、操作符组成
Scala中有4种标识符:
字母数字标识符:以字母或下划线开始,后面可以跟字母、数字或下划线。注:$ 字符本身也是当作字母的,但被Scala编译器保留作为特殊标识符使用,所以用户定义的标识符中最好不要包含 $ 字符,尽管能够编译通过。另外,虽然下划线可以用来做为标识符,但同样也有很多其他非标识符用法,所以也最好避免在标识符中含有下划线
Java中常量习惯全大写,且单词之间使用下划线连接,但Scala里习惯第一个字母必须大写,其他还是驼峰形式
操作符标识符:由一个或多个操作符组成,操作符是一些如 +、:、?、~ 或 # 的可打印ASCII字符(精确的说,应该是除字母、数字、括号、方括号、花括号、单引号、双引号、下划线、句号、分号、冒号、回退字符\b),以下是一些操作符标识符:
+、++、:::、<?>、:->
Scala编译器内部会将操作符标识符转换成含有 $ 字符的Java标识符,如操作符标识符 :-> 将被编译器转换成相应Java标识符 $colon$minus$greater (colon:冒号,minus:负号,greater:大于),如果要从Java代码访问这个标识符,则应该使用这个转换后的标识符,而不是原始的
在Java里 x<-y 会被拆分成4个词汇符号,所以与 x < - y 一样,但在Scala里,<- 将被作为一个标识符,所以会被拆分成3个词汇,从而得到 x <- y,如果想要拆分成 < 与 – 的话,需要在 < 与 – 之间加上一个空格
混合标识符:由字母、数字组成,后面跟下划线和一个操作符标识符,如 unary_+ 被用做定义一元操作符 + 的方法名,myvar_= 被用做定义赋值操作符 = 的方法名(myvar_=是由编译器用来支持属性property的)
字面量标识符:是用反引 `...` 包括的任意字符串,如 `x` `<clinit>` `yield`,因为yield在Scala中是保留字,所以在Scala中不能直接调用java.lang.Thread.yield()(使当前线程从运行状态变为就绪状态),而是这样调用java.lang.Thread.`yield`()
2 示例:分数(有理数)运算
有理数是一个整数a和一个非零整数b的比,例如3/8,通则为a/b,又称作分数。
0也是有理数。有理数是整数和分数的集合,整数也可看做是分母为1的分数。
有理数的小数部分是有限或为无限循环的数。无理数的小数部分是无限不循环的数。
与浮点数相比较,有理数的优势是小数部分得到了完全表达,没有舍入或估算
下面设计分数这样的类:
class Rational(n: Int, d: Int) // 分子:n、分母:d
如果类没有主体,则可以省略掉花括号。括号里的n、d为类参数,并且编译器会创建带这两个参数的主构造器(有主就有从)
Scala编译器将把类的内部任何即不是字段也不是方法的定义代码编译到主构造器中,如:
class Rational(n: Int, d: Int) {
println("Created " + n + "/" + d)
}
scala> new Rational(1, 2)
Created 1/2
res0: Rational = Rational@90110a
Rational 类继承了定义在 java.lang.Object 类上的 toString方法,所以打印输出了“Rational@90110a”。重写toString方法:
class Rational(n: Int, d: Int) {
override def toString = n + "/" + d // 重写时override关键字不能省略,这与Java不一样
}
scala> val x = new Rational(1, 3)
x: Rational = 1/3
构造时,分母非0检查:
class Rational(n: Int, d: Int) {
require(d != 0) //此句会放入主构造器中
override def toString = n + "/" + d
}
require方法为scala包中Predef对象中定义的方法,编译器会自动引入到源文件中,所以可直接使用不需导入。传入的如果是false时,会抛java.lang.IllegalArgumentException异常,对象构造失败
实现add方法:
class Rational(n: Int, d: Int) {
require(d != 0)
override def toString = n + "/" + d
def add(that: Rational): Rational =
new Rational(n * that.d + d * that.n, d * that.d)
}
由于n、d只是类参数,在整个类范围内是可见(但因为只是类的参数,作用域比较广而已——相当于方法的参数来说,编译器是不会为这些类的参数自动创建出相应的成员字段的),但只能被调用它的对象访问,其他对象不能访问。代码中的that对象并不是调用add方法的对象,所以编译时出错:
所以需要将n、d转存到字段成员中才可访问:
class Rational(n: Int, d: Int) {
require(d != 0)
private val numer: Int = n //与Java一样,私有的也可以在同一类中的所有对象中访问
private val denom: Int = d
override def toString = n + "/" + d
// 1/2 + 2/3 = (1*3)/(2 *3) + (2*2)/(3*2)
def add(that: Rational): Rational =
new Rational(n * that.denom + d * that.numer, d * that.denom)
}
scala> val oneHalf = new Rational(1, 2)
oneHalf: Rational = 1/2
scala> val twoThirds = new Rational(2, 3)
twoThirds: Rational = 2/3
scala> oneHalf add twoThirds
res0: Rational = 7/6
比大小:
def lessThan(that: Rational) = numer * that.denom < that.numer * denom
返回较大的:
def max(that: Rational) = if (lessThan(that)) that else this //Scala 也使用 this 来引用当前对象本身
在定义类时,很多时候需要定义多个构造函数,在 Scala 中,除主构造函数之外的构造函数都称为辅助构造函数(或是从构造函数),Scala 定义辅助构造函数使用 this(…)的语法,所有辅助构造函数名称为 this。如当分母为1时,只需传入分子,分母固定为1,下面增加一个这样的从构造器:
def this(n: Int) = this(n, 1)
Scala 的从构造器以 def this(...) 定义形式开头。每个从构造器的第一个语句都是调用同类里的其他构造器,但最终都会以调用主构造器而结束(注:在类里面调用自身主构造器也是使用this(...)形式来调用的),这样使得每个构造函数最终都会调用主构造函数,因此主构造器是类的唯一入口点。在 Scala 中也只有主构造函数才能(会)去调用基类的构造函数
而在Java中,构造器的第一个语句只有两个选择:要么调用同类的其他构造器,要么直接调用父类的构造器,如果省略,则默认为super(),即默认会调用父类的无参默认构造器
加入分数最简形式,形如66/42可以简化为11/7,分子分母同时除以最大公约数6:
class Rational(n: Int, d: Int) {
//此句会放入主构造器中
require(d != 0)
//分子与分母的最大公约数。Scala也会根据成员变量出现的顺序依次初始化它们,所以一般在使用前定义并初始化它,虽然在Scala中可以将g定义在numer、denom后面,但这样易出错,这与Java不太一样
private val g = gcd(n.abs, d.abs)
private val numer: Int = n / g //与Java一样,私有的也可以在同一类中的所有对象中访问
private val denom: Int = d / g
//从构造器
def this(n: Int) = this(n, 1)//调用主构造器
override def toString = numer + "/" + denom
// 1/2 + 2/3 = (1*3)/(2 *3) + (2*2)/(3*2)
def add(that: Rational): Rational =
new Rational(numer * that.denom + denom * that.numer, denom * that.denom)
//求两个数的最大公约数
private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)
}
object T {
def main(args: Array[String]): Unit = {
println(new Rational(66, 42)) //11/7
val oneHalf = new Rational(1, 2)
val twoThirds = new Rational(2, 3)
println(oneHalf add twoThirds) //7/6
}
}
定义操作符:到目前为止,已实现了分数相加的方法add,但不能像Scala库里面数字类型那样使用 + 操作符来完成两个分数的相加,其实将add方法名改为 + 即可:
class Rational(n: Int, d: Int) {
//此句会放入主构造器中
require(d != 0)
//分子与分母的最大公约数
private val g = gcd(n.abs, d.abs)
private val numer: Int = n / g //与Java一样,私有的也可以在同一类中的所有对象中访问
private val denom: Int = d / g
//从构造器
def this(n: Int) = this(n, 1)
override def toString = numer + "/" + denom
// 1/2 + 2/3 = (1*3)/(2 *3) + (2*2)/(3*2)
def +(that: Rational): Rational =
new Rational(numer * that.denom + denom * that.numer, denom * that.denom)
//实现乘法
def *(that: Rational): Rational =
new Rational(numer * that.numer, denom * that.denom)
//求两个数的最大公约数
private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)
}
object T {
def main(args: Array[String]): Unit = {
val x = new Rational(1, 2)
val y = new Rational(2, 3)
println(x + x * y) //5/6
println((x + x) * y) //2/3
}
}
上面只是针对分数Rational进行加、乘运行,不能与Int进行运算,下面对这些方法进行重载,并加上减、除运算:
class Rational(n: Int, d: Int) {
//此句会放入主构造器中
require(d != 0)
//分子与分母的最大公约数
private val g = gcd(n.abs, d.abs)
private val numer: Int = n / g //与Java一样,私有的也可以在同一类中的所有对象中访问
private val denom: Int = d / g
//从构造器
def this(n: Int) = this(n, 1)
override def toString = numer + "/" + denom
//加: 1/2 + 2/3 = (1*3)/(2 *3) + (2*2)/(3*2)
def +(that: Rational): Rational =
new Rational(numer * that.denom + denom * that.numer, denom * that.denom)
def +(i: Int): Rational = new Rational(numer + i * denom, denom)//方法重载
//减: 1/2 - 2/3 = (1*3)/(2 *3) - (2*2)/(3*2)
def -(that: Rational): Rational = new Rational(numer * that.denom - that.numer * denom, denom * that.denom)
def -(i: Int): Rational = new Rational(numer - i * denom, denom) //方法重载
//乘:1/2 * 2/3 =(1*2)/(2*3)
def *(that: Rational): Rational =
new Rational(numer * that.numer, denom * that.denom)
def *(i: Int): Rational = new Rational(numer * i, denom) //方法重载
//除:1/2 / 2/3 =(1*3)/(2*2)
def /(that: Rational): Rational = new Rational(numer * that.denom, denom * that.numer)
def /(i: Int): Rational = new Rational(numer, denom * i) //方法重载
//求两个数的最大公约数
private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)
}
object T {
def main(args: Array[String]): Unit = {
println(new Rational(2, 3) * 2)// 4/3
}
}
上面只能使用 new Rational(2, 3) * 2 ,而不能倒过来 2 * new Rational(2, 3),因为Int类就没有针对 Rational类型进行运算的方法,Rational不是Scala的标准类型,如果想使用2 * new Rational(2, 3) 这种形式,则需要在使用前进行隐式转换,则Int类型转换为Rational类型:
//需要在使用之前定义从Int到Rational的隐式转换方法
implicit def int2Rational(x: Int) = new Rational(x)
println(2 * new Rational(2, 3)) // 4/3
增加这个隐式置换后,其实此时 Rational 的一些Int重载方法是多余的,因为第一个整型操作符已转换为Rational类型,且第二个操作符也是Rational类型,所以Int类型的重载方法就多余了
3 内置控制结构
和其它语言(比如 Java,C#)相比,Scala 只内置了为数不多的几种程序控制语句: if,while,for ,try,match 以及函数调用,Scala 没有内置很多控制结构,这是因为 Scala 赋予了程序员自己通过函数来扩展控制结构的能力
Scala的控制结构特点都是有返回值的,如果没有这种特点,程序员常常需要创建一个临时变量用来保存结果
3.1 If表达式
Java传统方式:
var filename = "default.txt"
if (!args.isEmpty)
filename = args(0)
Scala中写法:
val filename = if (!args.isEmpty) args(0) else "default.txt"
这段代码使用 val 而无需使用 var 类型的变量。使用 val 为函数式编程风格
3.2 While循环
//计算最大公约数:(6,9)= 3
def gcdLoop(x: Long, y: Long): Long = {
var a = x
var b = y
while (a != 0) {
val temp = a
a = b % a
b = temp
}
b
}
//从控制台循环读取输入行
var line = ""
do {
line = readLine()
println("Read: " + line)
} while (line != "")
while和do-while结构之所以称为“循环”,而不是表达式,是因为它们不能产生有意义的结果,循环结果返回结果的类型是Unit,写做()。()的存在是Scala的Unit不同于Java的void的地方:
//返回值为空
def greet() { println("hi") }
def main(args: Array[String]): Unit = {
println(greet() == ())//hi true
}
另外,在Java等编程语言中,赋值语句本身会返回被赋予的那值:
String line = "";
System.out.println(line = "hello");// hello
但在Scala中,赋值语句本身不会再返回被赋予的那值,而是Unit:
var line = ""
println(line = "ho") // ()
所以下面从控制台读取将永远不能结束:
var line = ""
while ((line = readLine()) != "") // 不起作用,因为赋值语句固定返回为Unit()
println("Read: " + line)
由于while循环不产生值,因此在纯函数式语言中不推荐使用,它适合于传统指令式编程,而使用递归的函数式风格可以替代while。下面使用这种递归的函数式风格来代替上面使用while指令式风格求最大公约数:
//计算最大公约数,使用递归函数实现
def gcd(x: Long, y: Long): Long =
if (y == 0) x else gcd(y, x % y)
3.3 For表达式
3.3.1 集合枚举
枚举当前目录下所有文件,Java传统做法:
File[] filesHere = new java.io.File(".").listFiles();
for (int i = 0; i < filesHere.length; i++) {
System.out.println(filesHere[i]);
}
Scala枚举当前目录下的所有文件(包括目录):
val filesHere = new java.io.File(".").listFiles
for (file <- filesHere)
println(file)
<–为提取符,提取集合中的元素
Java1.5以后也有类似的语法:
File[] filesHere = new java.io.File(".").listFiles();
for (File file : filesHere) {
System.out.println(file);
}
Scala 的 for 表达式支持所有类型的集合类型,而不仅仅是数组,比如下面使用 for 表达式来枚举一个 Range 类型:
for (i <- 1 to 4)
println("Iteration " + i)
for (i <- 1 until 4)//不包括边界4
println("Iteration " + i)
val filesHere = new java.io.File(".").listFiles
//也可按传统方式通过索引遍历数组元数,但不推荐这样使用
for (i <- 0 to filesHere.length - 1)
println(filesHere(i))
3.3.2 If守卫
在for语句中添加if过滤器:
val filesHere = (new java.io.File(".")).listFiles
//只打印出后缀名为 .project 的文件
for (file <- filesHere if file.getName.endsWith(".project"))
println(file)
也可以将if语句拿出来写在循环体中,但不推荐:
for (file <- filesHere)
if (file.getName.endsWith(".project"))
println(file)
可以添加多个过滤器:
for (file <- filesHere if file.isFile if file.getName.endsWith(".project"))
println(file)
3.3.3 嵌套枚举
for语句中可以使用多个 <- 提取符形成嵌套循环。
下面外层循环是针对扩展名为.classpath的文件进行循环,然后读取每个文件中含有con字符的文本行:
val filesHere = (new java.io.File(".")).listFiles
def fileLines(file: java.io.File) =
scala.io.Source.fromFile(file).getLines.toList
def grep(pattern: String) =
for (
file <- filesHere if file.getName.endsWith(".classpath"); //循环文件
line <- fileLines(file) if line.trim.matches(pattern) //循环文本行
) println(file + ": " + line.trim)
grep(".*con.*")
注:嵌套之间要使用分号分隔,不过可以使用花括号来代替小括号,此时嵌套之间的分号就可以省略了:
def grep(pattern: String) =
for {
file <- filesHere if file.getName.endsWith(".classpath")
line <- fileLines(file) if line.trim.matches(pattern)
} println(file + ": " + line.trim)
3.3.4 变量中间绑定
如果某个方法多次用,可以将其先赋给某个val变量,这样只需计算一次,如上面两处调用line.trim:
def grep(pattern: String) =
for {
file <- filesHere if file.getName.endsWith(".classpath")
line <- fileLines(file)
trimmed = line.trim //结果临时保存在 trimmed 变量,可以定义val中间变量,val关键字可以省略
if trimmed.matches(pattern)
} println(file + ": " + trimmed)
3.3.5 yield生成新的集合
for clauses yield body
关键字 yield 放在 body(这里的body指单条语句)的前面,for 每迭代一次,产生一个 body,yield 收集所有的 body 结果,返回一个 body 类型的集合(一般情况下,当返回的元素类型与源集合中的元素类型相同时,则返回的集合类型与源集合类型相同;如果返回的元素类型与源集合元素类型不同时,则两集合类型则可能不同,请看下面代码)。如列出所有名为 .scala 结尾的文件,返回这些文件的集合(scalaFiles: Array[java.io.File]):
val filesHere = (new java.io.File(".")).listFiles // filesHere: Array[java.io.File]
def scalaFiles = for {file <- filesHere if file.getName.endsWith(".scala")} yield file //
scalaFiles: Array[java.io.File]
yield与for一样是关键字,它应该放在循环体最前面,如果循环体有多个语句时,可以使用花括号包起来,yield放在花括号外:
for {子句} yield {循环体}
scala> val arr = Array("a", "b")
arr: Array[String] = Array(a, b)
scala> val arr2 = for (e <- arr) yield e.length
arr2: Array[Int] = Array(1, 1) // 注:返回的类型还是Array,但里面元素的类型变了
scala> val map = Map(1 -> "a", 2 -> "b")
map: scala.collection.immutable.Map[Int,String] = Map(1 -> a, 2 -> b)
scala> val map2 = for ((k, v) <- map) yield v.length
map2: scala.collection.immutable.Iterable[Int] = List(1, 1) // 注:这里返回的是List类型,不是Array,也不是Map,因为Map变长,所以返回的List
scala> val map3 = for ((k, v) <- map) yield (k, v)
map3: scala.collection.immutable.Map[Int,String] = Map(1 -> a, 2 -> b)
3.4 try
val half =
if (n % 2 == 0) n / 2
else
throw new RuntimeException("n must be even")
尽管throw不实际产生任何值,你还是可以把它当作表达式,throw语句返回的类型为Nothing:
scala> def t = throw new RuntimeException("n must be even")
t: Nothing
if 的一个分支计算值,另一个抛出异常并得出 Nothing,整个 if 表达式的类型就是那个实际计算值的分支的类型,所以上面的half类型为Int,因为Nothing是任何类型的子类型,所以整个if表达式的类型为父类型Int
import java.io.FileReader
import java.io.FileNotFoundException
import java.io.IOException
var f: FileReader = null
try {
f = new FileReader("input.txt")
// Use and close file
} catch {//如果打开文件出现异常,将先检查是否是 FileNotFoundException 异常,如果不是,再检查是否是 IOException,如果还不是,在终止 try-catch 块的运行,而向上传递这个异常
case ex: FileNotFoundException =>
// 文件不存在捕获后在此处理异常
// ...
case ex: IOException =>
// 其它 I/O 错误捕获后在此处理异常
// ...
} finally { // 与Java一样,不管 try 块是否抛出异常,finally块都会执行
f.close() // 文件一定会被关闭
}
注:与 Java 异常处理不同的一点是,Scala 不需要你捕获 checked 的异常,所以下面语句虽然要抛出检测异常FileNotFoundException,但不需要使用try-catch 块来包围,这在Java中是不行的:
var f = new FileReader("input.txt")
和其它大多数Scala控制结构一样,try-catch-finally也产生值(Scala的行为与Java的差别仅源于Java的try-finally不产生值),比如下面的例子尝试分析一个 URL,如果输入的 URL 无效,则使用缺省的 URL 链接地址:
import java.net.URL
import java.net.MalformedURLException
def urlFor(path: String) =
try {
new URL(path)
} catch {
case e: MalformedURLException =>
new URL("http://www.scalalang.org")//缺省的 URL
}
如果抛异常但未捕获异常,则表达式没有值。
finally子句当使用return显示的返回时,这个值将覆盖 try-catch 产生的结果:
def f(): Int = try { return 1 } finally { return 2 }
println(f()) //2 结果会被返回
否则即使finally块最后一句有值,也会被抛弃:
def g(): Int = try { 1 } finally { 2 }
println(g()) //1 结果会被抛弃
正是因为这样容易弄混,所以finally子句不要返回值,而只作如关闭资源、清理之类的工作
3.5 match
类似Java中的switch,从多个选择中选取其一。match 表达式支持任意的匹配模式
val firstArg = if (args.length > 0) args(0) else ""
firstArg match {
case "salt" => println("pepper")
case "chips" => println("salsa")
case "eggs" => println("bacon")
case _ => println("huh?")
}
_下划线表示其它,类似Java中的default
不像Java那样,firstArg可以是任何类型,而不只是整型或枚举,示例中是字符串。另外,每个可选项最后并没有break,隐含就有
match表达式还可以产生值:
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)
3.6 break、continue
Scala 内置控制结构特地去掉了 break 和 continue
如从一组字符串中寻找以“ .scala ”结尾的字符串,但跳过以“-”开头的字符串,Java中可以这样实现:
int i = 0;
boolean foundIt = false;
while (i < args.length) {
if (args[i].startsWith("-")) {
i = i + 1;
continue;
}
if (args[i].endsWith(".scala")) {
foundIt = true;
break;
}
i = i + 1;
}
完成可以这样,通过调代码结构可以去掉它们:
var i = 0
var foundIt = false
while (i < args.length && !foundIt) {
if (!args(i).startsWith("-")) {
if (args(i).endsWith(".scala"))
foundIt = true
}
i = i + 1
}
另外,在Scala中完使用可以递归函数来代替循环,下面使用递归实现上面同样功能:
def searchFrom(i: Int): Int =
if (i >= args.length) -1
else if (args(i).startsWith("-")) searchFrom(i + 1)
else if (args(i).endsWith(".scala")) i
else searchFrom(i + 1)
val i = searchFrom(0)
在函数化编程中使用递归函数来实现循环是非常常见的一种方法,我们应用熟悉使用递归函数的用法
如果你实在还是希望使用 break,Scala 在 scala.util.control 包中定义了 break 控制结构,它的实现是通过抛出异常给上级调用函数。下面给出使用 break 的一个例子,不停的从屏幕读取一个非空行,如果用户输入一个空行,则退出循环:
import scala.util.control.Breaks._
import java.io._
val in = new BufferedReader(new InputStreamReader(System.in))
breakable {//breakable是带了一个传名参数的方法
while (true) {
println("? ")
if (in.readLine() == "") break //break也是一个方法,会抛异常BreakControl
}
}
3.7 变量作用域
Scala允许你在嵌套范围内定义同名变量
大括号通常引入了一个新的范围,所以任何定义在花括号里的东西在括号之后就脱离了范围(但有个例外,因为嵌套for语句可以使用花括号来代替小括号,所以此种除外,可参见这里的中间变量trimmed):
def printMultiTable() {
var i = 1
// 这里只有i在范围内
while (i <= 10) {
var j = 1
// 这里i和j在范围内
while (j <= 10) {
val prod = (i * j).toString
// 这里i,j和prod在范围内
var k = prod.length
// 这里i,j,prod和k在范围内
while (k < 4) {
print(" ")
k += 1
}
print(prod)
j += 1
}
// i和j仍在范围内;prod和k脱离范围
println()
i += 1
}
// i仍在范围内;j,prod和k脱离范围
}
然而,你可以在一个内部范围内定义与外部范围里名称相同的变量:
val a = 1; //在这里要加上分号,因为此处不能推导
{
val a = 2
println(a) //2
}
println(a) //1
与Scala不同,Java不允许你在内部范围内创建与外部范围变量同名的变量。在Scala程序里,内部变量隐藏掉同名的外部变量,因此在内部范围内外部变量变得不可见
4 Scala方法和函数的区别
使用val(或var)语句可以定义函数,def语句定义方法:
class T{
def m(x: Int) = x + 3 //定义方法,m将是类T的成员方法
val f = (x: Int) => x + 3 //定义函数, f将是类T的成员字段
}
函数类型形如(T1, ..., Tn) => T(注意与传名参数类型区别:p: => T,p是参数名,当冒号后面无参,将无参空括号去掉就是传名参数了;如果参数定义成p:() => T,则是参数是函数类型,而非传名参数了),函数都实现了FuctionN(N[0..22])特质trait的对象,所以函数具有一些方法如equals、toString等,而方法不具有这些特性:
def m(x: Int) = x + 3
var f = (x: Int) => x + 3
// m.toString //编译失败
f.toString //编译通过
如果想把方法转换成一个函数,可以用方法名跟上下划线的方式:
def m(x: Int) = x + 3
(m _).toString//编译通过
通常在使用一个函数时是将赋值给函数变量或者是通过函数类型的参数传递给方法,函数的调用跟方法一样,也是在函数对象(值)后面接小括号进行函数的调用,在Java是不是允许在对象后面接小括号的(只能在方法名后面接小括号),这正因为apply是scala中的语法糖:可以在一个对象obj后面带上括号与参数(也可无参数),如obj(x,y),scala编译器会将obj(x,y)转换为obj.apply(x,y)方法的调用;而在一个类clazz上调用clazz(),scala编译器会转换为“类的伴生对象.apply()”(一般是工厂方法):
函数的调用必须通过后面接上括号,否则表示函数对象本身;而方法的调用可以就是方法名,而不需要接空括号
有两种方法可以将方法转换成函数:
val f1 = m _
在方法名称m后面紧跟一个空格和下划线告诉编译器将方法m转换成函数。也可以显示地告诉编译器需要将方法转换成函数:
val f1: (Int) => Int = m
通常情况下编译器会自动将方法转换成函数,例如在一个应该传入函数参数(即参数类型为函数)的地方传入了一个方法,编译器会自动将传入的方法转换成函数
将方法转换为函数的时候,如果方法有重载的情况,必须指定参数和返回值的类型:
scala> object Tool{
| def increment(n: Int): Int = n + 1
| def increment(n: Int, step: Int): Int = n + step
| }
scala> val fun = Tool.increment _
<console>:12: error: ambiguous reference to overloaded definition,
scala> val fun1 = Tool.increment _ : (Int => Int)
fun1: Int => Int = <function1>
scala> val fun2 = Tool.increment _ : ((Int, Int) => Int)
fun2: (Int, Int) => Int = <function2>
对于一个无参数的方法可以省略掉空括号,而无参函数是带空括号的:
scala> def x = println("Hi scala")//无参方法可省略掉空括号
x: Unit
scala> def x() = println("Hi scala")
x: ()Unit
scala> val y = x _
y: () => Unit = <function0> //无参函数类型是带空括号的
scala> y()
Hi scala
scala> x
Hi scala
方法是支持参数默认值的用法,但是函数会忽略参数默认值的,所以函数不能省略参数:
scala> def exec(s: String, n: Int = 2) = s * n
exec: (s: String, n: Int)String
scala> exec("Hi") //第二个参数使用了默认值
res0: String = HiHi
scala> val fun = exec _
fun: (String, Int) => String = <function2>
scala> fun("Hi") //无法使用默认值,不能省略参数
<console>:14: error: not enough arguments for method apply
scala> fun("Hi", 2) //必须设置所有参数
res2: String = HiHi
柯里化Currying函数可以只传入部分参数返回一个偏应用函数,而柯里化方法在转换成偏应用函数时需要加上显式说明,让编译器完成转换:
object TestCurrying {
def invoke(f: Int => Int => Int): Int = {//f是柯里化函数
f(1)(2)
}
def multiply(x: Int)(y: Int): Int = x * y // multiply是柯里化方法
def main(args: Array[String]) {
invoke(multiply) //编译器会自动将multiply方法转换成函数
// val partial1 = multiply(1) //multiply(1)相当于第二个方法的方法名,所以不能将方法赋值给变量
val partial2 = multiply(1):(Int => Int) //编译通过,且等效下面两个
val partial4 = multiply(1)_ // partial4的类型为 Int=>Int
val partial5: Int => Int = multiply(1)
val f = multiply _ //将multiply方法转换成柯里化函数f,f的类型为 Int=>(Int=>Int)
val partial3 = f(1) //只应用第1个参数返回函数,编译通过, partial3的类型为 Int=>Int
}
}
5 函数和闭包
5.1 方法
定义函数最通用的方法是作为某个对象的成员。这种函数被称为方法: method
//公有方法
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)
}
上面使用的是通常面向对象的编程方式
5.2 本地(内嵌、局部)函数
def processFile(filename: String, width: Int) {
def processLine(filename: String, width: Int, line: String) {//局部函数,只能在processFile方法(函数)中使用
if (line.length > width) print(filename + ": " + line)
}
val source = Source.fromFile(filename)
for (line <- source.getLines) {
processLine(filename, width, line)
}
}
本地函数可以直接访问所在外层函数的参数,所以上面可以改成:
def processFile(filename: String, width: Int) {
def processLine(line: String) {
if (line.length > width) print(filename + ": " + line)
}
val source = Source.fromFile(filename)
for (line <- source.getLines)
processLine(line)
}
5.3 函数字面量
你可以把函数写在一个没有名字的函数字面量(匿名字面量函数),并且可以把它当成一个值传递到其它函数,或赋值给其它变量
下面的例子为一个简单的函数字面量:
scala> (x: Int) => x + 1
res0: Int => Int = <function1> //res0为函数变量
=>表示这个函数将符号左边的东西(本例为一个整数),转换成符号右边的东西(加 1),=>符号左边是函数的参数,右边是函数体
函数字面量会被编译成类(实现了AbstractFunctionN抽象类的类),并在运行期实例化成函数值(即函数对象)。因此函数字面量和函数值的区别在于函数字面量存在于源代码中,而函数值则作为对象存在于运行期,这个区别很像类(源代码)与对象(运行期)之间的关系
任何函数值都是scala包中的FunctionN特质(trait)的一个实例,如不带参数的函数值是Function0特质的实例,带一个参数的函数值是Function1特质的实例等等:
scala> var inc = (x: Int) => x + 1
inc: Int => Int = <function1>
scala> inc.isInstanceOf[Function1[Int,Int]]
res8: Boolean = true
每个FunctionN特质都有一个apply方法,运行时实质上是由该方法来调用函数的
可以将函数字面量赋给一个函数类型的变量,并且可以参数变量调用:
scala> var increase = (x: Int) => x + 1
increase: (Int) => Int =
<function1> //
函数返回值为Int,函数体最后一条语句即返回值
scala> increase(10)
res0: Int = 11
由于函数字面量在编译时会被编译成相应的类以及实例化成相应的函数值对象,下面通过类的方式来实现上面函数字面量“(x: Int) => x + 1”所能自动化实现的过程:
//自定义类名为Increase的函数类
class Increase extends Function1[Int, Int] {
def apply(x: Int): Int = x + 2 //这里试着加2以示区别,加几并不重要
}
object T {
//创建匿名函数实例对象,匿名函数可以直接从Function1继承,并实现apply方法
var increase: Function1[Int, Int] = new Function1[Int, Int] {
def apply(x: Int): Int = x + 1
}
def main(args: Array[String]): Unit = {
println(increase(10)) //11。 变量后面可带括号参数,是因为该对象定义了相应的apply方法,increase(10) 等价于 increase.apply(10)
increase = new Increase()
println(increase(10)) //12
}
}
函数体有多条语句时,使用大括号:
scala> increase = (x: Int) => {
println("We")
println("are")
println("here!")
x + 1 // 函数返回值为Int,函数体最后一条语句即返回值
}
increase: (Int) => Int =
<function1>
scala> increase(10)
We
are
here!
res4: Int = 11
Iterable特质是 List, Set, Array,还有 Map 的共同超类,foreach 方法就定义在其中,它可以用来针对集合中的每个元素应用某个函数,即foreach方法参数允许传入函数:
scala> val someNumbers = List(-11, -10, -5, 0, 5, 10)
someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)
scala> someNumbers.foreach((x: Int) =>
println(x)) // 只是简单的打印每个元素
-11
-10
-5
0
5
10
集合中还有filter函数也是可以传递一个函数的,用来过滤元素,传递进去的函数要求返回Boolean:
scala> someNumbers.filter((x: Int) => x
> 0)
res6: List[Int] = List(5, 10)
5.4 简化函数字面量
去除参数类型,以及外面的括号:
someNumbers.filter((x: Int) => x
> 0)
因数函数是应用于集合中元素,所以会根据集合元素类型来推导参数类型。
5.5 占位符_
下划线“_”来替代一个或多个参数,只要某个参数只在函数体里出现一次,则可以使用下划线 _ 来替换这个参数:
scala> someNumbers.filter(_ > 0)
res9: List[Int] = List(5, 10)
_ > 0 相当于x => x > 0,遍历时会使用当前相应元素来替换下划线(你可以这样来理解,就像我们以前做过的填空题,“_”为要填的空,Scala 来完成这个填空题,你来定义填空题)
有多少个下划线,则就表示有多少个不同的参数。多个占位符时,第一个下划线表示第一个参数,第二个下划线表示第二个参数,以此类推;所以同一参数多处出现时是无法使用这种占位符来表示的。
使用占位符时,有时无法推导出类型,如:
scala> val f = _ + _
此时需明确写出类型:
scala> val f = (_: Int) + (_: Int)
f: (Int, Int) => Int = <function2>
scala> def sum = (_:Int) + (_:Int) + (_:Int) //注:这里的下划线不是偏应用,它是函数字面量的占位符简化,该函数字面量为sum方法体最后一个语句,所以该方法返回值类型为函数
sum: (Int, Int, Int) => Int // 方法由三部分组成:方法名(这里为sum)+ 参数列表(这里没有,也不带空括号)+ 返回类型(这里为函数值类型(Int, Int, Int) => Int)
scala> sum //调用无参无空括号方法。由于参数为空,定义时去掉了空括号,所以调用时不能带空括号
res0: (Int, Int, Int) => Int =<function3> //返回的是函数字面量
scala> sum (1,2,3) //由于sum方法定义成了无参无空括号的方法,所以单独的语句 sum 就表示对sum方法进行了一次调用,而sum后面的(1,2,3)则是对函数值进行再一次调用
res1: Int = 6
5.6 偏应用函数
偏应用函数(Partial Applied Function)是指在调用函数时,有意缺少部分参数的函数。
前面的例子下划线 _ 代替的只是一个参数,实际上你还可以用“_”来代替整个参数列表(有多少个参数,则代表多少个参数),如println(_) ,或更简洁println _ ,或干脆println :
someNumbers.foreach(println _)
Scala 编译器自动将上面代码解释成:
someNumbers.foreach(x => println(x))
(注:下面的 “println _” 却又是返回的是无参数的偏应用函数,Why?因为println函数本身就有不带参数的形式,又由于这里没有信息指引f函数变量要带参数,所以 “_”就优先代表了0参数列表,所以f函数变量最终是一个无参数的函数变量。而上面的List.foreach(println _)中,由于foreach方法要求是带一个参数的函数,所以此时的“_”就去匹配一个参数列表的println函数
scala> val f = println _
f: () => Unit = <function0>
如果要带参数,这样才可以带一个参数:
scala> val f = (x:String)=>println(x)
f: String => Unit = <function1>
)
这个例子的下划线不是单个参数的占位符,而是整个参数列表的占位符(虽然示例中是只带有一个参数的println函数)
由于someNumbers.foreach方法要求传入的就是函数,所以此时下划线也可以直接省略,更简洁的写法:
someNumbers.foreach(println)
注:只能在需要传入函数的地方去掉下划线,其他地方不能,如后面的sum函数:
scala> val c = sum
<console>:12: error: missing argument list for method sum
Unapplied methods are only converted to functions when a function type is expected.
You can make this conversion explicit by writing `sum _` or `sum(_,_,_)` instead of `sum`.
val c = sum
^
以上在调用方法时,使用下划线“_”来代替方法参数列表(而不是传入具体参数值),这时你就是正在写一个偏应用函数(Partially applied functions)
在 Scala 中,当你调用函数传入所需参数时,你就把函数“应用”到参数,比如一个加法函数:
scala> def sum(a: Int, b: Int, c: Int) = a + b
+ c
sum: (Int,Int,Int)Int
你就可以把函数 sum 应用到参数 1, 2 和 3 上,如下:
scala> sum(1, 2, 3)
res12: Int = 6
一个偏应用函数指的是你在调用函数时,不指定函数所需的所有参数(或只提供部分,或不提供任何参数),这样你就创建了一个新的函数,这个新的函数就称为原始函数的偏应用函数,如:
scala> val a = sum _ //
将sum方法转换为偏应用函数后赋值给名为a的函数变量
a: (Int, Int, Int) => Int = <function3>
scala> a(1, 2, 3)
res13: Int = 6
scala> var b = sum(1,2,3); //如果在定义时就传入了具体值,则返回的就是具体的值了,此时b变量是Int变量,而非函数变量
b: Int = 6
上面的过程是这样的:名为a的变量指向了一个函数值对象,这个函数值是由Scala编译器依照偏应用函数表达式sum _ 自动产生的类的一个实例。且这个类有一个带3个参数的apply方法,编译器会将a(1,2,3) 表达式翻译成对函数值的apply方法的调用。因此a(1, 2, 3)实质为:
scala> a.apply(1, 2, 3)
res14: Int = 6
这种由下划线代替整个参数列表的一个重要的用途就是:可以将def定义的方法转换为偏应用函数,尽管不能直接将def定义的方法或嵌套函数赋值给函数变量,或当做参数传递给其它的方法,但是如果把方法或嵌套函数通过在名称后面加一个下划线的方式转换为函数后,就可以做到了
偏应用函数还可以部分指定参数,如:
scala> val b = sum(1, _: Int, 3) //变量 b 的类型为函数,是由 sum方法应用了第一个和第三个参数后构成的
b: (Int) => Int = <function1>
只缺少中间一个参数,所以编译器会产生一个新的函数类,其 apply 方法带一个参数,所以调用b函数变量时只能传入一个:
scala> b(2)
res15: Int = 6
此时,b(2)实质上是对函数值的apply方法调用,即b.apply(2),而b.apply(2)再去调用sum(1,2,3)
5.7 闭包
函数字面量在运行时创建的函数值(对象)称为闭包
scala> var more = 1
more: Int = 1
scala> val addMore = (x: Int) => x
+ more // 函数值赋值给addMorey,该函数值就是一个闭包
addMore: (Int) => Int = <function1>
scala> addMore(10)
res19: Int = 11
在闭包创建之后,闭包之外的变量more修改后,闭包中的引用也会根着变化,因此 Scala 的闭包捕获的是变量本身而不是当时变量的值:
scala> more = 9999
more: Int = 9999
scala> addMore(10)
res21: Int = 10009
上面是闭包之外的变量修改会影响闭包中相应变量,同样,在闭包中修改闭包外的变量,则闭包外的变量也会跟着变化:
scala> val someNumbers = List(-11, -10, -5, 0, 5, 10)
someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)
scala> var sum = 0
sum: Int = 0
scala> someNumbers.foreach(sum += _) //在闭包中修改了闭包外的变量,外部变量也会跟着变化
scala> sum
res23: Int = -11
示例中的someNumbers.foreach(sum += _)语句中的 sum += _ 就是一个函数字面量,相当于 x => sum += x,具体参考前面的函数字面量与占位符
scala> var increase = (x: Int) => x
+ 1
increase: (Int) => Int = <function1>// 变量由两部分组成:变量名(这里为increase)+
类型(这里为函数值类型 (Int) => Int =
<function1> )
scala> def makeIncreaser(more: Int) = (x: Int) => x + more //这里的more 相当于闭包参数,要在调用时才能确定
makeIncreaser: (more: Int) Int => Int //方法由三部分组成:方法名(这里为makeIncreaser)+ 参数列表(这里为(more: Int))+ 返回类型(这里为函数值类型 Int => Int )
scala> val inc1 = makeIncreaser(1) //调用时确定闭包参数more为1,且返回函数值,并赋给inc1函数变量
inc1: Int => Int = <function1>
scala> val inc9999 =
makeIncreaser(9999)
inc9999: (Int) => Int = <function1>
上面每次makeIncreaser函数调用时都会产生一个闭包,且每个闭包都会有自己的more变量(即调用时传入的值)。
下面才开始真正调用函数字面量,且各自有自己的闭包参数more:
scala> inc1(10) //闭包参数more值为1
res24: Int = 11
scala> inc9999(10) //闭包参数more值为9999
res25: Int = 10009
5.8 可变参数
如果参数列表后面的参数类型都一样,可以使用*来代表参数列表,下面代表0个或多个String类型的参数,参数会存放到String类型的args数组中,即args类型为Array[String]:
scala> def echo(args: String*) =
for (arg <- args) println(arg)
echo: (String*)Unit
scala> echo()
scala> echo("one")
one
scala> echo("hello", "world!")
hello
world!
在函数内部,变长参数的类型,实际为一数组,比如上例的 String * 类型实际为 Array[String],然而,如今你试图直接传入一个数组类型的参数给这个参数,编译器会报错:
scala> val arr = Array("What's", "up", "doc?")
arr: Array[java.lang.String] = Array(What's, up, doc?)
scala> echo(arr)
<console>:7: error: type mismatch;
found :
Array[java.lang.String]
required: String
echo(arr)
ˆ
但你可以通过在变量后面添加一个冒号 : 和一个 _* 符号,这个符号告诉 Scala 编译器在传递参数时逐个传入数组的每个元素,而不是数组整体:
scala> echo(arr: _*)
What's
up
doc?
注:可变参数只能是参数列表中的最后一个
5.9 命名参数
通常情况下,调用函数时,参数传入和函数定义时参数列表一一对应。
scala> def speed(distance: Float, time:Float) :Float = distance/time
speed: (distance: Float, time: Float)Float
scala> speed(100,10)
res0: Float = 10.0
使用命名参数允许你使用任意顺序传入参数,比如下面的调用:
scala> speed( time=10,distance=100)
res1: Float = 10.0
scala> speed(distance=100,time=10)
res2: Float = 10.0
5.10 缺省参数值
Scala 在定义函数时,允许指定参数的缺省值,从而允许在调用函数时不传该参数,此时该参数使用缺省值。缺省参数通常配合命名参数使用,例如:
scala> def printTime(out:java.io.PrintStream = Console.out, divisor:Int =1 ) =
out.println("time = " + System.currentTimeMillis()/divisor)
printTime: (out: java.io.PrintStream, divisor: Int)Unit
scala> printTime()
time = 1383220409463
scala> printTime(divisor=1000)
time = 1383220422
5.11 尾(伪)递归
可以使用递归函数来消除需要使用 var 变量的 while 循环
def approximate(guess: Double): Double =
if (isGoodEnough(guess))
guess //该数已经足够好了,直接返回结果
else approximate(improve(guess)) //还不是最好,需继续改进
像上面,结尾是调用自己,这样的递归为尾递归
由于递归会产生堆栈调用而影响性能,所以你可能将递归修改为传递的While结构,如将上面的代码改进如下:
def approximateLoop(initialGuess: Double): Double = {
var guess = initialGuess
while (!isGoodEnough(guess))
guess = improve(guess)
guess
}
那么这两种实现哪一种更可取呢? 从简洁度和避免使用 var 变量上看,使用递归比较好。但依照以前经验递归比while循环慢,但实际上,经测试这两种方法所需时间几乎相同,Why?
其实,对于 approximate 的递归实现,Scala 编译器会做些优化,因为这里 approximate 的实现,最后一行是调用 approximate 本身,我们把这种递归叫做尾递归。Scala 编译器检测到尾递归时会自动使用循环来代替,因此,你应该还是多使用递归函数来解决问题,如果是尾递归,那么在效率上是不会有什么损失的
尾递归函数在每次调用不会构造一个新的调用栈。所有递归其实都在同一个执行栈中运行,而是Scala会使用While结构来优化这种递归
如下面的调用不是尾递归调用,因为最后一句虽然调用了自己,但在调用自己后,还进了增1操作:
scala> def boom(x: Int): Int ={
if (x == 0) throw
new Exception("boom!")
else boom(x - 1) + 1}
scala> boom(3)
java.lang.Exception: boom!
at .boom(<console>:5)
at .boom(<console>:6)
at .boom(<console>:6)
at .boom(<console>:6)
at .<init>(<console>:6)
...
从上面调用堆栈来看,boom函数是真正递归调用了多次(boom函数被调用了多次),所以不是尾递归。将上面的加一去掉后,才是尾递归调用,测试如下:
scala> def bang(x: Int): Int ={
if (x == 0) throw
new Exception("bang!")
else bang(x - 1)}
scala> bang(5)
java.lang.Exception: bang!
at .bang(<console>:5)
at .<init>(<console>:6)
...
从上面可以看出,函数bang只被调用了一次,即没有被递归调用,所以是尾递归
注:尾递归只在函数体最后一句直接调用函数本身,才能形成尾递归,其它任何情况下的间接调用则不会形成尾递归,如下面的间接调用不会形成尾递归:
def isEven(x: Int): Boolean = if (x == 0) true else isOdd(x - 1)
def isOdd(x: Int): Boolean = if (x == 0) false else isEven(x - 1)
虽然isEven 和 isOdd 都是在最后一句调用,它们是两个互相递归的函数,scala 编译器无法对这种递归进行优化,另外下面也不会形成尾递归:
val funValue = nestedFun _ //使用偏应用表达式将方法转换为函数值
def nestedFun(x: Int) {
if (x != 0) { println(x); funValue(x - 1) }
}
6 控制抽象
Scala 没有内置很多控制结构,这是因为 Scala 赋予了程序员自己通过函数扩展控制结构的能力
6.1 函数封装变化
如果方法中的某段逻辑是变化的,可以将这段逻辑封装在一个函数里,然后通过方法参数将该函数值传进去,这样就可以将方法中变化的逻辑剥离出来(使用Java中的接口也可以将变化封装起来)
比如下面实现一个过滤文件的方法,但过滤的算法是各种各样的,所以将过滤算法封装在函数里,然后在具体过滤时通过matcher函数类型参数传递过去:
object FileMatcher {
private def filesHere = (new java.io.File(".")).listFiles
//由于匹配的逻辑是变化的,所以将匹配的逻辑封装在函数里通过matcher参数传递进来,matcher参数类型中有=>,表示函数,该函数接收两个String类型参数,且返回布尔类型值
def filesMatching(query: String, matcher: (String, String) => Boolean) = {//此时的matcher函数带有两个参数
for (file <- filesHere; if matcher(file.getName, query)) //过滤出只需要的文件,但怎么过滤通过matcher传递进来
yield file
}
//然后这样使用:
def filesEnding(query: String) = filesMatching(query, _.endsWith(_)) //返回以query结尾的文件名
def filesContaining(query: String) = filesMatching(query, _.contains(_))//返回包含了query的文件名
def filesRegex(query: String) = filesMatching(query, _.matches(_)) //返回匹配query的文件名
}
这些调用用到了函数字面量占位符号法:
_.endsWith(_)相当于 (fileName: String, query: String) => fileName.endsWith(query) ,甚至可以省略参数的类型:(fileName, query) => fileName.endsWith(query) :
def filesEnding(query: String) = filesMatching(query, (fileName: String, query: String) => fileName.endsWith(query))
由于第一个参数fileName在函数字面量体中第一个位置被使用,第二个参数query在第二个位置中使用,所以你可以使用占位符语法来简化:_.endsWith(_),所以出现上面简洁写法
上面示例中 query传递给了 filesMatching,但 filesMatching方法中并没有直接使用它,而又是直接把它传给了matcher 函数,所以这个传来传去的过程不是必需的,因此可以将filesMatching方法 和 matcher 函数中的参数 query 去掉,而是在函数字面量体中直接使用闭包参数query(正是因为闭包才可以省去query参数的传递):
object FileMatcher2 {
private def filesHere = (new java.io.File(".")).listFiles
def filesMatching(matcher: String => Boolean) = {//此时的matcher函数只有一个参数
for (file <- filesHere; if matcher(file.getName))
yield file
}
def filesEnding(query: String) = filesMatching((fileName) => fileName.endsWith(query)) // 直接使用闭包参数query
def filesContaining(query: String) = filesMatching(_.contains(query))
def filesRegex(query: String) = filesMatching(_.matches(query))
}
下面我们再来看看Scala类库对变化封装的示例:
传统判断集合中是否存在负数的方式:
def containsNeg(nums: List[Int]): Boolean = {
var exists = false
for (num <- nums)
if (num < 0)
exists = true
exists
}
Scala集合类中的exists方法对是否存在这一变化的逻辑进行封装,只需传递判断的逻辑(即函数)即可,所以可以这样:
def containsNeg(nums: List[Int]) = nums.exists(_ < 0)
exists方法代表了控制抽象,其实是Scala将上面传统的代码替我们进行了封装(如循环相关的代码),我们只需传入变化的逻辑即可,下面是集合的exists方法源码:
def exists(p: A => Boolean): Boolean = {
var these = this
while (!these.isEmpty) {
if (p(these.head))
return true
these = these.tail
}
false
}
比如判断是否存在偶数,只需转入具体的判断逻辑:
def containsOdd(nums: List[Int]) = nums.exists(_ % 2 == 1)
6.2 柯里化currying
Scala 允许程序员自己新创建一些控制结构,并且可以使得这些控制结构在语法看起来和 Scala 内置的控制结构一样,在 Scala 中需要借助于柯里化(Currying)
柯里化将方法或函数是将一个带有多个参数的列表拆分成多个小的参数列表(一个或多个参数)的过程,并且将参数应用前面参数列表时会返回新的函数技术
scala> def plainOldSum(x: Int, y: Int) = x + y
plainOldSum: (x: Int, y: Int)Int
scala> plainOldSum(1, 2)
res4: Int = 3
将plainOldSum写成柯里化的curriedSum,前面函数使用一个参数列表,“柯里化”把函数定义为多个参数列表(且第一个参数列表只有一个参数,剩余的参数放在第二个参数列表中):
scala> def curriedSum(x: Int)(y: Int) = x + y //柯里化方法
curriedSum: (x: Int)(y: Int)Int
scala> curriedSum(1)(2)
res5: Int = 3
当你调用 curriedSum (1)(2) 时,实际上是依次调用两个普通函数(非柯里化函数),第一次调用使用一个参数 x,返回一个函数值,第二次使用参数y调用这个函数值。下面我们来用两个分开的定义来模拟 curriedSum 柯里化函数的过程:
scala> def first(x: Int) = (y: Int) => x + y
first: (x: Int)Int => Int
// first方法返回的是函数值(对象),x是既是方法参数,又是函数闭包参数
调用first方法会返回函数值,即产生第二个函数:
scala> val second = first(1) //产生第二个函数
second: (Int) => Int =
<function1> //second为函数变量,引用某个函数值
scala> second(2) //调用second函数产生最终结果
res6: Int = 3
上面first,second的定义演示了柯里化函数的调用过程,它们本身和 curriedSum 没有任何关系,但是可以引用到第二个函数second,如下:
scala> val second = curriedSum(1)_ //“curriedSum(1)” 相当于第二个方法的方法名。在前面示例中,当占位符标注用在传统方法上时,如 println _,你必须在名称和下划线之间留一个空格。在这里不需要,因为
println_ 是 Scala 里合法的标识符,
curriedSum(1)_不是
second: (Int) => Int =
<function1>
scala> onePlus(2)
res7: Int = 3
注意与下面的区别:
scala> val func = curriedSum _ //这里是将整个curriedSum方法转换为函数,该函数带两个参数,而前面只是将方法curriedSum的一部分(第二个参数列表)转换为函数,所以上面只带一个参数
func: Int => (Int => Int) = <function1>
再看一个柯里化的例子,把带有三个参数的函数f转换为只有一个参数的部分应用函数f:
scala> def curry[A, B, C, D](f: (A, B, C) => D): A => (B => (C => D)) = (a: A) => (b: B) => (c: C) => f(a, b, c)//柯里化函数
curry: [A, B, C, D](f: (A, B, C) => D)A => (B => (C => D))
scala> val f = curry((_: Int) + (_: Int) + (_: Int))
f: Int => (Int => (Int => Int)) = <function1> //将带有三个参数的函数柯里化成3个单一参数的函数
scala> f(1)
res4: Int => (Int => Int) = <function1>
scala> f(1)(2)
res5: Int => Int = <function1>
scala> f(1)(2)(3)
res6: Int = 6
下面与上面不同的是,把带有三个参数的函数f转换为第一个是单个参数,第二个包括所有余下参数的部分应用函数f:
scala> def curry2[A, B, C, D](f: (A, B, C) => D): A => ((B, C) => D) = (a: A) => (b: B, c: C) => f(a, b, c)
curry2: [A, B, C, D](f: (A, B, C) => D)A => ((B, C) => D)
scala> val f2 = curry2((_: Int) + (_: Int) + (_: Int))
f2: Int => ((Int, Int) => Int) = <function1>
scala> f2(1)
res9: (Int, Int) => Int = <function2>
scala> f2(1)(2,3)//第二个参数列表带两个参数
res10: Int = 6
甚至转换第一个参数列表带两个参数,第二个参数列表只带一个参数的函数,这也是可以的:
scala> def curry3[A, B, C, D](f: (A, B, C) => D): (A,B) => C => D = (a: A,b:B) => (c:C) => f(a, b, c)
curry3: [A, B, C, D](f: (A, B, C) => D)(A, B) => C => D
scala> val f3 = curry3((_: Int) + (_: Int) + (_: Int))
f3: (Int, Int) => Int => Int = <function2>
scala> f3(1,2) //第一个参数列表带两个参数
res12: Int => Int = <function1>
scala> f3(1,2)(3)
res13: Int = 6
上面是柯里化,下面进行反柯里化,将多个参数列表合并成一个参数列表:
scala> def uncurry[A, B, C](f: A => B => C): (A, B) => C = (a: A, b: B) => f(a)(b)
uncurry: [A, B, C](f: A => (B => C))(A, B) => C
scala> val uf = uncurry((a:Int)=>(b:Int)=>a + b)//反柯里化
uf: (Int, Int) => Int = <function2>
scala> uf(1,2)
res14: Int = 3
下面是接收两个参数的方法,进行部分应用。即我们有一个A和一个需要A和B产生C的函数,可以得到一个只需要B就可以产生C的函数(因为我们已经有A了)
scala> def curry1[A, B, C](a: A, f: (A, B) => C): B => C = (b: B) => f(a, b)//也可将(b: B) => f(a, b)写成f(a,_)
curry1: [A, B, C](a: A, f: (A, B) => C)B => C
//a参数会应用到f函数参数的第一个A类型的参数中,这样会返回只应用了第一个A类型参数的f1的偏应用函数
scala> val f1 = curry1(1,(_:Int) +(_:Int))//f1实为f函数的一个偏应用函数
f1: Int => Int = <function1>
scala> f1(2)
res1: Int = 3
如果将上面curry1方法中的f函数参数具体化,即在将f函数代码直接在curry1方法中写出来,而不是通过方法参数传递进去,下面示例是上面的具体化,函数代码直接在方法体中描述出来,而非参数传递进来:
scala> def makeIncreaser(more: Int) = (x: Int) => x + more
makeIncreaser: (more: Int) Int => Int
scala> val inc1 = makeIncreaser(1)
inc1: Int => Int = <function1>
scala> inc1(10)
res24: Int = 11
6.3 编写新的控制结构
将不要用户关心的逻辑封装起来,比如资源的打开与关闭:
def withPrintWriter(file: File, op: PrintWriter => Unit) {
val writer = new PrintWriter(file)
try {
op(writer)
} finally {
writer.close()
}
}
withPrintWriter方法只提供两个参数:一个是对哪个文件进行操作,二是对文件进行一个什么样的操作(写还是读?),除此之外如打开与关闭文件则封装起。
如下使用,对date.txt 文件进行写println操作,具体写什么则在函数里指定(这里写当前日期):
withPrintWriter(
new File("date.txt"),
w => w.println(new java.util.Date)
)
这样当调用withPrintWriter方法操作文件后,文件一定会关闭
在Scala里,如果调用的方法只有一个参数,就能可选地使用大括号替代小括号包围参数:
scala> println("Hello,
world!")
Hello, world!
你可以写成:
scala> println { "Hello,
world!" }
Hello, world!
上面第二种用法,使用{}替代了(),但这只适用在使用一个参数的调用情况。 前面定义 withPrintWriter 函数使用了两个参数,因此不能使用{}来替代(),但如果我们使用柯里化重新定义下这个函数如下:
def withPrintWriter(file: File)(op: PrintWriter => Unit) {
val writer = new PrintWriter(file)
try {
op(writer)
} finally {
writer.close()
}
}
将一个参数列表,变成两个参数列表,每个列表含一个参数,这样我们就可以使用如下语法来调用:
withPrintWriter(new File("date.txt")) {
writer => writer.println(new java.util.Date);
//上面的语句还可以简写如下:
//_.println(new java.util.Date)
}
第一个参数还是使用()将参数包围起来(也可以使用{}),第二个参数放在了花括号之间,这样withPrintWriter看起来和Scala内置控制结构(如if、while等)语法一样
6.4 传名参数by-name parameter
上篇我们使用柯里化函数定义一个控制机构 withPrintWriter,它使用时语法调用已经很像 Scala 内置的控制结构,有如if、while的使用一般:
withPrintWriter(new File("date.txt")) {
writer => writer.println(new java.util.Date)
}
不过仔细看一看这段代码,它和 scala 内置的 if 或 while 表达式还是有些区别的,withPrintWrite r的{}中的函数是带参数的含有“writer=>”。 如果你想让它完全和 if 和 while 的语法一致,在 Scala 中可以使用传名参数来解决这个问题。
Scala的解释器在解析函数参数(function arguments)时有两种方式:先计算参数表达式的值(reduce the arguments),再传递到函数内部;或者是将未计算的参数表达式直接应用到函数内部。前者叫做传值调用call-by-value,后者叫做传名调用call-by-name:
def addByName(a: Int, b: => Int) = a + b //传名
def addByValue(a: Int, b: Int) = a + b //传值
使用传名调用时,在参数名称和参数类型中间有一个“=>”符号。如果a = 1,b = 2 + 3,调用个方法的结果都是 6,但过程是不一样的:
· addByName(1, 2 + 3)
· ->1 + (2 + 3)
· ->1 + 5
· ->6
· addByValue(1, 2 + 3)
· ->addByValue(1, 5)
· ->1 + 5
· ->6
只有无参函数才能通过传名参数进行传递,在传入其它方法前,是不会执行的,而是将传入的函数逻辑代码直接嵌入(替换)到传名参数所在的地方(有点Include的意思)
下面设计一个myAssert断言方法,带一个函数值参数predicate,如果标志位assertionsEnabled被设置true(表示开启断言功能),且传入的函数返回true时,将什么也不做(即断言成功),如果传入的函数返回false时,则断言失败;如果标志位assertionsEnabled被设置false(表示关闭断言功能),则什么也不做:
scala>var assertionsEnabled = true
def myAssert(predicate: () => Boolean) =
if (assertionsEnabled && !predicate())
throw new AssertionError
myAssert: (predicate: () => Boolean)Unit //空括号表示predicate函数不带参数
scala> myAssert(() => 5 > 3) // 断言成功 ,这里是传值,传递的是函数值
调用myAssert时或许你想去掉空参数列表和=>符号 ()=>,写成如下形式,但报错:
scala> myAssert(5 > 3) //报错 ,但传名参数可以实现这种想法
<console>:15: error: type mismatch;
found : Boolean(true)
required: () => Boolean
myAssert(5 > 3)
^
上面使用的是按值传递(在传入到方法就已执行并计算出结果——该结果是无参函数字面量“() => 5 > 3”函数值对象),传递的是函数类型的值,我们可以把按值传递参数修改为按名称传递的参数。要实现一个传名参数,参数类型应该以 => 开头,而不是 ()=> 开头,如上面你可以将predicate参数的类型从“() => Boolean”改为“=> Boolean”,经过这种改造后,myAssert方法中的 predicate 参数则叫传名参数:
scala>def byNameAssert(predicate: => Boolean) = //去掉了=>前面的括号()
if (assertionsEnabled && !predicate) // predicate是名称参数,会将predicate替换成传入的函数逻辑代码。这里的predicate不是函数值对象,因为如果是函数值对象,调用时后面一定要接括号的,所以predicate在这里相当于一个占位符,将会使用传入的函数代码来替换
throw new AssertionError
byNameAssert: (predicate: => Boolean)Unit
此时调用byNameAssert方法时就可以省略空的参数() =>了,此时使用byNameAssert看上去好像在使用内建控制结构了:
scala> byNameAssert(5 > 3) // 断言成功。 另一实例参考这里的传名参数
注:此时不会将 5 > 3 先进行计算然后再传入byNameAssert方法,如果这样的话,传入的是Boolean类型,就不是函数值类型
上面的myAssert、byNameAssert两个方法只是写法上不一样,都可以正确断言。其实两者有着本质的区别,myAssert是传值参数,byNameAssert是传名参数。
或许你可能想将参数的类型从函数值类型直接定义为Boolean,下面的方法boolAssert虽然看上去与byNameAssert相似,但在某些情况下是不能正确实现断言功能 :
scala>def boolAssert(predicate: Boolean) =
if (assertionsEnabled && !predicate)
throw new AssertionError
此时下面的断言是可以成功的:
scala> byNameAssert(5 > 3) // 断言成功
但在断言标志assertionsEnabled设为false关闭断言时,针对“1 / 0 == 0”这样的断言就会抛异常(除0了):
scala> boolAssert(1 / 0 == 0)
java.lang.ArithmeticException: / by zero
但byNameAssert将不会抛异常:
scala> byNameAssert(1 / 0 == 0)
原因就是boolAssert方法中的参数类型直接是Boolean类型,则传入的“1 / 0 == 0”会先进行运行,此时 1 / 0 就会抛异常;而 byNameAssert(1 / 0 == 0),表达式 “1 / 0 == 0” 不会被事先计算好传递给 byNameAssert,而是先将 “1 / 0 == 0”创建成一个函数类型的参数值,然后将这个函数类型的值作为参数传给 byNameAssert ,实质上“1 / 0 == 0”是最后由这个函数的 apply 方法去调用,但此时的assertionsEnabled标志为false形成短路,所以最终没能执行,所以也不会抛异常
前面传名参数传递的都是函数逻辑代码,实质上传名参数可以接受任何逻辑代码块,只要代码块类型与传名参数类型相同:
def time(): Long = {
println("获取时间")
System.nanoTime()
}
def delayed(t: => Long): Long = {
println("进入delayed方法")
println("参数t=" + t)
t
}
def main(args: Array[String]) {
//还可以直接传递方法调用,实质上会使用这里 time() 代替delayed方法体内的 t 名称参数
delayed(time())
//由于time是无参方法,所以调用时也可能省略括号
//delayed(time)
println("-------------------")
delayed({ println("传名参数可接受任何逻辑代码块"); 1 })
}
前面的 withPrintWriter 我们暂时没法使用传名参数去掉参数里的 writer=>,因为传进去的op函数参数是需要参数的(即需要对哪个目标文件进行操作),不过我们可以看看下面的例子,设计一个 withHelloWorld 控制结构,即这个 withHelloWorld 总会打印一个“hello,world”:
import scala.io._
import java.io._
//op这个函数是不需参数的,所以可以设计成按名传递
def withHelloWorld(op: => Unit) {
op // op是名称参数,会将op替换成传入的函数逻辑代码
println("Hello,world")
}
调用一:
val file = new File("date.txt")
withHelloWorld { //调用时,会将上面方法体内的op传名参数所在地方,使用这对花括号中的逻辑代码块替换掉
val writer = new PrintWriter(file)
try {
writer.println(new java.util.Date)
} finally {
writer.close()
}
}
Hello,world
调用二:
withHelloWorld {
println("Hello,Guidebee")
}
Hello,Guidebee
Hello,world
可以看到 withHelloWorld 的调用语法和 Scala 内置控制结构非常象了
总结,传名参数的作用就是:给方法传递什么样的代码,那就会使用什么样的代码替换掉方法体内的传名参数
7 组合与继承
7.1 抽象类
abstract class Element {
def contents: Array[String]
}
一个含有抽象方法的类必须定义成抽象类,也就是说要使用abstract关键字来定义类
抽象类中不一定有抽像方法,但抽象方法所在的类一定是抽象类
abstract抽象类的不能实例化
contents 方法本身没有使用 abstract 修饰符,一个没有定义实现的方法就是抽象方法,和 Java 不同的是,抽象方法不需要使用 abstract 修饰符来表示,只要这个方法没有具体实现,就是抽象方法
声明: declaration
定义: definition。
类 Element 声明了抽象方法contents,但当前没有定义具体方法
7.2 无参方法
abstract class Element {
def contents: Array[String] //抽象方法
def height: Int = contents.length //无参方法,不带参数也不带括号
def width(): Int = if (height == 0) 0 else contents(0).length //空括号方法,不带参数但带括号
}
注:如果定义时去掉了空括号,则在调用时也只能去掉;如果定义时带上了空括号,则调用时可以省略,也可以不省略:假设e是Element实例,调用上面的height只可以是这样:e.height,而不能是e.height();但调用width方法时,即可以是e.width,也可以是e.width()
一般如果方法没有副作用(即只是读取对象状态,而不会修改对象的状态,也不会去调用其它类或方法)情况下,推荐使用这种无参方法来定义方法,因为这样访问一个方法时好像在访问其字段成员一样,这样访问代码做到了统一访问原则,也就是说height、width不管定义成字段还是方法(定义时省略空括号),客户端访问代码都可以不用变,因为此时访问的height、width方式都一样
不带括号的无参方法很容易变成属性字段,只需将def改为val即可:
abstract class Element {
def contents: Array[String] //抽象方法
val height = contents.length
val width = if (height == 0) 0 else contents(0).length
}
访问字段要比访问方法略快,因为字段在类的初始化时就已经计算过了,而方法则在每次调用时都要计算
由于Scala 代码可以直接调用 Java 函数和类,但 Java 没有使用“统一访问原则”,如在Java 里只能是 string.length(),而不能是 string.length。为了解决这个问题,Scala 对于Java里的空括号函数的使用也是一样,也可以省略这些空括号:
Array(1, 2, 3).toString
//实际上调用的是Java中Object中的toString()方法
"abc".length //实为调用的Java中String的length()方法
//以前这种在Java中常规调用法在Scala中也还是可以的
Array(1, 2, 3).toString()
"abc".length()
原则上,Scala的函数调用中可以省略所有的空括号,但如果使用的函数不是纯函数,也就是说这个不带参数的函数可能修改对象的状态或是我们利用它调用了其他一些功能(比如调用其它类打印到屏幕,读写 I/o等),一般的建议还是使用空括号:
"hello".length // 没有副作用,所以无须(),因为String是不可变类
println() // I/O操作,最好别省略()
总之,Scala推荐使用将不带参数且没有副作用的方法定义为无参方法,即省略空括号,但永远不要定义没有括号但带副作用的方法,因为那样的话方法调用看上去像是访问的字段,会让调用都感觉到访问属性还产生了其他作用呢?另外,从调用角度从发(前面是从定义角度出法),如果你的调用方法执行了其他操作就要带上括号,但如果方法仅仅是对某个属性字段的访问,则可以省略
7.3 extends
class ArrayElement(conts: Array[String]) extends Element {
def contents: Array[String] = conts
}
extends会将所有非私有的成员会继承过来
如果你在定义类时没有使用 extends 关键字,在 Scala 中这个定义类缺省继承自 scala.AnyRef,如同在 Java 中缺省继承自 java.lang.Object。这种继承关系如下图:
重写override:子类重写父子相同名称的方法(方法签名也要相同),或同名成员字段
实现implement:子类实现父类中抽象方法
scala> val ae = new ArrayElement(Array("hello", "world"))
ae: ArrayElement = ArrayElement@d94e60
scala> ae.width //访问从父类Element继承过来的成员
res1: Int = 5
val e: Element = new ArrayElement(Array("hello")) //父类的引用指向子类的实例
7.4 字段重写无参方法(或实现无参抽象方法)
和 Java 稍有不同的一点是,Scala 中方法与字段是在同一个命名空间,也就是说Scala 中不允许定义同名的无参成员函数(方法,不管是否有无空括号)和成员变量,这样的好处是可以使用成员变量来重写一个无参的成员函数(或实现抽象无参方法)。比如下面B类中的属性成员a字段实现了父类A中的a抽象方法:
abstract class A {
def a:Int //抽象方法
}
class B extends A {
val a = 1 //实现父类的抽象方法,这里是实现而非重写,所以前面可以省略 override
}
注:字段实现父类中同名抽象无参方法时,可以是val,也可以是var,这与字段与字段之间的重写不太一样
上面示例中属于实现,所以实现时可以省略override,但如果子类重写父类的非抽象方法(具体方法)时前面是要带override如:
class A {
def a: Int = 1
}
class B extends A {
override val a = 1 // 由于是重写父类同名非抽象方法,所以一定要加上 override 关键字
}
注:Java1.5 中, @Override 标注被引入并与 Scala 的 override 修饰符作用一样,但Java中的 override不是必需的
上面的示例都是子类中的成员字段实现或重写父类中同名的无参方法,但无参方法是不能重写同名成员字段:
scala> class A {
| var a: Int = 1
| }
defined class A
scala> class B extends A {
| override def a = 1 //这里编译出错
| }
<console>:13: error: overriding variable a in class A of type Int;
method a cannot override a mutable variable 方法不能重写变量(或常量,val定义的为常量)
override def a = 1
^
Scala 里禁止在同一个类里用同样的名称定义字段和方法,而在 Java 里这样做被允许。例如,下面的 Java 类能够很好地编译:
//在Java里的代码
class CompilesFine {
private int f = 0;
public int f() {
return 1;
}
}
但是相应的 Scala 类将不能编译:
class WontCompile
{
private var f = 0 // 编译不过,因为字段和方法重名
def f = 1
}
7.5 字段重写字段
子类的成员字段也是可以重写父类的同名字段的,但只有val类型的常量才能被重写,并且重写时也只能使用val来修饰:
class A {
val a: Int = 1
}
class B extends A {
override val a:Int = 2 //属性重写,不能省略override
}
如果将上面示例中的两个val其中任何一个修改成var就会报错。
另外,父类私有private的对于子类是不可见的,所以不能重写:
class A {
private val a: Int = 1
}
class B extends A {
val a: Int = 2
}
注:字段间的重写不能省略override关键字
abstract class Fruit {
val v: String
def m: String
}
abstract class Apple extends Fruit {
val v: String
val m: String // OK to override a ‘def’ with a ‘val’
}
abstract class BadApple extends Fruit {
def v: String // ERROR: cannot override a ‘val’ with a ‘def’
def m: String
}
7.6 参数化成员字段
上面的示例中,有这样一段类似定义的代码:
class T(a: Int) {
val f = a
}
其中a: Int为类的参数,在实例化时需要传入此参:
val t = new T(1);
scala> t.f
res0: Int = 1
scala> t.a // 不会产生名为a的成员变量
<console>:14: error: value a is not a member of T
t.a
^
虽然a是类一级别的参数,但它不会自动成为类的成员变量,所以t.a是错误的,此情况下的a仅仅是参数罢了,但如果在a: Int 的前面加上 val 或 var呢?请看下面:
class T(val a: Int) {
val f = a
}
val t = new T(1);
scala> t.f
res3: Int = 1
scala> t.a // 会产生名为a的成员变量
res4: Int = 1
scala> t.a = 2 // 由于定义的是 val 类型,所以不能修改其值,t.a在t构造时会被初使化;如果定义成var,则此处可以修改
<console>:13: error: reassignment to val
t.a = 2
^
上面示例说明,只要在类的参数前面加上 val 或 var,类的参数除了作为类构造时的一个参数外,类的参数还自动成为类的一个同名成员字段,这个成员字段与直接在类体中定义是没有区别的,即此时类参数与类成员合二为一了,即类的参数进一步提升为了参数化成员字段
参数化成员字段定义前面还可以加上private、protected、override修饰符,这跟在类中直接定义没有区别:
class Cat {
val dangerous = false
}
class Tiger(
override val dangerous: Boolean,//重写父类的属性成员,前面的override关键字不能省略
private var age: Int) extends Cat
上面Tiger 的定义实质上是下面代码写法的简写,这是一样的:
class Tiger(param1: Boolean, param2: Int) extends Cat {
override val dangerous = param1
private var age = param2
}
此时param1、param2仅仅是类参数而已,不会成为类的属性成员
7.6.1 var成员变量、getter与setter方法
在Scala中,对象的每个非private[this]访问的var类型成员变量都隐含定义了getter和setter方法(val变量不会生成setter方法),但这些getter和setter方法的命名方式并没有沿袭Java的约定,var变量x的getter方法命名为“x”,它的setter方法命名为“x_=”。例如,如果类中存在var定义:
var hour = 12//定义一个非私有的var变量时,除了会生成相应本地字段(private[this]修饰的字段)外,还会生相应的gtter与setter方法
则除了有一个成员字段会生产之外,还额外生成getter方法“hour”,以及setter方法“hour_=”。不管var前面是什么样的访问修饰符(除private[this]外,因为如果是private[this]则不会生成相应相应getter与setter方法),生成的成员字段始终会使用private[this]来修饰,表示只能被包含它的对象访问,即使是同一类但不同对象也是不能进行访问的;而生成的getter和setter方法前面的访问修饰符与原val前访问修饰符相同,即:如果var定义为public,则它生成的getter和setter方法前访问修饰符也是public,如果定义为protected,那么它们也是protected。
例如下面类型了一个Time类,里面定义了两个公开的var变量hour和minute:
class Time {
var hour = 12
var minute = 0
}
下面是上面public var变量实际所生成的类,这是完全相同的(类里定义的本地字段(private[this]修饰的字段)h和m的命令是随意命名的,要求是不与任何已经存在的名称冲突):
class Time {
private[this] var h = 12
private[this] var m = 0
def hour: Int = h
def hour_=(x: Int) { h = x }
def minute: Int = m
def minute_=(x: Int) { m = x }
}
所以,你也可以直接通过上面第二种getter、setter方式来取代var变量的定义,这样你可以在getter或setter方法里做一些控制。如下面再次改写上面的代码,加上数据有效性检测:
class Time {
private[this] var h = 12
private[this] var m = 12
def hour: Int = h
def hour_=(x: Int) {
require(0 <= x && x < 24) //参数有效性检测
h = x
}
def minute = m
def minute_=(x: Int) {
require(0 <= x && x < 60) //参数有效性检测
m = x
}
}
可以只定义getter和setter方法而不带有本地关联字段(private[this] var类型的字段),有时是需要这样做的:比如温度就有两种,摄氏度与华氏度,但它们之间是可以相互换算的,这样就没有必须设置两个var变量分别存储摄氏度与华氏度,而是以某一种来存储,另一种在setter与getter时进行相应换算即可。下面就是以摄氏度来存储,而华氏度有相应getter与setter方法来进行转换即可:
class Thermometer {
var celsius: Float = _ //摄氏度(℃),会自动生产相应的getter与setter方法
def fahrenheit = celsius * 9 / 5 + 32//以华氏度单位来显示
def fahrenheit_=(f: Float) {//传进来的是华氏度(℉),需要转换后存入
celsius = (f - 32) * 5 / 9
}
override def toString = fahrenheit + "F/" + celsius + "C"
}
上面的celsius变量,初始值设置为了“_”,这个符号表示根据变量的类型来初始化初值:对于数值类型会设置为0,布尔类型为false,引用类型为null
注意,Scala不可以随意省略“=_”初始化器,如果写成
var celsius: Float
这将表示celsius为抽象变量,这与Java中的成员变量省略初始化是不同的,Java成员变量(Java局部变量一定要显示初始化)如果省略初始化赋值,则还是会自动根据成员字段类型来进行初始化,而Scala中省略后则表示是一个抽象变量
7.7 调用父类构造器
要调用父类构造器(主或辅助构造器都可,以参数来决定),只要把你要传递的参数或参数列表放在父类名之后的括号里即可:
abstract class Element {
def contents: Array[String]
def height = contents.length
def width = if (height == 0) 0 else contents(0).length
}
class ArrayElement(conts: Array[String]) extends Element {
val contents: Array[String] = conts
}
//由于父类ArrayElement带了一个类参数conts,所以在定义子类LineElement时需要传递这个参数
class LineElement(s: String) extends ArrayElement(Array(s)) {
override def width = s.length
override def height = 1
}
7.8 多态
class UniformElement(ch: Char,
override val width: Int,
override val height: Int) extends Element {
private val line = ch.toString * width
def contents = Array.fill(height)(line)
}
val e1: Element = new ArrayElement(Array("hello", "world"))//父类引用指向子类对象,即多态
val ae: ArrayElement = new LineElement("hello")//父类引用指向子类对象
val e2: Element = ae//祖父类引用指向孙类对象
val e3: Element = new UniformElement('x', 2, 3)//父类引用指向子类对象
为了演示多态,先临时删除 Element 中所有成员,添加一个 demo 方法,定义如下:
abstract class Element {
def demo() {
println("Element's implementation invoked")
}
}
class ArrayElement extends Element {
override def demo() {
println("ArrayElement's implementation invoked")
}
}
class LineElement extends ArrayElement {
override def demo() {
println("LineElement's implementation invoked")
}
}
class UniformElement extends Element //没有重写父类的方法
object T {
//参数类型为父类,任何子类实例都可以传递进来
def invokeDemo(e: Element) {
e.demo() //多态,在运行时调用相应子类方法
}
def main(args: Array[String]) {
invokeDemo(new ArrayElement)
invokeDemo(new LineElement)
invokeDemo(new UniformElement) //由于没有重写父类方法,所以调用的是父类Element中的方法
}
}
7.9 final
与Java一样,final修饰方法或字段(只有Scala才支持字段重写),是不能被子类重写的;如果修饰的是类的话,则类是不能被继承
scala> class A{
| final def f = 1
| }
defined class A
scala> class B extends A{
| override val f = 1 //子类不能重写父类的final成员
| }
<console>:13: error: overriding method f in class A of type => Int;
value f cannot override final member
override val f = 1
^
scala> final class A{
| def f = 1
| }
defined class A
scala> class B extends A{ //final类不能被继承
| override val f = 1
| }
<console>:12: error: illegal inheritance from final class A
class B extends A{
^
7.10 组合与继承
继承与组合是实现类复用的两种最常用的技术
组合是has-a的关系
继承是is-a的关系
继承是is-a 的关系,比如说Student继承Person,则说明Student is a Person
缺点:
1) 子类从父类继承的方法在编译时就确定下来了,不支持动态继承,子类无法选择不同的父类,所以运行期间无法改变从父类继承的方法的行为
2) 继承关系最大的弱点是打破了封装,子类能够访问父类的实现细节,耦合度高,子类缺乏独立性,父类修改会影响所有子类
组合的优点:
1:不破坏封装,整体类与组合类之间松耦合,彼此相对独立,具有较好的可扩展性
3:支持动态组合。在运行时,整体对象可以选择不同类型的组合对象占为己用
Car表示汽车对象,Vehicle表示交通工具对象,Tire表示轮胎对象。三者的类关系如下图:
Car是Vehicle的一种,因此是一种继承关系(又被称为“is-a”关系);而Car包含了多个Tire,因此是一种组合关系(又被称为“has-a”关系),实现如下:
class Verhicle {
}
class Tire {
}
class Car extends Verhicle {
private Tire t = new Tire();
}
既然继承和组合都可以实现代码的重用,那么在实际使用的时候又该如何选择呢?一般情况下,遵循以下两点原则:
1) 除非两个类之间是“is-a”的关系,否则不要轻易地使用继承,不要单纯地为了实现代码的重用而使用继承,因为过多地使用继承会破坏代码的可维护性,当父类被修改的时候,会影响到所有继承自它的子类,从而增加程序的维护难度与成本
2) 不要仅仅为了实现多态而使用继承,如果类之间没有“is-a”的关系,可以通过接口+组合的方式来达到相同的目的。设计模式中的策略模式可以很好的说明这一点,采用接口与组合的方式比采用继承的方式具有更好的可扩展性。
由于Java语言只支持单继承,如果想同时继承两个类或多个类,在Java中是无法直接实现的,这可以通过组合来实现;另外如果继承使用太多,也会让一个class里面的内容变得臃肿不堪。所以在Java语言中,能使用组合的时候尽量不要使用继承。
7.11 工厂对象
使用工厂对象的好处是,可以提供统一创建对象的接口并且隐藏被创建具体对象的实现
实现 factory 对象的一个基本方法是采用 singleton 模式,在 Scala 中,可以使用类的伴生对象(companion 对象)来实现:
//对象 Element 的伴生类
abstract class Element {
def contents: Array[String]