Scala学习笔记(三)——面向对象、类、抽象类、伴生、内部类、样例类、枚举、泛型类、类型边界、型变、特质、动态混入、包

Scala面向对象

一、基本概念

1、类的概念

  • 类通过class关键字定义
  • 类通过new关键字创建实例
  • 类拥有成员变量和方法
  • 类的成员默认为public,也支持private、protected
  • 类中无法定义静态成员变量和方法
  • 类无需明确定义构造方法,通过构造参数列表声明为类的一部分

类是对象的抽象,而对象是类的具体实例。类是抽象的,不占用内存,而对象是具体的,占用存储空间。类是用于创建对象的蓝图,它是一个定义包括在特定类型的对象中的方法和变量的软件模板。

我们可以使用 new 关键字来创建类的对象,实例如下:

class Point(xc: Int, yc: Int) {
   var x: Int = xc
   var y: Int = yc

   def move(dx: Int, dy: Int) {
      x = x + dx
      y = y + dy
      println ("x 的坐标点: " + x);
      println ("y 的坐标点: " + y);
   }
}

Scala中的类不声明为public,一个Scala源文件中可以有多个类。

以上实例的类定义了两个变量 xy ,一个方法:move,方法没有返回值。

2、类成员访问修饰符

Scala 访问修饰符基本和Java的一样,分别有:private,protected,public。

如果没有指定访问修饰符,默认情况下,Scala 对象的访问级别都是 public。

Scala 中的 private 限定符,比 Java 更严格,在嵌套类情况下,外层类甚至不能访问被嵌套类的私有成员。

Scala和Java访问修饰符对比

  • Java
ModifierClassPackageSubClassWorld
publicYYYY
protectedYYYN
defaultYYNN
privateYNNN
  • Scala
ModifierClassCompanionSubclassPackageWorld
default(public)YYYYY
protectedYYYNN
privateYYNNN

私有成员(private)

用 private 关键字修饰,带有此标记的成员仅在包含了成员定义的类或对象内部可见,同样的规则还适用内部类。

class Outer{
    class Inner{
    private def f(){println("f")}
    class InnerMost{
        f() // 正确
        }
    }
    (new Inner).f() //错误
}

(new Inner).f( ) 访问不合法是因为 f 在 Inner 中被声明为 private,而访问不在类 Inner 之内。

但在 InnerMost 里访问 f 就没有问题的,因为这个访问包含在 Inner 类之内。

Java中允许这两种访问,因为它允许外部类访问内部类的私有成员。

保护成员(protected)

在 scala 中,对保护(Protected)成员的访问比 java 更严格一些。因为它只允许保护成员在定义了该成员的的类的子类中被访问。而在java中,用protected关键字修饰的成员,除了定义了该成员的类的子类可以访问,同一个包里的其他类也可以进行访问。

package p{
class Super{
    protected def f() {println("f")}
    }
    class Sub extends Super{
        f()
    }
    class Other{
        (new Super).f() //错误
    }
}

上例中,Sub 类对 f 的访问没有问题,因为 f 在 Super 中被声明为 protected,而 Sub 是 Super 的子类。相反,Other 对 f 的访问不被允许,因为 other 没有继承自 Super。而后者在 java 里同样被认可,因为 Other 与 Sub 在同一包里。

公共成员(public)

Scala中,如果没有指定任何的修饰符,则默认为 public。这样的成员在任何地方都可以被访问。

class Outer {
   class Inner {
      def f() { println("f") }
      class InnerMost {
         f() // 正确
      }
   }
   (new Inner).f() // 正确因为 f() 是 public
}

二、类

1、类的定义

  • 构造器

    • 主构造器:在一个 Scala 类中,有且仅有一个主构造器。在我们对一个类实例化的时候,一定会调用主构造器,或显式调用或隐式调用的区别而已

    • 辅助构造器:Scala 对于辅助构造器的数量没有限制,我们可以在一个类中定义 0 ~ n 个辅助构造器。可以直接由辅助构造器来实例化一个类。我们甚至可以认为 Scala 的辅助构造器就是 Java 或者 C++ 中的普通构造器,因为它们之间的区别仅有以下两点

      1. 构造器的名称统一为 this ;

      2. 每一个辅助构造器都必须显式地调用其它辅助构造器或者主构造器。

  • 成员变量变量和方法

  • 类的实例化

