您的位置:首页 > 运维架构

Scala Cookbook读书笔记 Chapter 4.Classes and Properties 第二部分

2016-09-29 14:35 357 查看

4.8 分配块或函数给字段

使用代码块或者调用一个函数初始化类里的字段

4.8.1 解决方案

设置字段等于需要的代码块或者函数

class Foo {

// set 'text' equal to the result of the block of code
val text = {
var lines = ""
try {
lines = io.Source.fromFile("/etc/passwd").getLines.mkString
} catch {
case e: Exception => lines = "Error happened"
}
lines
}
println(text)
}

object Test extends App {
val f = new Foo
}


上面分配代码块给text字段和println语句都在类Foo的主体部分中,他们都是类的构造函数,当创建一个类的实例时他们会被执行。

同样方式。分配方法或者函数给类字段

class Foo {
import scala.xml.XML

// assign the xml field to the result of the load method
val xml = XML.load("http://example.com/foo.xml")

// more code here ...
}


4.8.2 讨论

如果定义一个字段为lazy,意味着不会立马执行直到字段被获取调用:

class Foo {
val text =
io.Source.fromFile("/etc/passwd").getLines.foreach(println)
}

object Test extends App {
val f = new Foo
}


上面代码忽略潜在错误,如果运行在Unix系统,会打印/etc/passwd文件

class Foo {
lazy val text =
io.Source.fromFile("/etc/passwd").getLines.foreach(println)
}

object Test extends App {
val f = new Foo
}


上面声明成lazy字段的代码编译执行后,没有任何输出。因为text字段不会初始化直到它被获取。

4.9 设置未初始化的var字段类型

问题: 想要设置一个未初始化var字段类型,所以开始写代码如下,然后如何完成表达式。

var x =


4.9.1 解决方案

一般来说,定义字段为Option。对于具体的类型,比如String和numeric字段,可以指定默认的初始化值,下面的address字段可以定义成Option,初始化如下。

case class Person(var username: String, var password: String) {

var age = 0
var firstName = ""
var lastName = ""
var address = None: Option[Address]
}

case class Address(city: String, state: String, zip: String)


使用Some[Address]赋值

val p = Person("alvinalexander", "secret")
p.address = Some(Address("Talkeetna", "AK", "99676"))


如想要获取address字段,有很多方法可见20.6章。可以使用foreach循环打印:

p.address.foreach { a =>
println(a.city)
println(a.state)
println(a.zip)
}


如果address未赋值,那么address是一个None,调用foreach没有问题,循环会自动跳出。如果已经赋值,那么address是一个Some[Address],循环会进入然后打印。

4.9.2 讨论

很容易创建一个Int和Double字段

var i = 0 // Int
var d = 0.0 // Double


上面例子中编译器会自动区分需要的类型。如果需要不同的数字类型,方法如下:

var b: Byte = 0
var c: Char = 0
var f: Float = 0
var l: Long = 0
var s: Short = 0


查看更多

Option class

不要设置字段为null,更多见20.5章:“Eliminate null Values from Your Code”

20.6章:“Using the Option/Some/None Pattern”

4.10 当继承类时处理构造函数参数

问题:当继承一个基类时,需要处理基类声明的构造函数参数以及子类新的参数

4.10.1 解决方案

往常一样使用val或者var构造函数参数声明基类,当定义子类构造函数时,去掉两个类中相同字段前的val或者var声明,当定义子类新的构造函数参数时使用val或者var声明。

class Person (var name: String, var address: Address) {
override def toString = if (address == null) name else s"$name @ $address"
}

case class Address (city: String, state: String)


子类Employee

class Employee (name: String, address: Address, var age: Int)
extends Person (name, address) {
// rest of the class
}


创建Employee实例

val teresa = new Employee("Teresa", Address("Louisville", "KY"), 25)


输出

scala> teresa.name
res0: String = Teresa

scala> teresa.address
res1: Address = Address(Louisville,KY)

scala> teresa.age
res2: Int = 25


4.10.2 讨论

理解Scala编译器如何转换你的代码有助于理解子类构造函数参数如何工作,下面代码放到文件Person.scala中:

