Characteristics of Scala

A class extends from one or more traits in order to use the services provided by these traits. Trait may require that the classes that use it support a particular feature. However, unlike the Java interface, the Scala trait can give a default implementation of these features, so the trait is much more useful than the interface.
point:
1. Classes can implement any number of traits.
2. Traits can require that the class implementing them has specific fields, methods, and superclasses.
3. Unlike the Java interface, the Scala trait can provide methods and field implementations.
4. When multiple traits are superimposed together, the order is very important, and the trait whose method is executed first is ranked further back.

1. Why there is no multiple inheritance
Scala, like Java, does not allow classes to inherit from multiple superclasses. If you just assemble unrelated classes together, there is no problem with multiple inheritance, but if these classes have common methods or fields, it will be very troublesome. Java has adopted a very restrictive strategy. Classes can only be extended from a superclass. It can implement any number of interfaces, but interfaces can only contain abstract methods, not fields. We usually want to call other methods to implement other methods, but in the Java interface, we can't do it, so we often see the practice of colleagues providing interfaces and abstract base classes in Java, but the symptoms are not cured. Scala provides traits rather than interfaces. Traits can have both abstract and concrete methods, and classes can implement multiple traits.

2. Traits as an interface Using the
Scala trait can work exactly like an interface, such as:

trait Logger {
  def log(msg: String)  //抽象方法
}

We don't need to declare the method as abstract-unimplemented methods in the trait are abstract methods by default.
Subclasses can give implementations:

class ConsoleLogger extends Logger {   //使用extends而不是implements
	def log(msg: String) {println(msg)}  //不需要写override
}

The override keyword does not need to be given when rewriting trait abstract methods.
Scala does not have a special keyword to mark the implementation of traits. Compared to Java interfaces, traits are more like classes.
If you need more than one trait, you can use the with keyword to add additional traits:

class ConsoleLogger extends Logger with Cloneable with Serializable {...}

The Cloneable and Serializable interfaces of the Java class library are used here, and all the interfaces can be used as the traits of Scala.
Like Java, the scala class can only have one superclass, but it can have any number of qualities.

3. Trait with concrete implementation
In scala, the method of trait does not need to be abstract. We can turn ConsoleLogger into a trait:

trait ConsoleLogger {
  def log(msg: String) {println(msg)}
}

The ConsoleLgger feature provides a method with an implementation-in this example, the method prints information on the console
. Example of use:

class SavingAccount extends Account with ConsoleLogger {
  def withdraw(amount: Double) {
    if (amount > balance) log("Insufficient funds")
    else balance -= amount
    ..
  }
}

Pay attention to the difference between Scala and Java. SavingAccount gets a specific log method implementation from the ConsoleLogger trait. With the Java interface, it cannot be done.
However, there is a drawback to let the trait have specific behavior, that is, when the trait changes, all the classes mixed in the trait must be recompiled.

4. Objects with traits
When constructing a single object, you can add traits to it. We use the Logged trait in the standard scala class library as an example. It is very similar to our Logger, but it comes with everything. Implementation not done:

trait Logged {
  def log(msg: String)
}

We use this trait in the class definition:

class SavingAccount extends Account with Logged {
  def withdraw(amount: Double) {
    if (amount > balance) log("Insufficient funds")
    else ...
  }
}

Now the log method does nothing, but we can "mix in" a better implementation of the logger when constructing the object. The standard ConsoleLogger extends from Logged traits:

trait ConsoleLogger extends Logged {
  override def log(msg: String) {println(msg)}
}

We can add this feature when constructing objects:

val acct = new SavingAccount with ConsoleLgger

