十六、枚举
scala没有枚举类型。scala提供了一个叫做Enumeration的抽象类,可以通过继承该类实现类似于枚举功能。
scala中枚举示例:
object Color extends Enumeration{
case class Val(val name:String, val code:Int) extends super.Val
val YELLOW = Val("yellow", 1)
val RED = Val("red", 2)
val GREEN = Val("green", 3)
}
上面示例虽然可以让scala实现类似枚举类的功能,但是与java还是有所不同,在java中枚举项的类型均是枚举类的类型,但是scala却无法实现这样的特征,因为scala的枚举类要实现类似java的枚举类使用方式,其枚举类型一定要是object,而object确切的说应该是对象而不是类型。当然由于scala没有提供枚举类型,因此实现枚举的类似功能有多种方式,但是最贴近java的用法的还是上面例子的实现方式。
十七、构造方法
我在第一章中已经介绍过scala的构造方法,这里详细介绍其定义语法。
在scala定义一个class时,其class名称后可以有一个参数列表的,如果没有参数的话,参数列表可以省略,类名一起定义的构造方法就是主要构造方法,或者说是基本构造方法。在scala的类体中可以定义多个辅助构造方法,但是这些构造方法的方法体中必须要显示调用主构造方法或者其他辅助构造方法。
scala的class的构造方法示例:
class B (val s : String = "b"){
var num:Int = -1
def this() = {
this("test")
}
def this(i:Int) = {
this()
this.num = i
}
def this(s:String, i:Int) = {
this(s)
this.num = i
}
}
scala的主构造方法的参数是有var或val修饰符的,如果省略了默认是val的,但是省略后的参数只能class内部能够访问到,外部引用是无法访问的。
scala的辅助构造方法的参数是不能有val或var修饰符的,这些参数如果在基本构造方法中没有声明的话,只能在类里声明相应的成员变量,这样的变量只能是var的。
只有主构造方法的参数可以在定义构造方法时赋给默认值。
scala中class的实例创建可以在参数列表中指定参数名,这样创建实例时参数列表可以不必按构造方法的参数列表顺序传递。例如上面示例中,创建B的实例可以这样做:val aB = new B(i=1,s="123")。但是这种方式创建实例需要注意一点:如果类的构造方法中存在两个构造方法的参数列表的参数名、参数个数、及对应类型都相同,只是参数顺序不同。这样的构造方法定义时是符合语法的,但是使用这种按名称传递参数的方式来创建实例时,编译就会报错。
十八、继承和多态
scala的继承延续了java的风格,使用extends关键字,具有java但继承特性,即是一个class只能extends一个类。java提供了interface来扩展class能继承的功能,相应的scala提供了with关键字,可以使得classs能继承多个Trait。
在前面我们介绍过,scala的Trait类是类似于java中的抽象类,而scala也支持abstract关键字来定义抽象类。Trait与abstract class有所不同:
abstract在被继承时,必须在extends关键字之后,切必须调用一个构造方法。
当被继承的多个类中存在同名成员(包括变量和方法,同名变量即类型和名称相同;同名函数即名称和参数列表相同),当同名变量均没有赋值或均已赋值,则需要在子类中使用override关键了重新声明覆盖这个变量;当同名方法均没有实现或均已有实现时,也需要在子类中使用override声明覆盖方法。
子类中存在未被赋值的成员变量,或未实现的方法,则子类也必须被声明为abstract的。
示例:
trait ATrait {
val s :String = "1"
def doWork {
println("test")
}
def printSome
}
abstract class AAbstract(val some:String) {
val s:String = "2"
def doOther {
println("abstract")
}
def this() = {
this("auxiliary")
println(some)
}
def printSome
}
class CI(val somei:String="inherited") extends AAbstract with ATrait{
override val s = "3"
def this() = {
this("ci auxiliary")
}
override def printSome {
super.doWork
println(somei)
}
}
无论是Trait还是abstract class,未赋值的成员变量,或未实现的成员方法,均不是必要的。
子类的辅助构造方法中,是无法调用父类的构造方法的。但是当子类在创建实例时,就先调用了一个父类的构造方法了,此父类构造方法就是在子类继承语法中,如上面CI在继承AAbstract类时就调用了AAbstract的this()构造方法了。
scala编程中的多态思想与java的是一致的。你可以在创建一个子类实例时,显示声明应用的类型为某个父类类型。
多态示例:
val ci:AAbstract = new CI
val ci1:ATrait = new CI("ci1")
ci.printSome
ci.doOther
println(ci.s)
ci1.printSome
scala中多态与java中有一点重要区别:在java中一个方法声明的返回值类型可以是抽象类或接口,方法的返回值则是具体的实现类;但是,在scala中却不能这么做,scala的方法父类的返回值类型不能接受子类的实例。
十九、伴生对象及伴生类
伴生对象是指与class在一个scala文件中进行声明,与class拥有同样的名称的一个object。这个object即被称为该class的伴生对象,相对应的这个class被称为伴生类。
我观察scala编译后,伴生类和伴生对象总共会生成两个class文件,一个是以类名命名的文件,一个是以类名加$符号命名的文件。通过此来观察单例对象,发现如果scala源文件里没有定义对应的伴生类,其实scala在编译后也会生成一个对应的伴生类。再通过一些观察发现,当定义一个class时,如果该class的基本构造方法的参数赋给了初始值,此时scala编译源码后,会生成一个伴生对象,该半生对象主要是为这个参数赋初始值。scala编译器在将源码编程成class时做了很多事情,由此可见一斑,这一点就让开发在定位由scala编写的代码时,难以通过反编译的class文件来推测scala源码的原貌,增加了scala代码问题定位的难度。
伴生对象的一些特殊方法:
1.apply
通常按照java的习惯,我们在创建一个类实例时是使用new关键字的,scala给了我们一种简便的方法来创建实例。scala里,我们可以在伴生对象里定义一些apply为名的方法,这些方法的参数与伴生类的构造方法的参数列表相对应,如此做的话,我们在创建该类的实例时就可以省略new关键字了,例如:
class Temple(s1:String) {
def this() = {
this("a str")
}
}
object Temple {
def apply() = new Temple
def aplly(s:String) = new Temple(s)
}
我们在创建这个Temple类的实例时就可以这么写val tmp = Temple或val tmp = Temple("test"),这其实是调用了Temple伴生对象的apply方法,即Temple等同于Temple.apply,Temple("test")等同于Temple.apply("test")。由此,存在于伴生对象里的apply方法就具有特别意义。
总的来说,apply方法在伴生对象里的意义就是创建实例对象。
2.unapply
这个方法与scala一个概念有关,即Extractor(抽取器)。Extractor是专门针对match的模式匹配的,关于match的模式匹配有两种方式:一种是case class(案例类);另一种就是Extractor。
要实现使用Extractor实现模式匹配,就需要在class的伴生对象里实现unapply方法。
unapply方法其实主要是对一个对象进行拆解,并取出其中的一些数据返回,但返回的对象的参数列表要与某个apply方法的参数列表相匹配。由unapply方法返回的一些数据可以在case的执行语句中使用。
trait IP {
def isValid:Boolean
def parse:Array[String]
}
class IPV4(private val ipStr:String) extends IP {
val expr = """((\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])"""
override def isValid:Boolean = {
if(null != ipStr){
ipStr.matches(expr)
}else{
false
}
}
override def parse:Array[String] = {
ipStr.split(".")
}
}
object IPV4 {
def apply(ipStr:String) = new IPV4(ipStr)
def unapply(str:String) : Option[String] = {
val ipv4 = IPV4(str)
if(ipv4.isValid){
Some(str)
}else{
None
}
}
}
class IPV6(private val ipStr:String) extends IP {
val expr = """((\d|[a-f]|[A-F]){0,4}:){7}((\d|[a-f]|[A-F]){0,4})"""
override def isValid:Boolean = {
if(null != ipStr){
ipStr.matches(expr)
}else{
false
}
}
override def parse:Array[String] = {
ipStr.split(":")
}
}
object IPV6 {
def apply(ipStr:String) = new IPV6(ipStr)
def unapply(str:String) : Option[String] = {
val ipv6 = IPV6(str)
if(ipv6.isValid){
Some(str)
}else{
None
}
}
}
上面简单例子,关于ip的两种协议的地址校验(ps:其中ipv6的是只考虑8部分地址全有的情况,其地址压缩写法及混合写法没有考虑进去)。我们可以看看Extractor在match模式匹配中的使用。
def protocol(str:String) : String = str match {
case IPV4(ipStr) => "IPV4"
case IPV6(ipStr) => "IPV6"
case _ => "invalid"
}
val str1 = "127.0.0.1"
val str2 = "0000:0000:0000:0000:0000:0000:0000:0000"
val str3 = "123"
println(protocol(str1))
println(protocol(str2))
println(protocol(str3))
执行结果如预期