Class and Object Basics
类使用关键字class声明,其单例对象使用object声明.
限制从一个类中派生新类时使用final关键字.
标识一个抽象类时使用abstract关键字,比如只包含 字段/方法或类型的声明而没有具体实现时.即使不包含任何未定义的成员,仍然可以声明为一个抽象类.
一个实例可以使用this关键字来引用自身.this在java中比较常见,而scala则使用稀少,原因是scala中缺少构造器模板.
一个java的实例:
public class JPerson {
private String name;
private int age;
public JPerson(String name, int age) {
this.name = name;
this.age = age;
}
publicvoid setName(Stringname) {this.name=name;}
public String getName() { return this.name; }
public void setAge(int age) { this.age = age; }
public int getAge() { return this.age; }
}
然后对比一下scala中的代码,所有的构造器模板都被隐藏了:
class Person(var name: String, var age: Int)
字段名以var前缀表示是一个可变字段,或叫做实例变量或属性.val则表示不可变字段.使用case 关键字声明类时默认指定为val并提供额外的方法:
case class ImmutablePerson(name: String, age: Int)
Scala支持overloaded methods,多个方法可以使用一个name,但是签名必须是唯一的,签名部分包含类型名,方法名,参数列表类型,参数名本身没有限制.在JVM中,不同的返回值类型不足够去区分一个方法.
术语member以泛型的方式被引用为field,method,type,与java不同的是,如果方法有一个参数列表,则方法可以和字段同名.
Scala中可以使用type声明类型成员,这些类型成员是类型参数化的一种补充机制,经常被用来作为复杂类型的别名,以提供可读性.
Type名必须唯一.
Scala没有静态成员,通常使用object存放一些跨实例的成员,比如常量.
如果一个object和一个class同名并在同一个文件,则该object作为该类的半生对象. 对于case类,编译器会自动生成一个伴生对象.
Reference Versus Value Types
Java中short, int, long, float, double, boolean, char, byte, void作为原始类型,他们被存储在堆栈中,或者为了更高的性能存储在CPU寄存器.除此之外均称为引用类型,他们的实例被分配在堆中,引用这些实例的变量实际上是指向了堆中的对应位置.
Scala遵循了这些规则,但是更清晰的分离了原始类型和引用类型.
引用类型都是AnyRef的子类型.AnyRef为Any的子类型,Scala类型系统的底层类型.所有值类型都是AnyVal的子类型,同时属于Any的子类型.而Java中根类型是object,更接近于AnyRef而不是Any.
引用类型的实例由new关键字创建,跟没有参数的方法一样,如果构造器为空则同样可以省略括号.
值类型没有构造器,所以像 val a = new Int(1) 这样的表达式将无法通过编译.
Short, Int, Long, Float, Double, Boolean, Char, Byte, and Unit被称为值类型,与java一致,这些值类型均是AnyVal的子类型.
Value Classes
Scala中经常引入包装类型来实现新类型,这被称为扩展方法,但是对值类型的包装,会将值类型编程引用类型,从而失去原始类型的性能.
Scala中的解决方案是 value class,和一个附带的特质,称为通用特质.这些类型限制了可声明范围,但也带来了好处: 避免封装分配在堆上.
定义一个value class:
class Dollar(val value: Float) extends AnyVal { // 只有一个val型参数,继承自AnyVal
override def toString = "$%.2f".format(value)
}
val benjamin = new Dollar(100)
// Result: benjamin: Dollar = $100.00
要称为一个value calss,必须遵循以下规则:
- 有且只有一个公共的val型参数
- 参数类型必须不为该value class本身
- 如果该类被参数话,不能使用@specialized注释
- 不可定义辅助构造器
- 只能定义方法,不能有其他的val或var
- 不能重写equals和hashCode
- 不能定义嵌套的raits, classes, or objects
- 不能被子类化
- 只能继承universal traits
- 必须是一个顶层类型或被引用的对象
上面的例子中,Dollar在编译时是一个外部类型,而在运行时,该类型是一个被包装过的类型,即float.
通常情况下,被包装过的类型是AnyVal的子类型之一,但并不必须这样,如果换成引用类型,仍然可以收益于内存不再堆上分配的优势.
下面使用String而不是原始类型来实现一个value class:
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""", "")
}
val number = new USPhoneNumber("987-654-3210")
// 结果: number: USPhoneNumber = (987) 654-3210
同样特质的特性:
- 可以从Any派生(而不能从其他同样特质派生)
- 只能定义方法
- 没有对自身做初始化
下面是一个改进版的USPhoneNumber,混入了两个通用特质:
trait Digitizer extends Any { // 1
def digits(s: String): String = s.replaceAll("""\D""", "")
}
trait Formatter extends Any { // 2
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) // 3
val areaCode = digs.substring(0,3)
val exchange = digs.substring(3,6)
val subnumber = digs.substring(6,10)
format(areaCode, exchange, subnumber) // 4
}
}
val number = new USPhoneNumber("987-654-3210")
// 结果: number: USPhoneNumber = (987) 654-3210
- 定义了一个通用特质,以实现digits方法
- 定义一个通用特质,实现format方法
- 分别调用特质中的方法
Formatter解决了一个设计问题,因为USPhoneNumber中只能传入一个参数,但是如果想要传入别的参数来实现可配置的格式化时,同过特质传入参数能解决value class的参数限制问题.
但是通用特质有时会触发实例化(即将实例的内存分配到堆中),下面是会触发实例化的情况:
- 当value class的实例被传递给函数作为参数,而该函数预期参数为通用特质且需要被实例实现.不过,如果函数的参数是value class本身,则不需要实例化.
- value class 的实例被赋值给数组
- value class 的类型被用于类型参数
比如,当用USPhoneNumber调用一下方法时,会创建一个USPhoneNumber实例:
def toDigits(d: Digitizer, str: String) = d.digits(str)
...
val digs = toDigits(new USPhoneNumber("987-654-3210"), "123-Hello!-456")
// 结果: digs: String = 123456
或者用USPhoneNumber调用一下参数化类型的方法时,也会产生USPhoneNumber的实例:
def print[T](t: T) = println(t.toString)
print(new USPhoneNumber("987-654-3210"))
// 结果: (987) 654-3210
总结: value class 提供了一个低开销的技术,用于定义扩展方法,并为类型定义有意义的类型名称,这利用了被包装值的类型安全性.
Parent Types
与java一样,scala只支持但继承,不支持多继承,一个子类只能有一个父类.
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 // 指定类型成员In为String类型
def read: String = source
}
class FileBulkReader(val source: java.io.File) extends BulkReader {
type In = java.io.File
def read: String = {...}
}
Constructors in Scala
Scala通常有一个主构造器,0个或多个辅助构造器. 主构造器即整个class的主体部分,所有构造器需要的参数都列在类名后.
case class Address(street: String, city: String, state: String, zip: String) {
def this(zip: String) = // 1
this("[unknown]", Address.zipToCity(zip), Address.zipToState(zip), zip)
}
object Address {
def zipToCity(zip: String) = "Anytown" // 2
def zipToState(zip: String) = "CA" }
case class Person(name: String, age: Option[Int], address: Option[Address]) { // 3
def this(name: String) = this(name, None, None) // 4
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))
}
- 一个仅接受zip参数的辅助构造器,它调用辅助方法来推断city和state,但是不能推断street.
- 辅助方法从zip参数查找对应的city和state(假设实现)
- 指定age和address参数可选
- 提供多种构造器以便用户提供一个或多个参数
辅助构造器均被命名为this,它的第一个构造器必须调用主构造器或其他辅助构造器.并且编译器要求被调用的构造器必须先于当前构造器出现,因此必须小心排列构造器顺序.
通过强制让所有构造器最终都调用主构造器,可以将代码冗余最小化,并确保新实例的初始化逻辑一致.
在使用中会发现,Person有很多参数类似的次级构造器.这时可以对可选参数指定默认值,而在创建实例时可以指定参数名来提供具体参数:
case class Person2( name: String, age: Option[Int] = None, address: Option[Address] = None)
但是在创建实例时,除非是调用主构造器,否则必须使用 new 关键字,因为编译器不会自动为case类的次级构造器创建apply方法.
调用实例:
import progscala2.basicoop.{Address, Person}
val a1 = new Address("1 Scala Lane", "Anytown", "CA", "98765")
// Result: Address(1 Scala Lane,Anytown,CA,98765)
val a2 = new Address("98765")
// Result: Address([unknown],Anytown,CA,98765)
new Person("Buck Trends1")
// Result: Person(Buck Trends1,None,None)
new Person("Buck Trends2", Some(20), Some(a1))
// Result: Person(Buck Trends2,Some(20),
// Some(Address(1 Scala Lane,Anytown,CA,98765)))
new Person("Buck Trends3", 20, a2)
// Result: Person(Buck Trends3,Some(20),
// Some(Address([unknown],Anytown,CA,98765)))
new Person("Buck Trends4", 20)
// Result: Person(Buck Trends4,Some(20),None)
这时我们可以在伴生对象中(case类的object)重载apply方法,以获得便利的构造器,而不需要再使用new关键字.
case class Person3( name: String, age: Option[Int] = None, address: Option[Address] = None)
object Person3 {
// 由于我们重载的是普通方法,而不是构造器, 所以必须明确地指定返回类型(因为如果通过使用this来重载构造器,返回的仍然是该类型的实例),在这里返回类型是Person3。
def apply(name: String): Person3 = new Person3(name)
def apply(name: String, age: Int): Person3 = new Person3(name, Some(age))
def apply(name: String, age: Int, address: Address): Person3 = new Person3(name, Some(age), Some(address))
def apply(name: String, address: Address): Person3 = new Person3(name, address = Some(address))
}
注意,与构造器不同,apply这样的重载方法必须指定返回类型.
在使用时,如果提供了与主构造器签名一致的参数,则会调用case类自动生成的apply方法,否则会依次去匹配其他的辅助构造器.
Fields in Classes
class Name(var value: String)
被展开:
class Name(s: String) {
private var _value: String = s // 1
def value: String = _value // 2
def value_=(newValue: String): Unit = _value = newValue // 3
}
- 内部可变字段
- getter
- setter
上面setter中的 value_= 是一个完整的方法名,(newValue: String) 是其参数列表.当编译器看到这样一个方法名时,会允许去掉_,使用中缀表达式,就好像我们是在设置一个对象的字段一样:
name.value_=("Bubba") // 默认的调用方法
name.value = "Hank" // 使用中缀表达式的方法
如果参数声明为val,则不会生成setter方法,因为其不可变.
同时,对于非case类,如果我们向构造器传递参数时不希望该参数称为类的一个字段,可以在构造器中省略val或var.但是该值仍然在类体的作用范围内,仍然指向构造实例时所用的参数,只是没有被声明为类的字段,在类体中都是可见的,只能被类的成员在类内部使用,比如类的成员方法,但对类的客户端不可见.
Scala并没有采用Java风格的getter和setter方法,而是支持统一访问原则.在统一访问原则中,读写”裸”字段的语法和通过间接调用方法读写的语法是一样的.
一元方法
case class Complex(real: Double, imag: Double) {
def unary_- : Complex = Complex(-real, imag) // 1
def -(other: Complex) = Complex(real - other.real, imag - other.imag)
}
val c1 = Complex(1.1, 2.2)
val c2 = -c1 // Complex(-1.1, 2.2)
val c3 = c1.unary_- // Complex(-1.1, 2.2)
val c4 = c1 - Complex(0.5, 1.0) // Complex(0.6, 1.2)
- 方法名
unary_X, X就是我们想要的操作符,上例中为-,注意在:之前有一个空格,以告诉编译器方法名是以-结尾而不是:.
一旦定义了一元操作符,就可以将操作符放在实例之前进行调用.
验证输入
如果需要验证输入的参数,以确保创建有效的实例,可以使用Predef中定义的require方法.
比如对一个邮编进行验证:
case class ZipCode(zip: Int, extension: Option[Int] = None) {
require(valid(zip, extension), s"Invalid Zip+4 specified: $toString") // 1
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
}
/**这是个有效的美国邮政编码吗? */
protected def validUSPS(i: Int, e: Int): Boolean = true // 2
override def toString = // 3
if (extension != None) s"$zip-${extension.get}" else zip.toString
}
object ZipCode { // 4
def apply (zip: Int, extension: Int): ZipCode = new ZipCode(zip, Some(extension))
}
- 使用 require 方法验证参数输入
- 真正的方法实现是查询USPS认可的数据库来验证编码是否真的存在
- 重写toString方法,返回标准的邮编格式
- 同时提供了一个apply方法,以提供便利的实例化方法,在提供可选参数extension时不需要再提供Some()
同时还可以使用Predef中的ensuring和assume实现类似功能.
调用父类构造器
子类的构造器必须调用父类的构造器,可以是父类的主构造器或辅助构造器.
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)"
}
val a1 = new Address("1 Scala Lane", "Anytown", "CA", "98765")
val a2 = new Address("98765")
Person是一个case类,而Employee被声明为一个普通的类.子类中新增的两个字段title和manager需要使用val关键字声明,因为Employee不是case类.同时调用了Person的主构造器,调用时只需要提供参数名即可.重写了toString方法,如果不重写则会调用父类的toString.
在Java中需要定义构造方法,并用super调用父类的初始化逻辑,但是在Scala中,使用 ChildClass(...) extends ParentClass(...) 这样的语法隐式的调用父类构造器.
良好的面向对象设计
可以从一个case类中派生一个普通类,或者反过来,但是不能从一个case类派生另一个case类,因为,自动生成的toString,equals,hashCode方法在子类中无法正常工作.
case类为方便简单的域类型提供了模式匹配和实例分解,但支持继承结构并不是他们的目的.
当使用继承时,可以遵循以下规则:
- 一个抽象的基类或者trait,只被下一层的具体的类继承,包括case类
- 具体类的子类永远不会再次被继承,除了两种情况: 类中混入了定义于trait中的其他行为,理想情况下,这些行为是不重叠的.只用于自动化单元测试的类.
- 当使用子类继承似乎是争取的做法时,考虑将行为分离到trait中,然后在类里面混入这些trait
- 切勿将逻辑状态跨越子类和父类,比如一些私有的专门实现的状态,这种状态不影响外部可见性,相等,散列逻辑.