Книга: Learn Scala for Java Developers
Назад: Classes and Fields
Дальше: Inheritance

Classes and Objects

In this chapter we’ll look at:

Classes Without Constructor Arguments

Let’s begin by looking at how we create fields within classes without defining them on the class definition line. If you were to create a class in Scala with no fields defined on the class definition, like this:

  // scala   class Counter 

…the Scala compiler would still generate a primary constructor with no arguments, a lot like Java’s default constructor. So the Java equivalent would look like this:

  // java   public class Counter {       public Counter() {       }   } 

In Java you might initialise a variable and create some methods.

  // java   public class Counter {          private int count = 0;          public Counter() {       }          public void increment() {           count++;       }          public int getCount() {           return count;       }   } 

You can do the same in Scala.

  // scala   class Counter {     private var count = 0        def increment() {   // brackets to denote this is a "mutator"       count += 1     }        def getCount = count   }     

Within the primary constructor (i.e., not in the class definition but immediately afterwards in the class body), the val and var keywords will affect the bytecode like this:

Declared in primary constructor val x var x x private val x private var x
Getter (x()) Y (public) Y (public) N/A Y (private) Y (private)
Setter (x_=(y)) N Y (public) N/A N Y (private)

As you can see, this is consistent with the table we saw earlier. Getters are generated by default for val and var types and will all be public. Adding private to the field declaration will make the generated fields private and setters are only generated for vars (which are, again, public by default).

Additional Constructors

Let’s create an alternative Java version of our Customer class, this time with additional constructors.

  // java   public class Customer {        private final String fullname;        public Customer(String forename, String initial, String surname) {           this.fullname =               String.format("%s %s. %s", forename, initial, surname);       }        public Customer(String forename, String surname) {           this(forename, "", surname);       }   } 

We’ve defaulted the customer’s initial and allowed clients to choose if they want to supply it.

We should probably tidy up the main constructor to reflect the fact that the variable could come through as an empty string. We’ll add an if-condition and format the string depending on the result.

  // java   public class Customer {       private final String fullname;        public Customer(String forename, String initial, String surname) {           if (initial != null && !initial.isEmpty())               this.fullname =                   String.format("%s %s. %s", forename, initial, surname);           else               this.fullname = String.format("%s %s", forename, surname);       }        public Customer(String forename, String surname) {           this(forename, "", surname);       }        public static void main(String... args) {           System.out.println(new Customer("Bob", "J", "Smith").fullname);           System.out.println(new Customer("Bob", "Smith").fullname);       }   } 

Creating additional or auxiliary constructors in Scala is just a matter of creating methods called this. The one constraint is that each auxiliary constructor must call another constructor using this on its first line. That way, constructors will always be chained, all the way to the top.

Scala has the notion of a primary constructor; it’s the code in the class body. Any parameters passed in from the class definition are available to it and if you don’t write any auxiliary constructors, the class will still have a constructor; it’s the implicit primary constructor.

  // scala   class Customer(forename: String, initial: String, surname: String) {     // primary constructor   } 

So, if we create a field within the primary constructor and assign it some value,

  // scala   class Customer(forename: String, initial: String, surname: String) {     val fullname = String.format("%s %s. %s", forename, initial, surname)   } 

…it would be equivalent to the following Java:

  // java   public class Customer {       private final String fullname;        public Customer(String forename, String initial, String surname) {           this.fullname =               String.format("%s %s. %s", forename, initial, surname);       }   } 

If we can add an another auxiliary constructor to the Scala version, we can refer to this to chain the call to the primary constructor.

  // scala   class Customer(forename: String, initial: String, surname: String) {     val fullname = String.format("%s %s. %s", forename, initial, surname)      def this(forename: String, surname: String) {       this(forename, "", surname)     }   } 

Using Default Values

