2018年第44周-scala入门-面向对象基础语法

scala和java都是可以运行在JVM上, 所以scala和java是可以互相调用, 那么问题来了, 既然已经有java语言存在, 为什么还要发明scala语言. 存在即合理, 所以我就想找下scala的合理之处. 其中一个, 那就是把java的繁琐的语法给尽量去掉. 让程序员敲代码的速度变快, 同时也方便阅读和修改(这有点不太准确, 是因为得看站在什么角度).

基于上面的理论, 并带着java面向对象的思维, 会更好理解scala的面向对象的语法.

这里得有几个共识:

类里面的函数, 叫方法
类的字段, 也称为成员变量, 它们对应的英文都是field.
类实例化后, 叫对象(object), 也叫实例.
类里的字段, 方法, 统称为成员.

代码这个东西, 在你眼睛没达到能看清细节之前, 看到的代码, 基本就是个宏观的图片. 宏观的信息很少, 所以需要自己动手, 进入细节, 才获取更多的细节信息.

定义一个简单的类

定义类, 包含field以及方法

class HelloWorld{
  private var name="jc"
  def sayHello(){print("Hello "+name)}
  def getName=name
}

//创建类对象, 并调用其方法
val helloWorld = new HelloWorld
helloWorld.sayHello()
println(helloWorld.getName)  //如果定义方法时没有带括号, 则调用方法时不能带括号

主construtor

scala中, 主constructor是与类名放在一起的, 这与java不一样
而且类中 , 主constructor里的代码, 是没有定义在任何方法或者是代码块中, 这点与java对比, 就没那么直观看出那些是主构造方法的代码.

class Student(val name:String, val age:Int){
  println("your name is "+name+", your age is "+age)
}

scala> var s = new Student("jc",29)
your name is jc, your age is 29
s: Student = Student@6155fa78

scala> s.name
res13: String = jc

scala> s.age
res14: Int = 29

主constructor中还可以通过使用默认参数, 来给参数默认的值

class Student(val name:String="jc", val age:Int=30){
   println("your name is" + name+", your age is "+age )
}

如果主constructor传入的参数没有修饰符, 比如Student(name:String,age:Int), 那么如果类内部有方法使用到这些传入的参数, 则会声明为private[this] name; 否则没有该field, 就只能被constructor代码使用而已. 简单的说就是外部不能访问name和age

class Student(name:String, age:Int){
  println("your name is "+name+", your age is "+age)
}

scala> var s = new Student("jc",29)
your name is jc, your age is 29
s: Student = Student@5ed1f76d

scala> s.name
<console>:14: error: value name is not a member of Student
       s.name
         ^

scala> s.age
<console>:14: error: value age is not a member of Student
       s.age
         ^



class Student(name:String, age:Int){
  println("your name is "+name+", your age is "+age)
  def print={println(name)}
}
scala> var s = new Student("jc",29)
your name is jc, your age is 29
s: Student = Student@6db3cc5b

scala> s.name
<console>:14: error: value name is not a member of Student
       s.name
         ^

scala> s.age
<console>:14: error: value age is not a member of Student
       s.age

辅助constructor

scala中, 可以给类定义多个辅助constructor, 类似与java中的构造函数重载
辅助constructor之间可以互相调用, 而且必须第一行调用主constructor

class Student{
  private var name=""
  private var age=0
  def this(name:String){
    this()
    this.name=name
  }
  
  def this(name:String, age:Int){
    this(name)
    this.age=age
  }
   
  def print={println(name+", "+age)}
}

object

object, 就是class的单个实例, 这里很特别, scala可以直接定义object, 通常在里面放一些静态的field或者method, 那就跟java的静态类很像
第一次调用object的方法时, 就会执行object的constructor, 也就是object内部不在method的代码; 但是object不能定义接受参数的constructor
注意, object的constructor只会在其第一次被调用时执行一次, 以后再次调用就不会再次执行constructor了
object通常用于作为单例模式的实现, 或者放class的静态成员, 比如工具方法

object Person{
  private var eyeNum=2
  println("this Person Object!")
   def getEyeNum=eyeNum
}

