|
Step 11. Understand classes and singleton objects
Up to this point you've written Scala scripts to try out the concepts presented in this article. For all but the simplest projects, however, you will likely want to partition your application code into classes. To give this a try, type the following code into a file called greetSimply.scala:
// In greetSimply.scala
class SimpleGreeter {
val greeting = "Hello, world!"
def greet() = println(greeting)
}
val g = new SimpleGreeter
g.greet()
greetSimply.scala is actually a Scala script, but one that contains a class definition. This first, example, however, illustrates that as in Java, classes in Scala encapsulate fields and methods. Fields are defined with either val or var. Methods are defined with def. For example, in class SimpleGreeter, greeting is a field and greet is a method. To use the class, you initialize a val named g with a new instance of SimpleGreeter. You then invoke the greet instance method on g. If you run this script with scala greetSimply.scala, you will be dazzled with yet another Hello, world!.
Although classes in Scala are in many ways similar to Java, in several ways they are quite different. One difference between Java and Scala involves constructors. In Java, classes have constructors, which can take parameters, whereas in Scala, classes can take parameters directly. The Scala notation is more concise—class parameters can be used directly in the body of the class; there’s no need to define fields and write assignments that copy constructor parameters into fields. This can yield substantial savings in boilerplate code; especially for small classes. To see this in action, type the following code into a file named greetFancily.scala:
// In greetFancily.scala
class FancyGreeter(greeting: String) {
def greet() = println(greeting)
}
val g = new FancyGreeter("Salutations, world")
g.greet
Instead of defining a constructor that takes a String, as you would do in Java, in greetFancily.scala you placed the greeting parameter of that constructor in parentheses placed directly after the name of the class itself, before the open curly brace of the body of class FancyGreeter. When defined in this way, greeting essentially becomes a value (not a variable—it can't be reassigned) field that's available anywhere inside the body. In fact, you pass it to println in the body of the greet method. If you run this script with the command scala greetFancily.scala, it will inspire you with:
Salutations, world!
This is cool and concise, but what if you wanted to check the String passed to FancyGreeter's primary constructor for null, and throw NullPointerException to abort the construction of the new instance? Fortunately, you can. Any code sitting inside the curly braces surrounding the class definition, but which isn't part of a method definition, is compiled into the body of the primary constructor. In essence, the primary constructor will first initialize what is essentially a final field for each parameter in parentheses following the class name. It will then execute any top-level code contained in the class's body. For example, to check a passed parameter for null, type in the following code into a file named greetCarefully.scala:
// In greetCarefully.scala
class CarefulGreeter(greeting: String) {
if (greeting == null) {
throw new NullPointerException("greeting was null")
}
def greet() = println(greeting)
}
new CarefulGreeter(null)
In greetCarefully.scala, an if statement is sitting smack in the middle of the class body, something that wouldn't compile in Java. The Scala compiler places this if statement into the body of the primary constructor, just after code that initializes what is essentially a final field named greeting with the passed value. Thus, if you pass in null to the primary constructor, as you do in the last line of the greetCarefully.scala script, the primary constructor will first initialize the greeting field to null. Then, it will execute the if statement that checks whether the greeting field is equal to null, and since it is, it will throw a NullPointerException. If you run greetCarefully.scala, you will see a NullPointerException stack trace.
In Java, you sometimes give classes multiple constructors with overloaded parameter lists. You can do that in Scala as well, however you must pick one of them to be the primary constructor, and place those constructor parameters directly after the class name. You then place any additional auxiliary constructors in the body of the class as methods named this. To try this out, type the following code into a file named greetRepeatedly.scala:
// In greetRepeatedly.scala
class RepeatGreeter(greeting: String, count: Int) {
def this(greeting: String) = this(greeting, 1)
def greet() = {
for (i <- 1 to count)
println(greeting)
}
}
val g1 = new RepeatGreeter("Hello, world", 3)
g1.greet()
val g2 = new RepeatGreeter("Hi there!")
g2.greet()
RepeatGreeter's primary constructor takes not only a String greeting parameter, but also an Int count of the number of times to print the greeting. However, RepeatGreeter also contains a definition of an auxiliary constructor, the this method that takes a single String greeting parameter. The body of this constructor consists of a single statement: an invocation of the primary constructor parameterized with the passed greeting and a count of 1. In the final four lines of the greetRepeatedly.scala script, you create two RepeatGreeters instances, one using each constructor, and call greet on each. If you run greetRepeatedly.scala, it will print:
Hello, world
Hello, world
Hello, world
Hi there!
Another area in which Scala departs from Java is that you can't have any static fields or methods in a Scala class. Instead, Scala allows you to create singleton objects using the keyword object. A singleton object cannot, and need not, be instantiated with new. It is essentially automatically instantiated the first time it is used, and as the “singleton” in its name implies, there is ever only one instance. A singleton object can share the same name with a class, and when it does, the singleton is called the class's companion object. The Scala compiler transforms the fields and methods of a singleton object to static fields and methods of the resulting binary Java class. To give this a try, type the following code into a file named WorldlyGreeter.scala:
// In WorldlyGreeter.scala
// The WorldlyGreeter class
class WorldlyGreeter(greeting: String) {
def greet() = {
val worldlyGreeting = WorldlyGreeter.worldify(greeting)
println(worldlyGreeting)
}
}
// The WorldlyGreeter companion object
object WorldlyGreeter {
def worldify(s: String) = s + ", world!"
}
In this file, you define both a class, with the class keyword, and a companion object, with the object keyword. Both types are named WorldlyGreeter. One way to think about this if you are coming from a Java programming perspective is that any static methods that you would have placed in class WorldlyGreeter in Java, you'd put in singleton object WorldlyGreeter in Scala. In fact, when the Scala compiler generates bytecodes for this file, it will create a Java class named WorldlyGreeter that has an instance method named greet (defined in the WorldlyGreeter class in the Scala source) and a static method named worldify (defined in the WorldlyGreeter companion object in Scala source). Note also that in the first line of the greet method in class WorldlyGreeter, you invoke the singleton object's worldify method using a syntax similar to the way you invoke static methods in Java: the singleton object name, a dot, and the method name:
// Invoking a method on a singleton object from class WorldlyGreeter
// ...
val worldlyGreeting = WorldlyGreeter.worldify(greeting)
// ...
To run this code, you'll need to create an application. Type the following code into a file named WorldlyApp.scala:
// In WorldlyApp.scala
// A singleton object with a main method that allows
// this singleton object to be run as an application
object WorldlyApp {
def main(args: Array[String]) {
val wg = new WorldlyGreeter("Hello")
wg.greet()
}
}
Because there's no class named WorldlyApp, this singleton object is not a companion object. It is instead called a stand-alone. object. Thus, a singleton object is either a companion or a stand-alone object. The distinction is important because companion objects get a few special privileges, such as access to private members of the like-named class.
One difference between Scala and Java is that whereas Java requires you to put a public class in a file named after the class—for example, you'd put class SpeedRacer in file SpeedRacer.java—in Scala, you can name .scala files anything you want, no matter what Scala classes or code you put in them. In general in the case of non-scripts, however, it is recommended style to name files after the classes they contain as is done in Java, so that programmers can more easily locate classes by looking at file names. This is the approach we've taken with the two files in this example, WorldlyGreeter.scala and WorldlyApp.scala.
Neither WorldlyGreeter.scala nor WorldlyApp.scala are scripts, because they end in a definition. A script, by contrast, must end in a result expression. Thus if you try to run either of these files as a script, for example by typing:
scala WorldlyGreeter.scala # This won't work!
The Scala interpreter will complain that WorldlyGreeter.scala does not end in a result expression. Instead, you'll need to actually compile these files with the Scala compiler, then run the resulting class files. One way to do this is to use scalac, which is the basic Scala compiler. Simply type:
scalac WorldlyApp.scala WorldlyGreeter.scala
Given that the scalac compiler starts up a new JVM instance each time it is invoked, and that the JVM often has a perceptible start-up delay, the Scala distribution also includes a Scala compiler daemon called fsc (for fast Scala compiler). You use it like this:
fsc WorldlyApp.scala WorldlyGreeter.scala
The first time you run fsc, it will create a local server daemon attached to a port on your computer. It will then send the list of files to compile to the daemon via the port, and the daemon will compile the files. The next time you run fsc, the daemon will already be running, so fsc will simply send the file list to the daemon, which will immediately compile the files. Using fsc, you only need to wait for the the JVM to startup the first time. If you ever want to stop the fsc daemon, you can do so with fsc -shutdown.
Running either of these scalac or fsc commands will produce Java class files that you can then run via the scala command, the same command you used to invoke the interpreter in previous examples. However, instead of giving it a filename with a .scala extension containing Scala code to interpret6 as you did in every previous example, in this case you'll give it the name of a class containing a main method. Similar to Java, any Scala class with a main method that takes a single parameter of type Array[String] and returns Unit7 can serve as the entry point to an application. In this example, WorldlyApp has a main method with the proper signature, so you can run this example by typing:
scala WorldlyApp
At which point you should see:
Hello, world!
You may recall seeing this output previously, but this time it was generated in this interesting manner:
* The scala program fires up a JVM with the WorldlyApp's main method as the entry point.
* WordlyApp's main method creates a new WordlyGreeter instance via new, passing in the string "Hello" as a parameter.
* Class WorldlyGreeter's primary constructor essentially initializes a final field named greeting with the passed value, "Hello" (this initialization code is automatically generated by the Scala compiler).
* WordlyApp's main method initializes a local \@val@ named wg with the new WorldlyGreeter instance.
* WordlyApp's main method then invokes greet on the WorldlyGreeter instance to which wg refers.
* Class WordlyGreeter's greet method invokes worldify on singleton object WorldlyGreeter, passing along the value of the final field greeting, "Hello".
* Companion object WorldlyGreeter's worldify method returns a String consisting of the value of a concatenation of the s parameter, which is "Hello", and the literal String ", world!".
* Class WorldlyGreeter's greet method then initializes a \@val@ named worldlyGreetingwithplaces the "Hello, world!" String returned from the worldify method.
* Class WorldlyGreeter's greet method passes the "Hello, world!" String to which worldlyGreeting refers to println, which sends the cheerful greeting, via the standard output stream, to you. |
|