Scala has language support for default values on method signatures, so we could have written this using just parameters on the class definition, and avoided the extra constructor. We’d just default the value for initial to be an empty string. To make the implementation handle empty strings better, we can put some logic in the primary constructor like before.

  class Customer(forename: String, initial: String = "", surname: String) {     val fullname = if (initial != null && !initial.isEmpty)       forename + " " + initial + ". " + surname     else       forename + " " + surname   } 

When calling it, we may need to name default values; for example:

  new Customer("Bob", "J", "Smith") 

"Bob", "J", "Smith" is ok, but if we skip the initial variable, we’d need to name the surname variable like this:

  new Customer("Bob", surname = "Smith") 

Singleton Objects

In Java you can enforce a single instance of a class using the singleton pattern. Scala has made this idea as a feature of the language: as well as classes, you can define (singleton) objects.

The downside is that when we talk about “objects” in Scala, we’re overloading the term. We might mean an instance of a class (for example, a new ShoppingCart(), of which there could be many) or we might mean the one and only instance of a class; that is, a singleton object.

A typical use-case for a singleton in Java is if we need to use a single logger instance across an entire application.

  // java   Logger.getLogger("example").log(INFO, "Everything is fine."); 

We might implement the singleton like this:

  // java   public final class Logger {        private static final Logger INSTANCE = new Logger();        private Logger() { }        public static Logger getLogger() {           return INSTANCE;       }        public void log(Level level, String string) {           System.out.printf("%s %s%n", level, string);       }   } 

We create a Logger class, and a single static instance of it. We prevent anyone else creating one by using a private constructor. We then create an accessor to the static instance, and finally give it a rudimentary log method. We’d call it like this:

  // java   Logger.getLogger().log(INFO, "Singleton loggers say YEAH!"); 

A more concise way to achieve the same thing in Java would be to use an enum.

  // java   public enum LoggerEnum {        LOGGER;        public void log(Level level, String string) {           System.out.printf("%s %s%n", level, string);       }   } 

We don’t need to use an accessor method; Java ensures a single instance is used and we’d call it like this:

  // java   LOGGER.log(INFO, "An alternative example using an enum"); 

Either way, they prevent clients newing up an instance of the class and provide a single, global instance for use.

The Scala equivalent would look like this:

  // scala   object Logger {     def log(level: Level, string: String) {       printf("%s %s%n", level, string)     }   } 

The thing to notice here is that the singleton instance is denoted by the object keyword rather than class. So we’re saying “define a single object called Logger” rather than “define a class”.

Under the covers, Scala is creating basically the same Java code as our singleton pattern example. You can see this when we decompile it.

 1   // decompiled from scala to java  2   public final class Logger$ {  3       public static final Logger$ MODULE$;  4   5       public static {  6           new scala.demo.singleton.Logger$();  7       }  8   9       public void log(Level level, String string) { 10           Predef..MODULE$.printf("%s %s%n", (Seq)Predef..MODULE$ 11             .genericWrapArray((Object)new Object[]{level, string})); 12       } 13  14       private Logger$() { 15           Logger$.MODULE$ = this; 16       } 17   } 

There are some oddities in the log method, but that’s the decompiler struggling to decompile the bytecode, and generally how Scala goes about things. In essence though, it’s equivalent; there’s a private constructor like the Java version, and a single static instance of the object. The class itself is even final.

There’s no need to new up a new Logger; Logger is already an object, so we can refer to it directly. In fact, you couldn’t new one up if you wanted to, because there’s no class definition and so no class to new up.

Incidentally, you replicate Java’s static main method by adding a main method to a Scala singleton object, not a class.

Companion Objects

You can combine objects and classes in Scala. When you create a class and an object with the same name in the same source file, the object is known as a companion object.

Scala doesn’t have a static keyword but members of singleton objects are effectively static. Remember that a Scala singleton object is just that, a singleton. Any members it contains will therefore be reused by all clients using the object; they’re globally available just like statics.

You use companion objects where you would mix statics and non-statics in Java.