内部类

scala中, 同样可以在类中定义内部类, 但是与java不同的是, 每个外部类的对象的内部类, 都是不同的类

scala> :paste
// Entering paste mode (ctrl-D to finish)

import scala.collection.mutable.ArrayBuffer

class Outer {
  class Inner(val name:String){}


  val inners = new ArrayBuffer[Inner]

  def getInner(name:String) ={
    new Inner(name)
  }
}




// Exiting paste mode, now interpreting.

import scala.collection.mutable.ArrayBuffer
defined class Outer

scala> val out1 = new Outer
out1: Outer = Outer@5479b0b1

scala> val inner1 = out1.getInner("inner1")
inner1: out1.Inner = Outer$Inner@7161bdc3

scala> out1.inners+=inner1
res3: out1.inners.type = ArrayBuffer(Outer$Inner@7161bdc3)

scala> val out2 = new Outer
out2: Outer = Outer@54c2ca03

scala> val inner2 = out2.getInner("inner2")
inner2: out2.Inner = Outer$Inner@747a5354

scala> out1.inners+=inner2
<console>:17: error: type mismatch;
 found   : out2.Inner
 required: out1.Inner
       out1.inners+=inner2

java内部类的对比:
Outer.java文件

package com.java;

import java.util.ArrayList;
import java.util.List;

public class Outer {

    public class Inner{
        String name;
        public Inner(String name){
            this.name = name;
        }
    }

    public List<Inner> inners = new ArrayList<>();

    public Inner getInner(String name){
        return new Inner(name);
    }

}

App.java文件

package com.app;

import com.java.Outer;

public class App {

    public static void main(String[] args) {
        Outer  out1 = new Outer();
        Outer.Inner inner1 = out1.getInner("inner1");
        out1.inners.add(inner1);

        Outer  out2 = new Outer();
        Outer.Inner inner2 = out2.getInner("inner1");
        out1.inners.add(inner2);
    }
}

java和scala的内部类, 提供的价值是, 让你逻辑地组织类, 并且控制它们的访问.

访问修饰符

包, 类, 对象的成员都可以被修饰为private或protected来控制其是否可被访问(调用).
所有修饰符, 无论是java还是scala, 都是用于在编译器发现错误. 所以当不能访问(调用)都会出现编译错误. 所以这里的访问(access), 也可以说是调用, 引用等都没关系, 因为都不会到达运行时才发现错误.

private成员

一个被修饰为private的成员, 只能被其class和object里的成员访问(注意是"和", 后续伴生类概念时会有所体现).

private修饰符的访问限制, 跟java的很像, 但注意还是有不一样.

一样的是: java和scala的private修饰的成员任何其他类不能调用.
不一样的是: java内部类是可以调用其外部类的private成员, 外部类也是可以调用内部类的private成员. 而scala不可以

我们先理解上面一句话的"任何其他类".
在java里的"任何其他类"是除class类内部.
内部类是可以调用其外部类的private成员, 外部类也是可以调用内部类的private成员.

//编译通过
package com.java;

import java.util.ArrayList;
import java.util.List;

public class Outer {
    private String outerName ;
    class Inner{
        private void f(){
            System.out.printf(outerName);
        }
    }
    void accesF(){
        new Inner("").f();
    }
}

而在scala里的"任何其他类"包括就真的是任何其类, 内部类也属于任何其他类. 但一点跟java一样的是, 内部类可以访问外部类的private成员.

//编译失败
class Outer {
  class Inner{
    private def f(){println("f")}

    class InnerMost{
      f() //ok
    }
  }

  (new Inner).f() //error: f is not accessible

}

下面例子证明修饰符是用于编译期发现错误, 以下都是可以编译通过.

package com.java;

import java.util.ArrayList;
import java.util.List;

public class Outer {
    private int value ;
    public boolean compare(Out otherOuter){
    return this.value < otherOuter.value
    }
}
class Outer {
  private var value = 0
  def compare(otherOuter:Outer) ={
    this.value < otherOuter.value
  }
}

protected成员

