Cris 的 Scala 笔记整理(七):面向对象

1,044 阅读12分钟

7. 面向对象(重点)

7.1 Scala 面向对象基础

[修饰符] class 类名 {

类体

}

  1. scala语法中,类并不声明为public,所有这些类都具有公有可见性(即默认就是public)

  2. 一个Scala源文件可以包含多个类

定义一个最简单的类

object Demo {
  def main(args: Array[String]): Unit = {
    var man = new Man
    man.name = "cris"
    man.age = 12
    println(man.name + "----" + man.age) // cris----12
  }

}

class Man {
  var name = ""
  var age = 0
}

反编译对应的 class 文件

属性

属性是类的一个组成部分,一般是值数据类型,也可是引用类型

  def main(args: Array[String]): Unit = {
    val man = new Man()
    val pc = new PC
    man.pc = pc
    man.pc.brand = "惠普"
    // man.pc().brand()
    println(man.pc.brand) // 惠普
  }

class Man {
  var name = "" // 手动设置初始值,此时可以省略成员属性的数据类型声明
  var age = 0
  var pc: PC = _ // _ 表示让 Scala 自动赋默认值,此时声明带上成员属性的数据类型,否则编译器无法确定默认值
}

class PC {
  var brand: String = _
}

练习

  1. 针对 for(int i = 10;i>0;i--){System.out.println(i)} 翻译成 Scala 代码

    object Practice {
      def main(args: Array[String]): Unit = {
        for (i <- 0.to(10).reverse) {
          print(i + "\t") // 10  9  8  7  6  5  4  3  2  1  0  
        }
      }
    }
    
  2. 使用过程重写上面的 Scala 代码

    def func(x: Int) {
      for (i <- 0 to x reverse) {
        print(i + "\t")
      }
    }
    
  3. 编写一个for循环,计算字符串中所有字母的Unicode代码(toLong方法)的乘积。举例来说,"Hello"中所有字符串的乘积为9415087488L

    def cal(str:String): Unit ={
      var result = 1L
      for(x <- str){
        result*=x.toLong
      }
      print(result)
    }
    
  4. 使用 StringOps 的 foreach 方法重写上面的代码

    var r2 = 1L
    // _ 可以理解为字符串的每一个字符
    "Hello".foreach(r2 *= _.toLong)
    print(r2)
    
  5. 使用递归解决上面求字符串每个字符 Unicode 编码乘积的问题

    def recursive(str: String): Long = {
      if (str.length == 1) str.charAt(0).toLong
      /*drop(n)从索引为 1 开始切片到结尾*/
      else str.take(1).charAt(0).toLong * recursive(str.drop(1))
    }
    
  6. 编写函数计算 x^n,其中 n 是整数(负数,0,正数),请使用递归解决

    def pow(x: Int, n: Int): Double = {
      if (n == 0) 1
      else if (n < 0) {
        1.0 / x * pow(x, n + 1)
      } else {
        x * pow(x, n - 1)
      }
    }
    

对象

val | var 对象名 [:类型] = new 类型()

  1. 如果我们不希望改变对象的引用(即:内存地址), 应该声明为val 性质的,否则声明为var, scala设计者推荐使用val ,因为一般来说,在程序中,我们只是改变对象属性的值,而不是改变对象的引用

  2. scala在声明对象变量时,可以根据创建对象的类型自动推断,所以类型声明可以省略,但当类型和后面new 对象类型有继承关系即多态时,就必须写

方法

Scala中的方法其实就是函数,只不过一般将对象中的函数称之为方法

def 方法名(参数列表) [:返回值类型] = {

​ 方法体

}

