天天看点

Scala学习笔记3 - 类和对象===类(class)和对象(object)===抽象类和抽象成员

===类(class)和对象(object)

类(class)和构造器:

      类的定义形式如下:

              class MyClass(a: Int, b: Int) {

                     println(a.toString)

               }

      在scala中,类也可以带有类参数,类参数可以直接在类的主体中使用,没必要定义字段然后把构造器的参数赋值到字段里,但需要注意的是:类参数仅仅是个参数而已,不是字段,如果你需要在别的地方使用,就必须定义字段。不过还有一种称为参数化字段的定义形式,可以简化字段的定义,如下:

               class MyClass(val a: Int, val b: Int) {

                     println(a.toString)

               }

      以上代码中多了val声明,作用是在定义类参数的同时定义类字段,不过它们使用相同的名字罢了。类参数同样可以使用var作前缀,还可以使用private、protected、override修饰等等。scala编译器会收集类参数并创造出带同样的参数的类的主构造器,并将类内部任何既不是字段也不是方法定义的代码编译至主构造器中。除了主构造器,scala也可以有辅助构造器,辅助构造器的定义形式为def this(…)。每个辅助构造器都以“this(…)”的形式开头以调用本类中的其他构造器,被调用的构造器可以是主构造器,也可以是源文件中早于调用构造器定义的其他辅助构造器。其结果是对scala构造器的调用终将导致对主构造器的调用,因此主构造器是类的唯一入口点。在scala中,只有主构造器可以调用超类的构造器。

      你可以在类参数列表之前加上private关键字,使类的主构造器私有,私有的主构造器只能被类本身以及伴生对象访问。

      可以使用require方法来为构造器的参数加上先决条件,如果不满足要求的话,require会抛出异常,阻止对象的创建。

      如果类的主体为空,那么可以省略花括号。

访问级别控制:

      公有是scala的默认访问级别,因此如果你想使成员公有,就不要指定任何访问修饰符。公有的成员可以在任何地方被访问。

      私有类似于java,即在之前加上private。不同的是,在scala中外部类不可以访问内部类的私有成员。

      保护类似于java,即在之前加上protected。不同的是,在scala中同一个包中的其他类不能访问被保护的成员。

      scala里的访问修饰符可以通过使用限定词强调。格式为private[X]或protected[X]的修饰符表示“直到X”的私有或保护,这里X指代某个所属的包、类或单例对象。

      scala还有一种比private更严格的访问修饰符,即private[this]。被private[this]标记的定义仅能在包含了定义的同一个对象中被访问,这种限制被称为对象私有。这可以保证成员不被同一个类中的其他对象访问。

      对于私有或者保护访问来说,scala的访问规则给予了伴生对象和类一些特权,伴生对象可以访问所有它的伴生类的私有成员、保护成员,反过来也成立。

成员(类型、字段和方法):

      scala中也可以定义类型成员,类型成员以关键字type声明。通过使用类型成员,你可以为类型定义别名。

      scala里字段和方法属于相同的命名空间,scala禁止在同一个类里用同样的名称定义字段和方法,尽管java允许这样做。

getter和setter:

      在scala中,类的每个非私有的var成员变量都隐含定义了getter和setter方法,但是它们的命名并没有沿袭java的约定,var变量x的getter方法命名为“x”,它的setter方法命名为“x_=”。你也可以在需要的时候,自行定义相应的getter和setter方法,此时你还可以不定义关联的字段,自行定义setter的好处之一就是你可以进行赋值的合法性检查。

      如果你将scala字段标注为@BeanProperty时,scala编译器会自动额外添加符合JavaBeans规范的形如getXxx/setXxx的getter和setter方法。这样的话,就方便了java与scala的互操作。

样本类:

      带有case修饰符的类称为样本类(case class),这种修饰符可以让scala编译器自动为你的类添加一些句法上的便捷设定,以便用于模式匹配,scala编译器自动添加的句法如下:

      ①  帮你实现一个该类的伴生对象,并在伴生对象中提供apply方法,让你不用new关键字就能构造出相应的对象;

      ②  在伴生对象中提供unapply方法让模式匹配可以工作;

      ③  样本类参数列表中的所有参数隐式地获得了val前缀,因此它们被当作字段维护;

      ④  添加toString、hashCode、equals、copy的“自然”实现。

封闭类:

      带有sealed修饰符的类称为封闭类(sealed class),封闭类除了类定义所在的文件之外不能再添加任何新的子类。这对于模式匹配来说是非常有用的,因为这意味着你仅需要关心你已经知道的子类即可。这还意味你可以获得更好的编译器帮助。