在scala, protected修饰的成员和java的就不太一样. java修饰层次更鲜明点, java的protected是包内的类都能够访问.
在scala, protected修饰的成员只能其自身或子类访问.

package p {
  class Super {
    protected def f() { println("f") }

    class inner {
      (new Super).f()
    }
  }
  class Sub extends Super {
    f()
  }

  class Other {
    (new Super).f() // 这行编译失败 error: f is not accessible
  }
}

在java里, Other类是可以访问super.f(), 是因为Other和Super都在通过包p下.

public成员

这里又跟java不一样, 在scala里, 没有修饰符的, 默认是public.
public成员, 可以随意调用.

可设置保护范围

scala中的访问修饰符可以指定限定符(qualified).
一个private[X]或protected[X]这样的格式的修饰符, X可以是包, 可以是类, 也可是object.
这样的设置, 可以让你达到Java的protected修饰符的效果(只有本包下可以访问, 哪怕子包也不可以访问), 如我们修改下上面一小节protected成员的例子, 让它可以编译通过:

package p {
  class Super {
    protected[p] def f() { println("f") } //指定包名

    class inner {
      (new Super).f()
    }
  }
  class Sub extends Super {
    f()
  }

  class Other {
    (new Super).f() //这里编译通过了
  }
}

利用这语法, 可以让你更灵活的表达访问权限, 这是java所不具有的:

package bobsrockets

package navigation {

  private[bobsrockets] class Navigator {
    protected[navigation] def useStarChart() {}

    class LegOfJourney {
      private[Navigator] val distance = 100
    }

    private[this] var speed = 200
  }

}

package launch {

  import navigation._

  object Vehicle {
    private[launch] val guide = new Navigator
  }

}

Navigator类不修饰为private[bobsrockets], 这样只有包bobsrockets下面的类或对象才能访问.
Vehicle对象是不可以访问Navigator, 因为Vehicle对象是在launch包下, 那怕import navigation包, 也是不可以访问Navigator类

getter与setter

这一节, 感觉不太严谨, 因为都不是java概念中getter和setter, 是硬套进去的概念. 更多是上一节的访问修饰符的概念.
JVM会考虑以下情况getter与setter.
使用了private来修饰的field, 所以生成的getter和setter是private的.

class Student{
  private var name="jc"
}
scala> val jcStu = new Student
jcStu: Student = Student@5a3a27eb

scala> println(jcStu.name)
<console>:13: error: variable name in class Student cannot be accessed in Student
       println(jcStu.name)
scala> jcStu.name = "jc2"
<console>:12: error: variable name in class Student cannot be accessed in Student
       jcStu.name = "jc2"
             ^
<console>:13: error: variable name in class Student cannot be accessed in Student
       val $ires1 = jcStu.name

如果定义field是val, 则只会生成getter方法

class Student{
  val name="jc"
}
scala> val jcStu = new Student
jcStu: Student = Student@47573529

scala> jcStu.name
res9: String = jc

scala> jcStu.name = "jc2"
<console>:12: error: reassignment to val
       jcStu.name = "jc2"
                  ^

定义不带private的var field, 此时scala生成的面向JVM的类时, 会定义为private的name字段, 并提供public的getter和setter方法

class Student{
  var name="jc"
}
//调用getter和setter的方法
val jcStu = new Student
println(jcStu.name)  //其实是调用了getter方法
jcStu.name = "jc2" //其实是调用了setter方法

如果不希望生成setter和getter方法, 则将field生命为private[this]

class Student{
  private[this] var name="jc"
}
scala> val jcStu = new Student
jcStu: Student = Student@4ef55cb9

scala> jcStu.name
<console>:13: error: value name is not a member of Student
       jcStu.name
             ^

scala> jcStu.name = "jc2"
<console>:12: error: value name is not a member of Student
       jcStu.name = "jc2"
             ^
<console>:13: error: value name is not a member of Student
       val $ires3 = jcStu.name

getter和setter方法在scala的方法名是name和name_=

class Student{
  var nameField="jc"
  def name={ println("call getName"); nameField}
  def name_= (newName:String){ println("modify name, old name="+nameField);nameField=newName}
}

