Scala Cookbook读书笔记 Chapter 4.Classes and Properties 第一部分

4.0 本章概述

  • 尽管Scala和Java有很多相同点,但是类的声明,类构造函数和字段可见控制是两者之间最大的不同。Java是更加冗长,Scala是更加简洁。

4.1 创建主构造函数

  • 问题:创建主构造函数和Java不同

    4.1.1 解决方案

  • Scala的主构造函数由下面几部分组成:
    • 构造器参数
    • 类的主体部分被调用的方法
    • 类的主体部分执行的语句和表达式
  • Scala类主体部分声明的字段处理方式和Java相似,它们在类第一次实例化时被赋值。

    class Person(var firstName: String, var lastName: String) {
    
        println("the constructor begins")
    
        // some class fields
        private val HOME = System.getProperty("user.home")
        var age = 0
    
        // some methods
        override def toString = s"$firstName $lastName is $age years old"
        def printHome { println(s"HOME = $HOME") }
        def printFullName { println(this) } // uses toString
    
        printHome
        printFullName
        println("still in the constructor")
    }
    
  • 这些类里的方法仍然是构造函数的一部分,当一个Person类实例创建时,运行结果如下:

    scala> val p = new Person("Adam", "Meyer")
    the constructor begins
    HOME = /Users/Al
    Adam Meyer is 0 years old
    still in the constructor
    

4.1.2 讨论

  • Scala和Java声明主构造函数的过程完成不同,Java中可以很明显的看到是否在主构造函数中,但是Scala难以区分。然而一旦理解了这个方法,它会使得你的类声明比Java更加简洁。
  • 上面例子中,两个构造器参数被定义成var字段,意味着是可变的(variable)。初始化设置之后值还可以改变,Scala同时为var生成获取器(accessor)和修改器(mutator):

    //设值
    p.firstName = "Scott"
    p.lastName = "Jones"
    
    //取值
    println(p.firstName)
    println(p.lastName)
    
    p.age = 30
    println(p.age)
    
  • 字段HOME被声明成一个私有的val,相当于Java中的private and final。它不可以被其他对象直接获取,它的值也不可以改变。

  • 当你调用类里的方法时,比如调用printFullName方法,这个方法调用仍然是构造器的一部分。可以通过scalac把代码编译成Person.class文件,然后通过JAD工具反编译成Java源代码:

    public Person(String firstName, String lastName)
    {
        super();
        this.firstName = firstName;
        this.lastName = lastName;
        Predef$.MODULE$.println("the constructor begins");
        age = 0;
        printHome();
        printFullName();
        Predef$.MODULE$.println("still in the constructor");
    }
    
  • 上面可以看出printHome和printFullName方法在Person构造器里调用,同时age初始化赋值。
  • 反编译后,构造器参数和类字段如下:

    private String firstName;
    private String lastName;
    private final String HOME = System.getProperty("user.home");
    private int age;
    
  • 除了方法声明以外的类中定义的任何东西都是主类构造函数的一部分。因为辅助构造函数总是调用同一个类中先前定义的构造函数,辅助构造函数也将执行相同的代码。

