第1章 可伸展的语言
Scala应用范围广,从编写脚本,到建立大型系统。
运行在标准Java平台上,与Java库无缝交互。
更能发挥力量的地方:建立大型系统或可重用控件的架构。
将面向对象和函数式编程加入到静态类型语言。
在Scala中,函数就是对象。函数类型是能够被子类继承的类。
Scala是纯粹的面向对象语言:每个值都是对象,每个操作都是方法调用。如1+1,实际是调用Int里的+方法。
语言分类:
指令式语言 imperative
函数式语言 functional
函数式编程:
函数是对能产生值的表达式的定义。
两种指导理念:
1、函数是头等值。
函数也是值,与整数、字符串处于同一个地位;
可以当作参数传递给其他函数;
可以当作结果从函数中返回或保存在变量里;
可以在函数里定义其他函数,就好像在函数里定义整数一样;
可以定义匿名函数,并随意插入到代码的任何地方。
2、程序的操作应该把输入值映射为输出值而不是就地修改数据。
不可变数据结构是函数式语言的一块基石;
Scala中的不可变列表元组、映射表、集;
无副作用
Scala的兼容性:
Scala程序被编译成JVM字节码,性能与Java相当。
自由调用Java类库。
允许定义类型失配或者选用不存在的方法时使用隐式类型转换(implicit conversion)。
Scala定义类:
class MyClass(index:Int, name:String)
含有两个私有字段,以及一个构造函数。
测试只能证明错误存在,而非不存在。
有用的文档是那些程序的读者自己不可能很容易的看懂程序功能的文档。
类型推断 type inference
第2章 Scala入门初探第2章 Scala入门初探
Scala解释器:
resX 解释器自动产生的计算结果
val 只能赋值一次
var 可以赋值多次
两次回车键取消多行输入
函数定义:
def max(x:Int, y:Int):Int={function block;
}
参数类型必须指定。
函数结果(result type)类型 如果递归定义则必须指定,否则可以不用显示指定而用类型推断。
如果函数只有一行表达式,可省略{}
当函数没有返回值时返回Unit。
结果类型为Unit的函数是为了其运行效果,即side effect。
脚本:经常会被执行的,放在文件中的子序列。
> scala hello.scala arguments
脚本中访问脚本参数:args(0)
args.foreach(arg=>println(arg))
args.foreach(println)
函数字面量语法结构:括号、命名参数列表、右箭头、函数体
(x:Int, y:Int) => x+y
for(arg <- args)println(arg)
第3章 Scala入门再探
3.1 类型参数化数组
val greetStrings=new Array[String](3)
greetStrings(0)="Hello"
greetStrings(1)=", "
greetStrings(2)="world!\n"val greetStrings = Array("Hello",", ","world!\n")
for(i<-0 to 2)print(greetString(i))
1、用[String]新建一个String型的数组;
2、数组用(index)引用其中的值;
3、若方法只有一个参数,调用时可以省略点及括号;
(0).to(2) 0 to 2 println(10) Console println 10
4、Scala没有传统意义上的操作符,取而代之的是+-*/这样的字符作为方法名,1+2 是方法的调用:(1).+(2)
用括号访问数组:数组也是对象,实质为对象的值参数,调用的是apply方法。
用括号传递给变量一个或多个值参数时,scala会把它转换成对apply方法的调用。
任何对于对象的值参数应用都被转换为对apply方法的调用。前提是这个类型定义了apply方法。
arrayObject(i)被转换为arrayObject.apply(i)
当对带有括号并包括一到若干参数的变量赋值时,编译器将使用对象的update方法对括号里的参数和等号右边的对象执行调用。
greetStrings(0)=”Hello”
greetStrings.update(0,”Hello”)
val numNames = Array(“zero”,”one”,”two”)
val numNames = Array.apply(“zero”,”one”,”two”)
调用apply工厂方法,可以有不定个数的参数,定义在Array的伴生对象(companion object)中。
3.2 使用列表(list)
方法没有副作用是函数式编程的重要概念,计算并返回值应该是方法的唯一目的。
数组是可变的同类对象序列。
列表是不可变的同类对象序列,为的是方便函数式编程。
val oneTwo=List(1,2)
val threeFour=List(3,4)
val all=oneTwo::threeFour
val all=1::2::3::4::Nil
:::列表叠加,新建列表并返回
::前插 Nil是空列表 带冒号的方法调用者是后者,1::Nil Nil.::(1)
如何append?
1、先::前插,再reverse;
2、ListBuffer支持append,在toList。
3.3 使用元组(Tuple)
不可变,可包含不同类型的元素。
如果在方法里返回多个对象,java的做法是创建JavaBean以包含多个返回值,Scala里可以返回元组来达到目的。
val pair=(99,"Luftballons")
println(pair._1+" "+pair._2)
用点号、下划线和基于1
的索引访问其中的元素。
列表的apply方法始终返回同样的类型,所以可以用(index)访问,而元组中元素的类型都不同,所以用._index访问。
从1开始是依传统设定的。
3.4 使用集(set)和映射(map)
scala.collection.immutable.Set
scala.collection.mutable.Set
scala.collection.imutable.HashMap
scala.collection.mutable.HashMap
默认为immutable。
3.5 学习识别函数式风格
如果包含var变量,可能是指令式风格;
如果仅含val变量,可能是函数式风格。
指令式风格
def printArgs(args :Array[String]):Unit={var i = 0while(i<args.length){println(args(i))i+=1}
}
函数式风格
def printArgs(args:Array[String]):Unit={for(arg<-args)println(args) //args.foreach(println)
每个有用的程序都会有某种形式的副作用,否则就不可能向程序之外提供什么有价值的东西。
无副作用的代码使测试变得容易:
def formatArgs(args:Array[String])=args.mkString("\n")
val res=formatArgs(Array("zero","one","two"))
assert(res=="zero\none\ntwo")
Scala的assert方法检查传入的Boolean表达式,如果为假,抛出AssertionError。
Scala程序员的平衡感:
崇尚val、不可变对象、无副作用的方法;
只有在特定需要和加以权衡之后才选择var、可变对象、有副作用的方法。
3.6 从文件里读取文本行(脚本)
import scala.io.Source def widthOfLength(s:String)=s.length.toString.length if(args.length>0){val lines=Source.fromFile(args(0)).getLines.toListval 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=" "*numSpacesprint(padding+ling.length+"|"+line) } elseConsole.err.println("Please enter filename")
Source.fromFile(args(0)).getLines方法返回一个迭代器Iterator[String]。
getLines方法返回的是迭代器,一旦遍历完成就失效了,所以要toList。
第4章 类和对象
4.1 类、字段、方法
成员(member):
val或var定义字段
def定义方法
字段的另一种说法:实例变量(instance variable)
私有字段只能被同一个类里的方法访问,所有能更新字段的代码都被锁定在类里。
声明字段私有:private关键字
Public是Scala的默认访问级别。
class ChecksumAccumulator{private var sum=0def add(b:Byte):Unit=sum+=b //def add(b:Byte){sum+=b}def checksum():Int=~(sum&0xFF)+1
}
Scala里方法参数都是val,即C++的常引用。
如果没有显式返回语句,Scala方法将返回方法中最后一次计算得到的值。
如果函数只有一行表达式,可省略{}。
对结果类型为Unit的方法来说,执行的目的就是为了它的副作用——能够改变方法之外的某处状态或执行I/O活动。
过程(procedure)——一种仅为了副作用而执行的方法。
定义函数时,没有等号,返回的是Unit def add(b:Byte){sum+=b}
4.2 分号推断
语句末尾的分号通常可选,如果一行包含多条语句,必须加分号。
如val s = “hello”; println(s)
除非以下情况之一成立,否则行尾被认为是一个分号。
1、疑问行由一个不能合法作为语句结尾的字结束,如句点或中缀操作符;
2、下一行开始于不能作为语句开始的词;
3、行结束于()或[]内部,因为这些符号不能容纳多个语句。
4.3 Singleton对象
Scala不能定义静态成员,取而代之的是定义单例对象(singleton object)。
用object关键字替换class关键字。
import scala.collection.mutable.Map
object ChecksumAccumulator{private val cache=Map[String,Int]()def calculate(s:String):Int=if(cache.contains(s))cache(s) //返回键s对应的值else{val acc=new ChecksumAccumulaterfor(c<-s)acc.add(c.toByte)val cs=acc.checksum()cache+=(s->cs)cs}
}
单例对象,对应于java的静态方法工具类。
当单例对象与某个类共享同一个名称时,被称为这个类的伴生对象(companion object)。
同名类的伴生对象,与伴生类(companion class)在一个源文件中。
类和其伴生对象可以互相访问其私有成员。
单例对象名.方法名 CheckSumAccumulator.calculate(“Every value is an object”)
单例对象不是类型,它扩展了伴生类的父类并可以混入特质。
可以使用类型调用单例对象,或者类型的实例变量指代单例对象,并把它传递给需要类型参数的方法。
单例对象不带参数,而类可以。因为单例对象不是new实例化的,没机会传递给它实例化参数。
每个单例对象都被实现为虚构类(synthetic class)的实例,并指向静态的变量。
虚构类的名称是对象名后加一个美元符,单例对象ChecksumAccumulator的虚构类是ChecksumAccumulator$。
单例对象在第一次访问时被初始化。
没有伴生类的单例对象被称为独立对象(standalone object),可用于相关功能方法的工具类,或者定义Scala应用的入口点。
4.4 Scala程序
能独立运行的Scala程序,必须创建有main方法的单例对象——仅一个参数Array[String],且结果类型为Unit。
以定义结尾的都不是脚本。
脚本:以结果表达式结束。
$ scalac ChecksumAccumulator.scala Summer.scala 或者
快速Scala编译器,fast Scala compiler,将编译程序加载入后台
$ fsc ChecksumAccumulator.scala Summer.scala
$ fsc -shutdown
import ChechsumAcculator.calculate
object Summer{def main(args:Array[String]){for(arg<-grgs)println(arg+":"+calculate(arg))}
}
Scala源文件默认引用java.lang、scala、单例对象Predef。
println语句即为Predef单例对象的println(Predef.println转而调用Console.println)。
assert实际调用Predef.assert。
4.5 Application特质
import ChecksumAcculator.calculate
object FallWinterSpringSummer extends Application{for(season<-List("fall","winter","spring","summer"))println(season+":"+calculate(season))
}
特质Application声明了带有合适签名的main方法,并被单例对象继承。
大括号之间的代码被收集进了单例对象的主构造器(primary constructor),并在类初始化时执行。
Scala提供scala.Application特质,可以减少输入工作。
但无法访问命令行参数,无法应用于多线程。
数组Array 可变同类对象序列
列表List 不可变同类对象序列
元组Tuple 不可变非同类对象序列
集合Set 映射Map 有可变和不可变
第5章 基本类型和操作
5.1 基本类型和操作
整数类型 integral type
Byte
Short
Int
Long
Char
数类型 numeric type
Float
Double
String 属于java.lang包
Boolean
5.2 字面量
字面量literal——直接写在代码里的常量值。
原始字符串:raw string 三个双引号对之间的字符串,字符串中可以包含任意字符,不用转义。
“””|line1
|line2”””.stripMargin 此方法使结果符合预期
符号字面量——除了显示名字,什么都不能做。
符号字面量被写成’<标识符>
被映射成预定义类scala.Symbol的实例,即被编译器扩展为工厂方法调用:Symbol(“标识符”)。
应用场景:动态类型语言中使用一个标识符。
>val s = ‘aSymbol
>s.name #aSymbol
如果同一个符号字面量出现两次,那么两个字面量指向的是同一个Symbol对象。
5.3 操作符和方法
操作符实际只是普通方法调用的另一种表现形式。
值类型对象调用方法的简写形式。
任何方法都可以被当做操作符来标注。
>val s=”hello, world”
>s.indexof(‘o’)
>s indexof ‘o’
中缀标注
前缀标注 -2.0 实际为 (2.0).unary_- + - ! ~四种操作符可被用作前缀标注
后缀标注 s toLowerCase 不用点号或括号调用不带任何参数的方法
中缀是二元的,前缀和后缀是一元的。
在Scala中,方法调用的空括号,有副作用就加上,没副作用就省略。
5.7 对象的相等性
首先检查左侧是否为null,如果不是,调用左操作数的equals方法。
精确比较取决于左操作数的equals方法定义。
Java中的==对基本类型,比较的是值相等性;对于引用类型,比较的是引用相等性,即是否指向JVM堆里的同一个对象。
5.8 操作符的关联性
任何以:字符结尾的方法,右操作数调用方法,传入左操作数 a:::b b.:::(a)
其余相反。
5.9 富包装器
Scala基本类型的方法很多。
Scala基本类型都隐式转换(implicit conversion)为富包装器类型,可对外提供多种额外的方法。
想要看到基本类型的所有可用方法,可以查看基本类型的富包装器的API文档。
第6章 函数式对象
本章重点:函数式对象——不具有任何可改变状态的对象的类。
不可变对象的优点:
1、对象状态不可变,思路清晰;
2、无线程安全问题;
3、让哈希键值更安全。
缺点:需要赋值很大的对象表,可变对象可以原址更新。
6.1 类Rational的规格说明书
有理数:rational number 状态不可变,两个有理数运算后将产生新的有理数。
分子:numerator
分母:denominator
约分:normalized
6.2 创建Rational
class Rational(n:Int,d:Int)
如果类没有主体,不需要花括号。
n,d称为类参数(class parameter)。
Scala编译器会收集这两个类参数并创造出一个主构造器(primary constructor)。
类内部任何既不是字段又不是方法的代码将被编译进主构造器中,每次新建实例时执行。
class Rational(n:Int,d:Int){println("Create"+n+"/"+d)
}
6.3 重新实现toString方法
类默认继承了java.lang.Object类的toString实现,打印 类名@地址。
重写(override)toString方法:
class Rational(n:Int,d:Int){override def toStirng=n+"/"+d
}
6.4 检查先决条件
分子不能为零
先决条件是对传递给方法或构造器的值的限制,是调用者必须满足的需求。
为主构造器定义先决条件(precondition):
使用require方法:(定义在scala包的独立对象Predef上)
class Rational(n:Int,d:Int){require(d!=0)override def toStirng=n+"/"+d
}
Require方法带一个boolean参数,如果为真,正常返回;反之,require将抛出IllegalArgumentException阻止对象被构造。
6.5 添加字段
类参数是为了给字段赋值,所以要添加字段。
class Rational(n:Int,d:Int){required(d!=0)val numer:Int=nval denom:Int=doverride toString=numer+"/"+denomdef add(that:Rational):Rational=new Rational(numer*that.denom+that.num*denom,denom*that.denom)
}
添加了两个字段,并用参数初始化他们。
6.6 自指向
关键字this指向当前执行方法被调用的对象实例。
def lessThan(that:Rational)=this.numer*that.denom<that.num*this.denom
6.7 辅助构造器Auxiliary constructor
辅助构造器都是以this(...)形式开头。
每个辅助构造器的第一个动作都是调用同类的别的构造器。
被调用的构造器可以是主构造器,也可以是别的辅助构造器。
每个Scala构造器最终调用主构造器。主构造器是类的唯一入口点。
6.8 私有字段和方法
最大公约数:greatest common divisor
初始化器:initializer——对字段进行初始化的代码n/g d/g。
6.9 定义操作符
直接将add替换为+
6.10 Scala的标识符
1、字母数字式 alphanumeric identifier
字母、下划线开头,之后跟字母数字下划线。
$为编译系统所用的标识符,用户程序不应包含。不建议用“_”结尾
驼峰式camel case:类和特质开头大写,其余开头小写。
Java中常量名大写且用“_”分隔,Scala中的常量也用驼峰式。
2、操作符 operator identifier
Scala编译器将操作符标识符转换成合法的内嵌”$”的java标识符。
如:->将被内部表达为$colon$minus$greater。
3、混合标识符 mixed identifier
由字母数字组成,后面跟着下划线和一个操作标识符。
如unary_+ 前缀
myvar_=被用作定义赋值操作符的方法名,是由编译器产生的用来支持属性(property)的。
4、字面量标识符 literal identifier
用反引号包括的任意字符串`...`。
可以把运行时环境认可的任意字符串放在反引号之间当作标识符,结果总被当作是Scala标识符。即使反引号中的是保留字也有效。
在java的Thread类中访问静态的yield方法是典型应用:
因为yield是Scala的保留字,所以不能写成Thread.yield(),可以写成Thread.`yield`()。
6.11 方法重载 overload
6.12 隐式转换
类似于C++的转换构造函数。实际定义在伴生对象中。
implicit def intToRational(x:Int)=new Rational(x)
此时,可执行操作:
>val r=new Rational(2,3)
>2*r //将2转换为Rational
注意:要隐式转换起作用,需要将隐式转换函数定义在作用范围之内。
隐式转换函数是隐式调用。
编程时要在简洁性和可读性之间进行权衡。