scala> val jcStu = new Student
jcStu: Student = Student@e90dc68

scala> jcStu.name
call getName
res11: String = jc

scala> jcStu.name="jc2"
modify name, old name=jc
call getName
jcStu.name: String = jc2

//可以看出, name已经成为方法的名, 而字段name被改为nameField, 感觉getter和setter就是普通的方法, 与nameField关系不大了.  
而且与java不一样, getter方法参数不能与字段同名, 不过下面的构造方法可以使用this

自定义getter与setter

class Student{
  var nameField="jc"
  def name={ println("call getName"); nameField}
  def name_= (newName:String){ println("modify name, old name="+nameField);nameField=newName}
}
//在上面这个例子里, 其实存在两个getter和setter方法, getter方法分别是nameField和name, setter方法是nameField和和name_
所以在这里, 我们想控制getter和setter的话, 需要把字段改为private
class Student{
  private var nameField="jc"
  def name={ println("call getName"); nameField}
  def name_= (newName:String){ println("modify name, old name="+nameField);nameField=newName}
}
//自定义setter方法的时候一定要注意scala的语法限制, 签名, =, 参数不能有空格

仅暴露field的getter方法

如果你不希望field有setter方法, 可以将field定义为val, 但此时就再也不能修改field的值了.
所以如果你仅仅是想暴露getter方法, 但还可以继续修改field的值, 则需要综合使用private以及自定义getter方法.
当定义field是private时, setter和getter都是private, 对外界没有暴露, 自己可以实现修改field值的方法, 自己可以覆盖getter方法

class Student{
  private var myName="jc"
  
  //自定义setter方法
  def updateName(newName:String){
     if(newName == "jc2") myName = newName
     else println("not accept this new name")
  }
  //覆盖setter方法
  def name_=(newName:String){ myName=newName}
  def name="your name is " + myName
}

上述这例子, 虽然field名已经不是name了, 但可以对客户端来说, 我们的field就是name, 所以就可以呈现出name是个字段
看来scala的getter方法和setter方法的理解, 和java不太一样.

private[this]的使用

如果field使用private来修饰, 那么代表这个field是私有的, 在类的方法中, 可以直接访问类的其他对象的private field.
这种情况下, 如果不希望field被其他对象访问到, 那么可以使用private[this], 意味着对象私有的field, 只有本对象内可以访问到

class Student{
  private var myAge=0
  def age_=(newValue: Int){
    if(newValue>0) myAge = newValue
    else print("illegal age!")
  }

  def age = myAge
  def older(s:Student)={
    myAge > s.myAge
  }
}
scala> var jc = new Student
jc: Student = Student@384a1d47

scala> jc.age=29
jc.age: Int = 29

scala> var jay = new Student
jay: Student = Student@1a5b443b

scala> jay.age=38
jay.age: Int = 38

scala> jc.older(jay)
res0: Boolean = false

上面例子可以直接访问getter方法, 而下面这个就不可以访问myAge字段, 编译会失败

class Student{
  private[this] var myAge=0
  def age_=(newValue: Int){
    if(newValue>0) myAge = newValue
    else print("illegal age!")
  }

  def age = myAge
  def older(s:Student)={
    myAge > s.myAge
  }
}

<pastie>:21: error: value myAge is not a member of Student
    myAge > s.myAge
              ^

Java风格的getter和setter方法

scala的getter和setter方法的命名与java的是不同的, scala是field和field_=的方式
如果要让scala自动生成java风格的getter和setter方法, 只要field添加@BeanProperty注解即可
此时会生成4个方法, name:String, name_=(newValue:String):Unit, getName():String, setName(newValue:String):Unit

import scala.reflect.BeanProperty  //这是在2.10.*版本, 2.12版本这个类就不在这个包了

scala> :paste
// Entering paste mode (ctrl-D to finish)

import scala.beans.BeanProperty     //这是在2.12.*版本
class Student{
  @BeanProperty var name: String = _
}


// Exiting paste mode, now interpreting.

import scala.beans.BeanProperty
defined class Student

scala> val s = new Student
s: Student = Student@53449d36


