• Scala学习十——特质


    一.本章要点

    • 类可以实现任意数量的特质
    • 特质可以要求实现它们的类具备特定的字段,方法或超类
    • 和Java接口不同,Scala特质可以提供方法和字段实现
    • 当你将多个特质叠加在一起时,顺序很重要——其方法先被执行的特质排在更后面

    二.为什么没有多重继承

      Scala和Java一样,不允许使用多重继承(如果继承的多个超类具备某些共通的方法或字段,会引起混乱,还有可能引起菱形继承问题);

      Java中可以实现任意多个接口(接口中只能有抽象方法,且不能有字段,Java中使用抽象基类和接口的做法让可以实现一些方法[治标不治本,同时扩展两个基类会出问题]),Scala提供“特质”而不是接口,特质可以同时拥有抽象方法和具体方法,类可以实现多个特质。

    三.当作接口使用的特质

      

    trait Logger{
    def log(msg:String) //抽象方法
    }
    
    
    //子类实现(使用extends)
    class ConsoleLogger extends Logger{
    def log(msgLString){ //不需要写override
    println(msg)
    }
    
    }

      注:Scala并没有一个特殊的关键字标记特质的实现;

        使用with关键字添加多个特质(extends只需要一个,后面是一个整体,如class ConsoleLogger extends Logger with Cloneable with Serializable)

    四.带有具体实现的特质

      

    trait Logger{
    def log(msg:String) {
    println(msg)}
    }
    
    
    //子类实现
    class SavingAccount extends Account wirh Logger{
    def withdraw(amount:Double){
    if(amount>balance) log("Not enugth")
    }
    else ....
    }

      注:

        这就是具有具体实现的特质,但是让特质具有具体行为有一个弊端:当特质改变时,所有混入该特质的类都必须重新编译。

    五.带有特质的对象

      可以在构造对象时混入特质(可根据不同对象需要加入不同特质),例:

    trait Logger{
    def log(msg:String) {
    }
    }
    
    
    //子类实现
    class SavingAccount extends Account wirh Logger{
    def withdraw(amount:Double){
    if(amount>balance) log("Not enugth")
    }
    else ....
    }
    
    trait MyLogger extends Logger{
    def log(msg:String) {println(msg)
    }
    }
    
    //实例化加特质
    val acct=new SavingAccount with MyLogger

      

    六.叠加在一起的特质

      可以为对象或类添加多个互相调用的特质,对于分阶段加工处理某个值的场景非常有用。

        

      super.log调用的是特质等级中的下一个特质(根据特质的添加顺序决定):

      特质super.xxx执行的方法只依赖于这些特质的对象或类给出的顺序(更加灵活);

      可以控制具体是哪一个特质的方法被调用:super[ConsoleLogger].log(...),这里的类型必须是直接超类型,不能使用继承等级更远的特质或类。 

    七.在特质中重写抽象的方法

      

    traint Logger{
    def log(msg:String) //抽象方法
    }
    //错误的
    trait TimestampLogger extends Logger{
    override def log(msg:String){//重写抽象方法
    super.log(new  java.util.Date()+" "+msg)
    
    }
    }
    
    //正确使用
    trait TimestampLogger extends Logger{
    abstract override def log(msg:String){//重写抽象方法
    super.log(new  java.util.Date()+" "+msg)
    
    }
    }

      Logger.log没有实现,Scala认为TimestampLogger依旧是抽象的,它需要混入一个具体的log方法,因此必须使用abstract和override关键字。

    八.当做富接口使用的特质

      特质中可以使用抽象方法和具体方法(相当于Java中接口和抽象类一起实现),例:

    trait Logger{
    def log(msg:String)
    def info(msg:String){log("Info:"+msg)}
    def warn(msg:String){log("warn:"+msg)}
    def service(msg:String){log("service:"+msg)}
    
    
    }

    九.特质中的具体字段

      特质中的字段可以是具体的,也可以是抽象的。

      对于特质中的每一个字段,使用该特质的类都会获得特质中的字段(不是继承,只是简单的加入)。

    十.特质中的抽象字段

      特质中未被初始化的字段在具体的子类中必须重写(不需要写override)。

    十一.特质构造顺序

      和类一样,特质也有构造器,由字段的初始化和其他特质体中的语句构成。

      特质中的构造器语句在任何混入该特质的对象在构造时执行。

      构造器执行顺序:

      1. 首先调用超类的构造器;
      2. 特质构造器在超类构造器之后,类构造器之前执行;
      3. 特质由左到右被构造;
      4. 每个特质当中,父特质先被构造;
      5. 如果多个特质共有一个父特质,而那个父特质已经被构造,则不会再次构造;
      6. 所有特质构造完毕,子类被构造

     例

      注:构造器的顺序是类的线性化的反向

    十二.初始化特质中的字段

      特质不能有够构造器参数(类和特质的唯一区别),每个特质都有一个无参书的构造器。

      如果需要定制(如日志文件名),可以使用一个抽象字段来存文件名,注意这样的陷阱:

    traint FileLogger extends Logger{
    val filename:String
    val out=new PrintStream(filename)
    def log(msg:String){
    out.println(msg);out.rlush()
    }
    }
    
    
    //错误的(FileLogger的构造器先于子类构造器执行,所以FileLogger的构造器会抛出一个空指针异常)
     acct =new SavingsAccount with FileLogger(){
    val filename="myapp.log"
    }
    //解决方法一:可以使用“提前定义“解决
    val acct=new {//new之后的提前定义块
    val filename="myapp.log"
    }with SavingsAccount with FileLoggere
    //类中使用提前定义
    class SavingAccount extends {//extends后的提前定义块
    val filename="myapp.log"
    }with Account with FileLogger{
    ...//SavingsAccount的实现
    }
    //解决方法二:使用懒值(并不是那么高效)
    trait FileLogger extends Logger{
    val filename:String
    lazy val out=new PrintStream(filename)
    def log(msg:String){out.println(msg)//不需要重写override}}

       

    十三.扩展类的特质

      特质可以扩展另一个特质,同时特质也可以扩展类(这个类将会自动成为所有混入该特质的超类),例:

     

    //LoggedException 扩展自Exception类
    
    trait LoggedException extends Exception with Logged{
    def log(){
    log(getMessage())
    }
    }
    
    //物质的超类也称为这个类的超类
    class UnhappyException extends LogedException{
    
    //该类扩展自一个特质
    override def getMessage()="test"
    }
    //IOException是Exception的子类
    class UnhappyException extends IOException with LoggedException{
    }

      注:如果我们的类已经扩展了另一个类,只要它是特质的超类的子类即可,如果扩展了一个不相关的类,那么就不能混入这个特质了。

    十四.自身类型

      当特质扩展类时,编译器能确保所有混入这一特质的类都认这个类为超类,自身类型也能保证。

      自身类型定义如下:this:类型=>,例:

    //该特质并不扩展Exception类,而是有一个自身类型Exception,因此只能混入Exception的子类,this一定是一个Excption
    trait LoggedException extends Logged{
    this :Exception=>{
    def log(){
    log(getMessage())}}}

      注:自身类型和带有超类型的特质很像,都能确保混入该特质的类都能使用某个特定类型的特性,在某些情况下,自身类型比超类版更灵活,而且自身类型可以解决循环依赖(有两个彼此需要的特质时循环依赖就产生了);

        自身类型也可以处理结构类型:只给出类必须拥有的方法,而不是类名称,例:

    trait LoggedException extends Logged{
    this :def getMessage():String=>{
    def log(){
    log(getMessage())}}}

    十五.背后发生了什么

      Scala需要将特质翻译成JVM的类和接口。

      只有抽象方法的特质,例:

    //Scala
    trait Logger{
    def log(msg:String)
    }
    //JVM对应生成的Java接口
    public interface Logger{
    void log(String msg);
    }

      具有具体方法的特质,Scala会为我们创建伴生类,该伴生类用静态方法存放特质的方法,例:

    //Scala代码
    trait ConsoleLoggger extends Logger{
    def log(msg:String){println(msg)}
    }
    //JVM对应Java代码
    public interface ConsoleLogger extends Logger{
    //生成的Java接口
    void log(String msg);
    }
    
    public class ConsoleLogger$class{
    //生成的Java伴生类,注意伴生类中不会有任何字段
    public static void log(ConsoleLogger self,String msg){
    System.out.println(msg);
    }
    }

       伴生类中不会有任何字段,特质中的字段对应接口中的抽象的getter和setter方法,例:

    //Scala代码
    trait ShortLogger extends Logger{
    val maxLength=15//具体字段
    ...}
    //Java对应
    public interface ShortLogger extends Logger{
    public abstract int maxLength();
    public abstract void weired_prefix$maxLength_$eq(int);
    ...    
    }
    
    
    public class ShortLogger$class{
    //以weird开头的setter方法是需要的,用来初始化该字段
    //当特质混入类时,类会有一个setter和getter方法和maxLength字段,类的构造器会调用初始化方法
    public void $init$(ShortLogger self){
    self.weird_prefix$maxLength_$eq(15)}
    } 

    十六.练习

  • 相关阅读:
    ionic3使用@angular/http 访问nodejs(koa2框架)服务不能返回数据
    FacebookFriendAdderPro
    SEO记录-1
    thanos 实现 prometheus 高可用 数据持久化2
    Prometheus + consul + grafana 监控体系搭建1
    解决问题方法
    原则设定
    docker-基本概念、架构和使用
    如何有效学习
    社会~
  • 原文地址:https://www.cnblogs.com/lyq-biu/p/11958451.html
Copyright © 2020-2023  润新知