练习

  1. 嵌套循环打印图形

    def func1(): Unit ={
        for (i <- 1 to 4; j <- 1 to 3) {
            if (j == 3) println("*")
            else print("*\t")
        }
    }
    

  2. 计算矩形的面积

    class Test {
      def area(): Double = {
        (this.width * this.length).formatted("%.2f").toDouble
      }
    
    
      var width: Double = _
      var length: Double = _
    

构造器

java 的构造器回顾

[修饰符] 方法名(参数列表){

构造方法体

}

  1. 在Java中一个类可以定义多个不同的构造方法,构造方法重载

  2. 如果程序员没有定义构造方法,系统会自动给类生成一个默认无参构造方法(也叫默认构造器)

3)一旦定义了自己的构造方法,默认的构造方法就覆盖了,就不能再使用默认的无参构造方法,除非显示的定义一下,即: Person(){}

Scala 构造器

和Java一样,Scala构造对象也需要调用构造方法,并且可以有任意多个构造方法。

Scala类的构造器包括: 主构造器 和 辅助构造器

基础语法

class 类名(形参列表) { // 主构造器

// 类体

def this(形参列表) { // 辅助构造器

}

def this(形参列表) { //辅助构造器可以有多个...

}

}

简单示例

abstract class Dog {
  var name = ""
  var age = 0
  val color: String

  def this(name: String, age: Int) {
    this()
    this.name = name
    this.age = age
  }

  def eat(): Unit = {
    println("吃狗粮")
  }

  def run()
}
class Cat(var name: String, val color: String) {
  println("constructor is processing")

  def describe: String = name + "--" + color
}
  def main(args: Array[String]): Unit = {
	var cat = new Cat("tom", "gray")
    println(cat.describe)
    var cat2 = new Cat("jack", "red")
    println(cat2.describe)
  }

细节

  1. Scala构造器作用是完成对新对象的初始化,构造器没有返回值。

  2. 主构造器的声明直接放置于类名之后 [反编译]

  3. 主构造器会执行类定义中的所有语句,这里可以体会到Scala的函数式编程和面向对象编程融合在一起,即:构造器也是方法(函数),传递参数和使用方法和前面的函数部分内容没有区别

  4. 如果主构造器无参数,小括号可省略,构建对象时调用的构造方法的小括号也可以省略

  5. 辅助构造器名称为this(这个和Java是不一样的),多个辅助构造器通过不同参数列表进行区分, 在底层就是java的构造器重载,辅助构造器第一行函数体必须为 this.主构造器

abstract class Dog {
    var name = ""
    var age = 0
    val color: String

    def this(name: String, age: Int) {
        this()
        this.name = name
        this.age = age
    }

    def eat(): Unit = {
        println("吃狗粮")
    }

    def run()
}

6)) 如果想让主构造器变成私有的,可以在()之前加上private,这样用户只能通过辅助构造器来构造对象了,说明:因为Person3的主构造器是私有,因此就需要使用辅助构造器来创建对象

class Car private(){}
  1. 辅助构造器的声明不能和主构造器的声明一致,会发生错误

属性高级

  1. Scala类的主构造器函数的形参未用任何修饰符修饰,那么这个参数是局部变量

  2. 如果参数使用val关键字声明,那么Scala会将参数作为类的私有的只读属性使用

  3. 如果参数使用var关键字声明,那么那么Scala会将参数作为类的成员属性使用,并会提供属性对应的xxx()[类似getter]/xxx_$eq()[类似setter]方法,即这时的成员属性是私有的,但是可读写

class Counter {

  /*1. 有公开的 getter 和 setter 方法*/
  var count = 0
  /*2. 私有化 getter 和 setter,可以手动提供 setter 和 getter*/
  private var number = 1
  /*3. 只能被访问getter,无法修改setter,final 修饰的 age 属性*/
  val age = 12
  /*4. 对象级别的私有*/
  private[this] var length = 12

  def compare(other: Counter): Boolean = other.number > number

  //  def compareLength(other: Counter): Boolean = length > other.length

  def increase(): Unit = {
    number += 1
  }

  /*无参方法可以省略(),{}也可以省略*/
  def current: Int = number
}

def main(args: Array[String]): Unit = {
    var c = new Counter()
    c.count = 3
    println(c.count) // 3

    c.increase()
    println(c.current) // 2

    println(c.age) // 12
}

如果在主构造器中为属性设置了默认值,那么就不必在函数体内再去声明属性以及赋值了,大大简化代码的书写

