限制在对象或特质的 body 里初始化逻辑的代码
在编译特质的时候, Scala 创建一个接口/实现对(interface/implementation pair,接口的类名是特质的名字,实现的类名是特质的名字加上 $class
),接口用于 JVM 交互,实现则是一组静态方法,在类实现特质时可以用到。
$ cat Test.scala
trait Application {
def main(args: Array[String]){
}
}
object Test extends Application {
println("hello world!")
}
// 编译后的接口和实现
$ javap -c Test{,\$.class}
// 接口类
Compiled from "Test.scala"
public final class Test {
public static void main(java.lang.String[]);
Code:
0: getstatic #16 // Field Test$.MODULE$:LTest$;
3: aload_0
4: invokevirtual #18 // Method Test$.main:([Ljava/lang/String;)V // 转发调用实现类的方法
7: return
}
// 实现类
Compiled from "Test.scala"
public final class Test$ implements Application {
public static final Test$ MODULE$;
// 静态初始化块,实例化实现类
public static {};
Code:
0: new #2 // class Test$
3: invokespecial #14 // Method "<init>":()V
6: return
public void main(java.lang.String[]);
Code:
0: aload_0
1: aload_1
2: invokestatic #21 // Method Application$class.main:(LApplication;[Ljava/lang/String;)V
5: return
}
目前,静态初始化代码块里的方法不能应用 HopSpot 优化。
延迟调用
DelayedInit 特质是为编译器提供的标记性的特质。当实现一个继承 DelayedInit 的类时,整个构造器被包装成一个函数并传递给 delayedInit 方法。DelayedInit 特质定义如下:
trait DelayedInit {
def delayedInit(x: => Unit): Unit
}
示例:
$ cat TestDelayedInit.scala
trait App extends DelayedInit {
var x: Option[Function0[Unit]] = None
override def delayedInit(cons: => Unit) {
x = Some(() => cons)
}
def main(args: Array[String]) {
x.foreach(_())
}
}
$ scala -cp .
scala> new App {println("now, i am inited .")}
res0: App = $anon$1@7646731d
scala> res0.main(Array())
now, i am inited .
DelayedInit 特质可以用于延迟整个对象的构造过程,直到所有的属性都被注入完成。
DelayedInit 特质解决了构造和初始化对象需要根据外部约束在不同时间点进行的问题。这种分离在实践中是不推荐的。
使用早期成员定义
早期成员定义(early member definition):是一个匿名代码块。
trait Property {
val name: String // 抽象成员
override val toString = "Property (" + name + ")" // 具体成员,依赖于抽象成员 name
}
// 使用早期成员定义
class X extends {val name = "HI"} with Property
// 更简单的形式
new { val name = "Simple HI" } with Property
早期成员定义解决的问题是当特质定义抽象成员并且其具体成员依赖于抽象成员时发生的。
为特质的抽象方法提供空实现
Scala 的特质有个非常有用的属性,就是直到超类被混入(mixed in)并且初始化之后才定义超类。也就是说特质的实现不是必须知道 “super” 的类型是什么,直到一个叫作”线性化“的过程发生时。
线性化是指为某个类的超类们指定线性顺序的过程。
在通过特质创建可混入的层级结构时,需要确保以下需求:
- 需要有一个特质可以假定为父类的”混入点“;
- ”可混入特质“以有意义的方式代理给它们的父类;
- ”混入点“为命令链风格的方法提供默认实现。
组合可以包含继承
Java 社区格言:组合优于继承,favor composition over inheritance。
继承 VS 对象组合的问题
问题 | Java 接口 | Java 抽象类 | Scala 特质 |
可在子类中覆盖行为 re-implement behavior |
不支持 | 支持 | 支持 |
只能和父母行为组合 | 支持 | 不支持 | 支持 |
打破封装 | 不支持 | 不支持 | 不支持 |
需要调用构造器来组合 | 不支持 | 不支持 | 不支持 |
通过继承来组合成员
// 定义一个抽象成员组合类(abstract member-composititon class),定义一些可被覆盖的成员。
// HasLogger 特质只做一件事:容纳一个 logger 成员
trait HasLogger {
def logger: Logger = new Logger
}
trait DataAccess extends HashLogger {
def query[A](in: String): A = {
logger.log("QUERY", in)
}
}
包含多个抽象成员的特质有时候称为”环境“,因为这个特质包含其他类运行时所需要的”环境“。
Scala 有以下特性来避免创建抽象成员组合类:
- 命名和默认参数;
- 将构造器参数提升为成员。
class DataAccess (val logger: Logger = new Logger{}) {
def query[A](in: String): A = {
logger.log("QUERY", in)
}
}
编译器编译默认参数的方式:当方法 a 有默认参数时,编译器生成一个获取默认值的静态方法。然后当用户代码调用 a 方法时,如果没有提供默认参数值,编译器会调用静态方法获取默认值传递给 a 方法。在 a 方法是个构造器的情况下,这些参数值被放在该类的伴生对象里,如果没有伴生对象则会创建一个。伴生对象会包含生成每个参数值的方法,这些生成参数值的方法会进行某种形式的名称重整,以便编译器能准确地调用正确的方法。名称重整的格式是方法名加上参数顺序值,全部用 $
分隔。
在 Java 字节码里,构造器叫作 init
。Scala 用反引号(`)操作符来标注”这是一个包含非标准字符的可能造成解析错误的标识符“。
各种组合方式的优缺点:
方式 | 优点 | 缺点 |
成员组合(把另一个对象作为类的成员来组合) | Java 标准实践 | 没有多态,不灵活 |
继承组合(通过继承来组合行为) | 多态 | 破坏封装 |
抽象成员组合(通过抽象成员组合类来组合行为) | 最为灵活 | 代码膨胀–尤其是建立并行类继承关系的时候 |
用带默认参数的构造器里组合 | 减少代码量 | 无法很好地和继承协作 |
提升抽象接口为独立特质
把抽象接口提升到独立的特质,然后在整个项目生命周期里尽可能锁定这些特质。当抽象接口必须修改时,所有依赖于此的模块都要针对修改的特质升级,以便确保正确的运行时链接。
public 接口应当提供返回值
为避免泄漏实现细节,API 的 public 接口最好明确声明返回类型。
只有在以下场景下可以不明确声明返回类型:
- 封闭的单类继承树(sealed single-class hierarchy)
- 私有方法;
- 覆盖父类明确声明了返回类型的方法。
欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。