4.1.3 与Java进行比较

  • Java版Person类如下,可以看出Java比Scala复杂详细,不需要知道太多编译器做了什么:

    // java
    public class Person {
    
        private String firstName;
        private String lastName;
        private final String HOME = System.getProperty("user.home");
        private int age;
    
        public Person(String firstName, String lastName) {
            super();
            this.firstName = firstName;
            this.lastName = lastName;
            System.out.println("the constructor begins");
            age = 0;
            printHome();
            printFullName();
            System.out.println("still in the constructor");
        }
    
        public String firstName() { return firstName; }
        public String lastName() { return lastName; }
        public int age() { return age; }
    
        public void firstName_$eq(String firstName) {
            this.firstName = firstName;
        }
    
        public void lastName_$eq(String lastName) {
            this.lastName = lastName;
        }
    
        public void age_$eq(int age) {
            this.age = age;
        }
    
        public String toString() {
            return firstName + " " + lastName + " is " + age + " years old";
        }
    
        public void printHome() {
            System.out.println(HOME);
        }
    
        public void printFullName() {
            System.out.println(this);
        }
    
    }
    

    4.1.4 _$eq方法

  • 生成的修改器方法如下:

    public void firstName_$eq(String firstName) { ...
    public void age_$eq(int age) { ...
    
  • 这些名字是修改var变量的Scala语法的一部分,不需要考虑太多。下面的类有一个命名为name的var变量:

    class Person {
        var name = ""
        override def toString = s"name = $name"
    }
    
  • 编译时修改器就会自动命名为name_$eq。

    //代码修改变量
    p.name = "Ron Artest"
    
    //编译器自动转换
    p.name_$eq("Ron Artest")
    
  • 下面演示两种调用修改器的方法:

    object Test extends App {
        val p = new Person
    
        // the 'normal' mutator approach
        p.name = "Ron Artest"
        println(p)
    
        // the 'hidden' mutator method
        p.name_$eq("Metta World Peace")
        println(p)
    }
    
    //输出
    name = Ron Artest
    name = Metta World Peace
    

    4.1.5 总结

  • Java代码复杂,但是明确;Scala简洁,但是必须看一下构造器参数以便理解getters和setters是否已经生成,还需要知道类里调用的任何方法都是从主函数调用的。

4.2 控制构造器字段的显示

  • 问题:想要控制使用在构造器参数里的字段的显示

    4.2.1 解决方案

  • 关键在于字段是否声明成val,var,val和var都没有,或者是否添加了private声明:
    • 如果声明成var,Scala生成getter和setter方法
    • 如果声明成val,Scala只生成getter方法
    • 如果没有var和val修饰符,Scala不生成getter和setter方法
    • var和val字段前可以添加private关键词,阻止生成getter和setter方法

4.2.2 var字段

  • 构造器参数声明成var字段,那么字段的值可以改变,同时生成getter和setter方法:

    scala> class Person(var name: String)
    defined class Person
    
    scala> val p = new Person("Alvin Alexander")
    p: Person = Person@369e58be
    
    // getter
    scala> p.name
    res0: String = Alvin Alexander
    
    // setter
    scala> p.name = "Fred Flintstone"
    p.name: String = Fred Flintstone
    
    scala> p.name
    res1: String = Fred Flintstone
    

    4.2.3 val字段

  • 构造器参数声明成val字段,一旦赋值之后值不可以改变。类似于Java中的final。

    scala> class Person(val name: String)
    defined class Person
    
    scala> val p = new Person("Alvin Alexander")
    p: Person = Person@3f9f332b
    
    scala> p.name
    res0: String = Alvin Alexander
    
    scala> p.name = "Fred Flintstone"
    <console>:11: error: reassignment to val
        p.name = "Fred Flintstone"
               ^
    

    4.2.4 没有var和val

  • 不生成获取器和修改器

    scala> class Person(name: String)
    defined class Person
    
    scala> val p = new Person("Alvin Alexander")
    p: Person = Person@144b6a6c
    
    scala> p.name
    <console>:12: error: value name is not a member of Person
                p.name
                  ^
    

    4.2.5 给val或var添加private

  • 阻止生成getter和setter方法,所以只能在类成员里获取这个字段

    scala> class Person(private var name: String) { def getName {println(name)} }
    defined class Person
    
    scala> val p = new Person("Alvin Alexander")
    p: Person = Person@3cb7cee4
    
    scala> p.name
    <console>:10: error: variable name in class Person cannot be accessed in Person
                  p.name
                    ^
    
    scala> p.getName
    Alvin Alexander
    

4.2.6 讨论

  • 表如下:

    Table 4-1. The effect of constructor parameter settings
    |Visibility |Accessor?| Mutator?|
    | ——– | —–: | :—-: |
    |var | Yes | Yes |
    |val | Yes | No |
    |Default visibility (no var or val) | No | No |
    |Adding the private keyword to var or val | No | No |

  • 详细看4.6章,手动添加自己的获取器和修改器

    Case类

  • Case类的构造器参数和上面规则不同,默认是val声明,如果没有声明成val或者var,仍然可以获取这个字段:

    case class Person(name: String)
    
    scala> val p = Person("Dale Cooper")
    p: Person = Person(Dale Cooper)
    
    scala> p.name
    res0: String = Dale Cooper
    

4.3 定义辅助构造函数

  • 问题:一个类里定义一个或者更多辅助构造函数,可以给类消费者不同创建实例的方法

    4.3.1 解决方案

  • 使用名字this定义辅助构造函数作为类里的方法。可以定义多个辅助构造函数,而且必须有不同的参数列表。每个构造函数必须调用先前定义的构造函数中的一个。

    // primary constructor
    class Pizza (var crustSize: Int, var crustType: String) {
    
        // one-arg auxiliary constructor
        def this(crustSize: Int) {
            this(crustSize, Pizza.DEFAULT_CRUST_TYPE)
        }
    
        // one-arg auxiliary constructor
        def this(crustType: String) {
            this(Pizza.DEFAULT_CRUST_SIZE, crustType)
        }
    
        // zero-arg auxiliary constructor
        def this() {
            this(Pizza.DEFAULT_CRUST_SIZE, Pizza.DEFAULT_CRUST_TYPE)
        }
    
        override def toString = s"A $crustSize inch pizza with a $crustType crust"
    }
    
    object Pizza {
        val DEFAULT_CRUST_SIZE = 12
        val DEFAULT_CRUST_TYPE = "THIN"
    }
    
  • 通过下面的方法创建同一个pizza

    val p1 = new Pizza(Pizza.DEFAULT_CRUST_SIZE, Pizza.DEFAULT_CRUST_TYPE)
    val p2 = new Pizza(Pizza.DEFAULT_CRUST_SIZE)
    val p3 = new Pizza(Pizza.DEFAULT_CRUST_TYPE)
    val p4 = new Pizza  //无参数不需要括号,case类时需要
    

    4.3.2 讨论

  • 以下几个重要的点:
    • 通过创建this方法定义辅助构造方法
    • 每个辅助构造函数必须以先前定义的构造函数开始
    • 每个构造函数必须有不同的参数列表
    • 一个构造函数使用this调用另一个构造函数
  • 之前例子每个辅助构造函数都是调用主构造函数,但不是必须,只需要调用之前定义的构造函数其一即可:

    def this(crustType: String) {
        this(Pizza.DEFAULT_CRUST_SIZE)
        this.crustType = Pizza.DEFAULT_CRUST_TYPE
    }
    
  • 之前例子参数声明都是在主构造函数里,这不是必须,但这样做可以让Scala生成获取器和修改器。下面使用另一种方法,但是代码更多:

    class Pizza () {
    
        var crustSize = 0
        var crustType = ""
    
        def this(crustSize: Int) {
            this()
            this.crustSize = crustSize
        }
    
        def this(crustType: String) {
            this()
            this.crustType = crustType
        }
    
        // more constructors here ...
    
        override def toString = s"A $crustSize inch pizza with a $crustType crust"
    
    }
    
  • 总结。如果需要生成的获取器和修改器,把需要声明的参数放在主构造函数中

4.3.3 为case类生成辅助构造函数

  • case类是一个会生成一堆样板代码的特殊的类,case类添加辅助构造函数和其他类不一样。因为它们并不是真的构造函数,它们在类的同伴对象中应用方法。
  • 下面演示,有一个叫做Person.scala的文件:

    // initial case class
    case class Person (var name: String, var age: Int)
    
  • 不需要使用new关键词创建一个Person实例

    val p = Person("John Smith", 30)
    
  • 事实上,上面是一个小的语法糖————工厂方法,实际上Scala编译器把它转成如下:

    val p = Person.apply("John Smith", 30)
    
  • 是在Person类的同伴对象中调用一个apply方法,这个是编译器自动转换的。如果想要给case类添加新的“构造函数”,就需要写新的apply方法。

    // the case class
    case class Person (var name: String, var age: Int)
    
    // the companion object
    object Person {
        def apply() = new Person("<no name>", 0)
        def apply(name: String) = new Person(name, 0)
    }
    
  • 测试代码

    object CaseClassTest extends App {
    
        val a = Person() // corresponds to apply() 无参数也需要括号,正常类无参数时不需要括号
        val b = Person("Pam") // corresponds to apply(name: String)
        val c = Person("William Shatner", 82)
    
        println(a)
        println(b)
        println(c)
    
        // verify the setter methods work
        a.name = "Leonard Nimoy"
        a.age = 82
        println(a)
    }
    
  • 输出

    Person(<no name>,0)
    Person(Pam,0)
    Person(William Shatner,82)
    Person(Leonard Nimoy,82)
    
  • 查看更多
  • 6.8章,不使用new关键词创建Object实例
  • 4.5章,给构造函数参数提供默认值
  • 4.14章,生成Case类的样板代码

4.4 定义一个私有的主构造函数

  • 问题: 把类的主构造函数私有化,使之成为单例模式

    4.4.1 解决方案

  • 在类名和构造器参数之间插入private关键词:

    // a private no-args primary constructor
    class Order private { ...
    
    // a private one-arg primary constructor
    class Person private (name: String) { ...
    
  • 会阻止创建类的实例

    scala> class Person private (name: String)
    defined class Person
    
    scala> val p = new Person("Mercedes")
    <console>:9: error: constructor Person in class Person cannot be accessed
    in object $iw
            val p = new Person("Mercedes")
                    ^
    

    4.4.2 讨论

  • 单例模式的简单方法是设置主构造函数私有化,然后在类的同伴对象里添加一个getInstance方法:

    class Brain private {
        override def toString = "This is the brain."
    }
    
    object Brain {
        val brain = new Brain
        def getInstance = brain
    }
    
    object SingletonTest extends App {
        // this won't compile
        // val brain = new Brain
    
        // this works
        val brain = Brain.getInstance
        println(brain)
    }
    
  • 这里不是一定要命名成getInstance,这里这样使用是因为Java习惯
  • 同伴对象是一个在同一个类名文件中定义的简化对象,object和class使用同一个名字。如果有一个文件名叫做Foo.scala,类名叫做Foo,同一个文件中的对象叫做Foo,那么这个对象就是这个Foo类的同伴对象。
  • 同伴对象里声明的任何方法是这个对象的静态方法(static)

4.4.3 工具类

  • 根据需要,创建一个私有的类构造函数可能完全没有必要。把所有方法放到Scala的object里,这样就定义了static静态方法了

    object FileUtils {
    
        def readFile(filename: String) = {
            // code here ...
        }
    
        def writeToFile(filename: String, contents: String) {
            // code here ...
        }
    }
    
  • 如下方式调用方法:

    val contents = FileUtils.readFile("input.txt")
    FileUtils.writeToFile("output.txt", content)
    
  • 因为只有一个对象会被定义,下面的代码不会编译:

    val utils = new FileUtils // won't compile
    
  • 所有这个例子中,没有必要写一个私有类构造函数,只要不定义这个类就可以了。

4.5 给构造器参数提供默认值

  • 问题: 可以给其他类调用这个构造函数时指定参数值

    4.5.1 解决方案

  • 给定默认值

    class Socket (val timeout: Int = 10000)
    
  • 因为已经定义了一个默认值,所以调用的时候可以不指定参数,可以获得这个默认值

    scala> val s = new Socket
    s: Socket = Socket@7862af46
    
    scala> s.timeout
    res0: Int = 10000
    
  • 创建一个新的Socket时可以指定参数值

    scala> val s = new Socket(5000)
    s: Socket = Socket@6df5205c
    
    scala> s.timeout
    res1: Int = 5000
    
  • 也可以用下面方式,调用构造函数时使用已命名的参数

    scala> val s = new Socket(timeout=5000)
    s: Socket = Socket@52aaf3d2
    
    scala> s.timeout
    res0: Int = 5000
    

    4.5.2 讨论

  • 这个可以减少辅助构造函数的需求,下面的一个构造函数相当于两个构造函数:

    class Socket (val timeout: Int = 10000)
    
  • 如果上面不存在,则需要两个构造函数:一个参数的主构造函数和一个无参数的辅助构造函数:

    class Socket(val timeout: Int) {
        def this() = this(10000)
        override def toString = s"timeout: $timeout"
    }
    

4.5.3 多个参数

  • 可以给多个参数指定默认值

    class Socket(val timeout: Int = 1000, val linger: Int = 2000) {
        override def toString = s"timeout: $timeout, linger: $linger"
    }
    
  • 尽管只定义了一个构造函数,但是这个类有3个构造函数

    scala> println(new Socket)
    timeout: 1000, linger: 2000
    
    scala> println(new Socket(3000))
    timeout: 3000, linger: 2000
    
    scala> println(new Socket(3000, 4000))
    timeout: 3000, linger: 4000
    

    4.5.4 使用命名参数

  • 创建对象时可以提供构造参数名字,这和Objective-C和其他语法很像。

    println(new Socket(timeout=3000, linger=4000))
    println(new Socket(linger=4000, timeout=3000))
    println(new Socket(timeout=3000))
    println(new Socket(linger=4000))
    

4.6 重写默认的获取器和修改器

  • 问题:重写Scala生成的getter和setter方法

    4.6.1 解决方案

  • 如果坚持使用Scala的命名规范,那么不可以重写默认生成的getter和setter方法:

    // error: this won't work
    class Person(private var name: String) {
        // this line essentially creates a circular reference
        def name = name
        def name_=(aName: String) { name = aName }
    }
    
  • 编译报错:

    Person.scala:3: error: overloaded method name needs result type
        def name = name
                   ^
    Person.scala:4: error: ambiguous reference to overloaded definition,
    both method name_= in class Person of type (aName: String)Unit
    and method name_= in class Person of type (x$1: String)Unit
    match argument types (String)
        def name_=(aName: String) { name = aName }
                                    ^
    Person.scala:4: error: method name_= is defined twice
        def name_=(aName: String) { name = aName }
            ^
    three errors found
    
  • 解释:构造器参数和getter方法都命名成name,则Scala不会允许
  • 解决:改变构造参数名,这样就不会和getter方法冲突,一般在构造参数名前加入下划线_,如_name:

    class Person(private var _name: String) {
        def name = _name // accessor
        def name_=(aName: String) { _name = aName } // mutator
    }
    
  • 构造器参数声明成private和var,private保证其他类不能获取声明的字段,var保证是变量
  • 创建一个getter方法叫做name,setter方法叫做name_=,调用如下:

    val p = new Person("Jonathan")
    p.name = "Jony" // setter
    println(p.name) // getter
    
  • 如果不喜欢Scala对于getter和setter的命名方式,可以使用任何其他的方法。如果想要如Java一样使用getName和setName,最好添加@BeanProperty注解,更多见17.6章。

4.6.2 讨论

  • 定义构造器参数为var,Scala使得变量私有化,同时自动生成getter和setter方法供其他类调用:

    class Stock (var symbol: String)
    
  • scalac编译后,使用javap反编译代码:

    $ javap Stock
    
    public class Stock extends java.lang.Object{
        public java.lang.String symbol();
        public void symbol_$eq(java.lang.String);
        public Stock(java.lang.String);
    }
    
  • 可以发现编译器生成两个方法:叫做symbol的getter方法,叫做symbol$eq的setter方法。第二个方法和我们命名的symbol=方法一样,但是Scala需要把=转换成JVM工作的$eq
  • 第二个方法命名有点不寻常,但它遵从Scala规范,当它使用魔法糖混合后,可以通过以下方式设值:

    stock.symbol = "GOOG"
    
  • 编译器把上面代码转换成:

    stock.symbol_$eq("GOOG")
    

4.6.3 总结

  • 总结如下:
    • 创建private var构造器参数,例子中命名成 _name
    • 定义需要被其他类调用的getter和setter方法,例子中getter名字是name,setter名字是name_=
    • 修改getter和setter方法的主体部分
  • 需要记住必须使用private关键词。如果没有使用,则需要隐藏的自动生成的getter/setter方法不会消失:

    // intentionally left the 'private' modifier off _symbol
    class Stock (var _symbol: String) {
    
        // getter
        def symbol = _symbol
    
        // setter
        def symbol_= (s: String) {
            this.symbol = s
            println(s"symbol was updated, new value is $symbol")
        }
    
    }
    
  • 编译反编译后:

    public class Stock extends java.lang.Object{
        public java.lang.String _symbol();      // error
        public void _symbol_$eq(java.lang.String);      // error
        public java.lang.String symbol();
        public void symbol_$eq(java.lang.String);
        public Stock(java.lang.String);
    }
    
  • 正确添加private关键词后编译反编译如下:

    public class Stock extends java.lang.Object{
        public java.lang.String symbol(); // println(stock.symbol)
        public void symbol_$eq(java.lang.String); // stock.symbol = "AAPL"
        public Stock(java.lang.String);
    }
    

4.7 阻止生成getter和setter方法

  • 问题:定义类字段为var时会自动生成getter和setter方法,定义类字段为val时会自动生成getter方法,但是如果getter和setter方法都不想要呢。

    4.7.1 解决方案

  • 使用private或者private[this]定义字段:

    class Stock {
        // getter and setter methods are generated
        var delayedPrice: Double = _
    
        // keep this field hidden from other classes
        private var currentPrice: Double = _
    }
    
  • 编译反编译后:

    // Compiled from "Stock.scala"
    public class Stock extends java.lang.Object implements scala.ScalaObject{
        public double delayedPrice();
        public void delayedPrice_$eq(double);
        public Stock();
    }
    

    4.7.2 讨论

  • 私有属性字段只存在于同一个类的实例中,下面例子中,任何Stock类的实例可以获取其他Stock类实例的私有属性:

    class Stock {
        // a private field can be seen by any Stock instance
        private var price: Double = _
        def setPrice(p: Double) { price = p }
        def isHigher(that: Stock): Boolean = this.price > that.price
    }
    
    object Driver extends App {
    
        val s1 = new Stock
        s1.setPrice(20)
    
        val s2 = new Stock
        s2.setPrice(100)
    
        println(s2.isHigher(s1))
    }
    

    4.7.3 object-private 字段

  • 使用private[this]定义字段意味着自由包含它的object可以获取这个字段。不同于private,这个字段不可以被同一类型的其他实例获取,比private设置更private。

    class Stock {
        // a private[this] var is object-private, and can only be seen
        // by the current instance
        private[this] var price: Double = _
    
        def setPrice(p: Double) { price = p }
    
        // error: this method won't compile because price is now object-private
        def isHigher(that: Stock): Boolean = this.price > that.price
    }
    
  • 编译结果如下:

    Stock.scala:5: error: value price is not a member of Stock
        def isHigher(that: Stock): Boolean = this.price > that.price
                                                           ^
    one error found