三、Scala之面向对象编程

Scala之面向对象编程

文章目录

1. 类与对象

定义类

1
2
3
[修饰符] class 类名 {
类体
}
  1. scala语法中,类并不声明为public,所有这些类都具有公有可见性(即默认就是public),[修饰符在后面再详解].
  2. 一个Scala源文件可以包含多个类.

属性/成员变量

  1. 属性的定义语法同变量,示例:[访问修饰符] var 属性名称 [:类型] = 属性值
  2. 属性的定义类型可以为任意类型,包含值类型或引用类型
  3. Scala中声明一个属性,必须显示的初始化,然后根据初始化数据的类型自动推断,属性类型可以省略(这点和Java不同)。
  4. 如果赋值为null,则一定要加类型,因为不加类型, 那么该属性的类型就是Null类型
1
2
3
4
5
6
class Person {
var age : Int = 10
var sal = 8090.9 //给属性赋初值,省略类型,会自动推导
var Name = null // Name 是Null类型
var address : String = null // address 是String类型
}
  1. 如果在定义属性时,如果用var修饰,则可暂时不赋值,也可以使用符号_下划线),让系统分配默认值。val类型的成员变量,必须要自己手动初始化,不能使用符号_(下划线)
1
2
3
4
5
6
7
8
class Person{
var age:Int=_
var name:String=_
}
class Person1(){
val name: String ="小明"//必须手动初始化
val age: Int =10
}

java中的boolean初始值也是false

  1. 不同对象的属性是独立,互不影响,一个对象对属性的更改,不影响另外一个。

属性的高级部分和构造器(构造方法/函数)相关,我们把属性高级部分放到构造器那里讲解。

如何创建对象

1
val | var 对象名 [:类型]  = new 类型()
  1. 如果我们不希望改变对象的引用(即:内存地址), 应该声明为val 性质的,否则声明为var, scala设计者推荐使用val ,因为一般来说,在程序中,我们只是改变对象属性的值,而不是改变对象的引用。
  2. scala在声明对象变量时,可以根据创建对象的类型自动推断,所以类型声明可以省略,但当类型和后面new对象类型有继承关系即多态时,就必须写了

访问属性:对象名.属性名;

方法

Scala中的方法其实就是函数,声明规则请参考函数式编程中的函数声明。

1
2
3
def 方法名(参数列表) [:返回值类型] = {
方法体
}
  1. 当我们scala开始执行时,先在栈区开辟一个main栈。main栈是最后被销毁
  2. 当scala程序在执行到一个方法时,总会开一个新的栈。
  3. 每个栈是独立的空间,变量(基本数据类型)是独立的,相互不影响(引用类型除外)
  4. 当方法执行完毕后,该方法开辟的栈就会被jvm机回收。

2. 构造器

Java构造器基本语法

1
2
3
[修饰符] 方法名(参数列表){
构造方法体
}

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

  • 如果程序员没有定义构造方法,系统会自动给类生成一个默认无参构造方法(也叫默认构造器),比如 Person (){}
  • 一旦定义了自己的构造方法(构造器),默认的构造方法就覆盖了,就不能再使用默认的无参构造方法,除非显示的定义一下,即: Person(){};

Scala构造器

和Java一样,Scala构造对象也需要调用构造方法,并且可以有任意多个构造方法(即scala中构造器也支持重载)。
Scala类的构造器包括: 主构造器 和 辅助构造器

基本语法

1
2
3
4
5
6
7
class 类名(形参列表) {  // 主构造器
// 类体
def this(形参列表) { // 辅助构造器
}
def this(形参列表) { //辅助构造器可以有多个...
}
}

辅助构造器可以有多个,编译器通过不同参数来区分

Scala构造器注意事项和细节

  1. Scala构造器作用是完成对新对象的初始化,构造器没有返回值。
  2. 主构造器的声明直接放置于类名之后 [反编译]
  3. 主构造器会执行类定义中的所有语句,这里可以体会到Scala的函数式编程和面向对象编程融合在一起,即:构造器也是方法(函数),传递参数和使用方法和前面的函数部分内容没有区别
1
2
3
4
5
6
7
8
9
def main(args: Array[String]): Unit = {
val ab=new AB //会输出ABABAB
}

class AB{
var cd=3
if(cd==3)//Scala的类中可以写任意代码
println("ABABAB")
}
  1. 如果主构造器无参数,小括号可省略,构建对象时调用的构造方法的小括号也可以省略
1
2
3
4
5
class AA{

}
var a1=new AA
var a2=new AA()
  1. 辅助构造器名称为this(这个和Java是不一样的),多个辅助构造器通过不同参数列表进行区分, 在底层就是构造器重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def main(args: Array[String]): Unit = {
val p=new Person("小明")
}

class Person() {
var name: String = _
var age: Int = _

def this(name: String) {
//辅助构造器无论是直接或间接,最终都一定要调用主构造器,执行主构造器的逻辑,而且需要放在辅助构造器的第一行
this() //直接调用主构造器
this.name = name
}

def this(name: String, age: Int) {
this() //直接调用主构造器
this.name = name
this.age = age
}

def this(age: Int) {
this("匿名") //间接调用主构造器,因为 def this(name : String) 中调用了主构造器!
this.age = age
}
}
  1. 如果想让主构造器变成私有的,可以在()之前加上private,这样用户只能通过辅助构造器来构造对象了【反编译查看】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object Test6 {
def main(args: Array[String]): Unit = {
val p=new Person("wxl")
}
}

class Person private(){
var name: String = _
var age: Int = _

def this(name: String) {
this()
this.name = name
}
}
  1. 辅助构造器的声明不能和主构造器的声明一致,会发生错误(即构造器名重复)

3. 属性高级

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

scala示例:

