一、面向对象初级
1.1 类与对象
1.1.1 概念
- 众所周知java是一门面向对象语言,对象是java的一等功臣,但java因种种原因还保留许多非面向对象的内容,如基本数据类型(虽然1.5引入了包装类)、静态(居然先于对象存在)等
- scala则是纯粹的面向对象,在scala中一切皆为对象
- scala改进了java所有非面向对象的内容
- scala基本数据类型也是对象,也可以调用方法
- 通过伴生类,伴生对象解决静态问题
- 移除static、braak、continue等关键词
- 解决方法,属性只能属于类(包对象)
- 等等等(主要是我只知道这么多)
1.1.2 快速入门
创建一个Person
类包含name
、age
属性和run()
方法
package oop.blog
object Person {
def main(args: Array[String]): Unit = {
val person = new Person
//val person = new Person()
person.name = "张三"
person.age = 20
person.run()
println(s"name = ${person.name}, age = ${person.age}")
}
}
class Person {
var name: String = _
var age: Int = _
def run(): Unit = {
println("人会跑")
}
}
1.1.3 细节说明(重点)
- scala也有构造器概念,同样会默认提供一个无参构造器(不太准确),因此当调用无参构造器时
()
可以省略,即val person = new Person
- scala的设计者推荐使用
val
接收对象,即对象一旦被创建引用不再更改 - scala要求类的属性在声明时一定要赋值(如果不赋值就是抽象属性,之后会解释这个神奇的现象)
- 续上一点,可以通过
_
进行赋值,此时会根据属性的类型自动赋值;因此使用_
就不能使用类型推导(省略类型)来赋值 - 关于属性权限问题,这一点很容易被带入java的坑中,下面详细说明
- 上述代码在站在java角度看
name
、age
是public
至少不是private
,但再scala底层它确实是private
- 通过反编译发现
name
、age
是私有的,至于为什么能通过对象直接访问是因为底层生成了公共的name()
、name_$eq()
- 通过对象直接访问属性其底层调用的是公共的getter/setter(只是方法名不同罢了)
- 上述代码在站在java角度看
有图有真相
1.1.4 类与对象关系
- scala的类与对象关系和java一样
- 类是抽象的,代表一类事物
- 对象是具体的,代表一个具体的事物
- 类是对象的模板,对象是类的实例
1.1.5 内存分配
scala和java一样的内存分配,即也是值传递,见下面的例子
package oop.blog
object Test {
def main(args: Array[String]): Unit = {
val person = new Person
println(person == test(person))
}
def test(p: Person): Person = p
}
结果为:true
1.2 方法
方法的定义格式
def 方法名(参数列表)[: 返回值类型] ={
方法体
}
1.3 构造器
1.3.1 概念
构造器的作用在于创建对象时初始化对象,对属性进行赋值
1.3.2 java 构造器
- 在java中一个类可以定义多个构造器,即构造器的重载
- 在java中构造器也成构造方法,该方法没有返回值,方法名为类名
- 在java中没有显式定义构造器时系统会提供一个无参构造器
- 在java中若显式定义构造器系统则不再提供,一般会在定义一个无参构造器
1.3.3 scala 构造器
- scala的构造器特点和java基本一致
- scala构造器支持重载,但分主构造器和辅助构造器
1.3.4 快速入门
package oop.blog
object Person {
def main(args: Array[String]): Unit = {
val person = new Person("张三", 21)
person.run()
println(s"name = ${person.name}, age = ${person.age}")
}
}
class Person(_name: String, _age: Int) {
var name: String = _name
var age: Int = _age
def this(name: String) {
this(name, 21)
}
def this() {
this("", 0)
}
def run(): Unit = {
println("人会跑")
}
}
1.3.5 主/辅助构造器
- 主构造器
- 紧跟类名后,如
class Person(_name: String, _age: Int)
- 若主构造器为无参则
()
可以不写即class Person <=> class Person()
- 紧跟类名后,如
- 辅助构造器
- 区别于java,scala的辅助构造器使用this
- 与主构造器没有地位之分(看反编译)
- 从反编译可以看出,辅助构造器参数不能和主构造器一致,即和java一样,根据参数列表调用指定的构造器
1.3.6 细节说明(重点)
- scala构造器的作用是完成新对象的初始化,不参与对象的创建过程
- 主构造器会执行类定义的所有语句,即在scala中类体可以写任何语句(java的语句就必须写在方法中)
class Person(_name: String, _age: Int) {
var name: String = _name
var age: Int = _age
println("java就不能这么写")
def this(name: String) {
this(name, 21)
}
def this() {
this("", 0)
}
def run(): Unit = {
println("人会跑")
}
}
//执行结果
java就不能这么写
人会跑
name = 张三, age = 21
那为什么scala可以吧语句写在类体中,且只有调用主构造器才会被执行呢?请看反编译
其实是把类体的语句放在了主构造器里依次执行,底层还是没有绕开java(毕竟是与java无缝对接)
- 多个辅助构造器和主构造器的本质是构造器的重载
- 辅助构造器细节
- 辅助构造器必须直接或者间接调用主构造器
- 通过this(…)调用且必须放在辅助构造器有效代码的第一行(和java一样)
- 辅助构造器互相调用必须先定义再调用,即只能从下往上调用,看下面代码
报:Error:(19, 5) self constructor invocation must refer to a constructor definition which precedes it, to prevent infinite cycles this("zs")
把this()
和this(name:String)
调换位置即可
- 至于为什么辅助构造器为什么一定要间接或者直接调用主构造器涉及到继承,即scala中不允许使用super()调用父类构造器,只有在调用主构造器时才能调用父类对应的构造器。
- 主构造器可以私有化,格式如下
class Person private(_name: String, _age: Int)
,在单例模式可以用的上
1.4 属性高级
1.4.1 构造器参数
对于上述所有主构造器里的参数都是局部变量,即只能在类体方法外中访问,从反编译可以看出这些参数最终会放在一个构造函数的参数列表中,所以其他地方无法访问,但真的就没有办法访问吗?其实不是,见下面代码
class Person private(var _name: String, val _age: Int) {
var name: String = _name
var age: Int = _age
println("java就不能这么写")
def this(name: String) {
this(name, 21)
}
def this() {
this("zs")
}
def run(): Unit = {
//在这里可以被调用
println(_name)
println(_age)
println("人会跑")
}
}
注意主构造器加了val/var修饰后就可以在类的任何地方访问,其原理是加入val/var后该变量会上升到类属性高度,这是编译会生成getter/setter方法即可正常使用,那么val和var修饰的区别在于val修饰的变量编译时不会生成setter方法
1.4.2 Bean 属性
通过@BeanProperty
注解作用于属性上,编译时会给属性生成get/set方法,注意:
- 这个注解是scala的
import scala.beans.BeanProperty
当然java也有不要用错了 - 生成的是标准getter/setter方法和原先的不冲突(反编译可以看出)
1.5 补充
scala对象创建流程
- 加载类信息(属性信息、方法信息)
- 堆中开辟空间
- 父类构造器(主或辅助)进行初始化
- 子类构造器(主或辅助)进行初始化
- 将开辟的对象地址赋值给某个引用
二、面向对象中级
2.1 包
2.1.1 java 包的作用
- 区分同名类
- 方便管理
- 控制访问权限
- 其本质是创建不同的文件夹
2.1.2 scala 包的作用
- 拥有java包的所有作用
- 可以对类进行拓展[重点]
2.1.3 快速入门
注意:scala中类名和包名可以不对应,即声明包名package com.xxx.xx
,我的类可以不放在这里(java则要求对应)
package packageobj.test.xxx.xx //瞎写的包名,不会报错
//package packageobj
object TestPackage {
def main(args: Array[String]): Unit = {
}
}
当编译完scala会根据定义的包路径创建对应的文件夹后把.class文件放进去,其实底层依然遵循着java的规则只是表现形式不同罢了,推荐还是按照java的规范
2.1.4 细节说明(有趣)
scala中循序在创建类的同时动态创建包,看下面代码
package packageobj {//表明在项目的根路径下创建了packageobj包
//在packageobj包下创建类C1
class C1 {}
//在packageobj包下创建类C2
class C2 {}
//创建了packageobj.p1包
package p1 {
//创建了packageobj.p1.p2包
package p2 {
在packageobj.p1.p2包下创建类C3
class C3 {}
}
}
}
object TestPackage {
def main(args: Array[String]): Unit = {
}
}
- 创建包时可以在
{}
里写class、object、trait,但不能写属性和方法(见包对象) - 可以嵌套定义包
- 关于嵌套包的可见性问题
- 可以直接向上访问,即子包可以直接访问父包内容
- 父包需通过import子包才能访问
- 各包有同名类时按就近原则访问,当然也可以通过父包路径来区分
2.1.5 包对象
上面说到包类不循序定义属性和方法,这是因为java的限制,因为java认为属性和方法属于类,因此不允许在类外定义它们,但scala中一切皆对象,因此scala的包也是对象,即包对象
1.定义包对象
package object packageobj {
val packageName: String = "包对象"
def packageObj(): Unit = {
println("这是包对象的方法")
}
}
2.使用包对象
定义在包对象中的内容可以在子包中的任意地方使用,整个包共享,其本质是生成了一个类,子包使用时会实例化该对象,反编译代码如下
3.细节说明
- 每个包都可以有一个包对象,需要在父包下定义
- 包对象必须和包名一致,一般用于对包的功能补充
2.2 权限
2.2.1 java 权限修饰符
访问级别 | 访问修饰符 | 同类 | 同包 | 子类 | 不同包 |
---|---|---|---|---|---|
公开 | public | ✔ | ✔ | ✔ | ✔ |
受保护 | protected | ✔ | ✔ | ✔ | ✖ |
缺省 | - | ✔ | ✔ | ✖ | ✖ |
私有 | private | ✔ | ✖ | ✖ | ✖ |
2.2.2 scala 权限修饰符
在scala中移除public关键字,默认即为公共的(角度不同,结论不同),同时还有private、protected,scala的权限访问控制的比java要明确,细分了很多。细节如下
- 对于属性而言
- 默认如
var name=""
在表现形式上是公有的,因为在任何地方都可以访问,但底层是私有的,上面解释过 private
修饰的属性只能在本类或者伴生类中访问(这里有很多细节,见2.2.3补充),其底层是私有的同时生成的getter/setter也是私有的protected
修饰的属性只能在子类中访问,同包下无法访问,较java更为严格,底层较为复杂见2.2.3补充
- 默认如
- 对于方法而言
- 默认为公有的
private
只能在本类中访问protected
只能在子类中访问
- 这样设计的好处
- 明确权限,不再像java那样子类同包或子类不同包权限不同
- 对于scala来说默认任何地方都能访问,private仅本类伴生类可以访问,protected仅自身和子类可以访问
2.2.3 细节说明
方法的权限没有什么细节所见即所想,底层源码也是一样,但对于属性不一样,见下面代码
package oop
class Cat {
var name: String = _
private var age: Int = _
protected var color: String = _
}
反编译源码长这样
源码就能够解释scala的权限问题
- 无论什么权限的属性底层都是private修饰
- 默认权限的属性底层生成公共的getter/setter,因此在任何地方都能访问
- private修饰的属性底层生成私有的getter/setter,因此只能在本类中访问,至于为什么伴生类可以访问见下一波代码
- protected修饰的属性底层生成居然是公共的getter/setter,那为什么其他地方不能访问?是因为虽然底层可以访问,但scala在编译器层面上做了限制,即理论上我可以访问,但是我编译器会给你报错甚至让你不可见。懂?
上面懂了就可以解释私有的为什么可以在伴生对象中访问,其实伴生类/伴生对象可以互相访问彼此的属性,若不了解他们的概念可以看后的知识,回过头再来看
package oop
//伴生类
class Cat {
var name: String = _
private var age: Int = _
protected var color: String = _
}
//伴生对象(可以理解为java的静态)
object Cat {
def main(args: Array[String]): Unit = {
val cat = new Cat
println(cat.name)
println(cat.age)
println(cat.color)
}
}
当我为Cat
类添加了一个伴生对象时底层代码会发生改变
首先私有属性的名称发生了变化,最终的是当伴生对象中对伴生类私有属性发生作用会改变底层getter/setter方法的权限,代码中我试图获取私有属性的值时,对应的getter方法变成了public,若试图修改私有属性的值时,对应的setter方法变成了public,此时外部依然无法访问私有属性,原因和protected一样,在编译器层面做了限制。
2.3 封装
2.3.1 概念
封装就是把数据和对数据的操作整合在一起,数据被保护在内部,程序对外提供公共的访问接口来操作内部属性
2.3.2 好处
- 隐藏实现细节
- 规范对数据的操作,保证安全合理
- 更容易拓展
- scala的封装和java一样
2.4 继承
scala的继承形式上和java一样
2.4.1 快速入门
package oop
class Cat {
var name: String = _
private var age: Int = _
protected var color: String = _
def method1(): Unit = {
println("method 1")
}
private def method2(): Unit = {
println("method 2")
}
protected def method3(): Unit = {
println("method 3")
}
}
class CatDog extends Cat {
}
object TestOOP {
def main(args: Array[String]): Unit = {
val dog = new CatDog
println(dog.name)
}
}
2.4.2 子类到底继承了什么
这种时候很容易想起java那句话,子类继承父类的非私有属性的方法,final修饰的不可以被继承,protected修饰的不能被重写等等,现象上没有错,但本质真的是这样的吗,通过debug看看
私有属性也被子类继承了,只是被隐藏了当然java也是这样做的
2.4.3 重写
1.重写方法
意外吧,重写居然还分标题,因为scala不仅可以重写方法连属性都可以重写;结合属性底层也是可以想通的
scala重写方法和java基本一致只需要注意几点即可
- 重写非抽象方法必须显式使用override关键字修饰
- scala中override是关键字,不是注解
- 通过spuer调用父类方法
override def method1(): Unit = {
super.method1()
}
2.重写属性
首先java不存在重写属性这一说法,通过隐藏属性代替重写属性,具体细节见多态
package oop
class Cat {
val name: String = "Cat"
}
class CatDog extends Cat {
override val name: String = "CatDog"
}
object TestOOP1 {
def main(args: Array[String]): Unit = {
val dog = new CatDog
println(dog.name)
}
}
val
只能重写另一个val
属性或者不带参数的def
,即属性不仅可以重写属性还可以重写方法,从反编译角度说明val
底层只会生成一个getter方法且方法名和属性名一致- 若
val
可以重写var
属性则会发生很多诡异事件,如val
有可能可以被修改,因为底层触发动态绑定时,即多态 val
可以重写不带参数方法也就可以说得通,其本质仍然是方法的重写,即getter方法重写了它,注意一定是无参数的方法
var
只能重写另一个var
属性,原因和上面差不多,本质是方法的重写
2.5 多态
scala的多态会比java更有意思,在java多态中通常会说:“父类的引用指向子类对象,调用父类方法显示子类信息”,这句话体现的真是多态的性质,其底层也是由于java的动态绑定
2.5.1 快速入门
package oop
class Cat {
val name: String = "Cat"
def run(): Unit = {
println("Cat run")
}
}
class CatDog extends Cat {
override val name: String = "CatDog"
override def run(): Unit = {
println("CatDog run")
}
}
object TestOOP1 {
def main(args: Array[String]): Unit = {
val dog1 = new CatDog // 多态?
val dog2: Cat = new CatDog //多态?
}
}
上面代码哪个才是多态?注意第二个才是多态val dog2: Cat = new CatDog
,第一个仅是对象的创建编译期执行类型推断,因此dog1
依然是子类引用,第二种加上了类型就不会再执行类型推断,这是才是一个多态
2.5.2 动态绑定
在java中由于动态绑定机制导致通过父类引用调用方法其实调用的是子类的方法,但调用属性时依然是谁的引用调用谁的属性,scala也是这样吗?不全是,见下面代码
package oop
class Cat {
val name: String = "Cat"
def run(): Unit = {
println("Cat run")
}
}
class CatDog extends Cat {
override val name: String = "CatDog"
override def run(): Unit = {
println("CatDog run")
}
}
object TestOOP1 {
def main(args: Array[String]): Unit = {
val dog2: Cat = new CatDog
dog2.run()
println(dog2.name)
}
}
//运行结果
CatDog run
CatDog
发现scala中方法和属性都存在动态绑定的机制,那是因为scala的属性底层调用的是其getter/setter方法,因此本质上依然是方法的动态绑定,只是表现形式为属性的动态绑定。反编译代码
package oop;
import scala.Predef.;
public final class TestOOP1$
{
public static final MODULE$ = new ();
public void main(String[] args) {
Cat dog2 = new CatDog();
dog2.run();
Predef..MODULE$.println(dog2.name());
}
}
三、面向对象高级
3.1 伴生类/对象
3.1.1 引入
scala是纯粹的面向对象的语言,所以剔除了静态的概念(静态早于对象存在,不符合面向对象范畴),但为了做到和java的无缝接轨,就必须产生一个特殊的对象来模拟静态功能,这就是伴生对象,即伴随着类的存在
3.1.2 快速入门
package oop
//伴生对象 静态
object TestApply {
var name: String = "静态属性"
def run(): Unit = {
println("静态方法")
}
}
//伴生类 非静态
class TestApply {
}
object TestStatic {
def main(args: Array[String]): Unit = {
println(TestApply.name)
TestApply.run()
}
}
在之后的开发中,完全可以把伴生对象当成静态类来使用,定义在伴生对象里的属性是静态属性,方法是静态方法,推荐使用类名调用
3.1.3 细节说明(重要)
- scala中伴生对象用关键字
object
声明,定义在其中的内容都是静态的 - 伴生类和伴生对象的名称要保持一致,否则就不是伴生关系
- 伴生对象里的内容推荐使用类名调用
- 从语法角度看,伴生对象其实是类的静态方法和属性的集合
- 从技术角度看,伴生对象其实是产生一个新的类,实现属性和方法的调用
- 伴生对象的声明要和伴生类在一个文件中,若没有伴生类的声明则伴生对象可以随便放
- 若
class A
单独存在那A
就是一个类,若object A
单独存在那A
就是一个类对象 - 在IDEA中伴生对象和伴生类在一个文件时,文件的图标会发生改变
3.2 单例对象
3.2.1 apply
在java中实现单例模式一般有懒汉式、饿汉式、静态内部类、枚举,常用的方法是私有化构造器,对外提供getInstance
方法之类的,在scala中也可以这么做,但它提供了一个非常有意思的方法可以更方便构建单例模式
apply
方法被要求对应在伴生对象中,即它是静态的,主要功能是简化对象的创建,见下面代码
在再伴生对象中定义apply
方法可以实现直接通过类名(参数)创建对象,省略new
关键字
3.2.2 实现单例模式
就把上面的猪变成单例猪吧
package oop.blog
object Pig {
lazy private val pig: Pig = new Pig
def apply(): Pig = pig
}
class Pig private {
}
object TestPig {
def main(args: Array[String]): Unit = {
val pig1 = Pig()
val pig2 = Pig()
println(pig1 == pig2)
}
}
类似java的饿汉式,但scala提供了lazy
关键字可以实现懒加载机制,因此此种方式线程安全的同时还实现了懒加载
3.3 抽象类
3.3.1 说明
scala的抽象类和java基本一致,只是scala存在抽象属性概念
3.3.2 快速入门
package oop.blog
abstract class TestAbstract {
var name: String
def getName: String
}
class ExtendAbstract extends TestAbstract {
override var name: String = _
override def getName: String = name
}
3.3.3 细节说明
- 为什么scala存在抽象属性?
- 在scala中属性底层对应的是方法
- 抽象属性即存在抽象方法
- 重写抽象属性即重写的是抽象方法
- 抽象类不能被实例化
- 抽象类可以存在非抽象内容
- 一旦类存在抽象内容,这个类必须用
abstract
修饰 - 子类实现抽象方法时,
override
可以省略 - 子类继承抽象类时必须重写所有抽象内容,否则自身声明为抽象类
3.4 特质
3.4.1 说明
- 严格来说java的接口不属于面向对象的范畴,因此scala没有接口
- scala中采用特质
trait
来替代接口的概念 trait
可以理解为java的接口+抽象类- scala的
trait
可以实现多继承(这部分是scala的特色)
3.4.2 快速入门
package oop
trait TestTrait1 {
def run()
def say(): Unit = {
println("hello")
}
}
trait TestTrait2 {
def run()
}
class ExtendTrait extends TestTrait1 with TestTrait2 {
override def run(): Unit = None
}
object Test1 {
def main(args: Array[String]): Unit = {
val extendTrait = new ExtendTrait
extendTrait.say()
}
}
3.4.3 细节说明
-
底层反编译
-
java的接口可以被当做特质使用
-
scala的特质也是对象因此使用extends关键字,若有多个特质或存在父类使用with连接
- 有父类:class 类名 extends 父类 with 特质1 with 特质2 …
- 没有父类:class 类名 extends 特质1 with 特质2 …
- 若有父类,则父类一定要写在最前面
-
特质同样可以存在抽象内容和非抽象内容
3.4.5 动态混入(重要)
喜欢scala的原因有两点:动态混入和隐式转换,真的是太强了,特别佩服scala开发者的脑洞
1.说明
- 在类声明时继承特质外,还可以在创建对象时混入特质,拓展目标类的功能即动态混入
- 动态混入还可以应用于抽象类
- 动态混入可以在不改变类的情况下,拓展类的功能,非常灵活低耦合
- 此特性遵循开闭原则,对修改关闭,对拓展开放
- java是没有这个功能的
2.快速入门
package oop
trait TestTrait1 {
def say(): Unit = {
println("hello")
}
}
class ExtendTrait {
}
object Test1 {
def main(args: Array[String]): Unit = {
val extendTrait = new ExtendTrait with TestTrait1
//这个拓展力真的太强了,当然隐式转换更强
extendTrait.say()
}
}
直接给extendTrait
拓展了一个新功能,同时动态混入还可以做到给系统类库丰富功能(还是没有隐式转换强),动态混入的本质
3.补充
创建对象的方式有几种?
- new 对象
- apply 创建
- 匿名子类
- 动态混入
只是方式不同,其本质都是一样即由jvm创建
3.4.6 叠加特质
1.快速入门
看下面代码
package oop.work
trait Trait {
println("顶层")
def run() //抽象方法
}
trait TraitA extends Trait {
println("A")
override def run(): Unit = {
println("A run")
}
}
trait TraitAA extends TraitA {
println("AA")
override def run(): Unit = {
println("AA run")
super.run()
}
}
trait TraitAB extends TraitA {
println("AB")
override def run(): Unit = {
println("AB run")
super.run()
}
}
class TestMixIn {}
object MixIn {
def main(args: Array[String]): Unit = {
val mixIn = new TestMixIn with TraitAA with TraitAB
println("=============================================")
mixIn.run()
}
}
UML类图如下
Trait
为顶层特质,TraitA
继承Trait
,TraitAA
,TraitAB
同时继承TraitA
;类TestMixIn
为普通类,通过动态混入拓展功能,混入顺序为new TestMixIn with TraitAA with TraitAB
2.提出问题
- 在创建对象时,这些特质是怎么初始化的?
答:初始化特质顺序为从左到右,即先初始化TraitAA
,准备初始化TraitAA
时发现它有个父类TraitA
,因此需要先初始化TraitA
,同理还需要初始化顶层特质Trait
,当TraitAA
初始化完成后开始初始化TraitAB
,但此时TraitAB
的父类已经创建成功,因此顺序是Trait
、TraitA
、TraitAA
、TraitAB
mixIn.run()
会输出什么?
答:这时候就很迷茫了,因为混入的两个特质都有run方法,那么该执行谁的呢?其实执行方法的顺序是从右往左(和混入顺序相反)即调用TraitAB
的run方法,但此时TraitAB
的run方法调用了父类的run方法,很显然调用它的父类方法!!!但结果却不是的,它调用了TraitAA
的run方法,这就很神奇了。可以把动态混入看成栈的数据结构,按顺序混入可以看成压栈操作,因此调用方法时从栈顶开始执行,调用父类方法不再遵守真实的继承关系,而是按照栈的顺序执行(若某个栈帧没有这个方法就不会执行)
3.细节说明
- 特质的混入顺序从左到右
- 特质方法执行顺序从右到左
- 若想按照真正的继承关系执行方法可以使用
spuer[特质].method(...)
指定超类