class Student {
   //定义成员变量
  var name="zhang"
  var age:Int=_   //默认0
  var gender:String=_   //默认null
  val classNo="1"

  //定义一个辅助构造器1
  def this(name:String,age:Int)={
    //辅助构造器必须从调用其他构造器开始
    this()
    this.name = name
    this.age = age
  }

  def this(gender:String)={
    this("lisi",18)
    this.gender = gender
  }

  //定义一个成员方法
  def study()={
    println("好好学习 天天向上")
  }
}

object Test01{
  def main(args: Array[String]): Unit = {
    val stu1 = new Student()
    println(stu1.name)
    println(stu1.gender)
    println(stu1.age)
    stu1.study()

    println("############################################")

    val stu2 = new Student("zhangsan",20)
    println(stu2.name)
    println(stu2.gender)
    println(stu2.age)
    stu2.study()

    println("############################################")

    stu2.age=25
    println(stu2.age)
    println(stu2.classNo)
    
  }
}
//输出结果
zhang
null
0
好好学习 天天向上
############################################
zhangsan
null
20
好好学习 天天向上
############################################
25
1

2、类的继承

  • Scala使用“extends”关键字实现继承
  • 子类重写父类方法必须使用“override”关键字
class XiaoXueSheng extends Student {
  override def study(): Unit = {
    println("小学生不喜欢学习")
  }

  def play()={
    println("小学生喜欢玩游戏")
  }
}

object Test02{
  def main(args: Array[String]): Unit = {
    val xiao = new XiaoXueSheng
    println(xiao.classNo)
    xiao.study
    xiao.play
  }
}

//输出结果
1
小学生不喜欢学习
小学生喜欢玩游戏

3、抽象类

  • 抽象类可包含未实现的方法,即抽象方法
  • 抽象类无法实例化
  • 抽象类使用“abstract”关键字修饰
    • 子类重写父类抽象方法时,“override”关键字可选
    • 子类重写父类非抽象方法,“override”关键字必写
//父类
//定义一个抽象类使用abstract修饰
//抽象类用来给子类继承
abstract class Shape {
  def draw():Unit

  def write()={
    println("写作")
  }
}
//子类
class Circle extends Shape {
  override def draw(): Unit = {
    println("画画")
  }
}
object Test03{
  def main(args: Array[String]): Unit = {
    val circle = new Circle
    circle.write()
    circle.draw()
  }
}

//输出结果
写作
画画

4、单例对象

在Scala的类中无法定义静态成员,即无static关键字。name下scala中如何像Java一样表达类的静态成员变量、成员方法与静态代码块?

  • Scala解决方案:单例对象
    • 使用“object”关键字声明,可包含变量、方法与代码定义
    • 单例对象中的成员变量、成员方法通过单例对象名直接调用
    • 单例对象第一次被访问时初始化,并执行全部代码块
    • 单例对象不能new,且无构造参数
    • 程序入口main()方法必须定义在单例对象中
    • 单例对象与同名类定义在同一文件中时形成绑定关系

Scala单例对象是十分重要的,没有像在Java一样,有静态类、静态成员、静态方法,但是Scala提供了object对象,这个object对象类似于Java的静态类,它的成员、它的方法都默认是静态的。单例对象是一种特殊的类,有且只有一个实例。和惰性变量一样,单例对象是延迟创建的,当它第一次被使用时创建。当对象定义于顶层时(即没有包含在其他类中),单例对象只有一个实例。当对象定义在一个类或方法中时,单例对象表现得和惰性变量一样。

object Blah {
    println("initalizing")
    def sum(l:List[Int]):Int=l.sum

    def main(args: Array[String]): Unit = {
      //单例对象可以通过单例对象名调用
      println(Blah.sum(List(1,2,3,4,5)))
    }
}

//输出结果
initalizing
15

5、伴生

单例对象与类同名时,这个单例对象被称为这个类的伴生对象,而这个类被称为这个单例对象的伴生类。伴生类和伴生对象要在同一个源文件中定义,伴生对象和伴生类可以互相访问其私有成员。不与伴生类同名的单例对象称为孤立对象。

  • 单例对象与同名类定义在同一文件中时形成绑定关系
    • 同名类称为单例对象的伴生类(class)
    • 单例对象称为同名类的伴生对象(object)
  • 伴生类与伴生对象可相互访问各自私有成员
  • 伴生对象可为伴生类增加静态成员
