7. 面向对象(重点)
7.1 Scala 面向对象基础
类
[修饰符] class 类名 {
类体
}
-
scala语法中,类并不声明为public,所有这些类都具有公有可见性(即默认就是public)
-
一个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 = _
}
复制代码
练习
-
针对 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 } } } 复制代码
-
使用过程重写上面的 Scala 代码
def func(x: Int) { for (i <- 0 to x reverse) { print(i + "\t") } } 复制代码
-
编写一个for循环,计算字符串中所有字母的Unicode代码(toLong方法)的乘积。举例来说,"Hello"中所有字符串的乘积为9415087488L
def cal(str:String): Unit ={ var result = 1L for(x <- str){ result*=x.toLong } print(result) } 复制代码
-
使用 StringOps 的 foreach 方法重写上面的代码
var r2 = 1L // _ 可以理解为字符串的每一个字符 "Hello".foreach(r2 *= _.toLong) print(r2) 复制代码
-
使用递归解决上面求字符串每个字符 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)) } 复制代码
-
编写函数计算 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 类型()
-
如果我们不希望改变对象的引用(即:内存地址), 应该声明为val 性质的,否则声明为var, scala设计者推荐使用val ,因为一般来说,在程序中,我们只是改变对象属性的值,而不是改变对象的引用
-
scala在声明对象变量时,可以根据创建对象的类型自动推断,所以类型声明可以省略,但当类型和后面new 对象类型有继承关系即多态时,就必须写了
方法
Scala中的方法其实就是函数,只不过一般将对象中的函数称之为方法
def 方法名(参数列表) [:返回值类型] = {
方法体
}
练习
-
嵌套循环打印图形
def func1(): Unit ={ for (i <- 1 to 4; j <- 1 to 3) { if (j == 3) println("*") else print("*\t") } } 复制代码
-
计算矩形的面积
class Test { def area(): Double = { (this.width * this.length).formatted("%.2f").toDouble } var width: Double = _ var length: Double = _ 复制代码
构造器
java 的构造器回顾
[修饰符] 方法名(参数列表){
构造方法体
}
-
在Java中一个类可以定义多个不同的构造方法,构造方法重载
-
如果程序员没有定义构造方法,系统会自动给类生成一个默认无参构造方法(也叫默认构造器)
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)
}
复制代码
细节
-
Scala构造器作用是完成对新对象的初始化,构造器没有返回值。
-
主构造器的声明直接放置于类名之后 [反编译]
-
主构造器会执行类定义中的所有语句,这里可以体会到Scala的函数式编程和面向对象编程融合在一起,即:构造器也是方法(函数),传递参数和使用方法和前面的函数部分内容没有区别
-
如果主构造器无参数,小括号可省略,构建对象时调用的构造方法的小括号也可以省略
-
辅助构造器名称为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(){}
复制代码
- 辅助构造器的声明不能和主构造器的声明一致,会发生错误
属性高级
-
Scala类的主构造器函数的形参未用任何修饰符修饰,那么这个参数是局部变量
-
如果参数使用val关键字声明,那么Scala会将参数作为类的私有的只读属性使用
-
如果参数使用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", "黄色")
}
复制代码
-
加载类信息(属性信息,方法信息)
-
在堆中,给对象开辟空间
-
调用主构造器对属性进行初始化
-
使用辅助构造器对属性进行初始化
-
把对象空间的地址,返回给 bike 引用
7.2 面向对象进阶
包(难点)
回顾 Java 的包知识
-
作用
-
区分相同名字的类
-
当类很多时,可以很好的管理类
-
控制访问范围
-
-
打包基本语法
package com.cris;
-
打包的本质分析
实际上就是创建不同的文件夹来保存类文件
-
示例代码
先在不同的包下建立同名的类
如果想要在一个类中同时使用上面的两个 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 要求源码所在路径和字节码文件所在路径必须保持一致,如果我们此时去修改源码的打包路径
-
基本语法
import java.awt.* or import java.util.List
-
注意事项: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中的包的功能更加强大,使用也相对复杂些
-
基本语法 package 包名
-
Scala包的三大作用(和Java一样)
- 区分相同名字的类
- 当类很多时,可以很好的管理类
- 控制访问范围
-
Scala中包名和源码所在的系统文件目录结构要可以不一致,但是编译后的字节码文件路径和包名会保持一致(这个工作由编译器完成)
-
图示
-
命名规范
只能包含数字、字母、下划线、小圆点.,但不能用数字开头, 也不要使用关键字
一般是小写字母+小圆点一般是
com.公司名.项目名.业务模块名
-
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 包下生成 package.class 和 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=》等价于使用了 package$.class 中的 MODULE$.eat()
println(salary) // 1000.0=> 等价于使用了 package$.class 中的 MODULE$.salary()
}
}
}
}
复制代码
使用反编译工具打开瞧瞧
具体的执行流程第二章节已经解释过,这里不再赘述
注意事项:
- 每个包都可以有一个包对象,但是需要在父包中定义它
- 包对象名称需要和包名一致,一般用来对包(里面的类)的功能做补充
包的可见性
在Java中,访问权限分为: public,private,protected和默认。在Scala中,你可以通过类似的修饰符达到同样的效果。但是使用上有区别
-
当属性访问权限为默认时,从底层看属性是private的,但是因为提供了xxx_$eq()[类似setter]/xxx()[类似getter] 方法,因此从使用效果看是任何地方都可以访问)
-
当方法访问权限为默认时,默认为public访问权限
-
private为私有权限,只在类的内部和伴生对象中可用
示例:
-
protected为受保护权限,scala中受保护权限比Java中更严格,只能子类访问,同包无法访问
-
在scala中没有public关键字,即不能用public显式的修饰属性和方法。
包访问权限(表示属性有了限制。同时增加了包的访问权限),这点和Java不一样,体现出Scala包使用的灵活性
包的引入
细节说明
-
在Scala中,import语句可以出现在任何地方,并不仅限于文件顶部,import语句的作用一直延伸到包含该语句的块末尾。这种语法的好处是:在需要时在引入包,缩小import 包的作用范围,提高效率
示例如下:
-
Java中如果想要导入包中所有的类,可以通过通配符*,Scala中采用下 _
-
如果不想要某个包中全部的类,而是其中的几个类,可以采用选取器(大括号)
-
如果引入的多个包中含有相同的类,那么可以将不需要的类进行重命名进行区分,这个就是重命名
-
或者使用 import java.util.{HashMap => _ } 对冲突的包进行隐藏
练习
-
编写一个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 } } } 复制代码
-
创建一个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 } 复制代码
-
编写一段程序,将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}")
}
}
}
复制代码