def main(args: Array[String]): Unit = {
	val dog = new Dog()
    println(dog.name) // cris
    println(dog.age)  // 10
  }
}

class Dog(var name :String= "cris",var age:Int = 10){
    
}

JavaBean 注解

JavaBeans规范定义了Java的属性是像getXxx()和setXxx()的方法。许多Java工具(框架)都依赖这个命名习惯。为了Java的互操作性。将Scala字段加@BeanProperty时,这样会自动生成规范的 setXxx/getXxx 方法。这时可以使用 对象.setXxx() 和 对象.getXxx() 来调用属性

给某个属性加入@BeanPropetry注解后,会生成getXXX和setXXX的方法

并且对原来底层自动生成类似xxx(),xxx_$eq()方法,没有冲突,二者可以共存

对象创建流程分析

请针对以下代码简述对象创建流程

class Bike {
  var brand = ""
  var color = ""

  def this(brand: String, color: String) {
    this
    this.brand = brand
    this.color = color
  }
}

def main(args: Array[String]): Unit = {
   var bike = new Bike("ofo", "黄色")   
}
  1. 加载类信息(属性信息,方法信息)

  2. 在堆中,给对象开辟空间

  3. 调用主构造器对属性进行初始化

  4. 使用辅助构造器对属性进行初始化

  5. 把对象空间的地址,返回给 bike 引用

7.2 面向对象进阶

包(难点)

回顾 Java 的包知识

  1. 作用

    1. 区分相同名字的类

    2. 当类很多时,可以很好的管理

    3. 控制访问范围

  2. 打包基本语法

    package com.cris;

  3. 打包的本质分析

    实际上就是创建不同的文件夹保存类文件

  4. 示例代码

    先在不同的包下建立同名的类

    如果想要在一个类中同时使用上面的两个 Pig,Java 的解决方式如下:

        public static void main(String[] args) {
            Pig pig1 = new Pig();
            cris.package2.Pig pig2 = new cris.package2.Pig();
    //        pig1.getClass() = class cris.package1.Pig
            System.out.println("pig1.getClass() = " + pig1.getClass());
    //        pig2.getClass() = class cris.package2.Pig
            System.out.println("pig2.getClass() = " + pig2.getClass());
        }
    

    再来看看我们的源码所在路径和字节码文件所在路径,都是一一对应的

    Java 要求源码所在路径和字节码文件所在路径必须保持一致,如果我们此时去修改源码的打包路径

  5. 基本语法

    import java.awt.* or import java.util.List

  6. 注意事项:java中包名和源码所在的系统文件目录结构要一致,并且编译后的字节码文件路径也和包名保持一致

接着看看 Scala 是如何处理的

我们使用 Scala 重写上面的 Java 包案例

def main(args: Array[String]): Unit = {
  var b1 = new cris.package1.Bird1
  var b2 = new cris.package2.Bird2
  //    class cris.package1.Bird1
  println(b1.getClass)
  //    class cris.package2.Bird2
  println(b2.getClass)
}

此时我们如果修改了 Bird1 的打包路径

再看看源代码和字节码文件所在的路径

Scala 的包

和Java一样,Scala中管理项目可以使用包,但Scala中的包的功能更加强大,使用也相对复杂些

  1. 基本语法 package 包名

  2. Scala包的三大作用(和Java一样)

    1. 区分相同名字的类
    2. 当类很多时,可以很好的管理类
    3. 控制访问范围
  3. Scala中包名和源码所在的系统文件目录结构要可以不一致,但是编译后的字节码文件路径包名会保持一致(这个工作由编译器完成)

  4. 图示

  5. 命名规范

    只能包含数字、字母、下划线、小圆点.,但不能用数字开头, 也不要使用关键字

    一般是小写字母+小圆点一般是 com.公司名.项目名.业务模块名

  6. Scala 自动 import 的包有:java.lang.*,scala,Predef 包

