Scala之继承详解

1.扩展类
使用extends关键字扩展类

class Employee extends Person {
	var age = 10
	...
}

在定义中给出子类需要而超类没有的字段和方法,或者重写超类的方法。和Java一样,可以将类声明为final,这样它就不能被扩展,还可以将当个方法或字段声明为final,以确保他们不能被重写。

2.重写方法
在scala中重写一个非抽象方法必须使用override修饰符,例如:

public class Person {
  ...
  override def toString = getClass.getName + "[name=" + name + "]"
}

override修饰符可以在多个常见情况下给出错误提示:
1.拼错了要重写的方法名
2.不小心在新方法中使用了错误的参数类型
3.在超类中引入了新的方法,而这个新的方法与子类的方法相抵触

在scala中调用超类的方法和java一样,使用super关键字:

public class  Employee extends Person {
	...
	override def toString = super.toString
}

super.toString会调用超类的toString方法,即Person.toString。

3.类型检查和转换
需测试某个对象是否属于某个给定的类,可以用isInstanceOf方法,如果测试成功,就可以使用asInstanceOf方法将引用转为子类的引用。

if (p.isInstanceOf[Employee]) {
  val s = p.asInstanceOf[Employee]  //s类型为Employee
}

如果p指向的是Employee类及子类(比如假若有一个Manager)的对象,则p.isInstanceOf[Employee]会成功,返回true。
如果p是null,则p.isInstanceOf[Employee]将会返回false,且p.asInstanceOf[Employee]将会返回null。
如果p不是一个Employee,则p.asInstanceOf[Employee]将会抛出异常。
如果要测试p指向的是一个Employee对象但又不是其子类的话,可以:

if (p.getClass == classOf[Employee])

classOf方法定义在scala.Predef对象中,因此会被自动引入。
scala的类型检查和转换和Java的对比:
在这里插入图片描述
但与类型检查和转换相比,模式匹配通常是更好的选择,如:

p match {
  case s: Employee => ... 
  case _ => ...
}

4.受保护字段和方法
和Java一样,可以将字段或方法声明为protected。这样的成员可以被任何子类访问,当不能被其他位置所看到。
与Java不同,protected的成员对于类所属的包而言是不可见的,如果需要这种可见性,则可以用包修饰符。
还有一种protected[this],将访问权限定在当前的对象,类似于private[this]。

5.超类的构造
我们知道类有一个主构造器和任意数量的辅助构造器,每个主构造器都必须以对先前
定义的辅助构造器或主构造器的调用开始。这样的后果是,辅助构造器永远不可能直接调用超类的构造器。
子类的辅助构造器最终都会调用主构造器。只有主构造器可以调用超类的构造器。
主构造器都是和类定义交织在一起的。调用超类构造器的方式也同样交织在一起的。
如:

class Employee(name: String,age: Int,val salary: Double) extends Person(name,age)

这段代码定义了一个子类 Employee和一个调用超类构造器的主构造器Employee(name: String,age: Int,val salary: Double)
将类和构造器交织在一起可以将代码变得更简洁,把主构造器的参数当做是类的参数也许更好理解。像Employee类中有三个参数
name,age,salary,其中两个被传递到了超类。如果是Java就要繁杂很多:

public class Employee extends Person {
  private double salary;
  public Employee(String name,Int age, double salary) {
	super(name,age);
	this.salary = salary;
  }
}

Scala类可以扩展Java类,这种情况下,它的主构造器必须调用Java超类的某一个构造方法,如:

class Square(x: Int,y: Int,width: Int) extends java.awt.Rectangle(x,y,width.width)

6.重写字段
Scala字段由一个私有字段和取值器/改值器方法构成,你可以用另一个同名的val字段去重写一个val(或不带参数的def)。
子类有一个私有字段和一个公有的getter方法,而这个getter方法重写了超类的getter方法。如:

class Person(val name: String) {
  override def toString = getClass.getName + name
}
class SecretAgent(codename: String) extends Person(codename) {
  override val name = "secret"
  override val toString = "secret"
}