//Student.scala
//伴生类
//伴生对象和伴生类
//如果有一个class,还有一个与class同名的object,那么就称这个object是class的伴生对象,class就是object的伴生类
//伴生对象和伴生类必须放在同一个.scala文件中
//伴生对象和伴生类,最大特点是可以互相访问private 变量
class Student(n: String, a: Int) {
  private var name = n    //私有变量,伴生对象可以访问
  private var age = a
}
//伴生对象
object Student {
  def apply(n: String, a: Int): Student = new Student(n, a)
      //使用伴生对象的apply()方法省掉new关键字。Student.apply()等价于Student()
  def main(args: Array[String]): Unit = {
    val student = new Student("zhangsan",15)//通过伴生对象的apply()方法创建实例
    println(student.name)
    println(student.age)
  }
}
//输出结果
zhangsan
15

6、内部类

  • 一个类可以作为另一个类的成员,称为内部类
    • Java内部类是外部类的成员
    • Scala内部类绑定到外部类的对象实例
class Graph {
  class Node {
    var connectedNodes: List[Node] = Nil
    def connectTo(node: Node) {
      if (connectedNodes.find(node.equals).isEmpty) {
        connectedNodes = node :: connectedNodes
      }
    }
  }
  var nodes: List[Node] = Nil
  def newNode: Node = {
    val res = new Node
    nodes = res :: nodes
    res
  }
}

val g: Graph = new Graph
val n1: g.Node = g.newNode
val n2: g.Node = g.newNode
n1.connectTo(n2)      // legal
val h: Graph = new Graph
val n3: h.Node = h.newNode
n1.connectTo(n3)      // illegal!
//n1与n3被认为是不同的类型
class Graph {
  class Node {
    var connectedNodes: List[Graph#Node] = Nil
        //可以接受任意外部类对象实例中的内部类
    def connectTo(node: Graph#Node) {
      if (connectedNodes.find(node.equals).isEmpty) {
        connectedNodes = node :: connectedNodes
      }
    }
  }
  var nodes: List[Node] = Nil
  def newNode: Node = {
    val res = new Node
    nodes = res :: nodes
    res
  }
}

7、样例类

  1. 定义
  • 样例类常用于描述不可变的值对象(Value Object)

    case class Student(name:String,age:Int)      //定义样例类
    val stu=Student("Jason",19)      //创建样例类的实例,无需new关键字
    println(stu.name)       //访问对象属性  
    
    • 样例类构造参数默认声明为“val”,自动实现类构造参数的getter
    • 样例类构造参数声明为“var”时,自动实现类构造参数的setter和getter
    • 样例类自动创建伴生对象
    • 样例类自动实现的其他方法
      • toString()、equals()、copy()、hashCode()
      • 伴生对象中的apply()、unapply()
        • **unapply()**接受一个对象,从对象中提取出相应的值,主要用于模式匹配中。
  1. 样例类和普通类

    • 区别

      (1)普通类在编译后只会生成一个类名.class文件;而样例类会生成伴生类.class文件和伴生对象的.class文件。

      (2)样例类自动实现了序列化接口

      (3)样例类的构造器中的参数如果不被声明为var的话,它默认的是val类型的,但一般不推荐将构造器中的参数声明为var。

      (4)样例类自动创建伴生对象,同时在里面给我们实现了apply方法,使我们在使用的时候可以不直接使用new创建对象。

      (5)伴生对象中同样会帮我们实现unapply方法,从而可以将case class应用于模式匹配匹配某个对象的时候。

      (6)实现自己的toString、hashCode、copy、equals方法

    • 最佳实践

      • 如果一个对象在内部执行有状态计算,或者表现出其他类型的复杂行为,那么它应该是一个普通类

8、枚举

  • 枚举

    object Weekday  extends Enumeration {
      //枚举值从0开始计数
      val Mon,Tue,Wed,Thu,Fri,Sat,Sun=Value
    }
    
    //枚举的使用
    object Test10{
      def main(args: Array[String]): Unit = {
        println(Weekday.Mon)
        println(Weekday.Fri.id)
        Weekday.values.foreach(print)
      }
    }
    
    //输出结果
    Mon
    4
    MonTueWenThuFriSatSun
    
  • 样例类与枚举区别

    • 枚举更简单,代码更少
    • 样例类的字段比枚举的值更强大
    • 样例类可扩展
    abstract class Term(code: String)
    case class Var(name: String) extends Term(name)
    case class Fun(arg: String, body: Term) extends Term(arg)
    case class App(f: Term, v: Term) extends Term("App")
    

9、泛型类

