Scala学习总结(二)
Scala学习总结
五、面向对象编程
1. 类和对象
Scala 语言来自于 Java ,所以天生就是面向对象的语言 ,而且 Scala 是纯粹的面向对象的语言 ,即在 Scala 中 ,一 切皆为对象。
1.1. 定义类
基本语法:
[修饰符] class 类名 { 类体 } |
注意:
- scala 语法中 ,类并不声明为 public ,所有这些类都具有公有可见性(即默认就是 public)。
- 一个 Scala 源文件可以包含多个类, 每个类默认都是 public
1.2. 定义属性
- 属性的定义语法同变量 ,示例: [访问修饰符] var 属性名称 [:类型] = 属性值
- 属性的定义类型可以为任意类型 ,包含值类型或引用类型
- Scala 中声明一个属性,必须显示的初始化 ,然后根据初始化数据的类型自动推断 ,属性类型可以省略
- 如果赋值为 null,则一定要加类型 ,因为不加类型, 那么该属性的类型就是 Null 类型
- 如果在定义属性时 ,暂时不赋值 ,也可以使用符号_(下划线) ,让系统分配默认值, 这时要求属性必须给定数 据类型
1.3. 创建对象
val | var 对象名 [:类型] = new 类型() |
- 如果我们不希望改变对象的引用(即: 内存地址), 应该声明为 val 性质的 ,否则声明为 var, scala 设计者推荐使 用 val ,因为一般来说 ,在程序中 ,我们只是改变对象属性的值 ,而不是改变对象的引用。
- scala 在声明对象变量时 ,可以根据创建对象的类型自动推断 ,所以类型声明可以省略 ,但当类型和后面 new 对象类型有继承关系即多态时 ,就必须写了。
2. 构造器
2.1. 构造器
构造器(constructor)又叫构造方法 ,是类的一种特殊的方法 ,它的主要作用是完成对新对象的初始化。
和 Java 一样 ,Scala 构造对象也需要调用构造方法 ,并且可以有任意多个构造方法 (即scala 中构造器也支 持重载) 。Scala 类的构造器包括: 主构造器 (一个) 和 辅助构造器(多个)
语法:
class 类名(形参列表) { // 主构造器 // 类体 def this(形参列表) { // 辅助构造器 } def this(形参列表) { //辅助构造器 函数的名称 this, 可以有多个 ,编译器通过不同参数 (个数或类型) 来区分。 } } |
注意:
- Scala 构造器作用是完成对新对象的初始化 ,构造器没有返回值。
- 主构造器的声明直接放置于类名之后。
- 主构造器会执行类定义中的所有语句 (把类中写的语句放入到主构造器) ,这里可以体会到 Scala 的函数式编 程和面向对象编程融合在一起 ,即:构造器也是方法 (函数) ,传递参数和使用方法和前面的函数部分内容没有区别。
- 如果主构造器无参数 ,小括号可省略 ,构建对象时调用的构造方法的小括号也可以省略。
- 辅助构造器名称为this (这个和 Java 是不一样的) ,多个辅助构造器通过不同参数列表进行区分 , 在底层就 是构造器重载 ,辅助构造器无论是直接或间接 ,最终都一定要调用主构造器 ,执行主构造器的逻辑 , 而且调 用主构造器语句一定要放在辅助构造器的第一行。
- 如果想让主构造器变成私有的 ,可以在()之前加上 private ,这样用户不能直接通过主构造器来构造对象。
- 辅助构造器的声明不能和主构造器的声明一致,会发生错误。
- Scala 类的主构造器的形参未用任何修饰符修饰 ,那么这个参数是局部变量。
- 如果参数使用 val 关键字声明 ,那么 Scala 会将参数作为类的私有的只读属性使用。
- 如果参数使用 var 关键字声明 ,那么那么 Scala 会将参数作为类的成员属性使用,并会提供属性对应的 xxx()[类似 getter]/xxx_$eq()[类似 setter]方法 ,即这时的成员属性是私有的 ,但是可读写。
2.2. 创建对象过程
- 加载类的信息(属性信息和方法信息)
- 在内存中(堆)给对象开辟空间
- 使用父类的构造器(主构造器/辅助构造器)完成父类的初始化 (多个父类)
- 使用本类的主构造器完成初始化( age = 90 name = "null")
- 使用本类的辅助构造器继续初始化(age =20,name = "小白")
- 将对象在内存中的地址赋给 p 这个引用
3. 包
3.1. Scala 包的特点概述
基本语法:
package com.xxx.xx.包名 //另一种写法 嵌套写法 package com{ package xxx{ package xx{ //类 } } } |
例:
//用嵌套风格定义包 package com{ //在外层包中定义单例对象 外层访问内层的对象需要导包 object Outer{ var out: String = "out" }
package xxx{ package scala{ //在内层包中定义单例对象 //内层可以直接访问外层的对象 object Inner{ def main(args: Array[String]): Unit = { println(Outer.out) Outer.out = "outer" println(Outer.out) } }
} } } |
Scala 包的三大作用:
- 区分相同名字的类
- 当类很多时,可以很好的管理类
- 控制访问范围
注意:
- Scala 中包名和源码所在的系统文件目录结构要可以不一致 ,但是编译后的字节码文件路径和包名会保持一致。
- 作用域原则:可以直接向上访问。即: Scala 中子包中直接访问父包中的内容, 大括号体现作用域。Java 中子包使用父包的类 ,需要 import。在子包和父包 类重名时 ,默认采用就近原则 ,如果希望指定使用某个类 ,则带上包名即可。
- 父包要访问子包的内容时 ,需要 import 对应的类等。
- 可以在同一个.scala 文件中 ,声明多个并列的 package(建议嵌套的 pakage 不要超过 3 层)。
- 包名可以相对路径也可以绝对路径 ,比如 ,访问 BeanProperty 的绝对路径是:root. scala.beans.BeanProperty ,在一般情况下:我们使用相对路径来引入包 ,只有当包名冲突时 ,使用绝对路径来处理
3.2. 包对象
包可以包含类、对象和特质(trait) ,但不能包含函数/方法或变量的定义。这是 Java 虚拟机的局限。为了弥补这一 点不足 ,scala 提供了包对象的概念来解决这个问题。
在scala中可以给每个包定义一个跟包同名的包对象 ,定义在包对象中的成员 ,作为其对应包下所有class和object的共享变量 ,可以被直接访问。
语法:
package object com{ val shareValue = "share" def shareMethod() = {} } |
例:
package com.ceshi{ //包 com.ceshi //说明 //1. 在包中直接写方法 ,或者定义变量 ,就错误==>使用包对象的技术来解决 //2. package object scala 表示创建一个包对象 scala, 他是 com.atguigu.scala 这个包对应的包对象 //3. 每一个包都可以有一个包对象 //4. 包对象的名字需要和子包一样 //5. 在包对象中可以定义变量 ,方法 //6. 在包对象中定义的变量和方法 ,就可以在对应的包中使用 //7. 在底层这个包对象会生成两个类 package.class 和 package$.class package object scala { var name = "king" def sayHi(): Unit = { println("package object scala sayHI~") } } package scala { //包 com.ceshi.scala class Person { // 表示在 com.ceshi.scala 下创建类 Person val name = "Nick" def play(message: String): Unit = { println(this.name + " " + message) } } class User { def testUser(): Unit = { println("name = " + name) sayHi() } } object Test100 { //表示在 com.ceshi.scala 创建 object Test def main(args: Array[String]): Unit = { println("name=" + name) name = "yy" sayHi() } } } } |
3.3. 包的引入
Scala 引入包也是使用 import, 基本的原理和机制和 Java 一样 ,但是 Scala 中的 import 功能更加强大 ,也更灵 活。因为 Scala 语言源自于 Java ,所以 java.lang 包中的类会自动引入到当前环境中 ,而 Scala 中的 scala 包和 Predef 包的类也会自动引入到当前环境中 ,即起其下面的类可以直接使用。如果想要把其他包中的类引入到当前 环境中 ,需要使用 import 语言
3.3.1 引入包的细节和注意事项
1. 在 Scala 中 ,import 语句可以出现在任何地方 ,并不仅限于文件顶部 ,import 语句的作用一直延伸到包含该 语句的块末尾。这种语法的好处是:在需要时在引入包 ,缩小 import 包的作用范围 ,提高效率。
class User { import scala.beans.BeanProperty @BeanProperty var name : String = "" }
class Dog { @BeanProperty var name : String = "" //可以吗? } |
2. 可以使用通配符(下划线 ,_ )导入包下所有的类
3. 如果不想要某个包中全部的类 ,而是其中的几个类 ,可以采用选取器(大括号)
def test(): Unit = { //可以使用选择器 ,选择引入包的内容 ,这里 ,我们只引入 HashMap, HashSet import scala.collection.mutable.{HashMap, HashSet} var map = new HashMap() var set = new HashSet() } |
4. 如果引入的多个包中含有相同的类 ,那么可以将不需要的类进行重命名进行区分 ,这个就是重命名
def test2(): Unit = { //下面的含义是 将 java.util.HashMap 重命名为 JavaHashMap import java.util.{ HashMap=>JavaHashMap, List} import scala.collection.mutable._ var map = new HashMap() // 此时的 HashMap 指向的是 scala 中的 HashMap var map1 = new JavaHashMap(); // 此时使用的 java 中 hashMap 的别名 } |
5. 如果某个冲突的类根本就不会用到 ,那么这个类可以直接隐藏掉
import java.util.{ HashMap=>_, _} // 含义为 引入 java.util 包的所有类 ,但是忽略 HahsMap 类 . var map = new HashMap() // 此时的 HashMap 指向的是 scala 中的 HashMap, 而且 idea 工具 ,的提示也不会显示 java.util 的 HashMaple |
4. 面向对象编程三大特征
面向对象编程有三大特征:封装、继承和多态。
4.1. 封装
封装(encapsulation)就是把抽象出的数据和对数据的操作封装在一起,数据被保护在内部 ,程序的其它部分只有通 过被授权的操作(成员方法) ,才能对数据进行操作。
4.1.1 封装的好处
- 隐藏实现细节
- 提可以对数据进行验证 ,保证安全合理
- 同时可以加入业务逻辑
4.1.2 封装的步骤
- 将属性进行私有化
- 提供一个公共的 set 方法 ,用于对属性判断并赋值
- 提供一个公共的 get 方法 ,用于获取属性的值
4.1.3 scala 封装的注意事项
- Scala 中为了简化代码的开发 ,当声明属性 var 时 ,本身就自动提供了对应 setter/getter 方法 ,如果属性声 明为 private 的 ,那么自动生成的 setter/getter 方法也是 private 的 ,如果属性省略访问权限修饰符 ,那么自 动生成的 setter/getter 方法是 public 的。
- 如果只是对一个属性进行简单的 set 和 get ,只要声明一下该属性(属性使用默认访问修饰符) 不用写专门的 get set ,默认会创建 ,访问时 ,直接对象.变量。这样也是为了保持访问一致性。
- 从形式上看 dog.food直接访问属性,其实底层仍然是访问的方法。
- 进行反射时 ,也支持对属性的直接反射。
4.2. 继承
子类继承父类的属性和方法
语法:
class 子类名 extends 父类名 { 类体 } |
4.2.1 scala继承的特点
- 子类继承了所有的属性 ,只是私有的属性不能直接访问 ,需要通过公共的方法去访问。
- scala 明确规定 ,重写一个非抽象方法需要用 override 修饰符 ,调用超类的方法使用 super 关键字。
要测试某个对象是否属于某个给定的类 ,可以用 isInstanceOf 方法。用 asInstanceOf 方法将引用转换为子类的引 用。classOf 获取对象的类名。类型检查和转换的最大价值在于:可以判断传入对象的类型 ,然后转成对应的子类 对象 ,进行相关操作。
- classOf[String]就如同 Java 的 String.class 。
- obj.isInstanceOf[T]就如同 Java 的 obj instanceof T 判断 obj 是不是 T 类型。
- obj.asInstanceOf[T]就如同 Java 的(T)obj 将 obj 强转成 T 类型。
4.3. 多态
- java中属性静态绑定 ,根据变量的引用类型确定 ,方法是动态绑定。
- 但scala中属性和方法都是动态绑定。就属性而言 ,其实也不应该在子类和父类中定义同名字段。 同java一样 ,所有实例方法都是虚方法 ,都可以被子类覆写。
- override关键字覆写。
- scala中属性 (字段) 也可以被重写 ,加override关键字。
4.4. 抽象类
定义:
在 Scala 中 ,通过 abstract 关键字标记不能被实例化的类。方法不用标记 abstract ,只要省掉方法体即可。抽象类可以拥有抽象字段 ,抽象字段/属性就是没有初始值的字段
注意事项:
- 抽象类不能被实例
- 抽象类不一定要包含 abstract 方法。也就是说,抽象类可以没有 abstract 方法
- 一旦类包含了抽象方法或者抽象属性,则这个类必须声明为 abstract
- 抽象方法不能有主体 ,不允许使用 abstract 修饰。
- 如果一个类继承了抽象类 ,则它必须实现抽象类的所有抽象方法和抽象属性 ,除非它自己也声明为 abstract 类。
- 抽象方法和抽象属性不能使用 private、final 来修饰 ,因为这些关键字都是和重写/实现相违背的。
- 抽象类中可以有实现的方法. 8) 子类重写抽象方法不需要 override ,写上也不会错.
4.5. 匿名子类
和 Java 一样 ,可以通过包含带有定义或重写的代码块的方式创建一个匿名的子类。
val/var p: baseClass = new baseClass { override ... } |
4.6. 伴生对象 (Companion Object)
4.6.1 定义
Scala 语言是完全面向对象(万物皆对象)的语言 ,所以并没有静态的操作(即在 Scala 中没有静态的概念)。
但是为了 能够和 Java 语言交互(因为 Java 中有静态概念) ,就产生了一种特殊的对象来模拟类对象 ,我们称之为类的伴生对象。这个类的所有静态内容都可以放置在它的伴生对象中声明和调用。
4.6.2 注意事项
- Scala 中伴生对象采用 object 关键字声明 ,伴生对象中声明的全是 "静态"内容 ,可以通过伴生对象名称直接调用。
- 伴生对象对应的类称之为伴生类 ,伴生对象的名称应该和伴生类名一致。
- 伴生对象中的属性和方法都可以通过伴生对象名直接调用访问
- 从语法角度来讲 ,所谓的伴生对象其实就是类的静态方法和静态变量的集合
- 从技术角度来讲 ,scala 还是没有生成静态的内容 ,只不过是将伴生对象生成了一个新的类 ,实现属性和方法的调用。
- 从底层原理看 ,伴生对象实现静态特性是依赖于 public staticfinal MODULE$ 实现的。
- 伴生对象的声明应该和伴生类的声明在同一个源码文件中(如果不在同一个文件中会运行错误!) ,但是如果没有伴生类 ,也就没有所谓的伴生对象了 ,所以放在哪里就无所谓了。
- 如果 classA 独立存在 ,那么 A 就是一个类 , 如果 objectA 独立存在 ,那么 A 就是一个"静态"性质的对象[即 类对象], 在 objectA 中声明的属性和方法可以通过 A.属性 和 A.方法 来实现调用
例:
object ChildGame { def main(args: Array[String]): Unit = { val child1 = new Child("蜘蛛精") val child2 = new Child("老鼠精") val child3 = new Child("黄鼠狼精") Child.joinGame(child1) //Child$.MODULE$ Child.joinGame(child2) Child.joinGame(child3) Child.showInfo() } } //小孩类 class Child(cName:String) { var name :String = cName } //伴生对象 object Child { var totalNum = 0 def joinGame(child:Child): Unit = { println(child.name + " 加入游戏 ..") totalNum += 1 } def showInfo(): Unit = { printf("当前有%d 个小孩玩游戏\n" , totalNum) } } |
4.6.3 伴生对象-apply 方法
- 在伴生对象中定义 apply 方法 ,可以实现:类名(参数) 方式来创建对象实例。
- 伴生对象实现apply方法后调用时可以省略 .apply ,直接使用 className(args) 。库中很多这种用法创建实例 ,是一个语法糖。
4.7. Trait (特征/特质)
- 替代java接口的概念。但比接口更为灵活 ,一种实现多继承的手段。
- 多个类具有相同的特征时 ,就可以将这个特征提取出来 ,用继承的方式来复用。
- 用关键字`trait`声明:
trait traitName { ... } |
- 引入/混入(mixin)特征:
- 有父类 class extends baseClass with trait1 with trait2 ... {}
- 没有父类 class extends trait1 with trait2 ... {}
- 其中可以定义抽象和非抽象的属性和方法。
- 匿名子类也可以引入特征。
- 特征和基类或者多个特征中重名的属性或方法需要在子类中覆写以解决冲突 ,最后因为动态绑定 ,所有使用的地方都是子类的字段或方法。属性的话需要类型一致 ,不然提示不兼容。方法的话参数列表不一致会视为重载而不是冲突。
- 如果基类和特征中的属性或方法一个是抽象的 ,一个非抽象 ,且兼容 ,那么可以不覆写。很直观 ,就是不能冲突不能二义就行。
- 多个特征和基类定义了同名方法的 ,就需要在子类重写解决冲突。其中可以调用父类和特征的方法 ,此时 super.methodName 指代按照顺序最后一个拥有该方法定义的特征或基类。也可以用 super.methodName 指代按照顺序最后一个拥有该方法定义的特征或基类。也可以用super[baseClassOrTraitName].methodName 直接指代某个基类的方法,注意需要是直接基类,间接基类则不行。
- 也就是说基类和特征基本是同等地位。
- 例子:
class Person {
val name: String = "Person"
var age: Int = 18
def sayHi(): Unit = {
println(s"hello from : $name")
}
}
trait Young {
// abstract and non‐abstract attribute
var age: Int
val name: String = "young"
// method
def play(): Unit = {
println(s"young people $name is playing")
}
def dating(): Unit
}
trait Knowledge {
var amount: Int = 0
def increase(): Unit = {
amount += 1
}
}
trait Talent {
def increase(): Unit = {
println("increase talent")
}
}
class Student extends Person with Young with Knowledge with Talent{
override val name: String = "alice"
def dating(): Unit = {
println(s"Sutdent $name $age is dating")
}
def study(): Unit = println(s"Student $name is studying")
override def sayHi(): Unit = {
super.sayHi()
println(s"hello from : student $name")
}
override def increase(): Unit = {
super.increase() // call Talent.increase(), just the last
println(s"studnet $name knowledge increase: $amount")
}
}
object Trait {
def main(args: Array[String]): Unit = {
val s = new Student()
s.sayHi()
s.increase()
s.study()
s.increase()
s.play()
s.increase()
s.dating()
s.increase()
}
}
|
- 特征的继承: trait childTrait extends baseTrait
- 特征的菱形继承解决方式:转换为线性的继承链条 ,在前面的成为基类 ,后面的成为子类。 例子:
trait Ball {
def describe(): String = "ball"
}
trait ColorBall extends Ball {
var color: String = "red"
override def describe(): String = color + "_" + super.describe()
}
trait CategoryBall extends Ball {
var category: String = "foot"
override def describe(): String = category + "_" + super.describe()
}
// equals to MyFootBall ‐> ColorBall ‐> CategoryBall ‐> Ball
class MyFootBall extends CategoryBall with ColorBall {
override def describe(): String = super.describe()
}
object TraitInheritance {
def main(args: Array[String]): Unit = {
val b = new MyFootBall()
println(b.describe()) // red_foot_ball
}
}
|
- 其实特征的多继承和C++的多继承已经很像了 ,只是名称冲突的解决方式不一样 ,菱形继承的解决方式也不一 样 ,而且不能访问间接基类。
- scala单继承多实现 ,实现体现在特征上。基类主要用于一个对象比较核心比较本质的部分上。
- 继承特征与类的区别:特征构造时不能给参数。其他都是同样的 ,都可以实现多态。
4.8. 自身类型 (self type)
自身类型(self-type):主要是为了解决特质的循环依赖问题 ,同时可以确保特质在不扩展某个类的情况下 ,依然可以做到限制混入该特质的类的类型。
- 可实现依赖注入的功能。
- 一个类或者特征指定了自身类型的话,它的对象和子类对象就会拥有这个自身类型中的所有属性和方法。
- 是将一个类或者特征插入到另一个类或者特征中,属性和方法都就像直接复制插入过来一样,能直接使用。但不是继承,不能用多态。
- 语法,在类或特征中: _: SelfType => ,其中 _ 的位置是别名定义,也可以是其他, _ 指代 this 。插入后就可以用 this.xxx 来访问自身类型中的属性和方法了。
- 注入进来的目的是让你能够使用,可见,提前使用应该拥有的属性和方法。最终只要自身类型和注入目标类型同时被继承就能够得到定义了。
- 例子:
class User(val name: String, val password: String)
// user database access object
trait UserDao {
// dependency injection from external
_: User => // self type
// simulate insert data to databse
def insert(): Unit = {
println(s"insert into db: $name $password")
}
}
// register user
class RegisterUser(name: String, password: String) extends User(name, password) with UserDao
object SelfType {
def main(args: Array[String]): Unit = {
val u = new RegisterUser("catholly", "nephren")
u.insert()
}
}
|
4.9. 枚举类
- 继承`Enumeration`。
- 用`Value`类型定义枚举值。
object WorkDay extends Enumeration { val MONDAY = Value(1, "Monday") val TUESDAY = Value(2, "Tuesday") val THURSDAy = Value(3, "Thrusday") }
object EnumClass { def main(args: Array[String]): Unit = { println(WorkDay.MONDAY) println(WorkDay.TUESDAY) } } |
4.10. 应用类
继承 `App` ,包装了 `main` 方法 ,就不需要显式定义 `main` 方法了 ,可以直接执行。
object TestApp extends App { println("hello,world!") } |
5. 隐式转换、 隐式函数 ,隐式值
5.1. 隐式转换
5.1.1 基本介绍
隐式转换函数是以 `implicit` 关键字声明的带有单个参数的函数。这种函数将会自动应用 ,将值从一种类型转换为另一种类型。
例:
object ImplicitDemo { def main(args: Array[String]): Unit = { //使用隐式函数 implicit def f1(d:Double): Int = { //底层会生成一个方法 f1$1 d.toInt } val n1: Int = 3.4 //=> val n1: Int = f1$1(3.4) val n2: Int = 5.6 val n3: Int = 1.2 println(n1 + " " + n2 + " " + n3) } } |
5.1.2 隐式转换的注意事项和细节
- 隐式转换函数的函数名可以是任意的 ,隐式转换与函数名称无关 ,只与函数签名 (函数参数类型和返回值类型) 有关。
- 隐式函数可以有多个(即:隐式函数列表) ,但是需要保证在当前环境下 ,只有一个隐式函数能被识别 代码:
//在当前环境中 不能存在满足条件的多个隐式函数 implicit def a(d: Double) = d.toInt implicit def b(d: Double) = d.toInt val i: Int = 3.5 // (X) 在转换时识别出有两个方法可以被使用 ,不确定调用哪一个 ,所以出错 println(i) |
5.1.3 隐式转换丰富类库功能
在当前程序中 ,如果想要给一个类增加功能是非常简单的 ,但是在实际项目中 ,如果想要增加新的功能就会需要改 变源代码 ,这是很难接受的。而且违背了软件开发的 OCP 开发原则 (开闭原则 open close priceple)。
在这种情况下 ,可以通过隐式转换函数给类动态添加功能。
object ImplicitDemo02 {
def main(args: Array[String]): Unit = {
//隐式函数
implicit def addDelete(mySQL: MySQL): DB = {
new DB
}
//创建 MySQL 对象
val mySQL = new MySQL
mySQL.delete() //底层(编译器做的转换)是这样的: addDelete$1(mySQL).delete()
}
}
//类‐insert
class MySQL {
def insert(): Unit = {
println("insert")
}
}
//类 DB‐delete
class DB {
def delete(): Unit = {
println("delete...")
}
}
|
5.2. 隐式值
隐式值也叫隐式变量 ,将某个形参变量标记为 implicit ,所以编译器会在方法省略隐式参数的情况下去搜索作用域内的隐式值作为缺省参数。
object ImplicitValDemo01 { def main(args: Array[String]): Unit = { //这里的 str1 就是隐式值 [=》跑龙套] implicit val str1: String = "jack" //说明 //1.implicit name: String 就是一个隐式参数 //2.当调用 hello 的时候 ,没有传入实参 ,则编译器会自动的将隐式值关联到 name 上 def hello(implicit name: String): Unit = { println(name + " hello") } hello //用到隐式值 底层 hello$1(str1) } } |
5.3. 隐式类
5.3.1 基本介绍
在 scala2.10 后提供了隐式类 ,可以使用 implicit 声明类 ,隐式类的非常强大 ,同样可以扩展类的功能 ,比前面使用隐式转换丰富类库功能更加的方便 ,在集合中隐式类会发挥重要的作用。
5.3.2 隐式类的特点
- 其所带的构造参数有且只能有一个
- 隐式类必须被定义在“类”或“伴生对象”或“包对象”里 ,即隐式类不能是 顶级的(top-level objects)
- 隐式类不能是 case class (case class 在后续介绍 样例类)
- 作用域内不能有与之相同名称的标识符
案例:
object ImplicitClass {
def main(args: Array[String]): Unit = {
//一个隐式类, 可以返回一个隐式类的实例,然后就可以调用隐式类的方法
implicit class DB1(val m: MySQL1) {
def addSuffix(): String = { //方法
m + " scala"
}
def sayHi(): Unit = {
println("sayHi..")
}
def sayHello(): Unit = {
println("hello")
m.sayOk()
}
}
val mySQL = new MySQL1
mySQL.sayOk() //
// 1.底层 DB1$1(mySQL).addSuffix()
// 2. DB1$1(mySQL) 返回的是 :ImplicitClass$DB1$2 实例
// 3. 通过返回的 ImplicitClass$DB1$2 实例.addSuffix()
println(mySQL.addSuffix()) //DB1$1(mySQL).addSuffix()
mySQL.sayHi()
mySQL.sayHello()
}
}
class MySQL1 { //普通类
def sayOk(): Unit = {
println("sayOk")
}
}
|
5.4. 隐式的转换时机
- 当方法中的参数的类型与目标类型不一致时
- 当对象调用所在类中不存在的方法或成员时 ,编译器会自动将对象进行隐式转换 (根据类型)
5.5. 隐式解析机制
- 首先会在当前代码作用域下查找隐式实体 (隐式方法、隐式类、隐式对象) 。(一般是这种情况)
- 如果第一条规则查找隐式实体失败 ,会继续在隐式参数的类型的作用域里查找。类型的作用域是指与该类型相关联的全部伴生模块 ,一个隐式实体的类型 T 它的查找范围如下(第二种情况范围广且复杂在使用时 ,应当尽量避免出现):
- 如果 T 被定义为 T with A with B with C,那么 A,B,C 都是 T 的部分 ,在 T 的隐式解析过程中 ,它们的伴生对象都会被搜索。
- 如果 T 是参数化类型 ,那么类型参数和与类型参数相关联的部分都算作 T 的部分 ,比如 List[String]的隐式搜索会搜索 List 的伴生对象和 String 的伴生对象。
- 如果 T 是一个单例类型 p.T ,即 T 是属于某个 p 对象内 ,那么这个 p 对象也会被搜索。
- 如果 T 是个类型注入 S#T ,那么 S 和 T 都会被搜索。
5.6. 隐式转换的前提
在进行隐式转换时 ,需要遵守两个基本的前提:
- 不能存在二义性
- 隐式操作不能嵌套使用
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律