In our previous article we covered very basic concepts on traits. In this article we will expand on our initial work and explore the inherent power of traits.
As we said here just like the Interfaces traits can have abstract methods. Also traits can extend other traits just like Interfaces can extend other interfaces.
also read:
trait Reader{ def read(source:String):String } trait StringReader extends Reader { override def read(source:String) = { Source.fromString(source).mkString } }
We have seen previously about adding trait to the class declaration, but that’s not the only way to add a trait. One can mix in a trait during the object creation as well. But before that lets modify the Reader trait to make the method read to return some default string.
Now lets look at an example to mixin a trait while creating object:
class Person(var name:String, var age:Int){ def getDetails = name+ " "+ age } class Student(name:String, age:Int, var moreDetails:String) extends Person(name,age) with Reader{ override def getDetails = { val details = read(moreDetails) "Student details\n"+name+ " "+age+"\n"+"More: "+details } }
Lets create an instance of above Student class without including the StringReader trait.
object Main extends App{ val student1 = new Student("Sana", 20, "About the student") println(student1.getDetails) }
The output would be:
Student details Sana 20 More: DEFAULT
Really not useful, the Reader trait is not enough, so we make use of adding a trait during object creation:
object Main extends App{ val student1 = new Student("Sana", 20, "About the student") with StringReader println(student1.getDetails) }
The output:
Student details Sana 20 More: About the student
Now we have more meaningful information and not the default implementation. Traits can also have fields- fields can be concrete and abstract. If an initial value is provided for the field in the trait then it becomes a concrete field, otherwise it is an abstract field, something like:
import scala.io.Source trait Reader{ var source = "DEFAULT" def read = source } trait StringReader extends Reader { override def read = { Source.fromString(source).mkString } } class Person(var name:String, var age:Int){ def getDetails = name+ " "+ age } class Student(name:String, age:Int, moreDetails:String) extends Person(name,age) with Reader{ source = moreDetails override def getDetails = { val details = read "Student details\n"+name+" "+age+"\n"+"More: "+details } } object Main extends App{ val student1 = new Student("Sana", 20, "About the student") with StringReader println(student1.getDetails) }
We just edited the trait and moved the parameter for read method into the field for the trait. And in the class Student we assign a new value to the source field in the trait.
Layering Traits
One can chain the traits such that one trait can invoke another version of the same method in a different trait. Lets add 2 more traits- FileReader and UrlReader. FileReader would read from a file and UrlReader would read content from a given URL.
trait FileReader extends Reader{ override def read(source:String) = { Source.fromFile(source,"UTF-8").mkString } } trait UrlReader extends Reader{ override def read(source:String) = { Source.fromURL(super.read(source),"UTF-8").mkString } }
Interesting to see super.read(source) in the UrlReader trait. Does that mean it inokves the read(source) in Reader trait? We wouldn’t expect anything useful from the read(source) version of Reader method. Instead, super.read(source) calls the next trait in the trait hierarchy, which depends on the order in which the traits are added. The traits are processed starting with the last one. Lets see how this makes sense:
object Main extends App{ //case 1 val student2 = new Student("Stud1",20, "/tmp/url") with FileReader with UrlReader println(student2.getDetails) //case 2 val student3 = new Student("Stud2",20,"https://javabeat.net") with StringReader with UrlReader println(student3.getDetails) }
In the case 1 we add FileReader and UrlReader traits. When UrlReader invokes super.read(source), the read(source) from the FileReader is invoked and you would expect to have a URL in the /tmp/url file.
In the case 2 we add StringReader and UrlReader traits. When the UrlReader invokes super.read(source), the read(source) from the StringReader is invoked.
The example above seems pretty naive and can be implemented in a more concise way. I havent been able to come up with a better example. But I hope I have been able to convey the concept though.
Another interesting concept to explore is how the traits are mapped to the classes which the JVM can consume.
A trait with abstract method:
trait Reader{ def read(source:String):String }
translates to a usual Interface in Java
Compiled from "TraitTrans.scala" public interface Reader { public abstract java.lang.String read(java.lang.String); }
A trait with method definition would translate into a Interface and a abstract class which has the implementations in the trait moved into static methods. Something like
trait Reader{ def read(source:String):String } trait StringReader extends Reader{ import scala.io.Source def read(source:String) = Source.fromString(source).mkString }
would create StringReader.class and StringReader$class.class files where in the StringReader.class is the interface and StringReader$class.class is an abstract class with the implementations in the static methods
mohamed@mohamed-Aspire-4710:~/scalaP$ javap -c StringReader.class Compiled from "TraitTrans.scala" public abstract class StringReader$class { public static java.lang.String read(StringReader, java.lang.String); Code: 0: getstatic #11 // Field scala/io/Source$.MODULE$:Lscala/io/Source$; 3: aload_1 4: invokevirtual #16 // Method scala/io/Source$.fromString:(Ljava/lang/String;)Lscala/io/Source; 7: invokeinterface #22, 1 // InterfaceMethod scala/collection/TraversableOnce.mkString:()Ljava/lang/String; 12: areturn public static void $init$(StringReader); Code: 0: return }
One can make out that the companion class generated contains the method implementations. Here is a superb description of how the traits and classes extending traits get translated to the classfiles for the JVM.
These are few concepts which are worth learning as part of Traits. Another important concept is the Trait construction order and Self Types which I might cover in future posts.