  • 泛型类指可以接受类型参数的类,泛型类在集合类中被广泛使用
  • 与Java不同,定义泛型类使用“[]”

举例:

object GenericClaDemo {
  class Stack[T] {
    //定义一个空list
    var elements: List[T] = Nil
    //定义一个方法,向list中添加元素
    def push(x: T) {
      elements = x :: elements
    }
    //获取元素
    def top: T = elements.head
    //弹出元素
    def pop() {
      var t = elements.head
      elements = elements.tail
      t
    }
    //查看元素
    def showElements(){
      elements.foreach(x=>print(s"$x "));println()}
  }

  def main(args: Array[String]): Unit = {
    val ms = new Stack[Int]()
    ms.push(10)
    ms.showElements()
    ms.push(20)
    ms.showElements()
    val t = ms.pop()
    ms.showElements()
  }
}
//输出结果
10 
20 10 
10 

10、类型边界

  • 在Scala中,类型参数可以有一个类型边界约束
  • 类型上界:将类型限制为另一种类型的子类
    • T<:A 表示类型变量T应该是类型A的子类
    • A是具体类型,T是泛型
  • 类型下界:将类型声明为另一种类型的超类
    • T>:A 表示类型变量T应该是类型A的超类
    • A是具体类型,T是泛型

11、型变

  • 协变

    trait Queue[+T] {}

    这是协变情况。这种情况下,当类型S是类型A的子类型,则Queue[S]也可以认为是Queue[A}的子类型,即Queue[S]可以泛化为Queue[A]。也就是被参数化类型的泛化方向与参数类型的方向是一致的,所以称为协变。

  • 逆变

    trait Queue[-T] {}

    这是逆变情况。这种情况下,当类型S是类型A的子类型,则Queue[A]反过来可以认为是Queue[S}的子类型。也就是被参数化类型的泛化方向与参数类型的方向是相反的,所以称为逆变。

  • 不变

    trait Queue[T] {}

    这是非变情况。这种情况下,当类型S是类型A的子类型,则Queue[S]不可认为是Queue[A]的子类型或父类型,这种情况是和Java一样的。

三、特质

1、概念

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

  • Scala中没有接口(interface)的概念
  • 特质用于在类之间共享程序接口和字段,类似Java接口
  • 特质是字段和方法的集合,可以提供字段和方法实现
  • 类和单例对象都可以扩展特质(extends)
  • 特质不能被实例化,因此没有构造参数,类似Java接口
  • 特质使用“trait”关键字定义
  • 实现特质中的方法使用“override”

2、使用特质

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

//创建一个Pet特质
trait Pet {
  val name:String
  def cry():Unit
}

class Cat() extends Pet{
  override val name: String = "Tom"
  override def cry(): Unit = {
    println("喵喵")
  }
}

class Dog(val name:String) extends Pet {
  override def cry(): Unit = {
    println("嘻嘻")
  }
}

object Test06 extends App{
  val dog1 = new Dog("Harry")
  println(dog1.name)
  dog1.cry()
  println("###################################")
  val cat1 = new Cat
  println(cat1.name)
  cat1.cry()
}

//输出结果
Harry
嘻嘻
###################################
Tom
喵喵

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

3、混入特质

