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

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}")
    }
  }
}
复制代码

猜你喜欢

转载自juejin.im/post/5c18f0925188250c4d7737e8