1
2
3
4
5
6
7
8
9
10
////1. 如果主构造器是Worker(inName:String),那么abc这个类的一个局部变量
class Worker1(abc: String) {a
var name = abc
var age:Int=_

def this(inName: String, age: Int) {
this(inName)
var t=abc
this.age = age
}

scala反编译如下,可以看到,abc这个主构造器的参数没有类似于get的方法,因此外界不能访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Worker1 {
private final String abc;

private String name;

private int age;

public Worker1(String abc) {
this.name = abc;
}

public String name() {
return this.name;
}

public void name_$eq(String x$1) {
this.name = x$1;
}

public int age() {
return this.age;
}

public void age_$eq(int x$1) {
this.age = x$1;
}

public Worker1(String inName, int age) {
this(inName);
String t = this.abc;
age_$eq(age);
}
}

(2)Scala类的主构造器的形参用var修饰,那么该参数是这个类的成员变量,且提供了get和set功能,外界可读可写

1
2
3
4
5
6
7
8
9
10
11
//2. 如果主构造器是Worker(var abc:String),那么abc是这个类的成员函数,提供了get和set功能,外界可读可写
class Worker2(var abc: String) {
var name = abc
var age:Int=_

def this(inName: String, age: Int) {
this(inName)
var t=abc
this.age = age
}
}

scala反编译为java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Worker2 {
private String abc;

public String abc() {
return this.abc;
}

public void abc_$eq(String x$1) {
this.abc = x$1;
}

private String name = abc();

private int age;

public String name() {
return this.name;
}

public void name_$eq(String x$1) {
this.name = x$1;
}

public int age() {
return this.age;
}

public void age_$eq(int x$1) {
this.age = x$1;
}

public Worker2(String abc) {}

public Worker2(String inName, int age) {
this(inName);
String t = abc();
age_$eq(age);
}
}

(3)Scala类的主构造器的形参用val修饰,那么该参数是这个类的成员变量,且只提供了get,外界只能读

1
2
3
4
5
6
7
8
9
10
11
//3. 如果主构造器是Worker(val abc:String),那么abc是这个类的成员函数,提供了set功能,外界只可读
class Worker3(val abc: String) {
var name = abc
var age:Int=_

def this(inName: String, age: Int) {
this(inName)
var t=abc
this.age = age
}
}

scala反编译为java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Worker3 {
private final String abc;

private String name;

private int age;

public String abc() {
return this.abc;
}

public Worker3(String abc) {
this.name = abc;
}

public String name() {
return this.name;
}

public void name_$eq(String x$1) {
this.name = x$1;
}

public int age() {
return this.age;
}

public void age_$eq(int x$1) {
this.age = x$1;
}

public Worker3(String inName, int age) {
this(inName);
String t = abc();
age_$eq(age);
}
}

(4)如果类的成员变量使用val关键字声明,则该属性必须手动初始化,不能用下划线初始化 ,并且类中默认只提供了get方法,所以只能读不能写【案例+反编译】

1
2
3
4
class Person1(){
val name: String ="小明"
val age: Int =10
}

scala反编译成java代码,可以看到,没有提供set方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Person1 {
private final String name = ";

public String name() {
return this.name;
}

private final int age = 10;

public int age() {
return this.age;
}
}

(5) 如果类的成员变量使用var关键字声明,则该可以用下划线初始化,并且类中默认提供了get和set方法,所以可读可写

scala语言

1
2
3
4
class Person() {
var name: String = _
var age: Int = _
}

scala反编译为java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Person {
private String name;

private int age;

public String name() {
return this.name;
}

public void name_$eq(String x$1) {
this.name = x$1;
}

public int age() {
return this.age;
}

public void age_$eq(int x$1) {
this.age = x$1;
}

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

scala示例

1
2
3
4
class Worker4(val abc: String) {
var name = abc
@BeanProperty var age:Int=_
}

反编译为java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Worker4 {
private final String abc;

private String name;

private int age;

public String abc() {
return this.abc;
}

public Worker4(String abc) {
this.name = abc;
}

public String name() {
return this.name;
}

public void name_$eq(String x$1) {
this.name = x$1;
}

public int age() {
return this.age;
}

public void age_$eq(int x$1) {
this.age = x$1;
}

public int getAge() {
return age();
}

public void setAge(int x$1) {
age_$eq(x$1);
}
}

4. 对象创建的流程

1
2
3
4
5
6
7
8
9
class Person {
var age: Short = 90
var name: String = _
def this(n: String, a: Int) {
this()
this.name = n
this.age = a
}}
var p : Person = new Person("小倩",20)

流程分析(面试题-写出)

  1. 加载类的信息(属性信息,方法信息)
  2. 在内存中(堆)开辟空间
  3. 使用父类的构造器(主和辅助)进行初始
  4. 使用主构造器对属性进行初始化 【age:90, naem nul】
  5. 使用辅助构造器对属性进行初始化 【 age:20, naem 小倩 】
  6. 将开辟的对象的地址赋给 p这个引用

5. 包

5.1 Scala包的特点概述

Java包的三大作用

  1. 区分相同名字的类
  2. 当类很多时,可以很好的管理类
  3. 控制访问范围

Java打包语法:如 package com.atguigu;

Java如何引入包:如 import java.awt.*;

Scala包的基本介绍

和Java一样,Scala中管理项目可以使用包,但Scala中的包的功能更加强大,使用也相对复杂些,下面我们学习Scala包的使用和注意事项。

1
2
3
4
5
6
7
8
9
10
11
package com.atguigu.chapter02.xh
class Cat {
}
package com.atguigu.chapter02.xm
class Cat {
}
//使用不同包下相同的类
var cat1 = new com.atguigu.chapter02.xh.Cat()
println("cat1" + cat1)
var cat2 = new com.atguigu.chapter02.xm.Cat()
println("cat2" + cat2)

基本语法:package 包名,和java相同

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

  1. 区分相同名字的类
  2. 当类很多时,可以很好的管理类
  3. 控制访问范围

命名规范com.公司名.项目名.业务模块名

比如:

1
2
3
4
com.atguigu.oa.model  
com.atguigu.oa.controller
com.sina.edu.user
com.sohu.bank.order

Scala会自动引入的常用包(不需要自己再手动引入)

1
2
3
java.lang.* (这个java也会自动引入)
scala._ ,如scala.List就不用手动引入就可以使用,但是其子包不能使用,比如scala.io.*里面的内容就不能使用
scala.Predef._ (一般很多的隐式转换都在该包下),如Predef.println等等常用方法可以使用

Scala中包名和源码所在的文件目录可以不一致,但是编译后的字节码文件(.class文件)路径和包名会保持一致(这个工作由编译器完成)。

5.2 Scala包注意事项和使用细节

1)scala进行package 打包时,可以有如下形式。

上面三种打包方式完全等价