case class Address (city: String, state: String)

class Person (var name: String, var address: Address) {
override def toString = if (address == null) name else s"$name @ $address"
}


上面字段是var变量,Scala编译器生成了获取器和修改器,编译Person.scala反编译Person.class如下:

$ javap Person
Compiled from "Person.scala"
public class Person extends java.lang.Object implements scala.ScalaObject{
public java.lang.String name();
public void name_$eq(java.lang.String);
public Address address();
public void address_$eq(Address);
public java.lang.String toString();
public Person(java.lang.String, Address);
}


新的问题:如果定义一个Employee类继承Person,如何处理Employee构造函数里的name和address字段?假设没有新的参数,至少有两种选择:

// Option 1: define name and address as 'var'
class Employee (var name: String, var address: Address)
extends Person (name, address) { ... }

// Option 2: define name and address without var or val
class Employee (name: String, address: Address)
extends Person (name, address) { ... }


因为Scala已经为Person类里的name和address声明了getter和setter方法,解决方法是不使用var进行声明:

// this is correct
class Employee (name: String, address: Address)
extends Person (name, address) { ... }


把下面代码编译反编译,文件名是Person.scala,反编译Employee.class

case class Address (city: String, state: String)

class Person (var name: String, var address: Address) {
override def toString = if (address == null) name else s"$name @ $address"
}
class Employee (name: String, address: Address)
extends Person (name, address) {
// code here ...
}


反编译结果如下:

$ javap Employee
Compiled from "Person.scala"
public class Employee extends Person implements scala.ScalaObject{
public Employee(java.lang.String, Address);
}


Employee继承Person,Scala不为name和address字段生成getter和setter方法,Employee类继承了Person类的行为。

4.11 调用超类构造函数

问题:想要控制当创建子类构造函数时调用的父类构造函数

4.11.1 解决方案

这有一个问题,你可以控制子类的主构造函数调用的父类构造函数,但是不可以控制子类的辅助构造函数调用的父类构造函数。

下面例子,定义一个Dog类去调用Animal类的主构造函数:

class Animal (var name: String) {
// ...
}

class Dog (name: String) extends Animal (name) {
// ...
}


如果Animal类有多个构造函数,那么Dog类的主构造函数可以调用任意一个

// (1) primary constructor
class Animal (var name: String, var age: Int) {

// (2) auxiliary constructor
def this (name: String) {
this(name, 0)
}
override def toString = s"$name is $age years old"
}

// calls the Animal one-arg constructor
class Dog (name: String) extends Animal (name) {
println("Dog constructor called")
}

// call the two-arg constructor
class Dog (name: String) extends Animal (name, 0) {
println("Dog constructor called")
}


4.11.2 辅助构造函数

辅助构造函数的第一行必须调用当前类的另一个构造函数,不可能调用父类的构造函数

case class Address (city: String, state: String)
case class Role (role: String)

class Person (var name: String, var address: Address) {

// no way for Employee auxiliary constructors to call this constructor
def this (name: String) {
this(name, null)
address = null
}

override def toString = if (address == null) name else s"$name @ $address"
}

class Employee (name: String, role: Role, address: Address)
extends Person (name, address) {

def this (name: String) {
this(name, null, null)
}

def this (name: String, role: Role) {
this(name, role, null)
}

def this (name: String, address: Address) {
this(name, null, address)
}

}


4.12 使用抽象类(Abstract Class)

问题:Scala有特质(trait),而且特质比抽象类更灵活,那么什么时候使用抽象类

4.12.1 解决方案

以下两点使用抽象类:

创建一个需要构造函数参数的基类

Scala代码会被Java代码调用

特质不允许有构造函数参数:

// this won't compile
trait Animal(name: String)

// this compile
abstract class Animal(name: String)


17.7章解决特质实现的方法不能被Java代码调用的问题

4.12.2 讨论

一个类智能继承一个抽象类。

声明抽象方法:

def speak // no body makes the method abstract


抽象方法不需要使用abstract关键词,去除方法的body就会变成抽象方法。这和在特质里定义抽象方法是一致的。