//另一种定义类方式
class Student(@BeanProperty var name:String)

伴生对象

如果有一个class, 还有一个与class同名的object, 那么就称这个object是class的伴生对象, class是object的伴生类
伴生类和伴生对象必须存放在一个scala文件中
伴生类和伴生对象, 最大的特点就在于, 互相可以访问private field, 不过需通过对象.访问

class Person(val name:String, val age:Int){
  def sayHello=println("Hi, "+name+", I guess you are "+age+" years old! And you must have "+Person.eyeNum+" eyes")
}

object Person{
  private val eyeNum=2
  def getEyeNum = eyeNum
}

让object继承抽象类

object的功能其实和class类似, 除了不能定义接受参数的constructor之外, 它也可以继承抽象类, 并覆盖抽象类中的方法

abstract class Hello(var message:String){
  def sayHello(name:String):Unit
}

object HelloImpl extends Hello("HELLO"){
  override def sayHello(name:String)={
    println(message+","+name)
  }
}

apply方法

object中非常重要的一个特殊方法, 就是apply方法
通常在伴生对象中实现apply方法, 并在其中实现构造伴生类的对象的功能
而创建伴生类的对象时, 通常不会使用new Class的方式, 而是使用Class()的方式, 隐式地调用伴生对象的apply方法, 这样会让对象的创建更加简洁.
比如, Array类的伴生对象的apply方法就实现了接受可变参数, 并创建一个Array对象的功能

val a= Array(1,2,3,4,5)

比如, 定义自己的伴生类和伴生对象

class Person(val name:String)
object Person{
   def apply(name: String) = new Person(name)
}

main方法

就跟java一样, 如果要运行一个程序, 就必须有个入口, 就是包含main方法的类; 在scala中, 如果要运行一个应用程序, 那么必须有一个main方法, 作为入口
scala中的main方法定义为def main(args:Array[String]), 而且必须定义在object中

object HelloWorld{
  def main(args: Array[String]){
    println("Hello World!!!") 
  }
}

除了自己实现main方法之外, 还可以继承App Trait, 然后将需要在main方法中允许的代码, 直接作为object的constructor代码; 二期用args可以接受传入的参数

object HelloWorld extends App{
   if(args.length>0) println("hello, "+args(0))
   else println("Hello World!!")
}

用object来实现枚举功能

scala没有直接提供类似于Java中的Enum这样的枚举特性, 如果要实现枚举, 则需要用object继承Enumeration类, 并且调用Value方法来初始化枚举值

object Season extends Enumeration{
  val SPRING, SUMEMER, AUTUMN, WINTER = Value
}

scala> for(ele <- Season.values) println(ele)
SPRING
SUMEMER
AUTUMN
WINTER

还可以通过Value传入枚举值的id和name, 通过id和toString可以获取; 还可以通过id和name来查找枚举值

object Season extends Enumeration{
  val SPRING = Value(0,"spring")
  val SUMMER = Value(1,"summer")
  val AUTUMN = Value(2,"autumn")
  val WINTER = Value(3,"winter")
}

Season(0)
Season.withName("spring")
scala> for(ele <- Season.values) println(ele)
spring
summer
autumn
winter

使用枚举object.values可以遍历枚举值
for(ele <- Season.values) println(ele)

extends

scala中, 让子类继承父类, 跟java一样, 也是使用extends关键字
继承就代表, 子类可以从父类继承父类的field和method, 然后子类可以在自己内部放入父类所有没有的, 子类特有的field和method, 使用继承可以有效复用代码, 玩出更高级的花样时, 那就是设计模式, 不仅仅复用代码, 面向扩展也是很方便, 维护也是很方便, 如你知道是装饰模式, 那么肯定知道有被装饰者和装饰者的存在.
子类可以覆盖父类的field和method, 但是如果父类用final修饰, field和method用final修饰, 则该类是无法被继承, field和method是无法被覆盖的.

class Person{
  private var name="jc"
  def getName= name
}

class Student extends Person{
  private var score = "A"
  def getSorce = score
}

override和super

