Scala-面向对象
1.类和对象
1.1类
基本语法:
[修饰符] class 类名 {
类体
}
注意:
- scala 语法中,类并不声明为
public
,所有这些类都具有公有可见性(即默认就是public
) - 一个 Scala 源文件可以包含多个类
1.2属性
属性是类的一个组成部分,一般是值数据类型,也可是引用类型。
基本语法:
[修饰符] var 属性名称 [:类型] = 属性值
例如:
package com.liuser.day03
object FunDemo4 {
def main(args: Array[String]): Unit = {
val p1 = new Person()
p1.teacher = new Teacher()
println(p1.age)
println(p1.teacher.name)
}
}
class Person {
// 声明属性, 而且必须给该属性赋值
var age = 10
// 属性可以设置默认值:使用 _
// 数值型的默认值: 0 布尔型的默认值是 false, 引用数据类型的默认值是 null
var teacher: Teacher = _
}
class Teacher {
var name = "liuser"
}
1.3 Bean 属性
JavaBeans 规范定义了 Java 的属性是像getXxx()
和setXxx()
的方法。许多Java 工具(框架)都依赖这个命名习惯。
为了Java的互操作性。将 Scala 字段加@BeanProperty
时,这样会自动生成规范的setXxx/getXxx
方法。
这时可以使用对象.setXxx()
和对象.getXxx()
来调用属性。
package com.liuser.day03
import scala.beans.BeanProperty
object FunDemo4 {
def main(args: Array[String]): Unit = {
val p1 = new Person()
p1.teacher = new Teacher()
println(p1.getAge())
p1.setAge(100)
println(p1.getAge)
}
}
class Person {
@BeanProperty
var age = 10
var teacher: Teacher = _
}
class Teacher {
var name = "liuser"
}
注意:
- 访问方法的时候要不要带圆括号? 一般遵循这样的约定: 访问
getXxx
的时候一般不带括号, 访问setXxx(参数)
的时候一般要带圆括号.
1.4创建对象
基本语法
val|var 对象名 [:类名] = new 类型()
说明:
- 如果我们不希望改变对象的引用(即:内存地址),应该声明为
val
性质的,否则声明为var
, - scala 设计者推荐使用
val
,因为一般来说,在程序中,我们只是改变对象属性的值,而不是改变对象的引用。
1.5构造器
Scala的构造器: 主构造器和辅构造器
基本语法:
class 类名(形参列表) { // 主构造器
// 类体
def this(形参列表) { // 辅助构造器
};
def this(形参列表) { //辅助构造器可以有多个...
}
}
说明:
- 主构造器和 Java 有很大不同. 主构造器位于类名之后.
- 辅构造器和java类似,只是在Scala中辅构造器的名字统一用
this
来表示 - 主构造器只有一个,而辅构造器可以有多个
- 在创建对象时,和java一样也是通过传入的参数来选择构造器
- 调用主构造器时,会执行类中的所有语句(不包括类中定义的方法)
案例1: 调用主构造函数
package com.liuser.day04
object ObjDemo {
def main(args: Array[String]): Unit = {
val p = new Person("z3", 20)
println(p.name + " " + p.age)
}
}
class Person(inName: String, inAge: Int) { // 主构造器
var name = inName
var age = inAge
// 类内也可以直接写代码,在调用主构造函数的时候会执行.
// 有点类似 Java 中的构造代码块
println("ok")
}
案例2: 辅构造函数
package com.liuser.scala.oop
object ClassDemo1 {
def main(args: Array[String]): Unit = {
println(new Student().name.toString)
}
}
//主构造函数
class Student(var name: String) {
//辅构造函数
def this(){
this("z3")
}
//
// var age = 10
// println("abc")
// for (i <- 1 to 10) println(i + age)
def foo() = println("...")
}
说明:
- 如果不想让外界通过主构造器创建对象, 可以把主构造器私有: 在类名和圆括号中间添加一个关键字
private
- 辅构造器仍然可以调用私有的主构造器. 私有之后只是不能在外界调用而已.
1.6 关于形参
class Person(@BeanProperty var name: String, @BeanProperty val age: Int, sex: String) {
override def toString: String = {
s"name=$name,age= $age,sex= $sex"
}
}
说明
var
修饰的会有类似java中的set,get
方法,val
修饰的只有类似 get
方法, 没有修饰的什么方法都没有,简单来说就是没用
添加@BeanProperty
后,对象就拥有了java中的set,get
方法,更好的对接java中的框架反射动态对对象的设置,获得
1.7 对象创建流程分析
class Person {
var age: Int = 90
var name: String = _
def this(n: String, a: Int) {
this()
this.name = n
this.age = a
}
}
var p: Person = new Person("小倩", 20)
流程分析(面试题)
- 加载类信息(属性信息,方法信息)
- 在堆中,给对象开辟空间
- 调用主构造器对属性进行初始化
- 使用辅助构造器对属性进行初始化
- 把对象空间的地址,返回给
p
引用
1.8 查看 Scala 生成的字节码
# 编译 scala 文件
scalac Person.scala
# 查看编译后的字节码 -private 表示也查看私有的成员
javap -private Person
#或者使用java 反编译文件查看详情
1.9 给类起别名
在使用一个大型类库写代码的时候你也许会遇到类名不符合自己心意的情况。类名要么 太长要么不灵巧,或者你只是觉得有一个更好的名字能够表达这种抽象。你拥有这种取别名 的自由,可以给一个类取一个赏心悦目的名字。
// 这是一个很长的类名, 将来使用的时候书写很不方便
class Person2Student
object Person {
def main(args: Array[String]): Unit = {
// 起个别名
type p2s = Person2Student
println(new p2s().getClass)
}
}
说明:
-
虽然起了别名, 但是并没有改变类的真实类型.
-
别名只对当前作用域有效
-
scala 标准库中很多类都起了别名. 例如,
Set
就是一个别名,它指向immutable
包中的Set
版本, 而不是mutable
包中的版本。
2.包
1.包对象
在 Java 中, 我们的一些工具函数或者常量需要专门添加到一个所谓的类中. 这种做法主要是 JVM 的限制, 但其实并不是很好的选择.
如果能把这些东西添加到包中是更好的一种选择.
Scala 提供的包对象
解决了这个问题
每个包都有一个包对象, 我们需要在类的父包中进行定义, 并且和父包的名字保持一致!
package com.liuser.scala.oop.packdemo
package abc{
object Person {
def main(args: Array[String]): Unit = {
val p1 = new Person()
p1.say()
}
}
}
package object abc{
var A:Int = 3
def foo()={
println(s"包对象中定义的函数")
}
}
package abc{
class Person{
def say()={
//调用包对象中的属性和方法时,直接调用,不需要前缀
println(A)
foo()
}
}
}
2.包的可见性
在 Java 中,访问权限分为: public
,private
,protected
和默认
。在Scala 中,你可以通过类似的修饰符达到同样的效果。但是使用上有区别。
-
当方法访问权限为默认时,默认为
public
访问权限 -
在 Scala 中没有
public
关键字,即不能用public
显式的修饰属性和方法。 -
private
为私有权限,只在类的内部和伴生对象中可用关于伴生对象和伴生类的概念, 后面再细说
package com.liuser.day04 /* 当在一个文件中, object 名和 class 名相同时 object 叫伴生对象, class 叫伴生类 */ object AccessTest { def main(args: Array[String]): Unit = { val test = new AccessTest // 伴生对象内部可以访问伴生类内部的私有属性和方法 println(test.a) } } class AccessTest{ private var a = 10 def foo(): Unit ={ println(a) } }
-
protected
为受保护权限,Scala 中受保护权限比 Java 中更严格,只能子类访问,同包无法访问。 -
包访问权限 一般是属性私有了, 可以增加包访问权限, 然后包内的其他类也可以访问到了. 这点体现出来了 Scala 包相比 Java 的灵活性.
3.封装
封装(encapsulation)就是把抽象出的数据和对数据的操作封装在一起,数据被保护在内部,程序的其它部分只有通过被授权的操作(成员方法),才能对数据进行操作。
我们前面使用@BeanProperty
已经很进行了相应的封装, 但是属性还是可以在外面直接使用.
所以还需要手动封装:
1)将属性进行私有化
2)提供一个公共的set方法,用于对属性判断并赋值
def setXxx(参数名 : 类型) : Unit = {
//加入数据验证的业务逻辑 属性 = 参数名
}
3)提供一个公共的get方法,用于获取属性的值
def getXxx() [: 返回类型] = {
return 属性
}
3.1 默认访问修饰符
Scala 的访问修饰符(access modifier)和 Java 有如下不同点。
- 如果不指定任何访问修饰符,那么 Java 会默认为包内部可见,而 Scala 则默认为公开(
public
)。 - Java 主张全有或全无,也就是说,对当前包的所有类可见或者对所有都不可见。而 Scala 对可见性的控制是细粒度的。
- Java 的
protected
是宽泛的,其作用域包括在任意包中的派生类和当前包中的任意类,而 Scala 的protected
与 C++和 C#的类似,只有派生类能够访问。然而,在 Scala 中protected
还有相当自由和灵活的用法。 - Java 的封装是类级别的。可以在一个类的实例方法中访问该类的任何实例的所有私有 字段和方法,在 Scala 中也一样,不过,在 Scala 中也可以进行定制,让其只能在当 前的实例方法中访问,这样就和 Ruby 比较像了。
3.2 定制修饰符
在不使用任何访问修饰符的情况下, Scala 默认认为类、字段和方法都是公开的。
如果想将一个成员标记为 private
或者 protected
, 只要像下面这样用相应的关键字标 记即可。
class Person {
def say() = println("say...")
private def eat() = println("eat...")
}
object Demo {
def main(args: Array[String]): Unit = {
var p = new Person
p.say() // ok
p.eat() // 编译错误
}
}
3.3 Scala 的 protected
在 Scala 中,protected
让所修饰的成员仅对自己和派生类可见。对于其他类来说,即使正好和所定义这个类处于同一个包中,也无法访问这些成员。更进一步,派生类在访问 protected
成员的时候,成员的类型也需要一致。
package bj
object Demo2{
def main(args: Array[String]): Unit = {
}
}
class Person{
protected def say() = {}
}
class Student extends Person{
def f1(): Unit ={
this.say() // ok
}
def f2(stu:Student): Unit ={
stu.say() // ok
}
def f3(per:Person): Unit ={
per.say() // 编译错误.
}
}
4.继承
1.语法
在 Scala 中扩展一个基类和 Java 中很像,只是多了两个非常好的限制:
- 其一,方法的重写必须用
override
关键字; - 其二,只有主构造器能传递参数给基类的构造器。
class 子类名 extends 父类名 {
//类体
}
说明:
- 继承的关键字与 Java 一样, 也是使用
extends
- 可以继承父类的属性和方法
- Scala 支持单继承, 不支持多继承
package com.liuser.exds
object Student {
def main(args: Array[String]): Unit = {
val s = new Student
println(s.age)
s.say()
s.study()
}
}
class Student extends Person {
def study(): Unit = {
println(this.name + " good good study, day day up")
}
}
class Person {
var age = 10
var name = "小明"
def say(): Unit = {
println("Person的say方法:" + this.age)
}
}
2.方法重写
scala 明确规定,重写一个非抽象方法需要用override
修饰符,明确需要调用超类的方法使用super
关键字
3.Instance
要测试某个对象是否属于某个给定的类,可以使用isInstanceof
方法.
用asInstanceof
方法 将引用转换为子类的引用
类型检查和转换的最大价值在于:
可以判断传入对象的类型,然后转成对应的子类对象,进行相关操作,这里也体现出多态的特点。
4.scala中超类的构造
1.只有主构造器才能调用父类的构造器,即可以调用父类的主构造器和辅构造器
5.重写字段
1.可以用val
重写val
或者是不带参数的def
,但是不能重写var
2.var
只能去重写另外一个抽象的var
(没有初始的变量,只能放在抽象类中)
6.抽象类
Scala 的抽象类的概念和 Java 的抽象类的概念是一样的, 都是表示的不能实例化的类.
Scala 的抽象类也是使用的关键字abatract
抽象类能有哪些成员:
-
普通类有的抽象类都可以有.
-
抽象字段 没有初始化值的字段
var a
-
抽象方法 省略方法体的方法
def foo(name:String)
下面就是一个抽象类:
abstract class Animal {
var name = "大黄" // 普通字段
var age: Int // 抽象字段
def say(): Unit ={ // 普通方法
}
def cry() // 抽象方法
}
说明:
abstract
关键字只用作class
前, 抽象字段和抽象方法前不能添加.- 抽象类的价值更多是在于设计,是设计者设计好后,让子类继承并实现抽象类(即:实现抽象类的抽象方法)
- 抽象类不能被实例
- 抽象类不一定要包含
abstract
方法。也就是说,抽象类可以没有abstract
方法 - 一旦类包含了抽象方法或者抽象属性,则这个类必须声明为
abstract
- 抽象方法不能有主体,不允许使用
abstract
修饰。 - 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法和抽象属性,除非它自己也声明为
abstract
类 - 抽象方法和抽象属性不能使用
private
、final
来修饰,因为这些关键字都是和重写/实现相违背的。 - 子类重写抽象方法不需要
override
,写上也不会错。
7.匿名子类
scala
中的匿名子类 和 java中的匿名类概念是一致的,一般只用在使用一次的场景
var a = new A{
override var name =_
override val age = _
override def say():String ={
"hello"
}
}
5.面向对象(下)
1. 单例对象
创建一个单例要使用关键字 object
而不是 class
。
因为不能实例化一个单例对象,所以不能传递参数给它的构造器。
import scala.collection._
class Marker(val color: String) {
println(s"Creating ${this}")
override def toString = s"marker color $color"
}
object MarkerFactory {
private val markers = mutable.Map(
"red" -> new Marker("red"),
"blue" -> new Marker("blue"),
"yellow" -> new Marker("yellow"))
def getMarker(color: String): Marker = markers.getOrElseUpdate(color, new Marker(color))
def main(args: Array[String]): Unit = {
println(MarkerFactory getMarker "blue")
println (MarkerFactory getMarker "blue")
println (MarkerFactory getMarker "red")
println (MarkerFactory getMarker "red")
println (MarkerFactory getMarker "green")
}
}
2. 独立对象和伴生对象
前面提到的 MarkerFactory
是一个独立对象(stand-alone object
)。它和任何类都没有自动的联系,尽管我们用它来管理 Marker
的实例。
伴生对象就是对象名和类名相同,他们之间可以互相访问私有的字段和方法
一个类的构造器,包括主构造器,也可以标记为 private
。
import scala.collection._
// 主构造函数私有. 将来只能在伴生对象中访问
class Marker private(val color: String) {
println(s"Creating ${this}")
override def toString = s"marker color $color"
}
object Marker {
private val markers = mutable.Map(
"red" -> new Marker("red"),
"blue" -> new Marker("blue"),
"yellow" -> new Marker("yellow"))
def getMarker(color: String): Marker = markers.getOrElseUpdate(color, new Marker(color))
def main(args: Array[String]): Unit = {
println(Marker getMarker "blue")
println(Marker getMarker "blue")
println(Marker getMarker "red")
println(Marker getMarker "red")
println(Marker getMarker "green")
}
}
说明:
Marker
的构造器被声明为private
;然而,它的伴生对象可以访问它。- 因此,我们可 以在伴生对象中创建
Marker
的实例。 - 如果试着在类或者伴生对象之外创建
Marker
的实例, 就会收到错误提示。 - 每一个类都可以拥有伴生对象,伴生对象和相应的伴生类可以放在同一个文件中。在 Scala 中,伴生对象非常常见,并且通常提供一些类层面的便利方法。
- 伴生对象还能作为一非常好的变通方案,弥补 Scala 中缺少
static
成员的事实
3. Scala 中的 static
Scala 没有 static
关键字,直接在一个类中允许 static
字段和 static
方法会破坏 Scala 提供的纯面向对象模型。
与此同时,Scala 通过单例对象和伴生对象完整支持类级别的操作和属性。
如果能够从 Marker
中获得所支持的颜色,就非常棒。
但是,直接在这个类的任何实例中查询并没有意义,因为这是一个类级别的操作。
换句话说,如果我们是在用 Java 写代码,就会在类 Marker
中把这个查询方法写成一个 static
方法。
但是,Scala 并不提供 static
。一开始就是这样设计的,以至于这些方法在 单例对象和伴生对象中作为常规方法存在。
我们来改一下 Marker
这个例子,并在伴生对象 中创建一些方法。
package scala.bj2
import scala.collection._
// 主构造函数私有. 将来只能在伴生对象中访问
class Marker private(val color: String) {
println(s"Creating ${this}")
override def toString = s"marker color $color"
}
object Marker {
private val markers = mutable.Map(
"red" -> new Marker("red"),
"blue" -> new Marker("blue"),
"yellow" -> new Marker("yellow"))
def apply(color: String): Marker = markers.getOrElseUpdate(color, new Marker(color))
def supportedColors = markers.keys
def main(args: Array[String]): Unit = {
println(Marker.supportedColors)
println(Marker("blue"))
println(Marker("yellow"))
}
}
说明:
println(s"Supported colors are : ${Marker.supportedColors}")
可以直接在外面这样调用刚刚创建的方法, 就想在调用 Java 的静态方法.
apply
方法
不用 new
关键字就可以创建伴生类的实例。 特殊的 apply()方法就是达到这种效果的关键。
在前面的例子中, 当我们调用 Marker ("blue")
时,实际上在调用 Marker.apply("blue")
。这是一种创建或者获得实例的轻量级语法。
在字节码层面上,单例中方法会被创建为 static
方法。这从与 Java 的互操作性上讲,是一个好消息。
小结:
- Scala 中伴生对象采用
object
关键字声明,伴生对象中声明的全是 "静态"内容,可以通过伴生对象名称直接调用 - 伴生对象对应的类称之为伴生类,伴生对象的名称应该和伴生类名一致
- 伴生对象中的属性和方法都可以通过伴生对象名(类名)直接调用访问
- 从语法角度来讲,所谓的伴生对象其实就是类的静态方法和成员的集合
- 从技术角度来讲,
scala
还是没有生成静态的内容,只不过是将伴生对象生成了一个新的类,实现属性的get方法的调用。 - 伴生对象的声明应该和伴生类的声明在同一个源码文件中(如果不在同一个文件中会运行错误!),但是如果没有伴生类,也就没有所谓的伴生对象了,所以放在哪里就无所谓了。
- 类和伴生对象之间可以互相访问对方的私有成员.
6.特质(trait)
Scala 中的 trait
非常类似于 Java 中的interface
6.1 java中的接口和scala中的trait
从面向对象来看,接口并不属于面向对象的范畴,Scala 是纯面向对象的语言,在 Scala 中,没有接口。
Scala 语言中,采用特质 trait
(特征, 特质)来代替接口的概念,也就是说,多个类具有相同的特征(特质)时,就可以将这个特质(特征)独立出来,采用关键字trait
声明。
特质声明语法:
trait 特质名 {
// 特质体
}
特质可以有抽象方法, 也可以有实体方法, 相比抽象类来说最大的优点就是可以实现多继承, 抽象类是类只能实现单继承.
6.2 最简单的特质:当做接口使用
trait Logger{
//未实现的方法就是抽象方法,不需要加abstract关键字
def log (msg:String)
}
子类实现
class A extends Logger{
def log(msf:String):Unit={
println(msg)
}
}
说明
-
实现特质方法的时候,加不加
override
都可以 -
如果一个类没有继承其他的任何的类,则使用
extends
类混入特质,否则只能使用with
-
使用多个特质,应该使用关键字
with
-
class ConsoleLogger extends Logger with Serializable with Cloneable { def log(msg: String): Unit = { println(msg) } }
-
所有的java接口都可以作为scala的特质使用
-
与java一样,Scala只能一个基础类,但是可以有任意多个特质
6.3 带有具体实现的特质
在 Scala 中,特质内的方法也可以有非抽象的方法
trait ConsoleLogger extends Logger{
def log(msg:String) : Int={
print(msg)
}
}
6.4动态混入特质
除了可以在类声明时继承特质以外,还可以在构建对象时混入特质,扩展目标类的功能.
package com.liuser.day03
import java.sql.Connection
object TraitMix {
def main(args: Array[String]): Unit = {
// 创建 Mysql 对象的时候, 指定一个新的特质
val mysql = new Mysql with BetterConnectDB
mysql.connectToMysql()
// 如果再创建新的对象, 也可以换另外的特质. 这就叫动态混入
}
}
//这个特质用来连接数据库
trait ConnectToDB {
// 什么都不做的实现方法
def getConn() {}
}
class Mysql extends ConnectToDB {
// 连接到Mysql
def connectToMysql(): Unit = {
// 获取连接
getConn()
//其他代码
}
}
/*
上面的 getConn其实什么都没有做, 感觉没有什么用
但是我们创建 Mysql对象的时候, 可以给他指定一个更好的特质
*/
// 一个更好的特质
trait BetterConnectDB extends ConnectToDB {
override def getConn(): Unit = { // 注意这个时候需要添加override
println("更好的获取连接....")
}
}
说明:
- 动态混入是Scala特有的方式(java没有动态混入),可在不修改类声明/定义的情况下,扩展类的功能,非常的灵活,耦合性低
- 动态混入可以在不影响原有的继承关系的基础上,给指定的类扩展功能
6.5叠加特质
构建对象的同时,如果混入多个特质,称之为叠加特质.
val test= new Test with A with B with C
说明:
package com.atguigu.day06
object OverflowTrait {
def main(args: Array[String]): Unit = {
val test = new Test with A with B with C
test.foo()
}
}
class Test{
}
trait Father{
def foo(): Unit ={
println("Father...")
}
}
trait A extends Father{
override def foo(): Unit = {
println("A...")
super.foo()
}
}
trait B extends Father{
override def foo(): Unit = {
println("B...")
super.foo()
}
}
trait C extends Father{
override def foo(): Unit = {
println("C...")
super.foo()
}
}
- 当构建一个混入对象时,构建顺序和 声明的顺序一致(从左到右),机制和类的继承一致. 所以对象中的
foo
方法一定是最后一个特质的方法. super.foo()
这里super
并不会直接去找父特质, 而是按照顺序去找上一个特质.- 如果不想按照顺序向上找, 可以直接指定该特质的直接父特质
super[Father].foo()
6.6 当做富接口使用的特质
富接口: 既有特质中既有的抽象方法,又有非抽象方法
trait Operate {
def insert( id : Int ) //抽象
def pageQuery(pageno:Int, pagesize:Int): Unit = { //实现
println("分页查询")
}
}
6.7特质的构造顺序
声明类的同时混入特质
-
首先调用超类的构造器
-
第一个特质的父特质构造器
-
第一个特质的构造器
-
第二个特质构造器的父特质构造器, 如果已经执行过,就不再执行
-
第二个特质的构造器
…
-
package com.atguigu.day07 object TDemo { def main(args: Array[String]): Unit = { val son = new Son println(son.a1) } } trait tr{ val a = "最上面的特质的 a1" println("最上面特质的构造器") } trait tr1 extends tr{ val a1 = "第一个特质的 a1" println("第一个特质的构造器") } trait tr2 extends tr{ val a1 = "第二个特质的 a1" println("第二个特质的构造器") } class Father{ val a1 = "父类的 a1" println("父类的构造器") } class Son extends Father with tr1 with tr2{ override val a1 = "子类的 a1" println("子类的构造器") }
- 当前的构造器
构建对象时,动态混入特质
-
当前类的超类构造器
-
当前类的构造器
-
第一个特质的父特质构造器
-
第二个特质构造器的父特质构造器,如果已经执行过则不再执行
-
第二个特质的构造器
-
package com.atguigu.day08 object TDemo2 { def main(args: Array[String]): Unit = { //动态混入特质 val son = new Son with tr1 with tr2 } } trait tr { println("最上面特质的构造器") } trait tr1 extends tr { println("第一个特质的构造器") } trait tr2 extends tr { println("第二个特质的构造器") } class Father { println("父类的构造器") } class Son extends Father { println("子类的构造器") }
6.8 特质继承类
注意:
- 如果混入该特质的类,已经继承了另一个类(A类),则要求A类是特质超类的子类,否则就会出现了多继承现象,发生错误
6.9 嵌套类
1.嵌套类和java的内部类类似
2.类型投影 #
看下面的问题:
如何解决问题呢?
使用类型投影解决!
在方法声明的时候使用外部类#内部类
这样的类型, 表示忽略内部类的对象关系.
等同于Java中内部类的语法操作,我们将这种方式称之为 类型投影(即:忽略对象的创建方式,只考虑类型)