abstract class BaseController(db: Database) {

def save { db.save }
def update { db.update }
def delete { db.delete }

// abstract
def connect

// an abstract method that returns a String
def getStatus: String

// an abstract method that takes a parameter
def setServerName(serverName: String)
}


子类继承之后需要实现抽象方法或者继续声明成抽象的,不实现方法会报出“class needs to be abstract”错误

scala> class WidgetController(db: Database) extends BaseController(db)
<console>:9: error: class WidgetController needs to be abstract, since:
method setServerName in class BaseController of type (serverName: String)Unit
is not defined
method getStatus in class BaseController of type => String is not defined
method connect in class BaseController of type => Unit is not defined
class WidgetController(db: Database) extends BaseController(db)
^


因为类只能继承一个抽象类,当决定使用特质还是抽象类时一般使用特质,除非基类需要构造函数参数

4.13 在抽象基类(或特质)中定义属性

问题:在抽象基类(或特质)中定义抽象或具体属性可供所有子类引用

4.13.1 解决方案

可以在抽象类或者特质里声明val和var字段。这些字段可以是抽象或者有具体实现。

4.13.2 抽象的val和var字段

下面抽象类有抽象的val和var字段,一个简单的具体方法:

abstract class Pet (name: String) {
val greeting: String
var age: Int
def sayHello { println(greeting) }
override def toString = s"I say $greeting, and I'm $age"
}


子类继承抽象类,然后为抽象的字段赋值,注意这些字段还是指定成val或者var:

class Dog (name: String) extends Pet (name) {
val greeting = "Woof"
var age = 2
}

class Cat (name: String) extends Pet (name) {
val greeting = "Meow"
var age = 5
}


object中演示调用:

object AbstractFieldsDemo extends App {
val dog = new Dog("Fido")
val cat = new Cat("Morris")

dog.sayHello
cat.sayHello

println(dog)
println(cat)

// verify that the age can be changed
cat.age = 10
println(cat)
}


结果输出:

Woof
Meow
I say Woof, and I'm 2
I say Meow, and I'm 5
I say Meow, and I'm 10


4.13.3 讨论

抽象类(或特质)里抽象字段的运行如下:

一个抽象的var字段会自动生成getter和setter方法

一个抽象的val字段会自动生成getter方法

当在抽象类或特质里定义一个抽象字段,Scala编译器不会在结果代码里创建一个字段,只会根据val或者var生成相应的方法

上面的代码通过 scalac -Xprint:all,或者反编译Pet.class文件,会发现没有greeting或者age字段。反编译输出如下:

import scala.*;
import scala.runtime.BoxesRunTime;

public abstract class Pet
{
public abstract String greeting();
public abstract int age();
public abstract void age_$eq(int i);

public void sayHello() {
Predef$.MODULE$.println(greeting());
}

public String toString(){
// code omitted
}
public Pet(String name){}
}


所以当你在具体的类里给这些字段提供具体的值时,必须重新定义字段为val或者var。因为在抽象类或者特质里这些字段实际并不存在,所以override关键词并不需要。

另一个结果,可以在抽象基类使用def定义无参取代使用val定义,然后可以在具体类里定义成val。

abstract class Pet (name: String) {
def greeting: String
}

class Dog (name: String) extends Pet (name) {
val greeting = "Woof"
}

object Test extends App {
val dog = new Dog("Fido")
println(dog.greeting)
}


4.13.4 抽象类里具体的val字段

抽象类里定义一个具体的val字段可以提供一个初始化值,然后可以在具体子类重写那个值。

abstract class Animal {
val greeting = "Hello" // provide an initial value
def sayHello { println(greeting) }
def run
}

class Dog extends Animal {
override val greeting = "Woof" // override the value
def run { println("Dog is running") }
}


上面例子中,两个类中都创建了greeting字段

abstract class Animal {
val greeting = { println("Animal"); "Hello" }
}

class Dog extends Animal {
override val greeting = { println("Dog"); "Woof" }
}

object Test extends App {
new Dog
}


结果输出:

Animal
Dog