  1. 包也可以像嵌套类那样嵌套使用(包中有包), 这个在前面的第三种打包方式已经讲过了,在使用第三种方式时的好处是:程序员可以在同一个文件中,将类(class / object)、trait 创建在不同的包中,这样就非常灵活了。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.atguigu {

//这个类就是在com.atguigu包下
class User {
}

//这个类也在com.atguigu包下
object Monster {
}

class Dog {
}
package scala {

//这个类就是在com.atguigu.scala包下
class User {
}

//这个Test 类对象
object Test {
def main(args: Array[String]): Unit = {
//子类可以直接访问父类的内容
var dog = new Dog()
println("dog=" + dog)
//在子包和父包 类重名时,默认采用就近原则.
var u = new User()
println("u=" + u)
//在子包和父包 类重名时,如果希望指定使用某个类,则带上包名即可
var u2 = new com.atguigu.User()
println("u2=" + u2)
}
}
  1. 父包要访问子包的内容时,需要import对应的类等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.atguigu {
//引入在com.atguigu 包中希望使用到子包的类Tiger ,因此需要引入.

import com.atguigu.scala.Tiger

//这个类就是在com.atguigu包下
class User {
}
package scala {

//Tiger 在 com.atguigu.scala 包中
class Tiger {}

}

object Test2 {
def main(args: Array[String]): Unit = {
//如果要在父包使用到子包的类,需要import
import com.atguigu.scala.Tiger
val tiger = new Tiger()
println("tiger=" + tiger)
}
}

}
  1. 可以在同一个.scala文件中,声明多个并列的package(建议嵌套的pakage不要超过3层)

  2. 包名可以相对也可以绝对,比如,访问BeanProperty的绝对路径是:_root_. scala.beans.BeanProperty ,在一般情况下:我们使用相对路径来引入包,只有当包名冲突时,使用绝对路径来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Manager(var name: String) {
//第一种形式
//@BeanProperty var age: Int = _
//第二种形式, 和第一种一样,都是相对路径引入
//@scala.beans.BeanProperty var age: Int = _
//第三种形式, 是绝对路径引入,可以解决包名冲突
@_root_.scala.beans.BeanProperty var age: Int = _
}

object TestBean {
def main(args: Array[String]): Unit = {
val m = new Manager("jack")
println("m=" + m)
}
}

5.3 包对象

基本介绍:包可以包含类、对象和特质trait,但不能包含函数/方法或变量的定义。这是Java虚拟机的局限。

1
2
3
4
5
6
7
8
9
10
package Test {

package scala {
//在包里面直接写方法或者定义变量就报错
var name = "" //报错
def test1 () {//报错

}
}
}

为了弥补这一点不足,scala提供了包对象的概念来解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.atguigu {

//每个包都可以有一个包对象。你需要在父包(com.atguigu)中定义它,
package object scala {
var name = "jack"

def sayOk(): Unit = {
println("package object sayOk!")
}
}

//这个要和包对象名一致
package scala {


object TestObj {
def main(args: Array[String]): Unit = {

//可以使用包对象中的变量和方法
println("name=" + name)
sayOk() //这个sayOk 就是包对象scala中声明的sayOk
}
}

}

}

包对象的注意事项

  1. 每个包都可以有一个包对象,而且只能有一个。你需要在父包中定义它。

  2. 在包对象中可以定义变量和方法,在包对象中定义的变量和方法就可以在对应的子包中使用

  3. 包对象名称需要和子包名一致,一般用来对包的功能补充

5.4 包的可见性

java提供四种访问控制修饰符号控制方法和变量的访问权限(范围):

  1. 公开级别:用public 修饰,对外公开
  2. 受保护级别:用protected修饰,对子类和同一个包中的类公开
  3. 默认级别:没有修饰符号,向同一个包的类公开.
  4. 私有级别:用private修饰,只有类本身可以访问,不对外公开.

Java访问修饰符使用注意事项

  1. 修饰符可以用来修饰类中的属性,成员方法以及类
  2. 只有默认的和public才能修饰类!,并且遵循上述访问权限的特点。

Scala中包的可见性

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package testA

object Testvisit {
def main(args: Array[String]): Unit = {
val c = new Clerk()
c.showInfo()
Clerk.test(c)
//c.sal不能访问
//Clerk.sal不能访问
}}
class Clerk {
var name : String = "jack"
private var sal : Double = 9999.9
def showInfo(): Unit = {
println(" name " + name + " sal= " + sal)
}}
object Clerk {
def test(c: Clerk): Unit = {
//这里体现出在伴生对象中,可以访问c.sal
println("test() name=" + c.name + " sal= " + c.sal)
}
}
  1. 当属性访问权限为默认时,从底层看属性是private的,但是因为提供了xxx_$eq()[类似setter]/xxx()[类似getter] 方法,因此从使用效果看是任何地方都可以访问)

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

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

  4. protected为受保护权限,scala中受保护权限比Java中更严格,只能子类访问,同包无法访问 (编译器)

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

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

下面的实例,相当于在private的基础上,扩大了访问权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package testA.layne

object Testvisit1 {
def main(args: Array[String]): Unit = {
val c = new Clerk()
//c.showInfo()加了private,方法就不能访问
val sal=c.sal
println(sal) //输出9999.9

}}
class Clerk {
var name : String = "jack"
//这里我们增加了一个包的访问权限,即在当前layne包下,sal属性可以访问
private[layne] var sal : Double = 9999.9
private def showInfo(): Unit = {
println(" name " + name + " sal= " + sal)
}
}

5.5 包的引入

Scala引入包也是使用import, 基本的原理和机制和Java一样,但是Scala中的import功能更加强大,也更灵活。

因为Scala语言源自于Java,所以java.lang包中的类会自动引入到当前环境中,而Scala中的scala包和Predef包的类也会自动引入到当前环境中,即起其下面的类可以直接使用。

如果想要把其他包中的类引入到当前环境中,需要使用import

Scala引入包的细节和注意事项

  1. 在Scala中,import语句可以出现在任何地方,并不仅限于文件顶部,import语句的作用一直延伸到包含该语句的块末尾。这种语法的好处是:在需要时在引入包,缩小import 包的作用范围,提高效率。
1
2
3
4
5
6
7
class User {
import scala.beans.BeanProperty
@BeanProperty var name : String = ""
}
class Dog {
@BeanProperty var name : String = "" //这里不可以使用上面引入的BeanProperty
}
  1. Java中如果想要导入包中所有的类,可以通过通配符*,Scala中采用下 _
1
2
//引入scala.io中所有的类
import scala.io._
  1. 如果不想要某个包中全部的类,而是其中的几个类,可以采用选取器(大括号
1
2
3
4
5
def test(): Unit = {
import scala.collection.mutable.{HashMap, HashSet}
var map = new HashMap()
var set = new HashSet()
}
  1. 如果引入的多个包中含有相同的类,那么可以将不需要的类进行重命名进行区分,这个就是重命名。
1
2
3
4
import java.util.{ HashMap=>JavaHashMap, List}
import scala.collection.mutable._
var map = new HashMap() // 此时的HashMap指向的是scala中的HashMap
var map1 = new JavaHashMap(); // 此时使用的java中hashMap的别名
  1. 如果某个冲突的类根本就不会用到,那么这个类可以直接隐藏掉。
1
2
import java.util.{ HashMap=>_, _} // 含义为 引入java.util包的所有类,但是忽略HahsMap类.
var map = new HashMap() // 此时的HashMap指向的是scala中的HashMap, 而且idea工具,的提示也不会显示java.util的HashMaple

6. 封装、继承、多态

6.1 封装

Scala封装的注意事项和细节

  1. Scala中为了简化代码的开发,当声明属性时,本身就自动提供了对应setter/getter方法,如果属性声明为private的,那么自动生成的setter/getter方法也是private的,如果属性省略访问权限修饰符,那么自动生成的setter/getter方法是public的。

  2. 因此我们如果只是对一个属性进行简单的set和get ,只要声明一下该属性(属性使用默认访问修饰符) 不用写专门的getset,默认会创建,访问时,直接对象.变量。这样也是为了保持访问一致性

  3. 从形式上看 dog.food 直接访问属性,其实底层仍然是访问的方法, 看一下反编译的代码就明白

  4. 有了上面的特性,目前很多新的框架,在进行反射时,也支持对属性的直接反射

6.2 继承

Java继承的简单回顾 class 子类名 extends 父类名 { 类体 },子类继承父类的属性和方法

(1)和Java一样,Scala也支持类的单继承

Scala继承的基本语法

1
class 子类名 extends 父类名  { 类体 }

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
var name: String = _
var age: Int = _

def showInfo(): Unit = {
println("学生信息如下:")
println("名字:" + this.name)
}
}

class Student extends Person {
def studying(): Unit = {
println(this.name + "学习 scala中....")
}
}

(2)子类继承了所有的属性,只是私有的属性不能直接访问,需要通过公共的方法去访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
object Extends02 {
def main(args: Array[String]): Unit =

val sub = new Sub()
sub.sayOk()
}
}

class Base {
var n1: Int = 1
protected var n2: Int = 2
private var n3: Int = 3

def test100(): Unit = {
println("base 100")
}

protected def test200(): Unit = {
println("base 200")
}

private def test300(): Unit = {
println("base 300")
}
}

class Sub extends Base {
def sayOk(): Unit = {
this.n1 = 20
this.n2 = 40
println("范围" + this.n1 + this.n2)
}
}

(3)scala明确规定,重写一个非抽象方法需要用override修饰符,调用超类的方法使用super关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
var name: String = "tom"

def printName() {
println("Person printName() " + name)
}
}

