Scala面向对象编程

Scala面向对象编程

目录:

1 类与对象初步
2 引用与值类型
3 价值类
4 父类
5 Scala的构造器
6 类的字段
    6.1 统一访问原则
    6.2 一元方法
7 验证输入
8 调用父类构造器与良好的面向对象设计
9 嵌套类型

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

猜你喜欢

转载自blog.csdn.net/u014646662/article/details/83818581