可以反编译Animal和Dog类,greeting字段声明成如下:

private final String greeting = "Hello";


抽象类中字段声明成final val那么具体子类中就不能重写这个字段的值:

abstract class Animal {
final val greeting = "Hello" // made the field 'final'
}

class Dog extends Animal {
val greeting = "Woof" // this line won't compile
}


4.13.5 抽象类里具体var字段

可以在抽象类或特质为var字段提供一个初始化值,然后在具体子类引用:

abstract class Animal {
var greeting = "Hello"
var age = 0
override def toString = s"I say $greeting, and I'm $age years old."
}

class Dog extends Animal {
greeting = "Woof" //调用setter方法
age = 2
}


这些字段在抽象基类里声明并赋值,反编译Animal类如下:

private String greeting;
private int age;

public Animal(){
greeting = "Hello";
age = 0;
}

// more code ...


因为在Animal基类里这个字段已经声明并且初始化,所以在具体子类里没有必要重新声明字段。

Dog类使用 scalac -Xprint:all 编译:

class Dog extends Animal {
def <init>(): Dog = {
Dog.super.<init>();
Dog.this.greeting_=("Woof");
Dog.this.age_=(2);
()
}
}


因为这个字段在抽象类里是具体的,他们只需要在具体子类里重新赋值即可

4.13.6 不要使用null

使用Option/Some/None模式初始化字段:

trait Animal {
val greeting: Option[String]
var age: Option[Int] = None
override def toString = s"I say $greeting, and I'm $age years old."
}

class Dog extends Animal {
val greeting = Some("Woof")
age = Some(2)
}

object Test extends App {
val d = new Dog
println(d)
}


输出如下:

I say Some(Woof), and I'm Some(2) years old.


4.14 Case类生成样本代码

问题: 在match表达式。actor或者其他使用case类生成样本代码的情况,生成包括获取器,修改器,apply,unapply,toString, equals和hashCode等等方法。

4.14.1 解决方案

定义一个case类如下:

// name and relation are 'val' by default
case class Person(name: String, relation: String)


定义一个case类会生成很多样本代码,有以下好处:

生成apply方法,所以不需要使用new关键词去创建这个类的实例

case类构造函数参数默认声明成val,会自动生成获取器方法,声明成var会自动生成获取器和修改器

生成默认的toString方法

生成unapply方法,可以在匹配表达式轻松使用case类

生成equals和hashCode方法

生成copy方法

定义case类,创建一个新的实例时不需要使用new关键词

scala> case class Person(name: String, relation: String)
defined class Person

// "new" not needed before Person
scala> val emily = Person("Emily", "niece")
emily: Person = Person(Emily,niece)


构造函数默认声明成val,所以会自动生成获取器方法,但不会生成修改器方法:

scala> emily.name
res0: String = Emily

scala> emily.name = "Fred"
<console>:10: error: reassignment to val
emily.name = "Fred"
^


构造函数参数声明成var,会自动生成获取器和修改器方法:

scala> case class Company (var name: String)
defined class Company

scala> val c = Company("Mat-Su Valley Programming")
c: Company = Company(Mat-Su Valley Programming)

scala> c.name
res0: String = Mat-Su Valley Programming

scala> c.name = "Valley Programming"
c.name: String = Valley Programming


Case类有一个默认的toString方法实现:

scala> emily
res0: Person = Person(Emily,niece)


自动生成提取器方法(unapply),当需要在匹配表达式提取信息时很好用(构造器从给定的参数列表创建一个对象, 而提取器却是从传递给它的对象中提取出构造该对象的参数):

scala> emily match { case Person(n, r) => println(n, r) }
(Emily,niece)


自动生成equals和hashCode方法,实例可以如下方法比较:

scala> val hannah = Person("Hannah", "niece")
hannah: Person = Person(Hannah,niece)

scala> emily == hannah
res1: Boolean = false


自动创建copy方法,当需要clone一个对象时很有帮助,在运行过程中还可以改变一些字段:

scala> case class Employee(name: String, loc: String, role: String)
defined class Employee