class Emp extends Person {
//这里需要显式的使用override
override def printName() {
println("Emp printName() " + name)
super.printName()
}
}

6.3 类型检查和转换

要测试某个对象是否属于某个给定的类,可以用isInstanceOf方法。用asInstanceOf方法将引用转换为子类的引用。classOf获取对象的类名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package testA.layne

object T3 {

def main(args: Array[String]): Unit = {
// 获取对象类型
println(classOf[String])//class java.lang.String
println(classOf[Emp])//class testA.layne.Emp
val s = "zhangsan"

//这种是Java中反射方式得到类型
println(s.getClass.getName)//java.lang.String
println(s.isInstanceOf[String])///true
//将s显示转换成String
println(s.asInstanceOf[String])//zhangsan
var p = new Person
val e = new Emp
p = e //将子类对象赋给父类。
p.name = "layne"
println(e.name)//layne
p.asInstanceOf[Emp].printName()//调用子类emp中的方法
}

}

class Person {
var name: String = "tom"

def printName() {
println("Person printName() " + name)
}
}

class Emp extends Person {
//这里需要显式的使用override
override def printName() {
println("Emp printName() " + name)
super.printName()
}
}
  • classOf[String]就如同Java的 String.class 。
  • obj.isInstanceOf[T]就如同Java的obj instanceof T 判断obj是不是T类型。
  • obj.asInstanceOf[T]就如同Java的(T)obj 将obj强转成T类型。

6.4 超类的构造

在Java中,创建子类对象时,子类的构造器总是去调用一个父类的构造器(显式或者隐式调用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
public A () {
System.out.println("A()");
}
public A (String name) {
System.out.println("A(String name)" + name);
}
}

class B extends A {
public B () {
//这里会隐式调用super(); 就是无参的父类构造器A()
System.out.println("B()");
}
public B (String name) {
super (name);
System.out.println("B(String name)" + name);
}
}

在Scala中,类有一个主构器和任意数量的辅助构造器,而每个辅助构造器都必须先调用主构造器(也可以是间接调用),这点在前面我们说过了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
var name = "zhangsan"
println("Person...")
}

class Emp extends Person {
println("Emp ....")

def this(name: String) {
this // 必须调用主构造器,这里的主构造器指的是Emp
this.name = name
println("Emp 辅助构造器~")
}
}

Scala中,只有主构造器可以调用父类的构造器。辅助构造器不能直接调用父类的构造器。在Scala的构造器中,你不能调用super(params)

1
2
3
4
5
6
7
8
class Person(name: String) { //父类的构造器
}
class Emp (name: String) extends Person(name) {// 将子类参数传递给父类构造器,这种写法√
// super(name) (×) 没有这种语法
def this() {
super("abc") // (×)不能在辅助构造器中调用父类的构造器
}
}

举一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package testA.layne

object T4 {

def main(args: Array[String]): Unit = {
val stu:PersonA=new Student("Layne")
stu.printName()
}
}

class PersonA(pname:String) {
var name: String = "tom"
println("Welcome to PersonA:"+pname)

def printName() {
println("Person printName() " + name)
}
}

class Student(studentname:String) extends PersonA(studentname){

var sno:Int=20

println("Welcome to Student:"+studentname)

override def printName(): Unit ={
println("Student priceName()"+name)
super.printName()
}
}

输出

1
2
3
4
Welcome to PersonA:Layne
Welcome to Student:Layne
Student priceName()tom
Person printName() tom

6.5 覆盖属性/方法

在Scala中,子类改写父类的字段,我们称为覆写/重写字段。覆写字段需使用override修饰。

现在,回顾一下java的动态绑定,下面可以看到,对象a都是调用的子类方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package testA.layne;

public class T5 {
public static void main(String[] args) {
A a = new B();
System.out.println(a.sum()); //40
System.out.println(a.sum1()); //30
}
}

class A {
public int i = 10;
public int sum() {
return getI() + 10;
}
public int sum1() {
return i + 10;
}
public int getI() {
return i;
}
}
class B extends A {
public int i = 20;
public int sum() {
return i + 20;
}
public int getI() {
return i;
}
public int sum1() {
return i + 10;
}
}

来看一个scala覆盖字段的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package testA.layne

object T6 {

def main(args: Array[String]): Unit = {
val obj : AA = new BB()
val obj2 : BB = new BB()
println(obj.age)//20
println(obj.age)//20
}
}

class AA {
val age : Int = 10
}

class BB extends AA {
override val age : Int = 20
}
  • def只能重写另一个def(即:方法只能重写另一个方法)
  • val只能重写另一个val 属性 或 重写不带参数的def
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//判断1
//代码正确吗?不正确,重写时类型一定要保持一致
class AAAA {
var name: String = ""
}
class BBBB extends AAAA {
override val name: String = "jj" //类型没保持一致
}

//判断2
//代码正确吗?不正确,用var修饰的变量不能被重写
class AAAA {
var name: String = ""
}
class BBBB extends AAAA {
override var name: String = "jj"
}

//判断3,正确,val变量可以重新一个不带参数的def
class A {
def sal(): Int = {
return 10
}}
class B extends A {
override val sal : Int = 0 //val变量可以重新一个不带参数的def
}

var只能重写另一个抽象的var属性

1
2
3
4
5
6
7
8
abstract class A03{
val age:Int=10
var name:String
}
class B03 extends A03{
override val age:Int=20
var name:String=_
}

抽象属性:声明未初始化的变量就是抽象的属性,抽象属性在抽象类

var重写抽象的var属性小结:

  1. 一个属性没有初始化,那么这个属性就是抽象属性
  2. 抽象属性在编译成字节码文件时,属性并不会声明,但是会自动生成抽象方法,所以类必须声明为抽象类