Scala 打包细节(难点)

  • 常用的两种打包形式

    • 源代码的路径和字节码文件路径保持一致

    • 源代码的路径和字节码文件路径不一致

    • 上面的演示中已经很清楚的展示了 Scala 包的这一特点,我们继续用下面代码演示 Scala 包的嵌套

      我们在 Detail 类文件中写入以上非常奇怪的代码,编译运行后再查看源代码和字节码文件的位置

      进一步印证了 Scala 中源文件和字节码文件路径可以不一致

  • 包也可以像嵌套类那样嵌套使用(包中有包), 见上面图示。好处是:程序员可以在同一个文件中,将类(class / object)、trait 创建在不同的包中,非常灵活

  • 作用域原则:可以直接向上访问。即: Scala中子包中直接访问父包中的内容, 大括号体现作用域。(提示:Java中子包使用父包的类,需要import)。在子包和父包 类重名时,默认采用就近原则,如果希望指定使用某个类,则带上包名即可

    示例代码

    package com.cris {
    
      class Apple {
    
      }
      package scala {
        
        class Apple {
    
        }
    
        object Boy {
          def main(args: Array[String]): Unit = {
            /*1. Scala 中子包可以直接访问父包的内容;2. 子包和父包的类重名,默认采取就近原则;3. 可以带上类的路径名指定使用该类*/
            val apple = new Apple
            val apple2 = new com.cris.Apple
            //        class com.cris.scala.Apple
            println(apple.getClass)
            //        class com.cris.Apple
            println(apple2.getClass)
          }
        }
      }
    }
    
  • 父包要访问子包的内容时,需要import对应的类

    package com.cris {
    	
      import com.cris.scala.Apple
    
      object Apple{
        def main(args: Array[String]): Unit = {
            // 推荐只在使用的时候再引用,控制作用域
    	  import com.cris.scala.Apple
          val apple = new Apple()
    //      class com.cris.scala.Apple
          println(apple.getClass)
        }
      }
    
      package scala {
    
        class Apple {
    
        }
      }
    }-
    
  • 可以在同一个.scala文件中,声明多个并列的package(建议嵌套的pakage不要超过3层)

包对象

基本介绍:包可以包含类、对象和特质trait,但不能包含函数或变量的定义。这是Java虚拟机的局限。为了弥补这一点不足,scala提供了包对象的概念来解决这个问

参见如下代码

package com.cris {

  // 不能直接在 package 中定义函数和变量
  //  var name = "cris"

  /**
    * 包对象的名字需要和包名一致
    * package object emp 会在 com.cris.emp 包下生成&emsp;package.class 和&emsp;package$.class
    */
  package object emp {
    def eat(): Unit = {
      println("eat")
    }

    val salary = 1000.0
  }

  package emp {

    object test {
      def main(args: Array[String]): Unit = {
        eat() // eat=》等价于使用了&emsp;package$.class 中的&emsp;MODULE$.eat()
        println(salary) // 1000.0=>&emsp;等价于使用了&emsp;package$.class 中的 MODULE$.salary()
      }
    }
  }
}

使用反编译工具打开瞧瞧

具体的执行流程第二章节已经解释过,这里不再赘述

注意事项:

  1. 每个包都可以有一个包对象,但是需要在父包中定义它
  2. 包对象名称需要和包名一致,一般用来对包(里面的类)的功能做补充

包的可见性

在Java中,访问权限分为: public,private,protected和默认。在Scala中,你可以通过类似的修饰符达到同样的效果。但是使用上有区别

  1. 当属性访问权限为默认时,从底层看属性是private的,但是因为提供了xxx_$eq()[类似setter]/xxx()[类似getter] 方法,因此从使用效果看是任何地方都可以访问)

  2. 当方法访问权限为默认时,默认为public访问权限

  3. private为私有权限,只在类的内部和伴生对象中可用

示例:

  1. protected为受保护权限,scala中受保护权限比Java中更严格,只能子类访问,同包无法访问

  2. 在scala中没有public关键字,即不能用public显式的修饰属性和方法。