scala> val fred = Employee("Fred", "Anchorage", "Salesman")
fred: Employee = Employee(Fred,Anchorage,Salesman)

scala> val joe = fred.copy(name="Joe", role="Mechanic")
joe: Employee = Employee(Joe,Anchorage,Mechanic)


4.14.2 讨论

case类主要目的是创建“不可变的记录”,这样可以很容易的在模式匹配表达式里使用。

4.14.3 生成的代码

文件Person.scala:

case class Person(var name: String, var age: Int)


编译后会创建两个class文件,Person.class和Person$.class

$ scalac Person.scala


反编译Person.class

$ javap Person

//结果
Compiled from "Person.scala"
public class Person extends java.lang.Object ↵
implements scala.ScalaObject,scala.Product,scala.Serializable{
public static final scala.Function1 tupled();
public static final scala.Function1 curry();
public static final scala.Function1 curried();
public scala.collection.Iterator productIterator();
public scala.collection.Iterator productElements();
public java.lang.String name();
public void name_$eq(java.lang.String);
public int age();
public void age_$eq(int);
public Person copy(java.lang.String, int);
public int copy$default$2();
public java.lang.String copy$default$1();
public int hashCode();
public java.lang.String toString();
public boolean equals(java.lang.Object);
public java.lang.String productPrefix();
public int productArity();
public java.lang.Object productElement(int);
public boolean canEqual(java.lang.Object);
public Person(java.lang.String, int);
}


反编译Person$.class

$ javap Person$

//结果
Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction2 ↵
implements scala.ScalaObject,scala.Serializable{
public static final Person$ MODULE$;
public static {};
public final java.lang.String toString();
public scala.Option unapply(Person);
public Person apply(java.lang.String, int);
public java.lang.Object readResolve();
public java.lang.Object apply(java.lang.Object,java.lang.Object);
}


去掉case,然后编译反编译如下:

public class Person extends java.lang.Object{
public java.lang.String name();
public void name_$eq(java.lang.String);
public int age();
public void age_$eq(int);
public Person(java.lang.String, int);
}


如果不需要那么多额外的函数,考虑使用正常的类。如果只想创建一个不适用new关键词创建实例的类,如下使用:

val p = Person("Alex")


此时,可以创建一个apply方法。详细看6.8章

查看更多

A Tour of Scala: Extractor Objects

4.15 定义一个equals方法(对象相等)

问题: 类中定义一个equals方法比较对象实例

4.15.1 解决方案

和Java一样,定义一个equals(和hashCode)方法比较两个实例,和Java不同的是,然后可以使用 == 方法比较两个实例是否相等。

class Person (name: String, age: Int) {

def canEqual(a: Any) = a.isInstanceOf[Person]

override def equals(that: Any): Boolean =
that match {
case that: Person => that.canEqual(this) && this.hashCode == that.hashCode
case _ => false
}

override def hashCode:Int = {
val prime = 31
var result = 1
result = prime * result + age;
result = prime * result + (if (name == null) 0 else name.hashCode)
return result
}

}


上面例子显示的是一个修改后的hashCode方法。

使用 == 方法比较两个实例:

import org.scalatest.FunSuite

class PersonTests extends FunSuite {

// these first two instances should be equal
val nimoy = new Person("Leonard Nimoy", 82)
val nimoy2 = new Person("Leonard Nimoy", 82)
val shatner = new Person("William Shatner", 82)
val ed = new Person("Ed Chigliak", 20)

// all tests pass
test("nimoy == nimoy") { assert(nimoy == nimoy) }
test("nimoy == nimoy2") { assert(nimoy == nimoy2) }
test("nimoy2 == nimoy") { assert(nimoy2 == nimoy) }
test("nimoy != shatner") { assert(nimoy != shatner) }
test("shatner != nimoy") { assert(shatner != nimoy) }
test("nimoy != null") { assert(nimoy != null) }
test("nimoy != String") { assert(nimoy != "Leonard Nimoy") }
test("nimoy != ed") { assert(nimoy != ed) }

}


上面的测试创建在ScalaTest FunSuite,和JUnit单元测试类似

4.15.2 讨论