单例对象(singleton object):

      scala没有静态方法,不过它有类似的特性,叫做单例对象,以object关键字定义(注:main函数也应该在object中定义,任何拥有合适签名的main方法的单例对象都可以用来作为程序的入口点)。定义单例对象并不代表定义了类,因此你不可以使用它来new对象。当单例对象与某个类共享同一个名称时,它就被称为这个类的伴生对象(companion object)。类和它的伴生对象必须定义在同一个源文件里。类被称为这个单例对象的伴生类。类和它的伴生对象可以互相访问其私有成员。不与伴生类共享名称的单例对象被称为独立对象(standalone object)。

      apply与update:在scala中,通常使用类似函数调用的语法。当使用小括号传递变量给对象时,scala都将其转换为apply方法的调用,当然前提是这个类型实际定义过apply方法。比如s是一个字符串,那么s(i)就相当于c++中的s[i]以及java中的s.charAt(i),实际上 s(i) 是 s.apply(i) 的简写形式。类似地,BigInt(“123”) 就是 BigInt.apply(“123”) 的简写形式,这个语句使用伴生对象BigInt的apply方法产生一个新的BigInt对象,不需要使用new。与此相似的是,当对带有括号并包含一到若干参数的变量赋值时,编译器将使用对象的update方法对括号里的参数(索引值)和等号右边的对象执行调用,如arr(0) = “hello”将转换为arr.update(0, “hello”)。

      类和单例对象之间的差别是,单例对象不带参数,而类可以。因为单例对象不是用new关键字实例化的,所以没机会传递给它实例化参数。单例对象在第一次被访问的时候才会被初始化。当你实例化一个对象时,如果使用了new则是用类实例化对象,无new则是用伴生对象生成新对象。同时要注意的是:我们可以在类或(单例)对象中嵌套定义其他的类和(单例)对象。

对象相等性:

      与java不同的是,在scala中,“==”和“!=”可以直接用来比较对象的相等性,“==”和“!=”方法会去调用equals方法,因此一般情况下你需要覆盖equals方法。如果要判断引用是否相等,可以使用eq和ne。

在使用具有哈希结构的容器类库时,我们需要同时覆盖hashCode和equals方法,但是实现一个正确的hashCode和equals方法是比较困难的一件事情,你需要考虑的问题和细节很多,可以参见java总结中的相应部分。另外,正如样本类部分所讲的那样,一旦一个类被声明为样本类,那么scala编译器就会自动添加正确的符合要求的hashCode和equals方法。

===抽象类和抽象成员

      与java相似,scala中abstract声明的类是抽象类,抽象类不可以被实例化。

      在scala中,抽象类和特质中的方法、字段和类型都可以是抽象的。示例如下:

                   trait MyAbstract {

                            type T                                     // 抽象类型

                            def transform(x: T):T            // 抽象方法

                            val initial: T                            // 抽象val

                            var current: T                         // 抽象var

                   }

      抽象方法:抽象方法不需要(也不允许)有abstract修饰符,一个方法只要是没有实现(没有等号或方法体),它就是抽象的。

      抽象类型:scala中的类型成员也可以是抽象的。抽象类型并不是说某个类或特质是抽象的(特质本身就是抽象的),抽象类型永远都是某个类或特质的成员。

      抽象字段:没有初始化的val或var成员是抽象的,此时你需要指定其类型。抽象字段有时会扮演类似于超类的参数这样的角色,这对于特质来说尤其重要,因为特质缺少能够用来传递参数的构造器。因此参数化特质的方式就是通过在子类中实现抽象字段完成。如对于以下特质:

                   trait MyAbstract {

                            valtest: Int

                            println(test)

                            defshow() {

                                      println(test)

                            }

                   }

      你可以使用如下匿名类语法创建继承自该特质的匿名类的实例,如下:

                   new MyAbstract {

                            val test = 1

                   }.show()

      你可以通过以上方式参数化特质,但是你会发现这和“new 类名(参数列表)”参数化一个类实例还是有区别的,因为你看到了对于test变量的两次println(第一次在特质主体中,第二次是由于调用了方法show),输出了两个不同的值(第一次是0,第二次是1)。这主要是由于超类会在子类之前进行初始化,而超类抽象成员在子类中的具体实现的初始化是在子类中进行的。为了解决这个问题,你可以使用预初始化字段和懒值。

预初始化字段:

      预初始化字段,可以让你在初始化超类之前初始化子类的字段。预初始化字段用于对象或有名称的子类时,形式如下:

                   class B extends {

                            val a = 1

                   } with A

      预初始化字段用于匿名类时,形式如下:

                   new {

                            val a = 1

                   } with A

      需要注意的是:由于预初始化的字段在超类构造器调用之前被初始化,因此它们的初始化器不能引用正在被构造的对象。

懒值:

      加上lazy修饰符的val变量称为懒值,懒值右侧的表达式将直到该懒值第一次被使用的时候才计算。如果懒值的初始化不会产生副作用,那么懒值定义的顺序就不用多加考虑,因为初始化是按需的。

继续阅读