上面的代码反编译为java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class ABC {
private int a = 10;

public int a() {
return this.a;
}

public void a_$eq(int x$1) {
this.a = x$1;
}

private final int b = 20;

public int b() {
return this.b;
}

public class B03 extends A03 {
private final int age = 20;

private String name;

public int age() {
return this.age;
}

public String name() {
return this.name;
}

public void name_$eq(String x$1) {
this.name = x$1;
}
}
  1. 如果是覆写一个父类的抽象属性,那么override 关键字可省略 [原因:父类的抽象属性,生成的是抽象方法,因此就不涉及到方法重写的概念,因此override可省略,当然也可以写override]

6.6 抽象类

抽象类基本语法

1
2
3
4
abstract class Person() { // 抽象类
var name: String // 抽象字段, 没有初始化
def printName // 抽象方法, 没有方法体
}

说明:抽象类的价值更多是在于设计,是设计者设计好后,让子类继承并实现抽象类(即:实现抽象类的抽象方法)

Scala抽象类使用的注意事项和细节讨论

  1. 抽象类不能被实例
  2. 抽象类不一定要包含abstract方法。也就是说,抽象类可以没有abstract方法
  3. 一旦类包含了抽象方法或者抽象属性,则这个类必须声明为abstract
  4. 抽象方法不能有主体,不允许使用abstract修饰。
  5. 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法和抽象属性,除非它自己也声明为abstract类。(java里面只要实现抽象方法就行了)
    6)抽象方法和抽象属性不能使用private、final 来修饰,因为这些关键字都是和重写/实现相违背的。
  6. 抽象类中可以有实现的方法
  7. 子类重写抽象方法不需要override,写上也不会错

匿名子类

和Java一样,可以通过包含带有定义或重写的代码块的方式创建一个匿名的子类.

回顾-java匿名子类使用

1
2
3
4
5
6
7
8
9
10
abstract class A2 {
abstract public void cry();
}

A2 obj = new A2() {
@Override
public void cry() {
System.out.println("okook!");
}
};

scala匿名子类

1
2
3
4
5
6
7
8
9
10
11
12
13
abstract class Monster {
var name: String

def cry()
}

var monster = new Monster {
override var name: String = "牛魔王"

override def cry(): Unit = {
println("牛魔王哼哼叫唤..")
}
}

继承层级

  1. 在scala中,所有其他类都是AnyRef的子类,类似Java的Object。
  2. AnyVal和AnyRef都扩展自Any类。Any类是根节点
  3. Any中定义了isInstanceOf、asInstanceOf方法,以及哈希方法等。
  4. Null类型的唯一实例就是null对象。可以将null赋值给任何引用,但不能赋值给值类型的变量。
  5. Nothing类型没有实例,它是任何类型的子类型,可以交给一个变量和函数。它对于泛型结构是有用处的,举例:空列表Nil的类型是List[Nothing],它是List[T]的子类型,T可以是任何类。

7. 静态概念与伴生对象

回顾下Java的静态概念

1
2
public static 返回值类型  方法名(参数列表) {方法体}
静态属性...

Java中静态方法并不是通过对象调用的,而是通过类对象调用的,所以静态操作并不是面向对象的。

Scala中静态的概念-伴生对象

Scala语言是完全面向对象(万物皆对象)的语言,所以并没有静态的操作(即在Scala中没有静态的概念)。但是为了能够和Java语言交互(因为Java中有静态概念),就产生了一种特殊的对象来模拟类对象,我们称之为类的伴生对象。这个类的所有静态内容都可以放置在它的伴生对象中声明和调用

伴生对象的快速入门

1
2
3
4
5
6
7
class ScalaPerson {//class ScalaPerson 是伴生类
var name : String = _
}
object ScalaPerson {//object ScalaPerson 是伴生对象
var sex : Boolean = true
}
println(ScalaPerson.sex)//可以直接输出
  1. Scala中伴生对象采用object关键字声明,伴生对象中声明的全是 "静态"内容,可以通过伴生对象名称直接调用
  2. 伴生对象对应的类称之为伴生类,伴生对象的名称应该和伴生类名一致。
  3. 伴生对象中的属性和方法都可以通过伴生对象名(类名)直接调用访问,伴生类可以直接使用伴生对象中的属性和方法【看第(5)的反编译】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
object Test12 {

def main(args: Array[String]): Unit = {

println(ScalaPerson1.sex)//伴生对象中的sex通过伴生对象名访问
val s=new ScalaPerson1 //name只能通过new出来的对象访问
println(s.name)

val a=ScalaPerson1//这种方式也是通过伴生对象访问,只不过赋值给了一个变量
println(a.sex)
}
}

object ScalaPerson1 {//object ScalaPerson 是伴生对象
var sex : Boolean = true
}
class ScalaPerson1 {//class ScalaPerson 是伴生类
var name : String = _
}
  1. 从语法角度来讲,所谓的伴生对象其实就是类的静态方法和成员的集合

  2. 从技术角度来讲,scala还是没有生成静态的内容,只不过是将伴生对象生成了一个新的类,实现属性和方法的调用。[反编译看源码]

将(3)中scala反编译为java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ScalaPerson1 {
private String name;

public static void sex_$eq(boolean paramBoolean) {
ScalaPerson1$.MODULE$.sex_$eq(paramBoolean);
}

public static boolean sex() {
return ScalaPerson1$.MODULE$.sex();
}

public String name() {
return this.name;
}

public void name_$eq(String x$1) {
this.name = x$1;
}
}

public final class ScalaPerson1$ {
public static ScalaPerson1$ MODULE$;

private boolean sex;

public boolean sex() {
return this.sex;
}

public void sex_$eq(boolean x$1) {
this.sex = x$1;
}

private ScalaPerson1$() {
MODULE$ = this;
this.sex = true;
}
}
  1. 从底层原理看,伴生对象实现静态特性是依赖于 public static final MODULE$ 实现的。

  2. 如果 class A 独立存在,那么A就是一个类, 如果 object A 独立存在,那么A就是一个"静态"性质的对象[即类对象], 此时在 object A中声明的属性和方法可以通过 A.属性A.方法 来实现调用

8)如果伴生对象和伴生类同时出现,伴生对象的声明应该和伴生类的声明在同一个源码文件中(如果不在同一
个文件中会运行错误,会提示类已存在的错误),但是如果没有伴生类,也就没有所谓的伴生对象了,所以放在哪里就无所谓了。

  1. 当一个文件中,存在伴生类和伴生对象时,文件的图标会发生变化

伴生对象-apply方法

先来看几个例子:

示例1

1
2
3
4
5
6
7
8
9
10
11
12
13
object Test15 {

def main(args: Array[String]): Unit = {
val t1 = new TestA
t1("layne1") //会输出 TestA apply method called:layne1
}
}

class TestA{
def apply(param:String){
println("TestA apply method called:" + param)
}
}

示例2

1
2
3
4
5
6
7
8
9
10
11
12
object Test16 {

def main(args: Array[String]): Unit = {
TestB("layne2")//会输出 TestB apply method called:layne2
}
}