Java中 == 操作符比较引用相等,Scala中 == 是比较两个实例是否相等的方法。

当使用继承时依旧可以继续使用上面的方法

class Employee(name: String, age: Int, var role: String)
extends Person(name, age)
{
override def canEqual(a: Any) = a.isInstanceOf[Employee]

override def equals(that: Any): Boolean =
that match {
case that: Employee =>
that.canEqual(this) && this.hashCode == that.hashCode
case _ => false
}
//上面case that: Employee保证that是Employee类型,that.canEqual(this)保证this也是Employee类型

override def hashCode:Int = {
val ourHash = if (role == null) 0 else role.hashCode
super.hashCode + ourHash
}
}


上面的代码使用canEqual,equals,hashCode相同方式,而且是一致的,尤其是比较子类实例和其父类实例

class EmployeeTests extends FunSuite with BeforeAndAfter {

// these first two instance should be equal
val eNimoy1 = new Employee("Leonard Nimoy", 82, "Actor")
val eNimoy2 = new Employee("Leonard Nimoy", 82, "Actor")
val pNimoy = new Person("Leonard Nimoy", 82)
val eShatner = new Employee("William Shatner", 82, "Actor")

test("eNimoy1 == eNimoy1") { assert(eNimoy1 == eNimoy1) }
test("eNimoy1 == eNimoy2") { assert(eNimoy1 == eNimoy2) }
test("eNimoy2 == eNimoy1") { assert(eNimoy2 == eNimoy1) }
test("eNimoy != pNimoy") { assert(eNimoy1 != pNimoy) }
test("pNimoy != eNimoy") { assert(pNimoy != eNimoy1) }
}


4.15.3 理论

Scaladoc表述:“这个方法的任何实现都应该是等价关系”,等价关系应该有以下3个特征:

自反性(reflexive):Any类型的实例x,x.equals(x)返回true

对称性(symmetric):Any类型的实例x和y,x.equals(y)和y.equals(x)返回true

传递性(transitive):AnyRef的实例x,y和z,如果x.equals(y)和y.equals(z)返回true,那么x.equals(z)也返回true

因此如果重写equals方法,应该确认你的实现保留了等价关系

查看更多

How to Write an Equality Method in Java

Eric Torreborre shares an excellent canEqual example on GitHub

“Equivalence relation” defined on Wikipedia

The Scala Any class

4.16 创建内部类

希望创建一个类作为内部类并且保持在公开API之外,或者否则封装你的代码

4.16.1 解决方案

在一个类里面声明另一个类

class PandorasBox {

case class Thing (name: String)

var things = new collection.mutable.ArrayBuffer[Thing]()
things += Thing("Evil Thing #1")
things += Thing("Evil Thing #2")

def addThing(name: String) { things += new Thing(name) }
}


PandorasBox类的使用者不需要担心Thing的实现就能获得things集合

object ClassInAClassExample extends App {
val p = new PandorasBox

p.addThing("Evil Thing #3")
p.addThing("Evil Thing #4")

p.things.foreach(println)
}


4.16.2 讨论

Scala和Java不同,“不同于Java语言内部类是封闭类的成员,Scala中内部类和外部对象(object)绑定”:

object ClassInObject extends App {

// inner classes are bound to the object
val oc1 = new OuterClass
val oc2 = new OuterClass
val ic1 = new oc1.InnerClass
val ic2 = new oc2.InnerClass
ic1.x = 10
ic2.x = 20
println(s"ic1.x = ${ic1.x}")
println(s"ic2.x = ${ic2.x}")
}

class OuterClass {
class InnerClass {
var x = 1
}
}


因为内部类绑定到他们的对象实例,打印如下:

ic1.x = 10
ic2.x = 20


更多用法,对象里包括类,类里包括对象:

object InnerClassDemo2 extends App {

// class inside object
println(new OuterObject.InnerClass().x)

// object inside class
println(new OuterClass().InnerObject.y)
}

object OuterObject {
class InnerClass {
var x = 1
}
}

class OuterClass {
object InnerObject {
val y = 2
}
}


查看更多

A Tour of Scala: Inner Classes
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息