Beginning Scala study note(3) Object Orientation in Scala
1. The three principles of OOP are encapsulation(封装性), inheritance(继承性) and polymorphism(多态性).
example:
class Shape{ def area: Double = 0.0 } # supertype # subtypes class Rectangle(val width: Double, val height: Double) extends Shape{ override def area: Double = width*height } class Circle(val radius: Double){ override def area: Double = math.Pi*radius*radius }
A subtype is guaranteed to have all the members of the supertype. Changing the implementation of a method of the supertype is called overriding. In encapsulation, the fields of an object are accessible only through its methods.
example:
def draw(s: Shape) val circle = draw(new Circle(3)) val rectangle = draw(new Rectangle(2,3))
Inheritance guarantees that any method we could call on an instance of Shape will be defined in the subtypes. This is the heart of polymorphism.
2. Classes and Objects
(1) example:
scala> class Book # defined Book class, corresponding to java declaration: public class Book{ } defined class Book scala> :javap -c Book # decompiling the Book class Compiled from "Book.scala" public class Book implements scala.ScalaObject { public Book(); Code: 0: aload_0 1: invokespecial #10; // Method java/lang/Object."<init>":()V 4: return } scala> new Book # create an object or an instance of Book res1: Book = Book@9206d91 scala> new Book() res2: Book = Book@3d189f33
(2) Constructors
1) Constructors with Parameters
a. Parameter Declared as a val
In this case, Scala generates only a getter method for it, a setter method is not generated.
scala> class Book(val title: String) scala> :javap -c Book Compiled from "Book.scala" public class Book implements scala.ScalaObject { public java.lang.String title(); //...... public Book(java.lang.String); //...... }
Scala generates a title method, which you can use to access the field. The value of the field is immutable by definition, because of val type.
scala> val book = new Book("Beginning Scala") book: Book = Book@39a02a5d scala> book.title res2: String = Beginning Scala scala> book.title = "new title" <console>:9: error: reassignment to val book.title = "new title" ^
In Scala, if the constructor or method takes zero parameters, you can omit the parameter list.
b. Parameter Declared as a var
In this case, Scala generates both setter and getter method.
scala> class Book(var title: String) scala> :javap -c Book Compiled from "Book.scala" public class Book implements scala.ScalaObject { public java.lang.String title(); // ...... public void title_$eq(java.lang.String); // ...... public Book(java.lang.String); // ...... }
If you decompile this code, you can see the generated mutator method with an unusual name, title_$eq
scala> val book = new Book("Beginning Scala") book: Book = Book@5b375aff scala> book.title res0: String = Beginning Scala scala> book.title = "new title" book.title: String = new title
c. Parameter Declared as a private val or var
In this case, it can prevent getter and setter methods from being generated. The fields could only be accessed from within members of the class.
scala> class Book(private var title: String) scala> :javap -c Book Compiled from "Book.scala" public class Book implements scala.ScalaObject { public Book(java.lang.String); // ...... } scala> class Book(private var title: String){ | def printTitle {println(title)} | } defined class Book scala> val book = new Book("Beginning Scala") book: Book = Book@70f19734 scala> book.title <console>:10: error: variable title in class Book cannot be accessed in Book book.title ^ scala> book.printTitle Beginning Scala
d. Parameter Declared without val or var
Scala does not generate getter or setter when neither val nor var are specified on constructor parameters.
scala> class Book(title: String) defined class Book scala> val book = new Book("Beginning Scala") book: Book = Book@1222007 scala> book.title <console>:10: error: value title is not a member of Book book.title ^
The different Parameter with val and Parameter without val/var:
scala> class Book(private val title: String){ | def printTitle(b: Book){ | println(b.title) | } | } defined class Book scala> val book = new Book("Beginning Scala") book: Book = Book@659a1780 scala> book.printTitle(book) Beginning Scala scala> book.printTitle(new Book("Beginning Erlang")) Beginning Erlang
You can change the title of the Book because the title is a private field that is accessible to "this " object and the other objects of Book.
class Book(title: String){ def printTitle(b: Book){ println(b.title) } } scala> scala Book.scala Book.scala:3: error: value title is not a member of this.Book println(b.title) ^ one error found
You can provide a default value for a constructor parameter that gives other classes the option of specifying that parameter when calling the constructor.
scala> class Book(val title: String = "Beginning Scala") defined class Book scala> val book = new Book # you can call the constructor without specifying a title book: Book = Book@7a0ad359 scala> book.title res0: String = Beginning Scala scala> val book = new Book("new title") # you can specify the title value book: Book = Book@23a79657 scala> book.title res2: String = new title scala> val book = new Book(title="Beginning Scala") # you can also choose to provide named parameters book: Book = Book@5f6af31 scala> book.title res3: String = Beginning Scala
2) Auxiliary Constructor(辅助构造函数)
Auxiliary constructors are defined by creating methods named "this". In this way, you can define multiple auxiliary constructors, but they must have different signatures. Each auxiliary must begin with a call to a previously defined constructor.
scala> class Book(var title: String, var ISBN: Int){ | def this(title: String){ | this(title,222) | } | def this(){ | this("Beginning Scala") | this.ISBN=111 | } | override def toString = s"$title ISBN- $ISBN" | } defined class Book scala> val book1 = new Book book1: Book = Beginning Scala ISBN- 111 scala> val book2 = new Book("Beginning Erlang") book2: Book = Beginning Erlang ISBN- 222 scala> val book3 = new Book("Beginning Clojure",2222) book3: Book = Beginning Clojure ISBN- 2222
(3) Method Declaration
1) It is recommended to use type inferencing judiciously(明智而谨慎地); if it's not immediately obvious what the return type is, declare it explicitly.
def myMethod(): String = "Moof" def myOtherMethod() = "Moof" def foo(a: Int):String = a.toString def f2(a: Int, b: Boolean):String = if(b) a.toString else "false"
2) You can pass the type of a parameter or the return type as a parameter. The following code takes a parameter p and a type parameter T and returns a List of T.
scala> def list[T](p:T):List[T]=p::Nil list: [T](p: T)List[T] scala> list(10) res9: List[Int] = List(10) scala> list("Hello") res10: List[String] = List(Hello) scala> list(List("1")) res11: List[List[String]] = List(List(1))
3) If the last parameter is a variable-length argument(可变长度参数), it is a Seq of the type of the variable-length argument, so in this case the as parameter is a Seq[Int]:
scala> def largest(as: Int*): Int = as.reduceLeft((a,b)=> a max b) largest: (as: Int*)Int scala> largest(1) res13: Int = 1 scala> largest(2,3,99) res14: Int = 99 scala> largest(33,22,33,22) res15: Int = 33
You can mix type parameters with variable-length arguments:
scala> def mkString[T](as: T*):String = as.foldLeft("")(_ + _.toString) mkString: [T](as: T*)String scala> mkString(2) res31: String = 2 scala> mkString(1,2,3) res32: String = 123 scala> mkString(1,2,3.0) res33: String = 1.02.03.0
And you can put bounds on the type parameters. In this case, the types that are passed in must be Number or a subclass of Number:
scala> def sum[T <: Number](as: T*): Double = as.foldLeft(0d)(_ + _.doubleValue) sum: [T <: Number](as: T*)Double scala> sum() res50: Double = 0.0
4) Methods can be declared within any code scope, except at the top level, where classes, traits, and objects are declared. Methods can reference any variables in their scope.
import java.io.BufferedReader def readLines(br: BufferedReader) = { var ret: List[String] = Nil def readAll(): Unit = br.readLine match { case null => case s => ret ::= s ; readAll() } readAll() ret.reverse }
The readAll method is defined inside the scope of the readLines method, Thus readAll method has access to the variables br and ret. The readAll method calls a method on br, and it updates ret.
5) Method that override declared methods must include the override modifier, except overriding abstract methods.
scala> abstract class Base{ | def thing: String | } defined class Base scala> class One extends Base{ | def thing = "Moof" | } defined class One scala> val one = new One one: One = One@1bb62bd scala> one.thing res1: String = Moof scala> class Base{ | def thing: String = "" | } defined class Base scala> class One extends Base{ | def thing: String = "Moof" | } <console>:9: error: overriding method thing in class Base of type => String; method thing needs `override' modifier def thing: String = "Moof" ^
Methods that take no parameters and variables can be accessed the same way, and a val can override a def in a superclass.
scala> class Two extends One{ | override val thing = (new java.util.Date).toString | } defined class Two scala> val two = new Two two: Two = Two@38ec38ad scala> two.thing res3: String = Tue Jun 28 16:24:51 CST 2016 scala> class Three extends One{ | override lazy val thing = super.thing + (new java.util.Date).toString | } defined class Three scala> val three = new Three three: Three = Three@779122a0 scala> three.thing res4: String = MoofTue Jun 28 16:26:14 CST 2016
(4) Code Blocks
1) Methods and variables also can be defined in code blocks that are denoted by curly braces:{ }. Code blocks may be nested. The result of a code block is the last line evaluated in the code block.
scala> def meth3(): String = {"Moof"} meth3: ()String scala> meth3 res5: String = Moof scala> def meth4(): String = { | val d = new java.util.Date() | d.toString | } meth4: ()String scala> meth4 res6: String = Tue Jun 28 16:42:29 CST 2016
Variable definitions can be code blocks as well.
scala> val x3:String = { | val d = new java.util.Date | d.toString | } x3: String = Tue Jun 28 16:45:28 CST 2016
(5) Call-by-Name
1) Scala gives an additional mechanism for passing parameters to methods( and functions): call-by-name, which passes a code block to the callee.
scala> def nano() = { | println("Getting nano") | System.nanoTime | } nano: ()Long scala> nano Getting nano res7: Long = 9437790603685212 scala> def delayed(t: => Long) = { # call by name parameter(按名调用参数) | println("In delayed method") | println("Param:"+t) | t q | } delayed: (t: => Long)Long scala> delayed(nano) In delayed method Getting nano Param:9438187859989956 Getting nano res10: Long = 9438187860305135
This indicates that delayed is entered before the call to nano and that nano is called twice.
scala> def notDelayed(t: Long) = { | println("In notDelayed method") | println("Param: "+t) | t | } notDelayed: (t: Long)Long scala> notDelayed(nano) Getting nano In notDelayed method Param: 9438465403584919 res11: Long = 9438465403584919
nano is called before notDelayed is called because the parameter nano is calculated before notDelayed is called. This is the way Java programmers expect code to work.
(6) Method Invocation(方法调用)
instance.method() # java dot notation instance.method # if a method does not take any parameters instance.method(param) # a single parameter in java instance.method param # methods that take a single parameter can be invoked without dots or parentheses instance.method(p1,p2) # invoke multiparameter methods instance.method[Type Param](p1,p2) # the type param will be inferred by the compiler, also you can explicitly pass
Because Scala allows method names to contain symbols such as +,-,*,?, Scala's dotless method notation creates a syntactically neutral way of invoking methods that are hard-coded operators in Java.
scala> 2.1.*(4.3) res10: Double = 9.03 scala> 2.1*4.3 res11: Double = 9.03
(7) Objects
In Scala, you can use object to refer to an instance of a class or a keyword.
1) Singleton Objects
Scala does not have static members. Instead, Scala has singleton objects. A singleton is a class that can have only one instance.
scala> object Car{ # define car object | def drive {println("drive car")} | } defined module Car scala> Car.drive drive car scala> object Main extends App{ # Call car methods | Car.drive | } defined module Main
Singleton objects cannot take parameter. There are two ways to create a launching point for your application: define an object with a properly defined main method or define an object that extends the App trait.
object Book extends App{ println("Hello World") }
Note that in both approaches, Scala applications are launched from an object, not a class.
2) Companion Objects(伴生对象)
When an object shares a name with a class, it's called a companion object, and the class is called a companion class. A companion object is an object that shares the same name and source file with another class or trait. Using this approach lets you create static members on a class. The companion object is useful for implementing helper methods and factory.
We use a companion class Shape and a companion object Shape, which acts as a factory.
trait Shape { def area: Double } object Shape{ private class Circle (radius: Double) extends Shape{ override val area = 3.14*radius*radius } private class Rectangle(height: Double, length: Double) extends Shape{ override val area = height * length } def apply(height: Double, length: Double): Shape = new Rectangle(height,length) def apply(radius: Double): Shape = new Circle(radius) }
A singleton object that does not share the same name with a companion class is called a standalone object.
(8) Packaging and Imports
1) The following statement imports the contents of the scala.xml package:
import scala.xml._
Import statements are made in the scope of prior imports. The following statement imports the scala.xml.transform._ package:
import transform._
You can import more than one class or object from a single package, for example:
import sclala.collection.immutable.{TreeMap, TreeSet}
You can import a class or object and rename it.
import scala.util.parsing.json.{JSON=> JsonParser}
Import can be used inside any code block, and the import will be active only in the scope of that code block.
scala> class Frog{ | import scala.xml._ | def n:NodeSeq = NodeSeq.Empty | } defined class Frog
Scala's import statement can also import the methods of an object so that those methods can be used without explicit reference to the object that owns them.This is much like Java's static import.
scala> object Moose{ | def bark = "woof" | } defined object Moose scala> import Moose._ import Moose._ scala> bark res2: String = woof
(9) Inheritance
1) Scala supports single inheritance.
scala> class Vehicle(speed: Int){ | val mph: Int = speed | def race() = println("Racing") | } defined class Vehicle
The different extending class between Scala and Java are:
a. method overriding requires the override keyword, except trait
b. only the primary constructor can pass parameters to the base constructor.
scala> class Car(speed: Int) extends Vehicle(speed){ | override val mph = speed | override def race() = println("Racing Car") | } defined class Car scala> class Bike(speed: Int) extends Vehicle(speed){ | override val mph: Int = speed | override def race() = println("Racing Bike") | } defined class Bike scala> val vehicle1 = new Car(200) vehicle1: Car = Car@1cfe29c scala> vehicle1.race Racing Car scala> vehicle1.mph res4: Int = 200 scala> val vehicle2 = new Bike(20) vehicle2: Bike = Bike@1973547 scala> vehicle2.mph res5: Int = 20 scala> vehicle2.race Racing Bike
(10) Traits
In Scala, when a class inherits from a trait, it implements the interface of the trait, and inherits all the code contained in the trait.
scala> trait flying{ | def fly() = println("flying") | } defined trait flying scala> trait gliding{ | def glide() = println("gliding") | } defined trait gliding
Now you can create the Batmobile class extends Vehicle class along with the flying and gliding traits.
scala> class Batmobile(speed: Int) extends Vehicle(speed) with flying with gliding{ | override val mph: Int = speed | override def race() = println("Racing Batmobile") | override def fly() = println("Flying Batmobile") | override def glide() = println("Gliding Batmobile") | } defined class Batmobile
n Scala, traits can inherit classes. The keyword extends is used: when a class inherits a trait, when the class mixes in other traits using the with keyword, when one trait is the child of another trait or class.
Now create a list of vehicles, then you can use the maxBy method provided by Scala collections library to find the fastest vehicle in the list.
scala> val vehicleList = List(vehicle1, vehicle2, batmobile) vehicleList: List[Vehicle] = List(Car@1cfe29c, Bike@1973547, Batmobile@8c2321) scala> val fastestVehicle = vehicleList.maxBy(_.mph) fastestVehicle: Vehicle = Batmobile@8c2321
(11) Case Classes
A case class provides the same facilities as a normal class, but the compiler generates toString, hasCode, and equals methods(which you can override). Case classes can be instantiated without the use of the new statement. By default, all the parameters in the case class's constructor become properties on the case class.
scala> case class Stuff(name: String, age: Int) defined class Stuff scala> val s = Stuff("David",45) # create an instance of Stuff without the keywod new(you can use new if you want) s: Stuff = Stuff(David,45) scala> s.toString # case class's toString method does the right thing res0: String = Stuff(David,45) scala> s == Stuff("David",45) # equal methods does a deep comparison res1: Boolean = true scala> s.name # the instance has properies res2: String = David scala> s.age res3: Int = 45
class Stuff(val name: String, val age: Int){
override def toString = "Stuff("+name+","+age+")"
override def hashCode = name.hashCode + age
override def equals(other: AnyRef) = other match {
case s: Stuff => this.name == s.name && this.age == s.age
case _ => false
}
}
object Stuff {
def apply(name String, age: Int) = new Stuff(name,age)
def unapply(s: Stuff) = Some((s.name,s.age))
}
(12) Value Classes
With value classes, Scala allows user-defined value classes that extend AnyVal. Value classes are a new mechanism in Scala to avoid allocating runtime objects. Value classes allow you to add extension methods to a type without the runtime overhead of creating instances.
Class SomeClass(val underlying: Int) extends AnyVal
SomeClass has a single, public val parameter that is the underlying runtime representation. The type at compile time is SomeClass, but at runtime, the representation is an Int. A value class can defines defs, but no vals, vars, or nested traits classes or objects.
# wrap the Int parameter and encapsulates a twice method. scala> class SomeClass(val i: Int) extends AnyVal{ | def twice() = i*2 | } defined class SomeClass scala> val v = new SomeClass(9) v: SomeClass = SomeClass@9 scala> v.twiceres5: Int = 18 scala> :javap –c SomeClass …... SomeClass$.twice$extension # at runtime, the expression a static object …...
One use case for value classes is to combine them with implicit classes. Using an implicit class provides a more convenient syntax for defining extension methods, while value classes remove the runtime overhead.
(13) Scala verus Java
The uniformly of instances in Scala means that the developer does not have to perform special tests or to have different code paths to deal with primitive types.
scala> 1.hashCode
res6: Int = 1 scala> 2.toString
res7: String = 2 scala> def with42(in: Int => Int) = in(42) # define a method that takes a function that transforms an Int to an Int
with42: (in: Int => Int)Int
scala> with42(33+)
res8: Int = 75