When we call the log method on the acct object, the log method of ConsoleLogger trait will be executed, of course, another object can add different traits`

val acct2 = new SavingAccount with FileLogger

5. Superimposed traits
We can add multiple traits that call each other to a class or object, starting with the last one. It is useful for scenarios where a certain value needs to be processed in stages.
This is an example of time stamping all log messages:

trait TimeStampLogger extends Logged {
  override def log(msg: String) {
    super.log(new java.util.Date() + " " + msg)
  }
}

Similarly, if we want to truncate log messages that are too long:

trait ShortLogger extends Logged {
  val maxLength = 15
  override def log(msg: String) {
    super.log{
      if (msg.length <= maxLength) msg else msg.substring(0,maxLength-3) + "..."
    }
  }
}

Note that each of the above log methods passes the modified message to super.log.
In terms of traits, super.log does not have the same meaning as classes. If it does have the same meaning, then these traits are meaningless. They extend from Logged, and the log method does nothing.
In fact, super.log calls the next trait in the hierarchy of traits, which one should be determined according to the order in which the traits are added. Generally speaking, the processing starts from the last trait. such as:

val acct1 = new SavingAccount with ConsoleLogger with TimeStampLogger with ShortLogger
val acct2 = new SavingAccount with ConsoleLogger with ShortLogger with TimeStampLogger

If you use acct1, you get such a message:
Sun Feb 06 13:33:22 ICT 2020 Insufficient ... As
you can see, the ShortLogger log method is executed first, and then his super.log calls TimeStampLogger.
And uses acct2 , You will get:
Sun Feb 06 1 ...
This will first call TimeStampLogger, first add a timestamp in the length judgment for truncation.

For traits, we cannot judge from the source code where super.Method will execute the method, which requires us to judge from the order given by the specific objects or classes that rely on using these traits. If we need to control which specific method is called, we can give the name in square brackets: super [ConsoleLogger] .log (…), the type in brackets must be a direct supertype, there is no way to use the inheritance hierarchy A trait or class farther away.

6. Rewrite the abstract method in the traits
We return to our own Logger traits, here we do not provide a specific implementation of the log method

trait Logger {
  def log(msg: String)  //抽象方法
}

Now, let ’s extend it with the timestamp feature, just like the example in the previous section

trait TimeStampLogger extends Logger {
  override def log(msg: String) {  //重写抽象方法
    super.log(new java.util.Date() + " " + msg)  //super.log定义了吗?
  }
}

But we will find that the compiler marks super.log as an error.
According to normal inheritance rules, this call is always wrong-the Logger.log method is not implemented.
As mentioned above, we have no way of knowing which log method is ultimately called, depending on the order in which the traits are mixed in.
Scala believes that TimeStampLogger is still abstract and needs to be mixed into a concrete log method. We must mark the method with the abstract keyword and the override keyword:

abstract override def log(msg: String) {
  super.log(new java.util.Date() + " " + msg)
}

7. The traits used as a rich interface
traits can contain a large number of tool methods, and these tool methods can rely on some abstract methods to achieve, for example, the Interator trait in scala uses the abstract next and hasNext to define dozens of methods.
Let's enrich the above log features. Generally, the log api allows a log message to specify a level to distinguish information-type messages from warnings and errors.

trait Logger {
  def log(msg: String)
  def info(msg: String) { log{"INFO: " + msg} }
  def warn(msg: String) { log{"WARN: " + msg} }
  def severe(msg: String) { log{"SEVERE: " + msg} }
}

Notice how we combine abstract methods and concrete methods.
In this way, classes using Logger traits can call these log message methods arbitrarily, such as:

class SavingAccount extends Account with Logger {
  def withdraw(amount: Double) {
     if (amount > balance) severe("Insufficient funds")
     else ...
  }
  ...
  override def log(msg: String) {println(msg)}
}

It is very common to use concrete and abstract methods in traits like this in scala. In java we need to declare an interface and an additional class that extends the interface.

8. Specific fields in
traits Fields in traits can be concrete or abstract, if the initial value is given, it is concrete.

trait ShortLogger extends Logger {
  val maxLength = 15 //具体字段
}

Classes mixed with this trait will automatically get a maxLength field. Generally, for each specific field in the trait, the class that uses the trait will get a field corresponding to it. These fields are not inherited, but simply to be added to the subclass. This looks like a subtle difference, but this difference is important:

class SavingAccount extends Account with ConsoleLogger with ShortLogger {
  var interest = 0.0
  def withdraw(amount: Double) {
    if (amount > balance) log("Insufficient funds")
    else ...
  }
}

Note that our subclass has a field interest, which is a common field in the subclass.
Assume that Account also has a field

class Account {
  var balance = 0.0 
}

The SavingAccount class inherits this field in the way of the journey. The SavingAccount object consists of all the fields of the superclass and the fields defined in any subclasses. In the JVM, a class can only extend a superclass, so fields from traits cannot be inherited in the same way. Because of this limitation, maxLength was added directly to the SavingAccount class, alongside interest.

9. Abstract fields in
traits Uninitialized fields in traits must be rewritten in concrete subclasses.
Such as:

trait ShortLogger extends Logged {
  val maxLength: Int //抽象字段
  override def log(msg: String) {
    super.log(
      if (msg.length <= maxLength) msg else msg.substring(0,maxLength - 3) + "..."
    )  //这个方法中使用到了maxLength字段
  }
}

When using this trait in a specific class, the maxLength field must be provided:

class SavingAccount extends Account with ConsoleLogger with ShortLogger {
  val maxLength = 20 //不需要写override
}

This way of providing trait parameter values ​​is very convenient when temporarily constructing an object.

10. Trait construction sequence
Like classes, traits can also have constructors, consisting of field initialization and other trait body statements.
Such as:

trait FileLogger extends Logger {
  val out = new PrintWriter("app.log") //这是特质构造器的一部分
  out.println("# " + new Date().toString) //也是构造器的一部分

  def log(msg: String) {out.println(msg);out.flush()}
}

These statements will be executed when any object mixed with this trait is constructed.
Constructors are executed in the following order:
1. First call the superclass constructor.
2. Trait constructor is executed after super class constructor and before class constructor.
3. Traits are constructed from left to right.
3. In each trait, the parent trait is constructed first.
4. If multiple traits share a parent trait, and that parent trait has already been constructed, it will not be constructed again.
5. After all traits are constructed, the subclasses are constructed.

For example, consider the following class

class SavingsAccount extends Account with FileLogger with ShortLogger

The constructor will be executed in the following order:
1.Account-superclass
2.Logger-parent trait of the
first trait 3.FileLogger-first trait
4.ShortLogger-the second trait, its parent Trait Logger has been constructed
5.SavingAccount-class

11. Initialize fields in a
trait A trait cannot have a constructor parameter, and each trait has a parameterless constructor.
The lack of constructor parameters is the only technical difference between traits and classes. In addition, traits can have the characteristics of all classes, such as concrete abstract fields and superclasses. This limitation has a problem for those traits that we need to customize to be useful. For example, if we want to specify a log file, but we cannot use construction parameters:

val acct = new SavingAccount with FileLogger("my.log")  //错误,特质没有构造器参数

Or we have a scheme, FileLogger can have an abstract field to store the file name.

trait FileLogger extends Logger {
  val filename: String
  val out = new PrintStream(filename)
  def log(msg: String) {out.println(msg)}
}

Classes that use this trait can override the filename field, but there is a trap here, which wo n’t work:

val acct = new SavingAccount with FileLogger {
  val filename = "myapp.log"  //这样行不通
}

The problem is that in the order of construction, the FileLogger constructor is executed before the subclass constructor. The subclass here is not so easy to distinguish: The
new statement constructs an instance of an anonymous class that extends from SavingAccount and mixes FileLogger traits. The initialization of filename only happens in this anonymous subclass. In fact, the initialization will not happen before the turn of the subclass-before the turn of the subclass, the FileLogger constructor will throw a null pointer exception.
One of the solutions is the advance definition mentioned earlier, such as:

val  acct = new {
  val filename = "myapp.log"
} with SavingAccount with FileLogger

The pre-definition occurs before the regular construction sequence. When the FileLogger is constructed, the filename is already initialized.
The definition in advance in the class is like this:

class SavingAccount extends {   //extends后提前定义块
  val filename = "my.log"
} with Account with FileLogger {
 ...  //SavingAccount的实现
}

Another solution is to use lazy values:

trait FileLogger extends Logger {
  val filename: String
  lazy val out = new PrintStrem(filename)
  def log(msg: String) {out.println(msg)} //不需要写override
}

In this way, the out field will be initialized when it is used for the first time, and at that time, the filename field should have been set to a value, but the lazy value and the lazy value will be checked whether they have been initialized before each use. .

Published 14 original articles · Like1 · Visits 684

Guess you like

Origin blog.csdn.net/qq_33891419/article/details/104092581