包访问权限(表示属性有了限制。同时增加了包的访问权限),这点和Java不一样,体现出Scala包使用的灵活性

包的引入

细节说明

  1. 在Scala中,import语句可以出现在任何地方,并不仅限于文件顶部,import语句的作用一直延伸到包含该语句的块末尾。这种语法的好处是:在需要时在引入包,缩小import 包的作用范围,提高效率

    示例如下:

  2. Java中如果想要导入包中所有的类,可以通过通配符*,Scala中采用下 _

  3. 如果不想要某个包中全部的类,而是其中的几个类,可以采用选取器(大括号)

  4. 如果引入的多个包中含有相同的类,那么可以将不需要的类进行重命名进行区分,这个就是重命名

  5. 或者使用 import java.util.{HashMap => _ } 对冲突的包进行隐藏

练习

  1. 编写一个Time类,加入只读属性hours和minutes,和一个检查某一时刻是否早于另一时刻的方法before(other:Time):Boolean。Time对象应该以new Time(hrs,min)方式构建

    object Practice {
      def main(args: Array[String]): Unit = {
        val time1 = new Time(4, 12)
        val result = time1.before(new Time(4, 14))
        println(result)
      }
    }
    
    class Time(val hour: Int, val minute: Int) {
      
      def before(other: Time) = {
        if (this.hour < other.hour) true
        else if (this.hour > other.hour) false
        else if (this.hour == other.hour) {
          if (this.minute < other.minute) true
          else if (this.minute > other.minute) false
          else false
        }
      }
    }
    
  2. 创建一个Student类,加入可读写的JavaBeans属性name(类型为String)和id(类型为Long)。有哪些方法被生产?(用javap查看。)你可以在Scala中调用JavaBeans的getter和setter方法吗?

    object Practice {
      def main(args: Array[String]): Unit = {
        var s = new Student
        println(s.getName)
        println(s.age)
      }
    }
    
    class Student {
      @BeanProperty var name = "好学生"
      @BeanProperty var age = 0
    
    }
    

  3. 编写一段程序,将Java哈希映射中的所有元素拷贝到Scala哈希映射。用引入语句重命名这两个类

    object Ex extends App {
    
      import java.util.{HashMap => JavaHashMap}
      import scala.collection.mutable.{HashMap => ScalaHashMap}
    
      var map1 = new JavaHashMap[Int, String]()
      map1.put(1, "cris")
      map1.put(2, "james")
      map1.put(3, "simida")
    
    
      var map2 = new ScalaHashMap[Int, String]()
      for (key <- map1.keySet().toArray()) { // key 的数据类型是 AnyRef
        // asInstanceOf 强制数据类型转换
        map2 += (key.asInstanceOf[Int] -> map1.get(key))
      }
      println(map2.mkString("||")) // 2 -> james||1 -> cris||3 -> simida
    
    }
    

抽象

我们在前面去定义一个类时候,实际上就是把一类事物的共有的属性和行为提取出来,形成一个物理模型(模板)。这种研究问题的方法称为抽象

示例代码

object Demo extends App {

  var account = new Account("招行:888888", 200, "123456")
  account.query("123456")

  account.save("123456", 100)
  account.query("123456")

  account.withdraw("123456", 250)
  account.query("123456")

}

class Account(val no: String, var balance: Double, var pwd: String) {
  def query(pwd: String): Unit = {
    if (pwd != this.pwd) {
      println("密码错误!")
    } else {
      println(s"卡号:${this.no},余额还有:${this.balance}")
    }
  }

  def save(pwd: String, money: Double): Unit = {
    if (pwd != this.pwd) {
      println("密码错误")
    } else {
      this.balance += money
      println(s"卡号:${this.no},存入:${money},余额为:${this.balance}")
    }
  }

  def withdraw(pwd: String, money: Double): Unit = {
    if (pwd != this.pwd) {
      println("密码错误")
    } else if (money > this.balance) {
      println("余额不足")
    } else {
      this.balance -= money
      println(s"卡号:${this.no},取出:${money},余额为:${this.balance}")
    }
  }
}