The Java version of Customer has fields for the customer’s name and address, and an ID to identify the customer uniquely.

  // java   public class Customer {        private final String name;       private final String address;        private Integer id;        public Customer(String name, String address) {           this.name = name;           this.address = address;       }   } 

Now we may want to create a helper method to create the next ID in a sequence. To do that globally, we create a static field to capture a value for the ID and a method to return and increment it. We can then just call the method on construction of a new instance, assigning its ID to the freshly incremented global ID.

  // java   public class Customer {        private static Integer sequenceOfIds;        private final String name;       private final String address;        private Integer id;        public Customer(String name, String address) {           this.name = name;           this.address = address;           this.id = Customer.nextId();       }        private static Integer nextId() {           return sequenceOfIds++;       }    } 

It’s static because we want to share its implementation among all instances to create unique IDs for each.

In Scala, we’d separate the static from non-static members and put the statics in the singleton object and the rest in the class. The singleton object is the companion object to Customer.

We create our class with the two required fields and in the singleton object, create the nextId method. Next we create a private var to capture the current value, assigning it the value of zero so Scala can infer the type as an Integer. Adding a val here means no setter will be generated, and adding the private modifier means the generated getter will be private. We finish off by implementing the increment in the nextId method and calling it from the primary constructor.

  // scala   class Customer(val name: String, val address: String) {     private val id = Customer.nextId()   }    object Customer {     private var sequenceOfIds = 0      private def nextId(): Integer = {       sequenceOfIds += 1       sequenceOfIds     }   } 

The singleton object is a companion object because it has the same name and lives in the same source file as its class. This means the two have a special relationship and can access each other’s private members. That’s how the Customer object can define the nextId method as private but the Customer class can still access it.

If you were to name the object differently, you wouldn’t have this special relationship and wouldn’t be able to call the method. For example, the class CustomerX object below is not a companion object to Customer and so can’t see the private nextId method.

   // scala    class Customer(val name: String, val address: String) {      private val id = CustomerX.nextId()         // compiler failure    }     object CustomerX {      private var sequenceOfIds = 0       private def nextId(): Integer = {        sequenceOfIds += 1        sequenceOfIds      }    } 

Other Uses for Companion Objects

When methods don’t depend on any of the fields in a class, you can more accurately think of them as functions. Functions generally belong in a singleton object rather than a class, so one example of when to use companion objects is when you want to distinguish between functions and methods, but keep related functions close to the class they relate to.

Another reason to use a companion object is for factory-style methods — methods that create instances of the class companion. For example, you might want to create a factory method that creates an instance of your class but with less noise. If we want to create a factory for Customer, we can do so like this:

  // scala   class Customer(val name: String, val address: String) {     val id = Customer.nextId()   }    object Customer {     def apply(name: String, address: String) = new Customer(name, address)   } 

The apply method affords a shorthand notation for a class or object. It’s kind of like the default method for a class, so if you don’t call a method directly on an instance, but instead match the arguments of an apply method, it’ll call it for you. For example, you can call:

  Customer.apply("Bob Fossil", "1 London Road") 

…or you can drop the apply and Scala will look for an apply method that matches your argument. The two are identical.

  Customer("Bob Fossil", "1 London Road") 

You can still construct a class using the primary constructor and new, but implementing the companion class apply method as a factory means you can be more concise if you have to create a lot of objects.

You can even force clients to use your factory method rather than the constructor by making the primary constructor private.

  class Customer private (val name: String, val address: String) {     val id = Customer.nextId()   } 

The Java analog would have a static factory method, for example createCustomer, and a private constructor ensuring everyone is forced to use the factory method.

  // java   public class Customer {        private static Integer sequenceOfIds;        private final String name;       private final String address;        private Integer id;        public static Customer createCustomer(String name, String address) {           return new Customer(name, address);       }        private Customer(String name, String address) {           this.name = name;           this.address = address;           this.id = Customer.nextId();       }        private static Integer nextId() {           return sequenceOfIds++;       }    } 
Назад: Classes and Fields
Дальше: Inheritance