scala中, 如果子类要覆盖一个父类的非抽象方法, 则必须使用override关键字
override关键字可以帮助我们尽早地发现代码里的错误,在编译器发现错误比运行时发现错误成本要低, 比如: override修饰的父类的方法的方法名我们品写错了, 比如要覆盖的父类方法的参数我没写错了,等等.
此外, 在子类覆盖父类方法之后, 如果我们在子类中就是要调用父类的被覆盖的方法呢? 那就可以使用super关键字, 显式地指定要调用父类的方法

class Person{
  private var name= "jc"
  def getName=name
}

class Student extends Person{
  private var score = "A"
  def getScore = score
  override def getName = "Hi, I'm "+super.getName
}

override field

scala中, 子类可以覆盖父类的val field, 而且子类的val field还可以覆盖父类的val field的getter方法, 只要子类使用override关键字即可

class Person{
  val name:String="Person"
  def age:Int=0
}

class Student extends Person{
  override val name:String="jc"
  override val age:Int=29
}

isInstanceOf和asInstanceOf

如果我们创建了子类的对象, 但是又将其赋值了父类类型的变量, 如果我们有需要将父类类型的变量转为子类类型的变量时, 该怎么做.
首先, 需要使用isInstanceOf判断对象是否是指定类的对象, 如果是的话, 则可以使用asInstanceOf将对象转为指定类型
注意, 如果对象是null, 则isInstanceOf返回false, asInstanceOf返回null
注意, 如果没有isInstanceOf先判断对象是否为指定类的实现, 就直接使用asInstanceOf转换, 可能会出现异常
跟java不一样, java是使用括号强转类型

class Person
class Student extends Person
val p:Person = new Student
var s:Student = null
if(p.isInstanceOf[Student]) s = p.asInstanceOf[Student]

getClass和classOf

isInstanceOf只能判断对象是否是指定类以及其子类的对象, 而不能精确判断出, 对象是否是指定类的对象
如果要求精确地判断对象就是指定类的对象, 那么就只能用getClass和classOf了
对象.getClass可以精确获取对象的类, classOf[类]可以精确获取类, 然后使用==操作符即可判断

class Person
class Student extends Person
val p:Person = new Student
p.isInstanceOf[Person]
p.getClass == classOf[Person]
p.getClass == classOf[Student]

使用模式匹配进行类型判断

这种方式更加简洁明了, 而且代码的可维护性和可扩展性也非常的高
使用模式匹配, 功能性上来说, 与isInstanceOf一样, 也是判断主要是该类以及该类的子类的对象即可, 不是精准判断的

class Person
class Student extends Person
val p:Person = new Student

p match{
  case per: Person=> println("it's Person's object")
  case _=> println("unknow type")
}

调用父类的constructor

scala中, 每个类可以有一个主constructor和任意多个辅助constructor, 而每个辅助constructor的第一行都必须是调用其他辅助constructor或者是主constructor; 因此子类的辅助constructor是一定不可能直接调用父类的constructor的.
只能在子类的主constructor中调用父类的constructor, 以下这种语法, 就是通过子类的主constructor调用父类的constructor
注意, 如果是父类中接受的参数, 比如name个age, 子类中接收时, 就不要用任何val和var来修饰了, 否则会认为是子类要覆盖父类的field

class Person(val name:String, val age:Int)
class Student(name:String, age:Int, var score:Double) extends Person(name,age){
  def this(name:String){
    this(name,0,0)
  }

  def this(age:Int){
    this("jc",age,0)
  }
}

匿名子类

在scala中, 匿名子类是非常常见, 而且非常强大的.
对应着Java的匿名内部类
匿名子类, 可以定一个类的没有名字的子类, 并直接创建其对象, 然后将对象的引用赋予一个变量.之后甚至可以将该匿名子类的对象传递给其他函数.

class Person(protected val name:String){
  def sayHello = "Hello, I'm "+name
}

val p = new Person("jc"){
  override def sayHello = "Hi, I'm "+ name
}

//函数可以指定结构体参数
def greeting(p:Person{def sayHello:String}){
  println(p.sayHello)
}

抽象类

