Scala and Java collaboration

Going back to the REPL, we are going to experiment further with mixing Scala and Java to explore some common integration needs, and in particular, testing and manipulating the Java REST API that we have built at the beginning of the chapter.

As a reminder on how to restart the REPL from the hello-scala project introduced in Chapter 1, Programming Interactively within Your Project, if you closed it in the meantime, just start a new terminal window, navigate to the root of the hello-scala project, and enter the following command in the command prompt:

> ./activator console

Converting between collection types

Let's start by comparing Java and Scala collection classes and see how we can go from one to the other. For instance, a Scala List (from the scala.collection.immutable package) is different from java.util.List, and sometimes, it can be useful to convert from one to the other. A convenient way in Java to create java.util.List is to use the java.util.Arrays utility method, asList, whose exact signature is public static<T> List<T> asList(T... a), where T refers to a generic type. Let's import it in the REPL as follows:

scala> import java.util.Arrays
import java.util.Arrays

As the JDK classes are in the class path, they can be directly accessed into the REPL as shown in the following code snippet:

scala> val javaList = Arrays.asList(1,2,3,4)
javaList: java.util.List[Int] = [1, 2, 3, 4]

Now that we have instantiated a Java list of integers, we want to convert it to its Scala equivalent and need to import the JavaConverters classes for that using the following lines of command:

scala> import scala.collection.JavaConverters._
import scala.collection.JavaConverters._

Looking at the documentation of JavaConverters in Scaladoc (which is similar to Javadoc, used to document Scala APIs, and available online at www.scala-lang.org/api/current/index.html), we can see, for example, that the equivalent of java.util.List is scala.collection.mutable.Buffer. So, if we invoke the asScala method on java.util.List, we will get exactly that:

scala> val scalaList = javaList.asScala
scalaList: scala.collection.mutable.Buffer[Int] = Buffer(1, 2, 3, 4)

Now, by invoking the asJava method on scalaList, we will get back our original java.util.List collection:

scala> val javaListAgain = scalaList.asJava
javaListAgain: java.util.List[Int] = [1, 2, 3, 4]

A good test to verify that we get back the original object after converting it to a target type and back again is to use an assert statement, as shown in the following command:

scala> assert( javaList eq javaListAgain)
[no output]

Having no output from assert means that it evaluated to True; otherwise, we would get a stack trace that shows why they are not equal. You might wonder where this assert method comes from; assert is a method of the Predef class, a Scala class imported by default containing useful aliases for commonly used types, assertions like the one we have used, and simple functions for console's I/O and implicit conversions.

JavaBean-style properties

To ensure compatibility with Java frameworks such as Hibernate or JMX, you may sometimes need Java-style getters and setters on the fields of your class. For example, if we declare a Company class in the REPL as follows:

scala> class Company(var name:String)
defined class Company

We have seen in Chapter 1, Programming Interactively within Your Project, that Scala accessor methods to read and mutate the name field are name and name_=, respectively, as shown in the following commands:

scala> val sun = new Company("Sun Microsystems")
sun: Company = Company@55385db5
scala> sun.name
res33: String = Sun Microsystems
scala> sun.name_=("Oracle")
[no output is returned]
scala> sun.name
res35: String = Oracle

A straightforward way to have Java-style getters and setters is to annotate the field with scala.beans.BeanProperty as shown in the following lines of command:

scala> import scala.beans.BeanProperty
import scala.beans.BeanProperty
scala> class Company(@BeanProperty var name:String)
defined class Company
scala> val sun = new Company("Sun Microsystems")
sun: Company = Company@42540cca
scala> sun.getName()
res36: String = Sun Microsystems
scala> sun.setName("Oracle")
[no output is returned]
scala> sun.name  (alternatively sun.getName)
res38: String = Oracle

Scala and Java object orientation

The interoperability between Scala and Java classes makes it very straightforward to replace or extend an existing Java class with a Scala class. Compiling a Scala class produces bytecode that is pretty similar to what Java produces. For example, let's take a shorter version of the Customer Java class we generated earlier:

public class Customer {

    private Integer customerId;
    private String zip;

    public Customer(Integer customerId) {
        this.customerId = customerId;
    }

    public Customer(Integer customerId, String zip) {
        this.customerId = customerId;
        this.zip = zip;
    }
    public Integer getCustomerId() {
        return customerId;
    }

    public void setCustomerId(Integer customerId) {
        this.customerId = customerId;
    }

    public String getZip() {
        return zip;
    }

    public void setZip(String zip) {
        this.zip = zip;
    }
}

If we refactor it into a Scala class with class parameters and create an instance, we get the following in the REPL:

scala> class Customer ( var customerId: Int, var zip: String) {
         def getCustomerId() = customerId
         def setCustomerId(cust: Int): Unit = {
           customerId = cust
         }
       }
defined class Customer
scala> val customer = new Customer(1, "123 45")
customer: Customer = Customer@13425838
scala> customer.zip
res5: String = 123 45

However, a constructor that takes only a single zip parameter does not exist in this definition:

scala> val otherCustomer = new Customer("543 21")
<console>:8: error: not enough arguments for constructor Customer: (customerId: Int, zip: String)Customer.
Unspecified value parameter zip.
       val otherCustomer = new Customer("543 21")
                           ^

To complete our refactoring of the Java class, we need an extra constructor as follows:

scala> class Customer ( var customerId: Int, var zip: String) {
     |   def this( zip: String) = this(0,zip)
     |   def getCustomerId() = customerId
     |   def setCustomerId(cust: Int): Unit = {
     |     customerId = cust
     |   }
     | }
defined class Customer
scala> val customer = new Customer("123 45")
customer: Customer = Customer@7944cdbd

This time, we were able to create an instance with the auxiliary constructor, which obeys to a couple of rules. They are as follows:

  • Any auxiliary constructor must immediately call another this(…) constructor
  • The primary constructor has to be called in the end to make sure all the parameters are initialized

Scala traits as enhanced Java interfaces

Software interfaces are useful mechanisms to make a piece of code interact via a contract to other external software systems, isolating the specification of what it does from its implementation. Although Java classes on the JVM have the limitation to only extend one single class, they can have multiple types by implementing several interfaces. However, Java interfaces are purely abstract, that is, they contain only constants, method signatures, and nested types, but no method bodies; for example, see the following code snippet:

interface VIPCustomer {
  Integer discounts();
}

In contrast, Scala traits are more powerful by allowing partial implementation of method bodies and therefore, more reusable. One can use a trait to mix in behavior into a class. Let's take an example in the REPL:

scala> class Customer(val name:String, val discountCode:String="N" ){
     |   def discounts() : List[Int] = List(5)
     |   override def toString() = "Applied discounts: " +
     |     discounts.mkString(" ","%, ","% ") 
     | }
defined class Customer

This class declares two fields, name and discountCode (initialized to "N" for normal), as well as two methods, discounts() and toString(), where discounts() accumulates discounts for a customer into List of integers (initialized to a 5 percent discount) and toString() displays it.

We can define a couple of traits that extends the class we just created:

scala> trait VIPCustomer extends Customer {
     |   override def discounts = super.discounts ::: List(10)
     | }
defined trait VIPCustomer

A VIPCustomer class is a customer who gets an extra 10 percent discount concatenated to the list of all of the already available discounts he/she has. The second trait is given as follows:

scala> trait GoldCustomer extends Customer {
     |   override def discounts =
     |     if (discountCode.equals("H"))
     |       super.discounts ::: List(20)
     |     else super.discounts ::: List(15)
       }
defined trait GoldCustomer

A GoldCustomer class is a customer who gets an additional 15 percent discount or even 20 percent if her rating, that is, discountCode is "H" (high).

Let's now write a Main class to show the addition of stackable traits when instantiating the Customer class. We use the with keyword to mix in these additional behaviors into the class as shown in the following lines of command:

scala> object Main {
     |   def main(args: Array[String]) {
     |     val myDiscounts = new Customer("Thomas","H") with
     |       VIPCustomer with GoldCustomer
     |     println(myDiscounts)
     |   }
     | }
defined module Main

We can now simply execute the main method and get the expected result as follows:

scala> Main.main(Array.empty)
Applied discounts:  5%, 10%, 20%

Note that the order in which traits are stacked is important. They are calling each other from right to left. GoldCustomer is, therefore, the first one to be called.

Traits lie between interfaces and abstract classes. However, you can only extend one abstract class whereas you can extend several traits.

Declaring objects

Java code often refers to the static keyword to refer to singleton methods and constants. Scala does not support the static identifier, but instead provides the notion of object in place of the class declaration. If you need to refactor Java code into Scala, by simply using the object declaration instead of class, you get singleton instances and you're done, having the extra advantage that such Scala objects can also extend interfaces and traits. A simple example of object is the declaration of the Main program we exhibited earlier in the usage of stackable traits, or the following simple hello world application:

scala> object Main {
     |   def main(args: Array[String]) {
     |     println("Hello Scala World !")
     |   }
     | }
defined module Main

