Scala面向对象编程
Scala程序设计 第2版 - 原版.pdf 下载:
https://download.csdn.net/download/u014646662/10805074
目录:
1 类与对象初步
2 引用与值类型
3 价值类
4 父类
5 Scala的构造器
6 类的字段
6.1 统一访问原则
6.2 一元方法
7 验证输入
8 调用父类构造器与良好的面向对象设计
9 嵌套类型
对人工智能感兴趣的同学,可以点击以下链接:
现在人工智能非常火爆,很多朋友都想学,但是一般的教程都是为博硕生准备的,太难看懂了。最近发现了一个非常适合小白入门的教程,不仅通俗易懂而且还很风趣幽默。所以忍不住分享一下给大家。点这里可以跳转到教程。
https://www.cbedai.net/u014646662
Scala 是一个函数式编程语言,也是一个面向对象的编程语言,与Java、Python、Ruby、Smalltalk 等其他语言一样。在此强调两点,首先,函数式编程已经成为解决现代编程问题的一项基本技能,这个技能对你而言可能是全新的。开始使用Scala 时,人们很容易把它作为一个“更好的Java”语言来使用,而忽略了它“函数式的一面”。其次,Scala 在架构层面上提倡的方法是:小处用函数式编程,大处用面向对象编程。用函数式实现算法、操作数据,以及规范地管理状态,是减少bug、压缩代码行数和降低项目延期风险的最好方法。另一方面,Scala 的OO 模型提供很多工具,可用来设计可组合、可复用的模块。这对于较大的应用程序是必不可少的。因此,Scala 将两者完美地结合在了一起。
1 类与对象初步
类用关键字class 声明,而单例对象用关键字object 声明;(单例模式和Java的实现方式不同)
在类声明之前加上关键字final,可以避免从一个类中派生出其他类;
abstract 关键字可以阻止类的实例化;
一个实例可以使用this 关键字指代它本身。(。尽管在Java 代码中经常看到this 的这种用
法,Scala 代码中却很少看到。原因之一是,Scala 中没有样板构造函数。)
Java创建一个Person类:
package cn.com.tengen.test;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(String name) { this.name = name; }
public String getName() { return this.name; }
public void setAge(int age) { this.age = age; }
public int getAge() { return this.age; }
}
但是如果用Scala实现就比较简单了
package cn.com.tengen.test
class Person(var name: String, var age: Int){
}
使用scala测试一下
package cn.com.tengen.test.obj
class Person(var name: String,var age: Int){
}
object Person extends App {
val p = new Person("Lucky",18)
p.age = 20
println(p.name+"---"+p.age)
}
输出结果:
Lucky---20
在构造参数前加上var,使得该参数成为类的一个可变字段,这在其他的OO 语言中也称为实例变量或属性。
在构造参数前加上val,使得该参数成为类的一个不可变字段,
用case 关键字可以推断出val,同时自动增加一些方法,如下所示:
package cn.com.tengen.test.obj
case class ImmutablePerson(name: String, age: Int) {
}
object ImmutablePerson extends App{
val p = new ImmutablePerson("Lucky",18)
println(p.name+"---"+p.age)
}
case做了那些事:
1.重写了toString
2.默认实现了equals 和hashCode
3.默认是可以序列化的,也就是实现了Serializable
4.自动从scala.Product中继承一些函数
5.case class构造函数的参数是public级别的,我们可以直接访问
6.支持模式匹配
7.实现了copy方法
method(方法)指与实例绑定在一起的函数。换句话说,它的参数列表中有一个“隐含”的this 参数。方法用关键字def 定义。当其他函数或方法需要一个函数作为参数时,Scala 会自动将可用的方法“提升”为函数,作为前者的函数参数。
如同大多数静态类型语言一样,Scala 允许方法重载。只要各方法的完整签名是唯一的,两个或更多方法就可以具有相同的名称。方法的签名包括返回类型,方法名称和参数类型的列表(参数的名称不重要)。因此,在JVM 中只凭不同的返回类型不足以区分不同的方法。
def f1(s : String): Unit ={
println(s)
}
def f1(s : String,i : Int): Unit ={
println(s)
}
类型名称必须唯一。
成员是类的字段、方法或类型的统称。与Java 不同,如果方法有参数列表,该方法可以与类的字段同名:
2 引用与值类型
Java 语法为JVM 实现数据的方式提供了模型。首先,它提供了一组原生类型:short、int、long、float、double、boolean、char、byte 和关键字void。它们被存储在堆栈中,或为了获得更好的性能,被存储于CPU 寄存器。
Scala 固然必须符合JVM 的规则,但Scala 做了改进,使得原生类型和引用类型的区别更明显。
所有引用类型都是AnyRef 的子类型。AnyRef 是Any 的子类型,而Any 是Scala 类型层次的根类型。所有值类型均为AnyVal 的子类型,AnyVal 也是Any 的子类型。Any 仅有这两个直接的子类型。需要注意,Java 的根类型Object(http://docs.oracle.com/javase/8/docs/api/java/lang/Object.html)实际上更接近Scala 的AnyRef,而不是Any。
引用类型仍用new 关键字来创建。类似其他不带参数的方法一样,如果构造器不带参数(在有的语言中称为“默认构造器”),我们在使用构造器时也可以去掉后面的括号。
Scala 沿用了Java 中数字和字符串的字面量语法。例如: 在Scala 中,val name ="Programming Scala" 与val name = new String("Programming Scala") 等价。不过,Scala还为元组增加了字面量语法,(1,2,3) 就等价于new Tuple3(1,2,3)。我们已经接触过Scala的一些语言特性,可以实现编译器原本不支持的字面量写法,如:用1 :: 2 :: 3 :: Nil表示Map("one" ->, "two" -> 2)。
用带apply 方法的对象创建引用类型的实例是很常见的做法,apply 方法起到工厂的作用(这种方法必须在内部调用new 或对应的字面量语法)。由于case 类会自动生成伴随对象及其apply 方法,因此case 类的实例通常就是用这种方法创建出来的。
Short、Int、Long、Float、Double、Boolean、Char、Byte 和Unit 类型称为值类型,分别对应JVM 的原型short、int、long、float、double、boolean、char、byte 和void 关键字。在Scala 的对象模型中,所有的值类型均为AnyVal 的子类型,AnyVal 是Any 的两个子类型之一。
值类型的“实例”不是在堆上创建的。相反,Scala 用JVM 原生类型来表示值类型,它们的值都存放在寄存器或栈上。值类型的“实例”总是用字面量来创建,如1,3.14,true。Unit 对应的字面量是(),不过我们很少显式地使用。
事实上,值类型没有构造器,所以像val I = new Int(1) 这样的表达式将无法通过编译。
3 价值类
package cn.com.tengen.test.obj
class Dollar(val value: Float) extends AnyVal {
override def toString = "$%.2f".format(value)
}
object Dollar extends App{
val hello = new Dollar(100)
println(hello)
}
//输出结果:$100.00
要成为一个有效的价值类,必须遵守以下的规则。
(1) 价值类有且只有一个公开的val 参数(对于Scala 2.11,该参数也可以是不公开的)。
(2) 参数的类型不能是价值类本身。
(3) 价值类被参数化时,不能使用@specialized(http://www.scala-lang.org/api/current/scala/specialized.html)标记。
(4) 价值类没有定义其他构造器。
(5) 价值类只定义了一些方法,没有定义其他的val 和var 变量。
(6) 然而,价值类不能重载equals 和hashCode 方法。
(7) 价值类定义没有嵌套的特征、类或对象。
(8) 价值类不能被继承。
(9) 价值类只能继承自通用特征。
(10) 价值类必须是对象可引用的一个顶级类型或对象的一个成员。
通常,被包装的类型是AnyVal 的子类型之一,但并不是必须如此。如果换成引用类型,我们仍然可以受益于内存不在堆上分配的优势。例如,下例中,隐含了对电话号码字符串的包装:
package cn.com.tengen.test.obj
class USPhoneNumber(val s: String) extends AnyVal {
override def toString = {
val digs = digits(s)
val areaCode = digs.substring(0,3)
val exchange = digs.substring(3,6)
val subnumber = digs.substring(6,10) // “客户编号”
s"($areaCode) $exchange-$subnumber"
}
private def digits(str: String): String = str.replaceAll("""\D""", "")
}
object USPhoneNumber extends App{
val number = new USPhoneNumber("987-654-3210")
println(number)
}
//输出结果:(987) 654-3210
一个通用特征具有以下特性:
(1) 它可以从Any 派生(而不能从其他通用特征派生)。
(2) 它只定义方法。
(3) 它没有对自身做初始化。
下面给出了一个改进版的USPhoneNumber,这里混用了两个通用特征:
package cn.com.tengen.test.obj
/**
* Digitizer 是一个通用特征,定义了我们之前的digits 方法。
*/
trait Digitizer extends Any {
def digits(s: String): String = s.replaceAll("""\D""", "")
}
/**
* Formatter 特征按我们想要的格式对电话号码进行格式化。
*/
trait Formatter extends Any {
def format(areaCode: String, exchange: String, subnumber: String): String =
s"($areaCode) $exchange-$subnumber"
}
class USPhoneNumber(val s: String) extends AnyVal
with Digitizer with Formatter {
override def toString = {
val digs = digits(s)
val areaCode = digs.substring(0,3)
val exchange = digs.substring(3,6)
val subnumber = digs.substring(6,10)
//调用Formatter.format。
format(areaCode, exchange, subnumber)
}
}
object USPhoneNumber extends App{
val number = new USPhoneNumber("987-654-3210")
println(number)
}
//输出结果:(987) 654-3210
Formatter 实际上解决了一个设计上的问题。我们可能要给USPhoneNumber 指定另一个参数作为格式字符串,或需要一些机制去配置toString 的格式,因为流行的格式可能有很多。但是,我们只允许传递一个参数给USPhoneNumber。针对这种情况,我们可以在通用特征中去配置我们想要的格式!然而,由于JVM 的限制,通用特征有时会触发实例化(即实例的内存分配于堆中)。这里将需要实例化的情况总结如下。
(1) 当价值类的实例被传递给函数作参数,而该函数预期参数为通用特征且需要被实例实现。不过,如果函数的预期参数是价值类本身,则不需要实例化。
(2) 价值类的实例被赋值给数组。
(3) 价值类的类型被用作类型参数。
4 父类
子类是从父类或基类中派生的派生类,是大部分面向对象语言的核心特征。这种机制用来重用、封装和实现多态行为(具体行为取决于实例在类型层次结构中的实际类型)。像Java 一样,Scala 只支持单一继承,而不是多重继承。子类(或派生类)可以有且只有一个父类(即基类)。唯一的例外是,Scala 的类型结构中的根类Any 没有父类。
实例:
abstract class BulkReader {
type In
val source: In
def read: String // Read source and return a String
}
class StringBulkReader(val source: String) extends BulkReader {
type In = String
def read: String = source
}
class FileBulkReader(val source: java.io.File) extends BulkReader {
type In = java.io.File
def read: String = {...}
}
如在Java 中一样,关键字extend 表示后面是父类,因此本例中的父类为BulkReader。
在Scala 中,当类继承trait 时,也用extend 表示(甚至当该类用with 关键字混入了其他trait时也是如此)。
此外,当trait 是其他trait 或类的子trait 时,也用extend。是的,trait 可以继承类。
如果我们不指定父类,默认父类为AnyRef。
5 Scala的构造器
Scala 将主构造器与零个或多个辅助构造器区分开,辅助构造器也被称为次级构造器。在Scala 中,主构造器是整个类体。构造器所需的所有参数都被罗列在类名称后面。
package cn.com.tengen.test.obj
case class Address(street: String, city: String, state: String, zip: String) {
def this(zip: String) = this("[unknown]", Address.zipToCity(zip), Address.zipToState(zip), zip)
}
object Address {
def zipToCity(zip: String) = "Anytown"
def zipToState(zip: String) = "CA"
}
case class Person(name: String, age: Option[Int], address: Option[Address]) {
def this(name: String) = this(name, None, None)
def this(name: String, age: Int) = this(name, Some(age), None)
def this(name: String, age: Int, address: Address) = this(name, Some(age), Some(address))
def this(name: String, address: Address) = this(name, None, Some(address))
}
构造器调用
object Main extends App{
var address = new Address("Lucky");
var p = Person("lucky",Some(20),Some(address));
println(p.name+"---"+p.age.get+"---"+p.address.get)
}
在我们的实现中,用户使用 new 来创建实例,使用主构造器创建实例时new可以省略
6 类的字段
如果在主构造函数参数的前面加上val 或var 关键字,该参数就成为实例的一个字段。对于case 类,val 是默认的。这样可以大大减少冗余的代码。是Scala 会自动做Java 代码中明显做的事情。类会创建一个私有的字段,并生产对应的getter 和setter 访问方法。
class Name (var value: String)
改代码等价于
class Name(s: String){
private var _value: String = s //不可见的字段,在本例中声明为可变变量。
def value: String = _value //getter,即读方法。
def value_= (newValue: String): Unit = _value = newValue //setter,即写方法。
}
注意value_= 这个方法名的一般规范。当编译器看到这样的一个方法名时,它会允许客户端代码去掉下划线_,转而使用中缀表达式,这就好像我们是在设置对象的一个裸字段一样:
object Main extends App{
val name = new Name("Lucky")
println(name.value)
name.value_=("Helen")
println(name.value)
name.value="Jon"
println(name.value)
name.value=("Lucy")
println(name.value)
}
运行结果:
Lucky
Helen
Jon
Lucy
6.1 统一访问原则
Scala 没有遵循JavaBeans 的约定规范,没有把value 字段的读方法和写方法分别命名为getValue 和setValue,但是Scala 遵循统一访问的原则。正如我们在Name 这个例子中看到的,客户端似乎可以不经过方法就对“裸”字段值进行读和写的操作,但实际上它们调用了方法。
请注意,用户的“体验”是一致的。用户代码不了解实现,这使我们可以在需要的时候,自由地将直接操作裸字段改为通过访问方法来操作。例如:我们要在写操作中添加某些验证工作,或者为了提高效率,在读取某个结果时采用惰性求值。这些情况下,我们可以通过访问方法来操作裸字段。相反地,我们也可以用公开可见性的字段代替访问方法,以消除该方法调用的开销(尽管JVM 可能会消除这种开销)。因此,统一访问原则的重要益处在于,它能最大程度地减少客户端代码对类的内部实现所必须了解的知识。尽管重新编译仍然是必需的,我们可以改变具体实现,而不必强制客户端代码跟着做出改变。
Scala 实现统一访问原则的同时,没有牺牲访问控制功能,并且满足了在简单的读写基础上增加其他逻辑的需求。
6.2 一元方法
package cn.com.tengen.test.obj
case class Complex(real: Double, imag: Double) {
//方法名为unary_X,这里X 就是我们想要使用的操作符。在本例中,X 就是-。注意-和: 之间的空格,空格在这里是必须的,它可以告诉编译器方法名以- 而不是: 结尾!为了比较,我们也实现了常见的减号操作符。
def unary_- : Complex = Complex(-real, imag)
def -(other: Complex) = Complex(real - other.real, imag - other.imag)
}
object Main extends App{
val c1 = Complex(88.8, 88.8)
val c2 = -c1
val c3 = c1.unary_-
val c4 = c1 - Complex(22.2, 22.2)
println(c1)
println(c2)
println(c3)
println(c4)
}
输出结果:
Complex(88.8,88.8)
Complex(-88.8,88.8)
Complex(-88.8,88.8)
Complex(66.6,66.6)
方法名为unary_X,这里X 就是我们想要使用的操作符。在本例中,X 就是-。
注意-和: 之间的空格,空格在这里是必须的,它可以告诉编译器方法名以- 而不是: 结尾!
我们一旦定义了一元操作符,就可以将操作符放在实例之前,就像我们在定义c2 时所做的那样。也可以像定义c3 时那样,将一元操作符当做其他方法一般进行调用。
7 验证输入
保证创建实例参数处于有效的状态,示例如下:
package cn.com.tengen.test.obj
case class ZipCode(zip: Int, extension: Option[Int] = None) {
// 使用require 方法验证输入。
require(valid(zip, extension), s"Invalid Zip+4 specified: $toString")
protected def valid(z: Int, e: Option[Int]): Boolean = {
if (0 < z && z <= 99999) e match {
case None => validUSPS(z, 0)
case Some(e) => 0 < e && e <= 9999 && validUSPS(z, e)
}
else false
}
/**
* 真正的方法实现应该查询USPS 认可的数据库来验证邮政编码是否确实存在。
*/
protected def validUSPS(i: Int, e: Int): Boolean = true
/**
* 覆写toString 方法,返回符合人们预期的邮政编码格式,对结尾可能的四位数字进行覆写toString 方法,
* @return 返回符合人们预期的邮政编码格式,对结尾可能的四位数字进行
*/
override def toString = if (extension != None) s"$zip-${extension.get}" else zip.toString
}
object ZipCode {
def apply (zip: Int, extension: Int): ZipCode =
new ZipCode(zip, Some(extension))
def main(args: Array[String]): Unit = {
var z1 = ZipCode(12345)
println(z1) //12345
var z2 = ZipCode(12345, Some(6789))
println(z2) //12345-6789
var z3 = ZipCode(123456)
println(z3) //异常 java.lang.IllegalArgumentException: requirement failed: Invalid Zip+4 specified: 123456
}
}
定义ZipCode 这种领域专用的类的充分理由是:这种类可以在构造时对值的有效性做一次 检验,然后类ZipCode 的使用者就不再需要再次检验了。
虽然我们在构造器的背景下讨论输入的验证,但实际上我们也可以在任何方法中调用这些断言方法。然而,价值类的类体是一个例外,它不能使用断言验证,否则就需要调用分配堆。不过,由于ZipCode 带有两个构造器参数,它无论如何不会是价值类。
8 调用父类构造器与良好的面向对象设计
派生类的主构造器必须调用父类的构造器,可以是父类的主构造器或次级构造器。
package cn.com.tengen.test.obj
case class Address(street: String, city: String, state: String, zip: String) {
def this(zip: String) = this("[unknown]", Address.zipToCity(zip), Address.zipToState(zip), zip)
}
object Address {
def zipToCity(zip: String) = "Jiaxing"
def zipToState(zip: String) = "CA"
}
case class Person( name: String, age: Option[Int] = None, address: Option[Address] = None)
class Employee( name: String, age: Option[Int] = None, address: Option[Address] = None, val title: String = "[unknown]", val manager: Option[Employee] = None)
extends Person(name, age, address) {
override def toString = s"Employee($name, $age, $address, $title, $manager)"
}
object Employee extends App {
val a1 = new Address("1 Scala Lane", "Anytown", "CA", "98765")
val a2 = new Address("98765")
val ceo1 = new Employee("Joe CEO", title = "CEO")
println(ceo1) //Employee(Joe CEO, None, None, CEO, None)
val ceo2 = new Employee("Buck Trends1");
println(ceo2) //Employee(Buck Trends1, None, None, [unknown], None)
}
在Java 中,我们会定义构造方法,并用super 调用父类的初始化逻辑。而Scala 中,我们用ChildClass(…) extends ParentClass(…) 的语法隐式地调用父类的构造器。
当使用继承时,建议遵循以下规则。
(1) 一个抽象的基类或trait,只被下一层的具体的类继承,包括case 类。
(2) 具体类的子类永远不会再次被继承,除了两种情况:
a. 类中混入了定义于trait中的其他行为。理想情况下, 这些行为应该是正交的, 即不重叠的。
b. 只用于支持自动化单元测试的类。
(3) 当使用子类继承似乎是正确的做法时,考虑将行为分离到trait 中,然后在类里混入这些trait。
(4) 切勿将逻辑状态跨越父类和子类。
换一种实现方式:
Employee 不再是Person 的一个子类,但它是PersonState 的一个子类,因为它混入了该trait。
package cn.com.tengen.test.obj
case class Address(street: String, city: String, state: String, zip: String)
object Address {
def apply(zip: String) = new Address("[unknown]", Address.zipToCity(zip), Address.zipToState(zip), zip)
def zipToCity(zip: String) = "Anytown"
def zipToState(zip: String) = "CA"
}
trait PersonState {
val name: String
val age: Option[Int]
val address: Option[Address]
}
case class Person(name: String,age: Option[Int] = None,address: Option[Address] = None)
extends PersonState
trait EmployeeState {
val title: String
val manager: Option[Employee]
}
case class Employee(name: String,age: Option[Int] = None, address: Option[Address] = None, title: String = "[unknown]", manager: Option[Employee] = None)
extends PersonState with EmployeeState
object Person extends App{
val ceoAddress = Address("1 Scala Lane", "Anytown", "CA", "98765")
println(ceoAddress)
val buckAddress = Address("98765")
println(buckAddress)
val ceo = Employee( name = "Joe CEO", title = "CEO", age = Some(50), address = Some(ceoAddress), manager = None)
println(ceo)
val ceoSpouse = Person("Jane Smith", address = Some(ceoAddress))
println(ceoSpouse)
val buck = Employee( name = "Buck Trends", title = "Zombie Dev", age = Some(20), address = Some(buckAddress), manager = Some(ceo))
println(buck)
val buckSpouse = Person("Ann Collins", address = Some(buckAddress))
println(buckSpouse)
}
输出结果:
Address(1 Scala Lane,Anytown,CA,98765)
Address([unknown],Anytown,CA,98765)
Employee(Joe CEO,Some(50),Some(Address(1 Scala Lane,Anytown,CA,98765)),CEO,None)
Person(Jane Smith,None,Some(Address(1 Scala Lane,Anytown,CA,98765)))
Employee(Buck Trends,Some(20),Some(Address([unknown],Anytown,CA,98765)),Zombie Dev,Some(Employee(Joe CEO,Some(50),Some(Address(1 Scala Lane,Anytown,CA,98765)),CEO,None)))
Person(Ann Collins,None,Some(Address([unknown],Anytown,CA,98765)))
9 嵌套类型
Scala 允许我们嵌套类型的成名和定义。例如:在对象中定义类型转义的异常和其他有用的类型,就是很常见的做法。以下是一个数据库层可能的骨架结构:
package cn.com.tengen.test.obj
object Database {
case class ResultSet(/*...*/)
case class Connection(/*...*/)
case class DatabaseException(message: String, cause: Throwable) extends RuntimeException(message, cause)
/**
* 使用sealed 的继承结构表示状态;所有允许的值都在这里定义。当实例实际上不携带状态信息时,使用case 对象。这些对象表现得像“标志位”,用来表示状态。
*/
sealed trait Status
case object Disconnected extends Status
case class Connected(connection: Connection) extends Status
case class QuerySucceeded(results: ResultSet) extends Status
case class QueryFailed(e: DatabaseException) extends Status
}
class Database {
import Database._
/**
* ??? 是定义在Predef 中的真实方法。它会抛出一个异常,用来表示某一方法尚未实现的情况。该方法是最近才引入Scala 库的。
*/
def connect(server: String): Status = ???
def disconnect(): Status = ???
def query(/*...*/): Status = ???
}
当case 类没有用任何字段表示状态信息时,考虑使用case 对象。
当方法还正处在开发阶段时,??? 方法作为占位符十分有用。因为这样可以使代码通过编译。问题是你没法调用那个方法!
为case 对象生成的hashCode 方法仅仅将对象的名称做了散列。
def main(args: Array[String]): Unit = {
println(Disconnected.hashCode())
println("Disconnected".hashCode())
}
输出结果:
-1217068453
-1217068453