object TestB{
def apply(param:String){
println("TestB apply method called:" + param)
}
}

可以看出,apply方法调用:用括号传递给类对象伴生对象一个或多个参数时,Scala会在相应的类或对象中查找方法名为apply且参数列表与传入的参数一致的方法,并用传入的参数来调用该apply方法,并获取apply方法的返回值。

另外,apply方法也支持重载。

在Scala中,我们把所有类的构造方法以apply方法的形式定义在它的伴生对象当中,这样伴生对象的方法就会自动被调用,调用就会生成类对象。

示例3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package Test

object Test17 {

def main(args: Array[String]): Unit = {
val mycar=Car("tom")
mycar.info()//输出Car name is tom
}
}

class Car(name:String){
def info(){
println("Car name is " + name)
}
}

object Car{
def apply(name:String) = new Car(name) //调用伴生类Car的构造方法
//def apply(name:String):Car = new Car(name) //这个写法同上
}

Scala之所以用apply方法是为了保持它的对象和函数之间使用的一致性,因为Scala融合了面向对象和函数式两种编程风格,它是一种混合式的编程。

  • 面向对象调用:对象.方法
  • 函数式调用:函数(参数)

Scala中函数式的调用可以转化成对象调用,同理对象调用也可以被转化成函数的调用。

1
2
3
4
5
6
7
//示例3里面
val mycar=Car("tom")//函数式调用,前提是定义了apply方法
mycar.info()//函数式调用转化为 面向对象调用

//在示例1里面
val t1 = new TestA
t1("layne1") //对象调用转化为函数式调用,前提是定义了apply方法

8 接口&trait(特征)

8.1 scala中的接口与trait

回顾Java接口

1
2
3
4
//声明接口
interface 接口名
//实现接口
class 类名 implements 接口名1,接口2
  1. 在Java中, 一个类可以实现多个接口。
  2. 在Java中,接口之间支持多继承
  3. 接口中属性都是常量
  4. 接口中的方法都是抽象的

Scala接口

从面向对象来看,接口并不属于面向对象的范畴,Scala是纯面向对象的语言,在Scala中,没有接口。

Scala语言中,采用特质trait(特征)来代替接口的概念,也就是说,多个类具有相同的特征(特征)时,就可以将这个特质(特征)独立出来,采用关键字trait声明。 理解trait 等价于(interface + abstract class)

trait 的声明

1
2
3
trait 特质名 {
trait体
}

Serializable就是scala的一个特质:

1
2
3
object T1 extends Serializable {

}

在scala中,java中的接口可以当做特质使用。

8.2 trait 的使用

一个类具有某种特质(特征),就意味着这个类满足了这个特质(特征)的所有要素,所以在使用时,也采用了extends关键字,如果有多个特质或存在父类,那么需要采用with关键字连接

可以把特质可以看作是对继承的一种补充,Scala的继承是单继承,也就是一个类最多只能有一个父类,这种单继承的机制可保证类的纯洁性,比c++中的多继承机制简洁。但对子类功能的扩展有一定影响,所以我们认为: Scala引入trait特征,第一可以替代Java的接口, 第二个也是对单继承机制的一种补充。

  1. Scala提供了特质(trait),特质可以同时拥有抽象方法和具体方法,一个类可以实现/继承多个特质。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package traitPackage

object T1 {

def main(args: Array[String]): Unit = {
val a:Trait01=new AA
a.sayOK()
a.sayHello()
val trait01Obj=new Trait01 {
override def sayOK(): Unit = {
println("trait01Obj sayOk")
}
}
trait01Obj.sayHello()
trait01Obj.sayOK()
}
}

trait Trait01{
def sayOK()
def sayHello():Unit={
println("Trait01 sayHello")
}
}

class AA extends Trait01{
override def sayOK(): Unit ={
println("AA sayOk")
}
}

上面的代码反编译为java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface Trait01 {
static void $init$(Trait01 $this) {}

default void sayHello() {
Predef$.MODULE$.println("Trait01 sayHello");
}

void sayOK();
}

public class AA implements Trait01 {
public void sayHello() {
Trait01.sayHello$(this);
}

public AA() {
Trait01.$init$(this);
}

public void sayOK() {
Predef$.MODULE$.println("AA sayOk");
}
}
  1. 特质中没有实现的方法就是抽象方法。类通过extends继承特质,通过with可以继承多个特质

  2. 所有的java接口都可以当做Scala特质使用

1
2
3
4
5
6
7
8
9
trait Logger {
def log(msg: String)
}

class Console extends Logger with Cloneable with Serializable {
def log(msg: String) {
println(msg)
}
}
  1. 和Java中的接口不太一样的是特质中的方法并不一定是抽象的,也可以有非抽象方法(即:实现了的方法)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
trait Operate {
def insert( id : Int ): Unit = {
println("保存数据="+id)
}
}
trait DB extends Operate {
override def insert( id : Int ): Unit = {
print("向数据库中")
super .insert(id)
}
}

class MySQL extends DB {
}

8.3 带有特质的对象

  1. 除了可以在类声明时继承特质以外,还可以在构建对象时混入特质,扩展目标类的功能
  2. 此种方式也可以应用于对抽象类功能进行扩展
  3. 动态混入是Scala特有的方式(java没有动态混入),可在不修改类声明/定义的情况下,扩展类的功能,非常的灵活,耦合性低 。
  4. 动态混入可以在不影响原有的继承关系的基础上,给指定的类扩展功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package traitPackage

object T2 {

def main(args: Array[String]): Unit = {
var oracle = new OracleDB with Operate3
oracle.insert(999)
val mysql = new MySQL3 with Operate3 {
override def connect(): Int = {
println("hello mysql connect")
1+1
}
}
mysql.insert(4)
mysql.connect()


}
}

trait Operate3 {
def insert(id: Int): Unit = {
println("插入数据 = " + id)
}
}

class OracleDB {
}

abstract class MySQL3 {
def connect():Int
}

在Scala中创建对象共有几种方式

  1. new 对象
  2. apply 创建
  3. 匿名子类方式
  4. 动态混入

8.4 叠加特质

叠加特指是本质上也是动态混入

构建对象的同时如果混入多个特质,称之为叠加特质,仔细看下面的代码注释,理解叠加特质的含义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package traitPackage

object T3 {

def main(args: Array[String]): Unit = {
// 1.Scala在叠加特质的时候,从左到右叠加,即对于mysql1来说先DB4,在File4
val mysql1 = new MySQL4 with DB4 with File4
/*
* mysql这里输出
* Operate4...
Data4
DB4
File4
* */
//val mysql2 = new MySQL4 with File4 with DB4
/*
* mysql2这里输出
* Operate4...
Data4
File4
DB4
* */

//在动态混入时,果调用super,并不是表示调用父特质的方法,而是向前面(左边)继续查找特质,如果找不到,才会去父特质查找
//下面输出: (File4)向文件(DB4)向数据库(Data4)插入数据 = 888
mysql1.insert (888)//最先执行File4里面,在调用super是DB4里面的
}
}