  • 当某个特质被用于组合类时,被称为混入
  • 一个类只能有一个父类但是可以有多个混入(分别使用关键字extends和with)
object MixinTrait extends App {
  val d = new D
  println(d.message)
  println(d.mes_toUpper)
}
//定义一个类
abstract class A{
  val message:String
}
//B继承抽象类A
class B extends A{
  override val message: String = "this is B"
}
//定义一个特质C,继承A
trait C extends A{
  def mes_toUpper=message.toUpperCase()
}
class D extends B with C {
}

//输出结果
this is B
THIS IS B

关于混入特质的顺序:

package com.hlf.scala.basic

/**
  * 测试多个特质中有同一个方法,混入顺序的不同最终调用的结果
  * 结果显示:
  * 如果混入的特质是子父类型的话,调用的是子的方法
  * 如果混入的特质最终父类是同一个的话,调用的是最右边的方法
  */
object TestTraitOrder {
  def main(args: Array[String]) {
    //先测试混入的特质有父子关系时的顺序
    val test1 =new TestOrder with Foo with First
    val test2 =new TestOrder with First with Foo
    test1.name  //结果是First..........
    test2.name  //结果是First.......... 说明混入有父子关系的特质最终是调用子的方法

    //测试同继承一个特质时的顺序
    val test3 = new TestOrder with First with Second
    val test4 = new TestOrder with Second with First
    test3.name  //结果是Second........
    test4.name  //结果是First.........说明同是继承一个父类时,最终调用的是右边的方法

    //测试特质间的关系不是一层继承关系时
    val test5 =  new TestOrder with First with Three
    val test6 =  new TestOrder with Three with First
    test5.name  //结果是Three........
    test6.name  //结果是First........说明继承的父类最终是同一个时,调用的是右边的方法

  }
}

trait Foo{ def name = println( "Fooo..........")}

trait First extends Foo{
  override def name: Unit = println( "First..........")
}

trait Second extends Foo{
  override def name: Unit = println( "Second........")
}

trait Three extends Second{
  override def name: Unit =  println( "Three........")
}

class TestOrder{}

4、动态混入特质

  1. Trait除了可以在类声明时继承特质以外,还可以在构建对象时混入特质,扩展目标类的功能
  2. 此种方式也可以应用于对抽象类功能进行扩展
  3. 动态混入是 Scala 特有的方式 (java 没有动态混入),可在不修改类声明/定义的情况下,扩展类的功能,非常的灵活,耦合性低 。
  4. 动态混入可以在不影响原有的继承关系的基础上,给指定的类扩展功能。
  5. 同时要注意动态混入时,如果抽象类有抽象方法,如何混入(内部类形式实现抽象方法)
class Drawing {
  //实现注入
  self:Shaping =>

  def start():Unit=draw()

}

trait Shaping{
  def draw():Unit
}

trait DrawSquare extends Shaping{
  override def draw(): Unit = {
    println("draw a square")
  }
}

trait DrawTriangle extends Shaping{
  override def draw(): Unit = {
    println("draw a Triangle")
  }
}

trait DrawRectangle extends Shaping{
  override def draw(): Unit = {
    println("draw a Rectangle")
  }
}

object Test08 extends App {
  val d1 = new Drawing with DrawSquare
  d1.start()
  println("###############################")
  val d2 = new Drawing with DrawTriangle
  d2.start()
  println("###############################")
  val d3 = new Drawing with DrawSquare with DrawTriangle with DrawRectangle
  d3.start()
  println("###############################")
  val d4 = new Drawing with DrawTriangle with DrawRectangle with DrawSquare
  d4.start()
}

//输出结果
draw a square
###############################
draw a Triangle
###############################
draw a Rectangle
###############################
draw a square
    
//总结来说 实现最后混入的特质的功能

5、特质与抽象类的选择

  • 优先使用特质
    • 抽象类只能继承一次
    • 特质可混入多个
  • 需要使用带参构造方法时,使用抽象类
  • 与Java互操作性
    • 抽象类与Java完全可互操作
    • 特质只有在不包含任何实现代码时才可互操作

四、包

1、包和包对象

  • Scala包:package 包名
    • 只能包含数字、字母、下划线、圆点
    • 不能用数字开头, 不能使用关键字
    • 可以在同一个.scala文件中,声明多个并列的package

package com.kgc{
	package scala1 {... }
	package scala2 {... }
}
package scala3 {... }
  • Scala包对象
    • 包可以包含类、对象和特质,但不能包含变量或方法的定义,应使用包对象解决这个问题


package com.kgc{
	package object scala {	//对应包com.kgc.scala,每个包都可以有一个包对象
   		val name="Wow"
	}...
	package scala{...}//与包对象同名的包可直接使用包对象中定义的变量和方法
}

2、包引用

  • import让包和包对象的成员可以直接通过名称访问
//易于访问Fruit
import cn.kgc.Fruit
//易于访问cn.kgc的所有成员
import cn.kgc._
//易于访问cn.kgc.Fruits的所有成员
import cn.kgc.Fruits._
//只引用Apple与Orange,并且Apple重命名为McIntosh
import cn.kgc.Fruits.{Apple=>McIntosh,Orange}

def showFruit(fruit:Fruit){
    import fruit._
    println(name+color)
}
  • import灵活引用
    • 可以出现在任何地方
    • 可以是对象和包
    • 可以重命名或隐藏一些被引用的成员
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值