这是Scala语言自学的第二篇:类与对象

这篇文章 cover 了 Programming in Scala 的第4章:Classes and Objects

Classes, fields, and methods

class ChecksumAccumulator {
  var sum = 0
}
val acc = new ChecksumAccumulator
val csa = new ChecksumAccumulator
objects in memory

Since sum, a field declared inside class ChecksumAccumulator, is a var, not a val, you can later reassign to sum a different Int value, like this:

acc.sum = 3

Now the picture would look like this:
objects in memory
acc is a val. What you can’t do with acc (or csa), given that they are vals, not vars, is reassign a different object to them. For example, the following attempt would fail:

// Won't compile, because acc is a val
acc = new ChecksumAccumulator

What you can count on, therefore, is that acc will always refer to the same ChecksumAccumulator object with which you initialize it, but the fields contained inside that object might change over time.

One important way to pursue robustness of an object is to ensure that the object’s state—the values of its instance variables—remains valid during its entire lifetime. The first step is to prevent outsiders from accessing the fields directly by making the fields private.

class ChecksumAccumulator {
  private var sum = 0
}
  • private fields can only be accessed by methods defined in the same class, all the code that can update the state will be localized to the class
  • The way you make members public in Scala is by not explicitly specifying any access modifier. Put another way, where you’d say “public” in Java, you simply say nothing in Scala. Public is Scala’s default access level.

Now that sum is private, the only code that can access sum is code defined inside the body of the class itself. Thus, ChecksumAccumulator won’t be of much use to anyone unless we define some methods in it:

class ChecksumAccumulator {
  private var sum = 0
  def add(b: Byte): Unit = {
    sum += b
  }
  def checksum(): Int = {
    return ~(sum & 0xFF) + 1
  }
}

One important characteristic of method parameters in Scala is that they are vals, not vars. If you attempt to reassign a parameter inside a method in Scala, therefore, it won’t compile:

def add(b: Byte): Unit = {
  b = 1  // This won't compile, because b is a val
  sum += b
}
  • Scala method returns the last value computed by the method
  • The recommended style for methods is in fact to avoid having explicit, and especially multiple, return statements.
  • Instead, think of each method as an expression that yields one value, which is returned.
  • This philosophy will encourage you to make methods quite small, to factor larger methods into multiple smaller ones.
  • It is often better to explicitly provide the result types of public methods declared in a class even when the compiler would infer it for you.
// In file ChecksumAccumulator.scala
class ChecksumAccumulator {
  private var sum = 0
  def add(b: Byte): Unit = { sum += b }
  def checksum(): Int = ~(sum & 0xFF) + 1
}

Final version of class ChecksumAccumulator

Semicolon inference

A semicolon is required if you write multiple statements on a single line:

val s = "hello"; println(s)

For just this reason, whenever you are chaining an infix operation such as +, it is a common Scala style to put the operators at the end of the line instead of the beginning:

x +
y +
z

Singleton objects

Scala cannot have static members. Instead, Scala has singleton objects.

  • When a singleton object shares the same name with a class, it is called that class’s companion object.
  • You must define both the class and its companion object in the same source file.
  • The class is called the companion class of the singleton object.
  • A class and its companion object can access each other’s private members.
// In file ChecksumAccumulator.scala
import scala.collection.mutable
object ChecksumAccumulator {
  private val cache = mutable.Map.empty[String, Int]
  def calculate(s: String): Int =
    if (cache.contains(s))
      cache(s)
    else {
      val acc = new ChecksumAccumulator
      for (c <- s)
        acc.add(c.toByte)
      val cs = acc.checksum()
      cache += (s -> cs)
      cs
    }
}

Companion object for class ChecksumAccumulator

  • We used a cache here to show a singleton object with a field.
  • A cache such as this is a performance optimization that trades off memory for computation time.
  • In general, you would likely use such a cache only if you encountered a performance problem that the cache solves, and might use a weak map, such as WeakHashMap in scala.collection.jcl, so that entries in the cache could be garbage collected if memory becomes scarce.
  • If you are a Java programmer, one way to think of singleton objects is as the home for any static methods you might have written in Java.
  • You can invoke methods on singleton objects using a similar syntax: the name of the singleton object, a dot, and the name of the method.
ChecksumAccumulator.calculate("Every value is an object.")

invoke the calculate method of singleton object ChecksumAccumulator

A singleton object is more than a holder of static methods, however. It is a first-class object. You can think of a singleton object’s name, therefore, as a “name tag” attached to the object:
objects in memory

  • Defining a singleton object doesn’t define a type (at the Scala level of abstraction).
  • Given just a definition of object ChecksumAccumulator, you can’t make a variable of type ChecksumAccumulator.
  • However, singleton objects extend a superclass and can mix in traits.

One difference between classes and singleton objects is that singleton objects cannot take parameters, whereas classes can.Because you can’t in- stantiate a singleton object with the new keyword, you have no way to pass parameters to it.

A singleton object is initialized the first time some code accesses it.

  • A singleton object that does not share the same name with a companion class is called a standalone object.
  • You can use standalone objects for many purposes, including collecting related utility methods together or defining an entry point to a Scala application.

A Scala application

To run a Scala program, you must supply the name of a standalone singleton object with a main method that takes one parameter, an Array[String], and has a result type of Unit.

Any standalone object with a main method of the proper signature can be used as the entry point into an application.

// In file Summer.scala
import ChecksumAccumulator.calculate
object Summer {
  def main(args: Array[String]) = {
    for (arg <- args)
      println(arg + ": " + calculate(arg))
  }
}

Scala implicitly imports members of packages java.lang and scala, as well as the members of a singleton object named Predef, into every Scala source file. Predef, which resides in package scala, contains many useful methods. For example, when you say println in a Scala source file, you’re actually invoking println on Predef. (Predef.println turns around and invokes Console.println, which does the real work.) When you say assert, you’re invoking Predef.assert.

The App trait

Scala provides a trait, scala.App, that can save you some finger typing.

import ChecksumAccumulator.calculate
object FallWinterSpringSummer extends App {
  for (season <- List("fall", "winter", "spring"))
    println(season + ": " + calculate(season))
}
  • Instead of writing a main method, you place the code you would have put in the main method directly between the curly braces of the singleton object.
  • You can access command-line arguments via an array of strings named args.

Conclusion

This chapter has given you the basics of classes and objects in Scala, and shown you how to compile and run applications. In the next chapter, you’ll learn about Scala’s basic types and how to use them.