上例展示了重写机制,但比较死板,更常用val重写抽象的def,如:

abstract class Person {
  def id: Int   //每个人都有某一种方式计算id
  ...
}
class Student(override val id: Int) extends Person

重写有如下限制:
1.def只能重写另一个def
2.val只能重写另一个val或不带参数的def
3.var只能重写另一个抽象的var

7.匿名子类
和Java一样,可以通过包含带有定义或重写的代码块的方式创建一个匿名子类,如:

val alien = new Person("Fred") {
  def greeting = "Grettings, Earthing, my name is Fred"
}

从技术上说,这会创建出一个结构类型的对象,该类型标记为Person{def greeting: String},我们可以用这个类型作为参数类型的定义:

def meet(p: Person{def greeting: String}) {
  println(p.name + "says" + p.greeting)
}

8.抽象类
和Java一样,可以用abstract关键字来标记不能被实例化的类,通常这是因为它的某个或某几个方法没有被完整定义,如:

abstract class Person(val name: String) {
  def id: Int //没有方法体,这是一个抽象方法
}

在这里我们说每个人有一个id,不过我们不知道怎么去计算它,每个具体的Person子类都要给出id方法,在Scala中不像Java,我们不需要对抽象方法使用abstract关键字,只是省去方法体即可,但和Java一样,如果某个类至少存在一个抽象方法,则该类必须声明为abstract。
在子类中重写超类的抽象方法时,不需要使用override关键字。

class Employee(name: String) extends Person(name) {
  def id = name.hashCode //无需override
}

9.抽象字段
除了抽象方法外,类还可以拥有抽象字段,抽象字段就是一个没有初始值的字段,如:

abstract class Person {
 val id: Int   //没有初始化,这是一个带有抽象的getter方法的抽象字段
 var name: String //另一个抽象字段,带有抽象getter和setter方法
}

该类为id和name字段定义了抽象的getter方法,为name字段定义了抽象的setter方法,生成的Java类不带字段。
具体的子类必须提供具体的字段,如:

class Employee(val id: Int) extends Person { //子类有具体的id属性
  var name = ""  //和具体点的name属性
} 

和方法一样,在子类中重写超类的抽象字段时,不需要override关键字。

10.构造顺序和提前定义
当我们在子类中重写val并且超类中的构造器也使用了该值的话,可能会造成一些不好的结果。
比如,动物可以感知周围,假设默认动物可以看到前方10个单位,那么我们可以:

class Creature {
  val range: Int = 10
  val env: Array[Int] = new Array[Int](range)
}
//但蚂蚁是近视的
class Ant extends Creature {
  override val range = 2
}

现在就会面临一个问题,range值在超类的构造器中初始化用到了,而超类的构造器先于子类的构造器运行的。具体发生的过程是这样的:
1.Ant的构造器在做自己的构造之前,调用Creature的构造器。
2.Creature的构造器将它的range字段设为10。
3.Creature的构造器为了初始化env数组,调用了range()取值器。
4.该方法被重写以输出(还未初始化的)Ant类的range字段值。
5.range方法返回0 (这是对象被分配空间时所有整型字段的初始值)。
6.env被设为长度为0的数组。
7.Ant构造器继续执行,将其range字段设为2。
虽然range字段看上去可能是2,但env被设成了长度为0的数组,这里的教训就是在构造器内不应该依赖val的值。
在Java中,当在超类的构造方法中调用方法时,会遇到相似的问题,被调用的方法会可能会被子类重写,因此可能不会按照预期行事,事实上,这就是问题核心所在,range表达式调用了getter方法。
有以下解决方法:
1.将val声明为final。安全但不灵活。
2.在超类中将val声明为lazy。安全但不高效。
3.在子类中使用提前定义语法。
提前定义语法可以在超类的构造器执行之前初始化子类的val字段:

class Ant extends {
  override val range = 2
} with Creature
发布了14 篇原创文章 · 获赞 1 · 访问量 684

猜你喜欢

转载自blog.csdn.net/qq_33891419/article/details/103853826
今日推荐