Scala 自定义实体
Class
- Scala是纯面向对象语言,所有变量都是对象,所有操作都是 方法调用
class
定义类,包括类名、主构造参数,其中成员可能包括- 普通类可用
new
创建类实例
- 普通类可用
成员说明
- 常/变量:声明时必须初始化,否则需定义为抽象类,
相应成员也被称为抽象成员常/变量
- 可以使用占位符
_
初始化AnyVal
子类:0
AnyRef
子类:null
- 可以使用占位符
- 抽象类型:
type
声明 - 方法、函数
- 内部类、单例对象:内部类、单例对象绑定至外部对象
- 内部类是路径依赖类型
- 不同类实例中的单例对象不同
- Java:仍然会为匿名类生成相应字节码文件
- Java:public类成员常/变量在编译后自动生成getter、 setter(仅变量)方法
- 即使是公有成员也会被编译为
private
- 对其访问实际上是通过setter、getter方法(可显式调用)
内部类型投影
类型投影:Outter#Inner
类型Inner
以Outter
类型作为前缀,
Outter
不同实例中Inner
均为此类型子类
1 | class Outter{ |
单例类型
单例类型:所有对象都对应的类型,任意对象对应单例类型不同
1 | import scla.reflect.runtime.universe.typeOf |
链式调用
1 | class Pet{ |
Constructor
1 | // 带默认参数的、私有主构造方法 |
主构造方法:类签名中参数列表、类体
其参数(类签名中)被自动注册为类成员变、常量
- 其可变性同样由
var
、val
声明 - 其访问控制类似类似类体中成员访问控制
- 但缺省继承父类可见性,否则为
private[this] val
- 参数中私有成员变、常量没有被使用,则不会被生成
case class
主构造方法中缺省为public val
- 其可变性同样由
其函数体为整个类体,创建对象时被执行
可类似普通方法
- 提供默认值来拥有可选参数
- 将主构造方法声明为私有
辅助构造方法:
this
方法- 辅助构造方法必须以先前已定义的其他辅助构造方法、或 主构造方法的调用开始
- 主构造方法体现于其中参数被自动注册为类成员
类继承
1 | // `extends`指定类继承 |
必须给出父类的构造方法(可以是辅助构造方法)
- 类似强制性C++中初始化列表
构造函数按以下规则、顺序执行
- 父类、特质构造函数
- 混入特质:多个特质按从左至右
- 当前类构造函数
可用
override
重写从父类继承的成员方法、常/变量- 重写父类抽象成员(未初始化)可省略
override
- java中方法都可以视为C++中虚函数,具有多态特性
- 重写父类抽象成员(未初始化)可省略
提前定义、懒加载
- 由于构造方法的执行顺序问题,定义于
trait
中的抽象成员 常/变量可能会在初始化化之前被使用 - 可以考虑使用提前定义、懒加载避免
1 | import java.io.PrintWriter |
访问控制
- 缺省:Scala没有
public
关键字,缺省即为publicprotected[<X>]
:在X
范围内的类、子类中可见private[<X>]
:在X
范围内类可见
X
可为包、类、单例对象等作用域- 缺省为包含当前实体的上层包、类、单例对象,即类似Java 中对应关键字
特别的
private[this]
表示对象私有- 只有对象本身能够访问该成员
类、伴生对象中也不能访问
1
2
3
4
5
6
7
8class Person{
private[this] age = 12
def visit() = {
val p = new Person
// 报错,`age`不是`Person`成员
println(p.age)
}
}
特殊类
abstract class
抽象类:不能被实例化的类
- 抽象类中可存在抽象成员常/变量、方法,需在子类中被具体化
1 | // 定义抽象类 |
case class
样例类
和普通类相比具有以下默认实现
apply
:工厂方法负责对象创建,无需new
实例化样例类unapply
:解构方法负责对象解构,方便模式匹配equals
:案例类按值比较(而不是按引用比较)toString
hashCode
copy
:创建案例类实例的浅拷贝,可以指定部分构造参数 修改部分值
类主构造函数参数缺省为
val
,即参数缺省不可变公开用途
- 方便模式匹配
- 样例类一般实例化为不可变对象,不推荐实例化为可变
1 | case class Message(sender: String, recipient: String, body: String) |
Value Class
Value Class:通过继承AnyVal
以避免运行时对象分配机制
1 | class Wrapper(val underlying: Int) extends AnyVal |
特点
- 参数:仅有被用作运行时底层表示的公有
val
参数 - 成员:可包含
def
自定义方法,但不能定义额外val
、var
、内嵌trait
、class
、object
- 继承:只能继承universal trait,自身不能再被继承
- 编译期类型为自定义类型,运行时类型为
val
参数类型 - 调用universal trait中方法会带来对象分配开销
- 参数:仅有被用作运行时底层表示的公有
用途
和隐式类联合获得免分配扩展方法
1
2
3
4// 标准库中`RichInt`定义
implicit class RichInt(val self Int) extends AnyVal{
def toHexString: String = java.lang.Integer.toHexString(self)
}不增加运行时开销同时保证数据类型安全
1
2
3class Meter(val value: Double) extends AnyVal{
def +(m: Meter): Meter = new Meter(value + m.value)
}
- JVM不支持value class,Scala有时需实例化value class
- value class作为其他类型使用
- value class被赋值给数组
- 执行运行时类型测试,如:模式匹配
Object
单例对象:有且只有一个实例的特殊类,其中方法可以直接访问, 无需实例化
单例对象由自身定义,可以视为其自身类的单例
- 对象定义在顶层(没有包含在其他类中时),单例对象只有
一个实例
- 全局唯一
- 具有稳定路径,可以被
import
语句导入
- 对象定义在类、方法中时,单例对象表现类似惰性变量
- 不同实例中单例对象不同,依赖于包装其的实例
- 单例对象和普通类成员同样是路径相关的
- 对象定义在顶层(没有包含在其他类中时),单例对象只有
一个实例
单例对象是延迟创建的,在第一次使用时被创建
object
定义- 可以通过引用其名称访问对象
- 单例对象被编译为单例模式、静态类成员实现
Companion Object/Class
- 伴生对象:和某个类共享名称的单例对象
- 伴生类:和某个单例对象共享名称的类
- 伴生类和伴生对象之间可以互相访问私有成员
- 伴生类、伴生对象必须定义在同一个源文件中
- 用途:在伴生对象中定义在伴生类中不依赖于实例化对象而存在
的成员变量、方法
- 工厂方法
- 公共变量
- Java中
static
成员对应于伴生对象的普通成员- 静态转发:在Java中调用伴生对象时,其中成员被定义为伴生类 中
static
成员(当没有定义伴生类时)
apply
apply
方法:像构造器接受参数、创建实例对象
1 | import scala.util.Random |
unapply
、unapplySeq
参见cs_java/scala/entity_components
应用程序对象
应用程序对象:实现有main(args: Array[String])
方法的对象
main(args: Array[String])
方法必须定义在单例对象中实际中可以通过
extends App
方式更简洁定义应用程序对象 ,trait App
中同样是使用main
函数作为程序入口1
2
3
4
5
6
7
8
9
10trait App{
def main(args: Array[String]) = {
this._args = args
for(proc <- initCode) proc()
if (util.Propertie.propIsSet("scala.time")){
val total = currentTime - executionStart
Console.println("[total " + total + "ms")
}
}
}
- 包中可以包含多个应用程序对象,但可指定主类以确定包程序 入口
case object
样例对象:基本同case class
- 对于不需要定义成员的域的
case class
子类,可以定义为case object
以提升程序速度case object
可以直接使用,case class
需先创建对象case object
只生成一个字节码文件,case class
生成 两个case object
中不会自动生成apply
、unapply
方法
用途示例
创建功能性方法
1 | package logging |
Trait
特质:包含某些字段、方法的类型
特点
- 特质的成员方法、常/变量可以是具体、或抽象
- 特质不能被实例化,因而也没有参数
- 特质可以
extends
自类
用途:在类之间共享程序接口、字段,尤其是作为泛型类型和 抽象方法
extend
继承特质with
混入可以组合多个特质override
覆盖/实现特质默认实现
子类型:需要特质的地方都可以使用特质的子类型替换
- Java:
trait
被编译为interface
,包含具体方法、常/变量 还会生成相应抽象类
Mixin
混入:特质被用于组合类
类只能有一个父类,但是可以有多个混入
- 混入和父类可能有相同父类
- 通过Mixin
trait
组合类实现多继承
多继承导致歧义时,使用最优深度优先确定相应方法
复合类型
复合类型:多个类型的交集,指明对象类型是某几个类型的子类型
1 | <traitA> with <traitB> with ... {refinement} |
sealed trait
/sealed class
密封特质/密封类:子类确定的特质、类
1 | sealed trait ST |
- 密封类、特质只能在当前文件被继承
- 适合模式匹配中使用,编译器知晓所有模式,可提示缺少模式
自类型
自类型:声明特质必须混入其他特质,尽管特质没有直接扩展 其他特质
- 特质可以直接使用已声明自类型的特质中成员
细分大类特质
1 | trait User{ |
定义this
别名
1 | class OuterClass{ |
Universal Trait
Universal Trait:继承自Any
、只有def
成员、不作任何
初始化的trait
- 继承自universal trait的value class同时继承该trait方法, 但是调用其会带来对象分配开销
泛型
泛型类、泛型trait
- 泛型类使用方括号
[]
接受类型参数
- 泛型类使用方括号
泛型方法:按类型、值进行参数化,语法和泛型类类似
- 类型参数在方括号
[]
中、值参数在圆括号()
中 - 调用方法时,不总是需要显式提供类型参数,编译器 通常可以根据上下文、值参数类型推断
- 类型参数在方括号
泛型类型的父子类型关系不可传导
- 可通过类型参数注释机制控制
- 或使用类型通配符(存在类型)“构建统一父类”
存在类型/类型通配符
存在类型:<type>[T, U,...] forSome {type T; type U;...}
,可以视为所有<type>[]
类型的父类
1 | // 可以使用通配符`_`简化语法 |
- 省略给方法添加泛型参数
类型边界约束
T <: A
/T >: B
:类型T
应该是A
的子类/B
的父类- 描述is a关系
T <% S
:T
是S
的子类型、或能经过隐式转换为S
子类型- 描述can be seen as关系
T : E
:作用域中存在类型E[T]
的隐式值
型变
型变:复杂类型的子类型关系与其组件类型的子类型关系的相关性
- 型变允许在复杂类型中建立直观连接,利用重用类抽象
1 | abstract class Animal{ |
Covariant
协变:+A
使得泛型类型参数A
成为协变
类型
List[+A]
中A
协变意味着:若A
是B
的子类型,则List[A]
是List[B]
的子类型使得可以使用泛型创建有效、直观的子类型关系
1 | def ConvarienceTest extends App{ |
Contravariant
逆变:-A
使得泛型类型参数A
成为逆变
- 同协变相反:若
A
是B
的子类型,则Writer[B]
是Writer[A]
的子类型
1 | abstract class Printer[-A] { |
协变泛型
- 子类是父类的特化、父类是子类的抽象,子类实例总可以 替代父类实例
- 协变泛型作为成员
- 适合位于表示实体的类型中,方便泛化、组织成员
- 作为[部分]输出[成员]
- 注意:可变的协变泛型变量不安全
逆变泛型
- 子类方法是父类方法特化、父类方法是子类方法抽象,子类 方法总可以替代父类方法
- 逆变泛型提供行为、特征
- 适合位于表示行为的类型中,方便统一行为
- 仅作为输入
协变泛型、逆变泛型互补
- 对包含协变泛型的某类型的某方法,总可以将该方法扩展 为包含相应逆变泛型的类
1 | trait Function[-T, +R] |
Invariant
不变:默认情况下,Scala中泛型类是不变的
- 可变的协变泛型变量不安全
1 | class Container[A](value: A){ |
Type Erasure
类型擦除:编译后删除所有泛型的类型信息
- 导致运行时无法区分泛型的具体扩展,均视为相同类型
类型擦除无法避免,但可以通过一些利用反射的类型标签解决
ClassTag
TypeTag
WeakTypeTag
- 参见cs_java/scala/stdlib中
- Java:Java初始不支持泛型,JVM不接触泛型
- 为保持向后兼容,泛型中类型参数被替换为
Object
、或 类型上限- JVM执行时,不知道泛型类参数化的实际类
- Scala、Java编译器执行过程都执行类型擦除
抽象类型
抽象类型:由具体实现决定实际类型
特质、抽象类均可包括抽象类型
1
2
3
4trait Buffer{
type T
val element: T
}抽象类型可以添加类型边界
1
2
3
4
5abstract class SeqBuffer extends Buffer{
type U
type T <: Seq[U]
def length = element.length
}含有抽象类型的特质、类经常和匿名类的初始化同时使用
1
2
3
4
5
6
7
8
9abstract class IntSeqBuffer extends SeqBuffer{
type U = Int
}
def newIntSeqBuf(elem1: Int, elem2: Int): IntSeqBuffer =
new IntSeqBuffer{
type T = List[U]
val element = List(elem1, elem2)
}
// 所有泛型参数给定后,得到可以实例化的匿名类
抽象类型、类的泛型参数大部分可以相互转换,但以下情况无法 使用泛型参数替代抽象类型
1
2
3
4
5
6
7
8
9
10abstract class Buffer[+T]{
val element: T
}
abstract class SeqBuffer[U, +T <: Seq[U]] extends Buffer[T]{
def length = element.length
}
def newIntSeqBuf(e1: Int, e2: Int): SeqBuffer[Int, Seq[Int]] =
new SeqBuffer[Int, List[Int]]{
val element = List(e1, e2)
}
包和导入
package
- Scala使用包创建命名空间,允许创建模块化程序
包命名方式
- 惯例是将包命名为与包含Scala文件目录名相同,但Scala 未对文件布局作任何限制
- 包名称应该全部为小写
包声明方式:同一个包可以定义在多个文件中
- 括号嵌套:使用大括号包括包内容
- 允许包嵌套,可以定义多个包内容
- 提供范围、封装的更好控制
- 文件顶部标记:在Scala文件头部声明一个、多个包名称
- 各包名按出现顺序逐层嵌套
- 只能定义一个包的内容
- 括号嵌套:使用大括号包括包内容
包作用域:相较于Java更加前后一致
- 和其他作用域一样支持嵌套
- 可以直接访问上层作用域中名称
- 行为类似Python作用域,优先级高于全局空间中导入
- 即使名称在不同文件中
包冲突方案:Java中包名总是绝对的,Scala中包名是相对的, 而包代码可能分散在多个文件中,导致冲突
- 绝对包名定位包:
__root__.<full_package_path>
- 导入时:
import __root__.<package>
- 使用时:
val v = new __root__.<class>
- 导入时:
- 串联式包语句隐藏包:包名为路径分段串,其中非结尾 包被隐藏
- 绝对包名定位包:
- Scala文件顶部一定要
package
声明所属包,否则好像不会默认 导入scala.
等包,出现非常奇怪的错误,如:没有该方法
import
import
语句用于导入其他包中成员,相同包中成员不需要import
语句1
2
3
4
5
6import users._
import users.{User, UserPreferences}
// 重命名
import users.{UserPreferences => UPrefs}
// 导入除`HashMap`以外其他类型
import java.utils.{HashMap => _, _}若存在命名冲突、且需要从项目根目录导入,可以使用
__root__
表示从根目录开始
scala
、java.lang
、object Predef
默认导入- 相较于Java,Scala允许在任何地方使用导入语句
包对象
包对象:作为在整个包中方便共享内容、使用的容器
1 | // in file gardening/fruits/Fruit.scala |
包对象中任何定义都认为时包自身的成员,其中可以定义 包作用域级任何内容
- 类型别名
- 隐式转换
包对象和其他对象类似,每个包都允许有一个包对象
- 可以继承Scala的类、特质
- 注意:包对象中不能进行方法重载
- 按照惯例,包对象代码通常放在文件
package.scala
中
Annotations
注解:关联元信息、定义
- 注解作用于其后的首个定义、声明
- 定义、声明之前可以有多个注解,注解顺序不重要
确保编码正确性注解
- 此类注解条件不满足时,会导致编译失败
@tailrec
@tailrec
:确保方法尾递归
1
2
3
4
5
6
7
8
9
import scala.annotations.tailrec
def factorial(x: Int): Int = {
def factorialHelper(x: Int, accumulator: Int): Int ={
if (x == 1) accumulator
else factorialHelper(x - 1, accumulator * x)
}
}
影响代码生成注解
- 此类注解会影响生成字节码
@inline
@inline
:内联,在调用点插入被调用方法
- 生成字节码更长,但可能运行更快
- 不能确保方法内联,当且仅当满足某些生成代码大小的 启发式算法时,才会出发编译器执行此操作
@BeanProperty
@BeanProperty
:为成员常/变量同时生成Java风格getter、setter
方法get<Var>()
、set<Var>()
- 对成员变/常量,类编译后会自动生成Scala风格getter、 setter方法:
<var>()
、<var>_$eq()
Java注解
- Java注解有用户自定义元数据形式
- 注解依赖于指定的name-value对来初始化其元素
@interface
1 | // 定义跟踪某个类的来源的注解 |