如果在弗雷中, 有某些方法无法立刻实现, 而需要依赖不同的子类来覆盖, 重写实现自己不同的方法实现. 此时可以将父类中的这些方法不给出具体的实现, 只有方法前面, 这种方法就是抽象方法
而一个类中如果有一个抽象方法, 那么类就必须用abstract来声明为抽象类, 此时抽象类是不可以实例化的
在子类中覆盖抽象类的抽象方法时, 不需要使用override关键字

abstract class Person(val name:String){
  def sayHello:Unit
}

class Student(name:String) extends Person(name){
  def sayHello:Unit = println("Hello, "+name)
}

抽象field

如果在父类中 ,定义field, 但没有给出初始值, 则此field为抽象field
抽象field意味着, scala会根据自己的规则, 为var或val类型的field生成对应的getter和setter方法, 但是父类中是没有该field的
子类必须覆盖field, 以定义自己的具体field, 并且覆盖抽象field, 不需要使用override关键字

abstract class Person{
  val name:String
}

class Student extends Person{
  val name:String = "jc"
}

将trait作为接口使用

scala中Trait是一种特殊的概念
首先我们可以将Trait作为接口来使用, 此时的Trait就与java中的接口非常类似了
在Trait中可以定义抽象方法, 就与抽象类中的抽象方法一样, 只要不给出方法的具体实现即可
类可以使用extends关键字继承Trait, 注意, 这里不是implement, 而是extends, 在scala中没有implement的概念, 无论继承类还是Trait, 统一都是extends
类继承Trait后, 必须实现其中的抽象方法, 实现时不需要使用override关键字
scala不支持类进行多继承, 但是支持多重继承Trait, 使用with关键字即可

trait HelloTrait{
  def sayHello(name:String)
}

trait MakeFriendsTrait{
  def makeFriends(p: Person)
}

class Person(val name:String) extends HelloTrait with MakeFriendsTrait with Cloneable with Serializable{
  def sayHello(name: String) = println("Hello, "+name)
  def makeFriends(p: Person) = println("Hello, my name is "+name+", your name is" +p.name)
}

在Trait中定义具体方法

scala中的Trait可以不是只定义抽象方法, 还可以定义具体方法, 此时Trait更像是包含了通用的工具方法的东西
有一个专有的名词来形容这种情况, 就是说Trait的功能混入了类
Java8 也支持这一功能, 叫默认方法
举例来说, Trait中可以包含一些很多类都通用的功能方法, 比如日志打印等, spark中就用了Trait来定义通用的日志打印方法

trait Logger{
  def log(message: String) = println(message)
}

class Person(val name:String) extends Logger{
  def makeFriends(p:Person){
    println("Hi, I'm " + name + ", I'm glad to make friends with you, " + p.name)
    log("makeFriends method is invoked with parameter Person[name="+p.name+"]")
  }
}

在Trait中定义具体字段

scala中的Trait可以定义具体field, 此时继承Trait的类就自动获得了Trait中定义的field
但是这种获取field的方式与继承class是不同的: 如果是继承clas获取field, 实际是定义在父类中, 而继承Trait获取的field, 就直接被添加到子类中

trait Person{
  val eyeNum: Int = 2
}

class Student(val name:String) extends Person{
  def sayHello = println("Hi, I'm " + name +", I have " + eyeNum +" eyes.")
}

在Trait中定义抽象字段

scala中的Trait可以定义抽象的field, 而Trait中的具体方法则可以基于抽象field来编写
但是继承Trait的类, 则必须覆盖抽象field, 提供具体值

trait SayHello{
   val msg:String
   def sayHello(name:String) = println(msg +", " +name)
}

class Person(val name:String) extends SayHello{
  val msg:String ="Hello"
  def makeFriends(p:Person){
    sayHello(p.name)
    println("I'm " +name+", I want to make friends with you!")
   }
}

为实例混入Trait

有时候我们可以在创建类的对象时, 指定该对象混入某个Trait, 这样, 就只有这个对象混入该Trait的方法, 而类的其他对象则没有

trait Logger{
  def log(msg:String) {}
}

trait MyLogger extends Logger{
  override def log(msg:String) {println("log: "+ msg)}
}