trait Operate4 {
println("Operate4...")

def insert(id: Int)
}

trait Data4 extends Operate4 {
println("Data4")

override def insert(id: Int): Unit = {
println("(Data4)插入数据 = " + id)
}
}

trait DB4 extends Data4 {
println("DB4")

override def insert(id: Int): Unit = {
print("(DB4)向数据库")
super.insert(id)
}
}

trait File4 extends Data4 {
println("File4")

override def insert(id: Int): Unit = {
print("(File4)向文件")
super.insert(id)
}
}

class MySQL4 {}

在特质中重写抽象方法特例

看下面的代码

1
2
3
4
5
6
7
8
9
trait Operate5 {
def insert(id : Int)
}
trait File5 extends Operate5 {
def insert( id : Int ): Unit = {
println("将数据保存到文件中..")
super.insert(id)
}
}

运行代码,编译无法通过,报错信息为:

1
2
method insert in trait Operate5 is accessed from super. It may not be abstract unless it is overridden by a member declared `abstract' and `override'
super.insert(id)

因为我们在特质File5中调用super的insert方法,而Operate5的该方法却没有具体实现,因此报错

解决方法

方式1 : 去掉 super()…

方式2: 声明 abstract override,编译通过

1
2
3
4
5
6
7
8
9
trait Operate5 {
def insert(id : Int)
}
trait File5 extends Operate5 {
abstract override def insert( id : Int ): Unit = {
println("将数据保存到文件中..")
super.insert(id)
}
}

理解 abstract override 的小技巧分享:
可以这里理解,当我们给某个方法增加了abstract override 后,就是明确的告诉编译器,该方法确实是重写了父特质的抽象方法,但是重写后,该方法仍然是一个抽象方法(因为没有完全的实现,需要其它特质继续实现[通过混入顺序])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package traitPackage

object T4 {

def main(args: Array[String]): Unit = {


val mysql5 = new MySQL5 with DB5 with File5
mysql5.insert(123)
}
}

trait Operate5 {
def insert(id : Int)
}
trait File5 extends Operate5 {
abstract override def insert( id : Int ): Unit = {
println("(File5)将数据保存到文件中..")
super.insert(id)
}
}
trait DB5 extends Operate5 {
def insert( id : Int ): Unit = {
println("(DB5)将数据保存到数据库中..")
}
}
class MySQL5 {}

输出:

1
2
(File5)将数据保存到文件中..
(DB5)将数据保存到数据库中..

结合上面的代码,看4个案例

1
2
3
4
5
6
7
8
var mysql2 = new MySQL5  with DB5 //ok
mysql2.insert(100)
var mysql3 = new MySQL5 with File5 //error
mysql2.insert(100)
var mysql4 = new MySQL5 with File5 with DB5// error
mysql4.insert(100)
var mysql4 = new MySQL5 with DB5 with File5// ok
mysql4.insert(100)

8.5 特质中的字段和方法

富接口:即该特质中既有抽象方法,又有非抽象方法

1
2
3
4
5
6
trait Operate {
def insert( id : Int ) //抽象
def pageQuery(pageno:Int, pagesize:Int): Unit = { //实现
println("分页查询")
}
}

特质中的具体字段

特质中可以定义具体字段,如果初始化了就是具体字段,如果不初始化就是抽象字段。混入该特质的类就具有了该字段,字段不是继承,而是直接加入类,成为自己的字段。

特质中的抽象字段

特质中未被初始化的字段在具体的子类中必须被重写。

8.6 特质构造顺序

特质也是有构造器的,构造器中的内容由“字段的初始化”和一些其他语句构成。具体实现请参考“特质叠加”。

第一种特质构造顺序(声明类的同时混入特质)

  1. 调用当前类的超类构造器
  2. 第一个特质的父特质构造器
  3. 第一个特质构造器
  4. 第二个特质构造器的父特质构造器, 如果已经执行过,就不再执行
  5. 第二个特质构造器
  6. …重复4,5的步骤(如果有第3个,第4个特质)
  7. 当前类构造器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package traitPackage

object T5 {

def main(args: Array[String]): Unit = {
val ff1 = new FF()
println(ff1)
val ff2 = new KK()
println(ff2)
}
}

trait AA {
println("A...")
}

trait BB extends AA {
println("B....")
}

trait CC extends BB {
println("C....")
}

trait DD extends BB {
println("D....")
}

class EE {
println("E...")
}

class FF extends EE with CC with DD {
println("F....")
}

class KK extends EE {
println("K....")
}

输出:

1
2
3
4
5
6
7
8
9
10
E...
A...
B....
C....
D....
F....
traitPackage.FF@1b40d5f0
E...
K....
traitPackage.KK@ea4a92b

第2种特质构造顺序(在构建对象时,动态混入特质)

  1. 调用当前类的超类构造器
  2. 当前类构造器
  3. 第一个特质构造器的父特质构造器
  4. 第一个特质构造器.
  5. 第二个特质构造器的父特质构造器, 如果已经执行过,就不再执行
  6. 第二个特质构造器
  7. …重复5,6的步骤(如果有第3个,第4个特质)
  8. 当前类构造器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package traitPackage

object T6 {

def main(args: Array[String]): Unit = {
val f=new MB with F3 with F2

}
}

trait F1{
println("F1...")
}

trait F2 extends F1{
println("F2...")
}

trait F3 extends F1{
println("F3...")
}

class MA{
println("MA...")
}

class MB extends MA{
println("MB....")
}

输出

1
2
3
4
5
MA...
MB....
F1...
F3...
F2...

分析两种方式对构造顺序的影响
第1种方式实际是构建类对象, 在混入特质时,该对象还没有创建。
第2种方式实际是构造匿名子类,可以理解成在混入特质时,对象已经创建了

8.7 扩展类的特质

特质可以继承类,以用来拓展该类的一些功能

1
2
3
4
5
trait LoggedException extends Exception{
def log(): Unit ={
println(getMessage()) // 方法来自于Exception类
}
}

所有混入该特质的类,会自动成为那个特质所继承的超类的子类

1
2
3
4
5
6
7
8
9
10
11
12
13
trait LoggedException extends Exception {
def log(): Unit = {
println(getMessage()) // 方法来自于Exception类
}
}

//因为LoggedException继承了Exception
//而UnhappyException继承了LoggedException
//所以UnhappyException是Excpetion的子类
class UnhappyException extends LoggedException {
// 已经是Exception的子类了,所以可以重写方法
override def getMessage = "错误消息!"
}

如果混入该特质的类,已经继承了另一个类(A类),则要求A类是特质超类的子类,否则就会出现了多继承现象,发生错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package  traitPackage

object T7{

def main(args: Array[String]): Unit = {
}
}

trait LoggedException extends Exception {
def log(): Unit = {
println(getMessage()) // 方法来自于Exception类
}
}

class UnhappyException extends LoggedException {
// 已经是Exception的子类了,所以可以重写方法
override def getMessage = "错误消息!"
}