In addition to the notion of object, Scala provides the notion of companion object, which consists of an object that cohabits with a class of the same name in the same package and file. This is why it is called companion.

Introducing companion objects

The companion object enables storing of static methods and from this, you have full access to the classes' members, including private ones. It is, for example, a good place to declare static factory methods, and case classes overload the apply factory method so that you are not required to use the new keyword when creating case class instances:

scala> case class Customer(val name:String)
defined class Customer
scala> val thomas = Customer("Thomas")
thomas: Customer = Customer(Thomas)

However, you can still use the new keyword if you want to, shown as follows:

scala> val thomas = new Customer("Thomas")
thomas: Customer = Customer(Thomas)

Under the hood, the case class is constructed as a regular class that has, among other things, a companion object similar to the following (although simplified) code snippet:

object Customer {
  def apply()= new Customer("default name")
}
class Customer(name:String) {
…
}

Handling exceptions

We conclude this section about how to migrate code from Java to Scala with exceptions, a notion that appears everywhere in Java. In a quite similar way to Java, you can write the try { } catch { } blocks to capture method invocations that might fail. In Java, you would write something similar to the following code snippet:

package com.demo.sample;

public class ConversionSample {

  static Integer parse(String numberAsString) {
    Integer number = null;
    try {    
      number = Integer.parseInt(numberAsString);
    } catch (NumberFormatExceptionnfe) {
      System.err.println("Wrong format for "+numberAsString);
    } catch (Exception ex) {
      System.err.println("An unknown Error has occurred");
    }
    System.out.println("Parsed Number: "+number);
    return number;
  }
  public static void main(String[] args) {
    parse("2345");
    parse("23ab");
  }
}

The preceding code produces the following output:

run:
Parsed Number: 2345
Wrong format for number 23ab
Parsed Number: null
BUILD SUCCESSFUL (total time: 0 seconds)

In Scala, you could translate it directly to the equivalent code:

scala> def parse(numberAsString: String) = 
         try {
           Integer.parseInt(numberAsString)
         } catch {
           case nfe: NumberFormatException =>
             println("Wrong format for number "+numberAsString)
           case e: Exception => println("Error when parsing number"+
             numberAsString)
         }
parse: (numberAsString:String)AnyVal
scala> parse("2345")
res10: AnyVal = "2345"
scala> parse("23ab")
Wrong format for number 23ab
res11: AnyVal = ()

However, in this case, the return value inferred by the compiler is not only empty but also of the wrong type, AnyVal, which is the common type found between an Int value and whatever is returned by the exception. To make sure we get an integer as the output, we need to return an Int value from all the possible cases found in the catch block:

scala> def parse(numberAsString: String) = 
         try {
           Integer.parseInt(numberAsString)
         } catch {
           case nfe: NumberFormatException =>
             println("Wrong format for number "+numberAsString); -1
           case _: Throwable =>
             println("Error when parsing number "+numberAsString)
            -1
         }
parse: (numberAsString:String)Int

This time we can capture the correct return type from the parsing invocation as follows:

scala> val number = parse("23ab")
Wrong format for number 23ab
number: Int= -1

In all cases, we return an Int value, -1 in case of failure. This solution is still only partly satisfying as the caller does not really know the reason of failure unless we display/log it. A better way is to use, for example, an Either class that represents a value of one of the two possible types, where its instances are either of the scala.util.Left or scala.util.Right type. In this case, we can use the Left part to handle the failure and the Right part to handle a successful result as shown in the following code snippet:

scala> case class Failure(val reason: String)
defined class Failure
scala> def parse(numberAsString: String) : Either[Failure,Int] = 
         try {
           val result = Integer.parseInt(numberAsString)
           Right(result)
         } catch {
           case _ : Throwable => Left(Failure("Error when parsing number"))
         }
parse: (numberAsString:String)Either[Failure,Int]
scala> val number = parse("23ab")
number: Either[Failure,Int] = Left(Failure(Error when parsing number))
scala> val number = parse("2345")
number: Either[Failure,Int] = Right(2345)

Writing explicitly the return type will cause a compilation error on these types of errors, and therefore, is highly recommended.

Finally, without going into too much detail, there is an even more appropriate way of handling the try and catch blocks that are derived from Either using the scala.util.Try class. Instead of handling the exception as Left and Right, it returns Failure[Throwable] or Success[T], T being a generic type. The advantage of this approach is that it can be used in for comprehensions (but we have not covered them yet, examples will come in Chapter 5, Getting Started with the Play Framework). Moreover, the semantics of Try for error handling is better than Either as it describes Success or Failure rather than the less meaningful and more generic terms Left and Right.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.15.43