class Person(val name:String) extends Logger{
  def sayHello{println("Hi, I'm "+ name); log("sayHello is invoked!")}
}
val p1 = new Person("jc")
p1.sayHello
val p2 = new Person("jay") with MyLogger
p2.sayHello

Trait调用链

scala中支持让类继承多个Trait后, 依次调用多个Trait中的同一个方法, 只要让多个Trait的同一个方法中, 在最后都执行super.方法 即可
类中调用多个Trait中都有的这个方法是, 首相会从最右边的Trait的方法开始执行, 然后依次往左执行, 形成一个调用链条
这种特性非常强大, 其实就相当于设计模式中的责任链模式的一种具体实现依赖

trait Handler{
  def handle(data:String){}
}

trait DataValidHandler extends Handler{
  override def handle(data:String){
     println("check data:"+data)
     super.handle(data)
  }
}

trait SignatureValidHandler extends Handler{
  override def handle(data: String){
    println("check signature:" +data)
    super.handle(data)
  }
}

//注: 不是靠super调用上级产生调用链, 而是Trait的特性
class Person(val name:String) extends SignatureValidHandler with DataValidHandler{
  def sayHello={println("Hello, "+name);handle(name)}
}

在Trait中覆盖抽象方法

在Trait中, 是可以覆盖父Trait的抽象方法的
但是覆盖时, 如果使用了super.方法的代码, 则无法通过编译. 因为super.方法就会去掉用父Trait的抽相反, 此时子Trait的该方法还是会被认为是抽象的
此时如果要通过编译, 就得给子Trait的方法加上abstract override修饰

trait Logger{
  def log(msg:String)
}

trait MyLogger extends Logger{
  abstract override def log(msg:String) {super.log(msg)}
}

混合使用Trait的具体方法和抽象方法

在Trait中, 可以混合使用具体方法和抽象方法
可以让具体方法依赖于抽象方法, 而抽象方法则放到继承Trait的类中去实现
这种Trait其实就是设计模式中的模板设计模式的体现

trait Valid{
   def getName:String
   def valid:Boolean={
     getName == "jc"
   }
}


class Person(val name:String) extends Valid{
   println(valid)
   def getName = name
}

Trait的构造机制

在scala中, trait也是有构造代码的, 也就是在trait中的, 不包含在任何方法中的代码
而继承了trait的类的构造机制如下:

  1. 父类的构造函数执行
  2. Trait的构造函数执行, 多个Trait从左到右依次执行
  3. 构造Trait时会先构造父类Trait, 如果多个Trait继承同一个父Trait, 则父Trait只会构造一次
  4. 所有Trait构造完毕后, 子类的构造函数执行
class Person{println("Person's constructor!")} 
trait Logger{println("Logger's constructor!")}
trait MyLogger extends Logger{println("MyLogger's constructor!")}
trait TimeLogger extends Logger{println("TimeLogger's constructor!")}
class Student extends Person with MyLogger with TimeLogger{
   println("Student's constructor!")
}

Trait字段的初始化

在scala中, Trait是没有接收参数的构造函数的, 这是Trait和class的唯一区别, 但是如果需求就是要trait能够对field进行初始化, 该怎么办? 只能使用scala中非常特殊的一种高级特性:提前定义

trait SayHello{
   val msg:String
   println(msg.toString)
}

class Person

val p = new {val msg:String="init"} with Person with SayHello

class Person extends { val msg:String = "init"} with SayHello{}

//另外一种方式就是使用lazy value
trait SayHello{
  lazy val msg:String = null
  println(msg.toString)
}

class Person extends SayHello{
   override lazy val msg:String = "init"
}

Trait继承class

在scala中, Trait也可以继承自class, 此时这个class就会成为所有继承该Trait的类的父类

class MyUtil{
  def printMessage(msg:String) = println(msg)
}

trait Logger extends MyUtil{
  def log(msg:String)  = printMessage("log: "+ msg)
}

class Person(val name:String) extends Logger{
  def sayHello{
   log("Hi, I'm " +name)
   printMessage("Hi, I'm "+name)
  }
}

相关推荐