Scala类、对象、继承和trait


Scala 类、对象、继承和trait

Scala中的类是用于创建对象的蓝图,其中包含了方法、常量、变量、类型、对象、特质、类,这些统称为成员。

类定义

Scala中,可以在类中定义类、以在函数中定义函数、可以在类中定义object;可以在函数中定义类,类成员的缺省访问级别是:public

class + 类名

class Point(private var x: Int, private var y: Int) {
  def move(mx: Int, my: Int): Unit = {
    x = x + mx
    y = y + my
  }

  def getX: Int = x

  def getY: Int = y
}

上面代码中,定义了一个名为 Point 的类,其中有5个成员变量 (方法也为对象) ,只有一个带两个参数的构造器,两个获取对象值的方法 getXgetY

在 Java 代码 中,类比可得(Scala 代码的默认访问修饰符是public

public Class Point {
 private int x;
 private int y;

 public Point(int x, int y) {
     this.x = x;
     this.y = y;
 }

 public int getX() {
     return this.x;
 }

 public int getY() {
     return this.y;
 }
}

由此可见,Scala 代码比 Java 代码更加简洁

相比上述形式,在Scala中更为常用的是

class Person {
 val id = "1"
 var age: Int = 18
 private var name = "zs"
 private[this] val pet = "xq"
}

/**
 * (单例对象,静态对象,伴生对象)
 * 伴生对象:与class名相同,并且在同一个文件中
 */
object Person {
 def main(args: Array[String]): Unit = {
  val p = new Person

  println(p.id +":"+p.age+":"+p.name)
  p.name = "ls"
  p.age = 20
  println(p.age+":"+p.name)
 }
}
  • val修饰的变量是只读属性,相当于Java中final修饰的变量,只提供get()
  • var修饰的变量,提供get()和set()
  • 类私有字段,只有本类和本类的伴生对象可以访问
  • 对象私有字段只有本类能访问
  • 跟类名一样的 object 修饰的 ,在此类中代表这个类的伴生对象,或者也可说单例对象静态对象

构造器

Scala主要分主构造器和辅助构造器两种:

  • 主构造器里面的变量会被执行,方法会被加载,调用的方法会被执行
  • 辅助构造器(相当于重载的构造函数)不可以直接调用超类的主构造器

主构造器

在Scala中,构造器可以通过提供一个默认值来拥有可选参数:

class Point(private var x: Int = 0, private var y: Int = 0) {
  def move(mx: Int, my: Int): Unit = {
    x = x + mx
    y = y + my
  }

  def getX: Int = x

  def getY: Int = y
}

再加上Scala可以指定函数参数的值,所以加上默认值之后就有了多种构造器可选

object Test {
  def main(args: Array[String]): Unit = {
    val point = new Point(1, 1) // x = 1, y = 1
    val point1 = new Point // x = 0, y = 0
    val point2 = new Point(1) // x = 1, y = 0
    val point3 = new Point(y = 1) // x = 0, y = 1
  }
}

但是如果在Java代码中调用Scala中有默认参数的构造器时,就不能忽略默认参数,当然也不能指定参数

辅助构造器

辅助构造器必须有 def this() 函数,并且每个辅助构造器必须以主构造器或其他的辅助构造器的调用开始

如下所示

class Student(val name: String, val age: Int) {
  // 主构造器会执行类定义中的所有语句
  println("执行主构造器")
  try {
    println("读取文件")
    throw new IOException("io exception")
  } catch {
    case e: NullPointerException => println("打印异常Exception : " + e)
    case e: IOException => println("打印异常Exception : " + e)
  } finally {
    println("执行finally部分")
  }

  private var gender = "male"

  // 用this关键字定义辅助构造器
  def this(name: String, age: Int, gender: String) {
    // 每个辅助构造器必须以主构造器或其他的辅助构造器的调用开始
    this(name, age)

    this.gender = gender
  }
}

私有化成员及 setting/getting

class Point {
  private var _x = 0
  private var _y = 0
  private val bound = 100

  def x = _x
  def x_= (newValue: Int): Unit = {
    if (newValue < bound) _x = newValue else printWarning
  }

  def y = _y
  def y_= (newValue: Int): Unit = {
    if (newValue < bound) _y = newValue else printWarning
  }

  private def printWarning = println("WARNING: Out of bounds")
}

上面这种方式,是官方定义的私有化成员及 setting/getting 方法

在这个版本的Point类中,数据存在私有变量_x_y中。def xdef y方法用于访问私有数据。def x_=def y_=是为了验证和给_x_y赋值。注意下对于 setter 方法的特殊语法:这个方法在 getter 方法的后面加上_=,后面跟着参数。

Class

Scala中类可以通过classOf[A]获取类型,Object单例/伴生只能通过.getClass获取。

classOf和getClass区别:

  • getClass方法得到的是Class[A]的某个子类
  • classOf[A]得到是正确的 Class[A]

但是去比较的话,这两个类型是equals为true的。这种细微的差别,体现在类型赋值时,因为java里的Class[T]是不支持协变的,所以无法把一个 Class[_ < : A] 赋值给一个 Class[A]。

对象

单例对象

单例对象
在Scala中没有静态方法和静态字段,但是可以使用object这个语法结构来达到同样的目的。主要作用:

  1. 存放工具方法和常量
  2. 高效共享单个不可变的实例
  3. 单例模式

单例对象,不需要new,用 [类名.方法] 调用单例对象中的方法

伴生对象

伴生对象

  • 在scala的类中,与类名相同且与该类在同一个文件的以object对象叫伴生对象。
  • 类和伴生对象之间可以相互访问私有的方法和属性,但类的字段被private[this]修饰的只有本类能访问

应用程序对象

通过如下这种方式,可以让程序运行起来,可以看出并未实现 main 方法,只是继承了 App 特质

object Demo extends App {
  println(add(1))

  def add(x: Int) = {
    x + 1
  }
}

The App trait can be used to quickly turn objects into executable programs. Here is an example:
object Main extends App {
Console.println(“Hello World: “ + (args mkString “, “))
}

Here, object Main inherits the main method of App.
args returns the current command line arguments as an array.

Caveats
It should be noted that this trait is implemented using the DelayedInit functionality, which means that fields of the object will not have been initialized before the main method has been executed.
It should also be noted that the main method should not be overridden: the whole class body becomes the “main method”.
Future versions of this trait will no longer extend DelayedInit.
作者:
Martin Odersky
版本:
2.1, 15/02/2011

上文是官方文档,大致意思是,在继承了 App 特质之后,整个对象都将变为 main 方法

apply和unapply方法

通常我们会在类的伴生对象中定义apply方法,当遇到类名(参数1,…参数n)时apply方法会被调用。

apply方法有点类似于java中的构造函数,接受构造参数变成一个对象。

unapply方法就刚好相反,它是接收一个对象,从对象中提取出相应的值,主要用于模式匹配(后文阐述)中。

object Demo {
  def main(args: Array[String]): Unit = {
    // 调用了Array伴生对象的apply方法
    // def apply(x: Int, xs: Int*): Array[Int]
    // arr1中只有一个元素5
    val arr1 = Array(5)
    // def apply[T: ClassTag](xs: T*): Array[T] = {}
    var arr2 = Array[Int](8)
    println(arr1.toBuffer)
    // new了一个长度为5的array,数组里面包含5个null(没有指定泛型)
    var arr3 = new Array(5)
  }
}

继承

在Scala中继承类的方式和Java一样都是使用extends关键字,但是不同的是,Scala可以继承多个类,只有在后面用with关键字关联就可以。

trait

特质定义

特质 (Traits) 用于在类 (Class)之间共享程序接口 (Interface)和字段 (Fields)。 它们类似于Java 8的接口。 类和对象 (Objects)可以扩展特质,但是特质不能被实例化,因此特质没有参数。

特征作为泛型类型和抽象方法非常有用。

trait Iterator[A] {
  def hasNext: Boolean
  def next(): A
}

扩展 trait Iterator [A] 需要一个类型 A 和实现方法hasNextnext

使用 extends 关键字来扩展特征。然后使用 override 关键字来实现trait里面的任何抽象成员:

trait Iterator[A] {
  def hasNext: Boolean
  def next(): A
}

class IntIterator(to: Int) extends Iterator[Int] {
  private var current = 0
  override def hasNext: Boolean = current < to
  override def next(): Int =  {
    if (hasNext) {
      val t = current
      current += 1
      t
    } else 0
  }
}


val iterator = new IntIterator(10)
iterator.next()  // returns 0
iterator.next()  // returns 1

这个类 IntIterator 将参数 to 作为上限。它扩展了 Iterator [Int],这意味着方法 next 必须返回一个Int。

凡是需要特质的地方,都可以由该特质的子类型来替换。

import scala.collection.mutable.ArrayBuffer

trait Pet {
  val name: String
}

class Cat(val name: String) extends Pet
class Dog(val name: String) extends Pet

val dog = new Dog("Harry")
val cat = new Cat("Sally")

val animals = ArrayBuffer.empty[Pet]
animals.append(dog)
animals.append(cat)
animals.foreach(pet => println(pet.name))  // Prints Harry Sally

在这里 trait Pet 有一个抽象字段 namename 由Cat和Dog的构造函数中实现。最后一行,我们能调用pet.name的前提是它必须在特质Pet的子类型中得到了实现

混入

当某个特质被用于组合类时,被称为混入。

abstract class A {
  val message: String
}
class B extends A {
  val message = "I'm an instance of class B"
}
trait C extends A {
  def loudMessage = message.toUpperCase()
}
class D extends B with C

val d = new D
println(d.message)  // I'm an instance of class B
println(d.loudMessage)  // I'M AN INSTANCE OF CLASS B

D有一个父类B和一个混入C。一个类只能有一个父类但是可以有多个混入(分别使用关键字extendswith)。混入和某个父类可能有相同的父类。

现在,让我们看一个更有趣的例子,其中使用了抽象类:

abstract class AbsIterator {
  type T
  def hasNext: Boolean
  def next(): T
}

该类中有一个抽象的类型T和标准的迭代器方法。

接下来,我们将实现一个具体的类(所有的抽象成员ThasNextnext都会被实现):

class StringIterator(s: String) extends AbsIterator {
  type T = Char
  private var i = 0
  def hasNext = i < s.length
  def next() = {
    val ch = s charAt i
    i += 1
    ch
  }
}

StringIterator带有一个String类型参数的构造器,可用于对字符串进行迭代。(例如查看一个字符串是否包含某个字符):

现在我们创建一个特质,也继承于AbsIterator

trait RichIterator extends AbsIterator {
  def foreach(f: T => Unit): Unit = while (hasNext) f(next())
}

该特质实现了foreach方法——只要还有元素可以迭代(while (hasNext)),就会一直对下个元素(next()) 调用传入的函数f: T => Unit。因为RichIterator是个特质,可以不必实现AbsIterator中的抽象成员。

下面我们要把StringIteratorRichIterator 中的功能组合成一个类。

object StringIteratorTest extends App {
  class RichStringIter extends StringIterator("Scala") with RichIterator
  val richStringIter = new RichStringIter
  richStringIter foreach println
}

新的类RichStringIter有一个父类StringIterator和一个混入RichIterator。如果是单一继承,我们将不会达到这样的灵活性。


文章作者: kanaikee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kanaikee !
  目录