//正确:因为IndexOutOfBoundsException是LoggedException特质的超累Exception的子类
class UnhappyException2 extends IndexOutOfBoundsException with LoggedException{
override def getMessage = "错误消息!"
}

class CCC{
}

//错误:因为CCC不是LoggedException特质的超累Exception的子类
class UnhappyException3 extends CCC with LoggedException{

}

这个错误信息如下:

1
2
3
4
illegal inheritance; superclass CCC
is not a subclass of the superclass Exception
of the mixin trait LoggedException
class UnhappyException3 extends CCC with LoggedException{

当然,如果该特质没有超类,则就没有这个限制了。即如果LoggedException没有超类,则对CCC没有任何限制了

1
2
3
4
5
6
7
8
9
10
11
12
class CCC{
}

trait AAA{

}
trait BBB{

}
class UnhappyException4 extends CCC with AAA with BBB{

}

对于上面这个例子来说,要么CCC、AAA、BBB有相同的父类,要么没有父类

8.8 自身类型(selftype)

自身类型:主要是为了解决特质的循环依赖问题,同时可以确保特质在不扩展某个类的情况下,依然可以做到限制混入该特质的类的类型。

所谓的循环依赖是指:两个类互相依赖,互相是对象父类或子类

1
2
3
4
5
6
7
8
9
10
11
12
//Logger就是自身类型特质(selftype),当这里做了自身类型后,那么就
//相当于 trait Logger extends Exception,要求混入该特质的类也是Exception子类
trait Logger {
// 明确告诉编译器,我就是Exception,如果没有这句话,下面的getMessage不能调用
this: Exception =>
def log(): Unit ={
// 既然我就是Exception, 那么就可以调用其中的方法
println(getMessage)
}
}
class Console extends Logger {} //对吗? 不对,Logger这个自身类型特质(selftype),要求混入该特质的类也是Exception子类
class Console extends Exception with Logger//对吗?正确,先继承了Eception,再混入该特质

2.9 嵌套类

9.1 嵌套类的使用

在Scala中,你几乎可以在任何语法结构中内嵌任何语法结构。如在类中可以再定义一个类,这样的类是嵌套类,其他语法结构也是一样。嵌套类类似于Java中的内部类。

Java内部类的分类

从定义在外部类的成员位置上来看,

  1. 成员内部类(没用static修饰)
  2. 和静态内部类(使用static修饰),
    定义在外部类局部位置上(比如方法内)来看:
  3. 分为局部内部类(有类名)
  4. 匿名内部类(没有类名)

这里我们就回顾一下成员内部类和静态内部类。

img

Scala嵌套类的使用1

请编写程序,定义Scala 的成员内部类和静态内部类,并创建相应的对象实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ScalaOuterClass {
class ScalaInnerClass { //成员内部类
}
}
object ScalaOuterClass { //伴生对象
class ScalaStaticInnerClass { //静态内部类
}
}
val outer1 : ScalaOuterClass = new ScalaOuterClass();
val outer2 : ScalaOuterClass = new ScalaOuterClass();
// Scala创建内部类的方式和Java不一样,将new关键字放置在前,使用 对象.内部类 的方式创建
val inner1 = new outer1.ScalaInnerClass()
val inner2 = new outer2.ScalaInnerClass()
//创建静态内部类对象
val staticInner = new
ScalaOuterClass.ScalaStaticInnerClass()
println(staticInner)

Scala嵌套类的使用2

请编写程序,在内部类中访问外部类的属性

方式1
内部类如果想要访问外部类的属性,可以通过外部类对象访问。即:访问方式:外部类名.this.属性名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ScalaOuterClass {
var name: String = "scott"
private var sal: Double = 1.2

class ScalaInnerClass { //成员内部类
def info() = {
// 访问方式:外部类名.this.属性名
// 怎么理解 ScalaOuterClass.this 就相当于是 ScalaOuterClass 这个外部类的一个实例,
// 然后通过 ScalaOuterClass.this 实例对象去访问 name 属性
// 只是这种写法比较特别,学习java的同学可能更容易理解 ScalaOuterClass.class 的写法.
println("name = " + ScalaOuterClass.this.name
+ " age =" + ScalaOuterClass.this.sal)
}
}

}

object ScalaOuterClass { //伴生对象
class ScalaStaticInnerClass { //静态内部类
}

}

方式2

内部类如果想要访问外部类的属性,也可以通过外部类别名访问(推荐)。即:访问方式:外部类名别名.属性名 【外部类名.this 等价 外部类名别名】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ScalaOuterClass {
myOuter => //这样写,你可以理解成这样写,myOuter就是代表外部类的一个对象.
class ScalaInnerClass { //成员内部类
def info() = {
println("name = " + ScalaOuterClass.this.name
+ " age =" + ScalaOuterClass.this.sal)
println("-----------------------------------")
println("name = " + myOuter.name
+ " age =" + myOuter.sal)
}
}
// 当给外部指定别名时,需要将外部类的属性放到别名后.
var name: String = "scott"
private var sal: Double = 1.2
}

9.2 类型投影

先看一段代码,引出类型投影

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package traitPackage

object T10 {

def main(args: Array[String]): Unit = {
val outer1: ScalaOuterClass3 = new ScalaOuterClass3();
val outer2: ScalaOuterClass3 = new ScalaOuterClass3();
val inner1 = new outer1.ScalaInnerClass3()
val inner2 = new outer2.ScalaInnerClass3()
inner1.test(inner1) // ok, 因为 需要outer1.ScalaInner
inner2.test(inner2) // ok, 因为 需要outer2.ScalaInner
//在默认情况下,scala的内部类的实例和创建该内部类实例的外部对象关联
inner1.test(inner2) // error, outer1需要outer1的那个内部类
}
}


class ScalaOuterClass3 {
myOuter =>

class ScalaInnerClass3 { //成员内部类
def test(ic: ScalaInnerClass3): Unit = {
System.out.println(ic)
}
}

}
  1. Java中的内部类从属于外部类,因此在java中 inner1.test(inner2) 就可以,因为是按类型来匹配的。
  2. Scala中内部类从属于外部类的对象,所以外部类的对象不一样,创建出来的内部类也不一样,无法互换使用
  3. 比如你使用ideal 看一下在inner1.test()的形参上,它提示的类型是 outer1.ScalaInnerClass

解决方式-使用类型投影

类型投影是指:在方法声明上,如果使用 外部类#内部类 的方式,表示忽略内部类的对象关系,等同于Java中内部类的语法操作,我们将这种方式称之为 类型投影(即:忽略对象的创建方式,只考虑类型)。

把上面的代码改为如下所示的代码,inner1.test(inner2)就不报错了

1
2
3
4
5
6
7
8
9
class ScalaOuterClass3 {
myOuter =>

class ScalaInnerClass3 { //成员内部类
def test(ic: ScalaOuterClass3#ScalaInnerClass3): Unit = {
System.out.println(ic)
}
}
}