Java uses the interface keyword class to declare an interface. When a concrete class implements the interface, the implements keyword is used. The existence of interfaces can be used to define specifications (similar to abstract classes) that are embodied in design functions.

Briefly review a few features of Java interfaces:

  1. A class can implement multiple interfaces.
  2. Multiple inheritance is allowed between Java interfaces.
  3. The attributes in the interface are all constants.
  4. Interface methods in Java are supposed to be abstract, but some changes have been made since the Java 8 release.
  5. After Java 8, if an interface has only one abstract method, we call itSimple Abstract MethodSAM for short. For such interfaces, we can use Lambda functions.

Java introduced interfaces for one reason: it doesn’t allow multiple inheritance like the C++ language does, because Java is trying to provide simpler OOP programming. So Java introduced the concept of interfaces to compensate for the defects of single inheritance to a certain extent.

The second reason is decoupling (this is how I understand decoupling) : interfaces separate declaration from implementation. The declaration of an abstract method allows classes A, B, and C to give different implementations without the strong dependency of IS-A between them.

However, decoupling in Java has a limit: a subclass inherits all methods from its parent, whether it really needs them or not. This sort of defeats the intent of interface decoupling: once a parent class implements an interface method, that method is inherited regardless of whether subclasses actually need it. Interfaces in Java are not really “hot deployed” due to the limitations of inheritance mechanisms. Scala tries to remedy this by introducing the concept of dynamic intermixing.

Traits: trait

Instead of preserving Java’s interface concept, Scala introduces the trait. When multiple classes have a “trait” in common, this trait can be extracted. Take the trait keyword to declare. Traits are the underlying equivalent of the Java language’s Interface (+ Abstract class). So it has all the features of Java interfaces and abstract classes.

All interfaces in Java can be replaced with Scala traits. Scala’s serialization feature, for example, is a simple wrapper around the original Java interface:

package scala

/** * Classes extending this trait are serializable across platforms (Java, .NET). */
trait Serializable extends Any with java.io.Serializable
Copy the code

Method of use

Once a class has a property, it means that the class satisfies all the elements of that property, so the extends keyword is also used (Scala does not use the implements keyword). Here are a few different declarations:

Use the with keyword when a class or trait inherits more than one trait:

class Clazz extends trait1 with trait2 with.
Copy the code

When a class inherits both a parent class and multiple attributes, the parent class is declared after the extends keyword and the with keyword is used to link the other attributes.

class Clazz extends Parent with trait1 with.
Copy the code

Traits can have companions.

trait Calculator {

  def add(a1: Int, a2: Int) :Int = a1 + a2
}

object Calculator {
   final val PI : Double = 3.1415
}
Copy the code

Traits can also inherit from each other, and even traits can inherit from a class.

trait Trait extends Object
Copy the code

Such a trait actually means that it is specifically used to extend the functionality of a particular class. Questions about this point will be addressed later in this article.

A simple primer

Suppose you now have the following scenario: Define a trait to regulate a database connection. MysqlConnector and OracleConnector provide connection implementations for different databases:

trait DBConnector {
  // Declare an abstract method without a function body
  def Connect() :Unit
}

class MySQLConnector extends DBConnector {
  override def Connect() :Unit = {
    println("Connect to Mysql")}}class OracleConnector extends DBConnector {
  override def Connect() :Unit = {
    println("Connect to Oracle")}}Copy the code

Observe the compilation results of attributes from below

When only abstract methods are declared in a trait, only a.class file is compiled underneath, as shown in the following code. In this case, traits are equivalent to interfaces.

//from jd-gui.exe
// Ignore irrelevant code
import scala.reflect.ScalaSignature;

public abstract interface DBConnector
{
  public abstract void Connect(a);
}
Copy the code

There are both abstract methods and concrete methods

This time we will add the specific methods to the DBConnector and observe how the compiler compiles:

trait DBConnector {
  // Abstract methods
  def Connect() :Unit

  // Specific methods
  def getURL() :String = "127.0.0.1"
}
Copy the code

Prior to Java 8, it was not possible to declare concrete methods directly in the interface, so For compatibility, Martin stored the concrete methods in another abstract class, which is equivalent to interface + Abstract class. In addition, the properties of both abstract and concrete methods are called rich interfaces.

In the current case, the trait generates two files:

The file name type
{traitName}.class interface
{traitName}$class.class abstract class

Dynamic blending features *

A brief review of the OCP principle: limited to modification, open to expansion.

In addition to inheriting attributes when declaring a class, you can also mix attributes dynamically when constructing an object. It can extend the functionality of a class without modifying its declaration, with low coupling. Dynamic mixing does not affect the original inheritance relationship while expanding the function of the class.

A simple example: Declare a normal Calculator whose Add function is “plugged” into the Calculate trait.

trait Calculate{
  // Declare specific methods in the attribute for the class that loads the interface to use.
  def add(a:Int,b:Int) :Int= a+b
}
class Calculator {}
Copy the code

When we instantiated Calculator, we incorporated this trait:

// Dynamically insert a trait directly using the with keyword when instantiating a class.
val c= new Calculator with Calculate

// This method cannot be used if traits are not mixed in
c.add(2.3)
Copy the code

For those of you wondering, is this Calculator instance mixed with traits or is it just a Calculator class? At this point, its type is Calculator with Calculate, neither pure Calculator nor pure Calculate.

val c: Calculate with Calculator = new Calculate with Calculator
Copy the code

The inheritance of traits

To see the order of construction, we add a println statement to the inner constructor:

trait Operator{

  def insert(value : Int) : Boolean
}

trait DBOperator extends Operator {

  println("build a DBOperator.")

  override def insert(value: Int) :Boolean = {
    println("insert value into database.")
    true}}trait MysqlOperator extends DBOperator{

    println("build a MysqlOperator.")

    override def insert(value: Int) :Boolean ={
        println("insert value into Mysql Database.")
        true}}Copy the code

Let’s declare another utility class for loading attributes:

class Connection(thisURL: String){

  val URL : String = thisURL

  println("build a Connection.")}Copy the code

Dynamically mix attributes and then run them in the main function:

def main(args : Array[String) :Unit = {

  val connection = new Connection("127.0.0.1") with MysqlOperator

  connection.insert(100)}Copy the code

What is the print order in the console? Attributes are constructed in the same order as classes, starting with the parent class and working down, so the console prints:

build a Connection.
build a Operator.
build a DBOperator.
build a MysqlOperator.
insert value into Mysql Database.
Copy the code

We can see that the Operator trait is initialized from the beginning down the inheritance line. Suppose the inheritance relationship becomes this structure:

The OracleOperator declaration is given below:

trait OracleOperator extends DBOperator{

  println("build a MysqlOperator.")

  override def insert(value: Int) :Boolean ={

    println("insert value into Oracle Database.")
  	// Pay attention to the super keyword! I'll come back to that.
  	super.insert(value)
  }
}
Copy the code

Suppose we mixed MysqlOperator and OracleOperator into an object in the main function, what would the console print?

val connection = new Connection("127.0.0.1") with MysqlOperator with OracleOperator
Copy the code

The console printed this message:

build a Connection.
build a Operator.
build a DBOperator.
build a MysqlOperator.
build a OracleOperator.
Copy the code

MysqlOperator () : MysqlOperator (); MysqlOperator () : MysqlOperator (); Since its parent has already been initialized, it will not initialize the repeated parent.

Scala overlay nature

In the example above, when we build an object and mix in multiple attributes, we call this phenomenon superimposed attributes. Attributes are declared and constructed from left to right, but method access is from right to left. To see if the order of method access is from right to left, mix in a few attributes and call the Insert method of connection.

val connection = new Connection("127.0.0.1") with MysqlOperator with OracleOperator
Copy the code

The console printed the following:

build a Connection.
build a Operator.
build a DBOperator.
build a MysqlOperator.
build a OracleOperator.
insert value into Oracle Database.
insert value into Mysql Database.
Copy the code

Why is MysqlOperator’s insert method called? Here’s the problem: the super keyword in a trait doesn’t necessarily refer to its parent trait first. In Scala’s dynamic blending of attributes, attributes are called from right to left. If the trait to the left of the trait has an existing namesake, the super refers to the trait to the left of the trait when it is dynamically mixed.

To be more specific, when we mix attributes, MysqlOperator is on the left and OracleOperator is on the right. This results in code executing the super keyword in the OracleOperator insert method first checking whether the MysqlOperator on the left has a specific insert method. If so, it is called.

If no reusable method can be found along the left, the super keyword refers to the overridden method of its parent trait. For example, we mixed in an unrelated attribute on the left side of the OracleOperator:

val connection = new Connection("127.0.0.1") with OtherTrait with OracleOperator
Copy the code

This time the OracleOperator super keyword has no choice but to insert the parent method.

build a Connection.
build a Operator.
build a DBOperator.
build a OracleOperator.
insert value into Oracle Database.
insert value into database.
Copy the code

If you explicitly specify that OracleOperator performs the parent DBOperator’s insert method, you can use the type parameter to indicate that super refers to the parent of the directly inherited relationship, regardless of whether there is a method with the same name to the left.

trait OracleOperator extends DBOperator{

  println("build a OracleOperator.")

  override def insert(value: Int) :Boolean ={

    println("insert value into Oracle Database.")
    // Specify super as DBOperator, regardless of the right-to-left access order.
    super[DBOperator].insert(value)
  }
}
Copy the code

Let’s see who OracleOperator’s super points to:

insert value into Oracle Database.
insert value into database.
Copy the code

Special use of the abstract keyword in attributes

Let’s start with a piece of code:

trait AbstractOp{

  def add(int: Int) : Int

}

trait ImpOp extends AbstractOp{

  override def add(int: Int) :Int =
  {
    println("execute add:")
    super.add(int)
  }

}
Copy the code

Notice the add method in the ImpOp code. According to the compiler, we seem to want to call the abstract Add method of the parent class, which logically doesn’t make sense: although we’ve mentioned before, the super keyword doesn’t necessarily refer to the parent in the trait, it might also refer to the trait declared on the left. To eliminate this misconception, we need to make some changes to ImpOp’s add method:

trait ImpOp extends AbstractOp{

  abstract override def add(int: Int) :Int =
  {
    println("execute add:")
    super.add(int)
  }

}
Copy the code

I’ve said before that the abstarct keyword in Scala can only be used to modify classes, but here’s a special case: The add method is now a semi-implemented abstract method: It implements some of the logic itself, but it also leaves some “empty” super.add methods, all of which are qualified by both abstract and override keywords.

Super at this point will refer to the trait to the left of it when it is dynamically mixed in. Before the program is run, the compiler will find the super methods that match it in order from right to left, otherwise, an error will be reported before compilation. Let’s declare a LeftOp property of the Add method that implements AbstractOp:

trait LeftOp extends AbstractOp{
  override def add(int: Int) :Int =
  {
    int + 3}}Copy the code

Leaving the rest of the logic intact, create an InstanceOp class in the main function (this class is only used for plugins, it has nothing else), and mix the attributes dynamically in order:

 val instanceOp = new InstanceOp with LeftOp with ImpOp

 println(instanceOp.add(4))
Copy the code

Execute the main function:

execute add:
7
Copy the code

Following the right-to-left execution order of dynamic mixin traits, ImpOp’s add method is called first. After printing a sentence “execute Add “, LeftOp’s Add method is called using the super keyword to supplement the complete logic.

At this point, the program will run without problems.

Implements the construction order of trait classes

Let’s discuss the order of trait construction in the two cases respectively:

  1. Inheriting attributes at class declaration time.
  2. Dynamic blending characteristics.

Case one: Inheriting attributes in class declarations.

This case follows the process of building the top-level parent class first and then down to this class. If the parent class also inherits attributes, the attributes of the parent class are first constructed in the order they are declared. If these traits also have inheritance relationships, the top-level traits are constructed first, and then the traits of the current parent class are constructed. When the attributes of the parent class are constructed, then the parent class itself is constructed, and then the next subclass is constructed, and the logic remains the same.

Given the following code:

trait traitA{

  println("construct traitA")}trait traitB extends traitA{
  println("construct traitB")}trait traitC extends traitB{
  println("construct traitC")}trait traitD{
  println("construct traitD")}trait traitE extends traitD{
  println("construct traitE")}class ObjA {
  println("construct ObjA")}class ObjB extends ObjA with traitB with traitC{
  println("construct ObjB")}class ObjC extends ObjB with traitE {
  println("construct ObjC")}Copy the code

Use a diagram to describe the relationship:

To construct an instance of ObjC in the main function, first construct the top-level parent ObjA and then attempt to construct the next-level parent ObjB.

Since ObjB also inherits some characteristics, the characteristics of ObjB should be constructed first. Traitbs are first constructed in the order in which objBs are declared. Because traitB inherits from traitA, traitA is constructed first. When a traitC is constructed, no construction is repeated because both traitB and traitA have already been constructed.

After dealing with the properties of ObjB, construct ObjB itself, and then construct the final ObjC, in the same logical order, first with traitD, then with traitE, and finally with ObjC. When constructing an instance of ObjC in the main function, the screen prints:

construct ObjA
construct traitA
construct traitB
construct traitC
construct ObjB
construct traitD
construct traitE
construct ObjC
Copy the code

Case two: Dynamic mixing characteristics.

In the above code, we remove ObjC’s extra implementation features and mix them in dynamically in the main program instead:

new ObjC with traitE with traitD
Copy the code

Diagrams are used to show the process, where dynamically mixed dependencies are dotted:

The difference with case 1 occurs in the process of constructing ObjC: when attributes are dynamically mixed, the class is constructed first and then the attributes are constructed.

Run the main function and observe:

construct ObjA
construct traitA
construct traitB
construct traitC
construct ObjB
construct ObjC
construct traitD  //why?
construct traitE  //why?
Copy the code

Summary of both cases

  1. If the class implements the attributes when it is declared, the attributes are constructed first and then the class instance is constructed.

  2. If a class has attributes mixed in dynamically during instantiation, the instance is constructed first, and then the attributes are constructed.

  3. In either case, no trait that has already been constructed is constructed repeatedly.

Extended class traits

Not only can a trait inherit from a trait, it can also inherit from a class and invoke superclass methods through the super keyword (although super is not always the case in dynamic mixing).

As shown in the figure, the trait traitA inherits Class A. It acts like A glue: any class that claims to inherit this property also inherits class A.

Or to put it this way: Class A wants to acquire some of the features of A trait that extends class A. So it is only natural that class B also needs to inherit from Class A. For example, there is a Device, Device, and a RedHatInstallTutorial feature that inherits the LinuxOS class. Device indirectly inherits LinuxOS by inheriting this trait.

class LinuxOS

trait RedHatInstallTutorial extends LinuxOS

// indirectly implement inheriting LinuxOS by inheriting RedHatInstallTutorial.
class Device extends RedHatInstallTutorial
Copy the code

Multiple inheritance cannot be implemented

At first glance, this might seem like a “curve-saving” approach to Scala’s multiple inheritance, but it doesn’t really work.

If class B inherits class C and you want to mix it with A trait A that inherits class A, class C must be A subclass of class A. Otherwise, an error will be reported when you compile it: illegal inheritance.

As an example of code, a device is attempting to install two unrelated systems at the same time:

class WindowsOS{}
class Device extends WindowsOS with RedHatInstallTutorial {}
Copy the code

Since there is no direct relationship between the LinuxOS inherited by WindowsOS and RedHatInstallTutorial, this is a false diamond inheritance. Now there is another system, CentOS, which belongs to the LinuxOS class. Therefore, Device inherits both CentOS and Redhat Installation Tutorial without causing problems.

class CentOS extends LinuxOS {}
class Device extends CentOS with RedHatInstallTutorial {}
Copy the code

So far, the only major language that supports multiple inheritance is C++. It seems that the vast majority of programming languages today do not accept multiple inheritance, but instead use other methods to compensate for the limitations of single inheritance.

Avoid the rhombus inheritance problem in dynamic mixing

In dynamic mixing, we should also avoid the phenomenon of diamond inheritance. For dynamic mixing, the trait trait A must meet one of two conditions:

If class B has no inheritance relationship when declared, the dynamically mixed trait A either has no inheritance declaration or can only inherit class B itself.

trait traitA extends classB {}
class classB {}
//-----main--------//
// Dynamic mixin
val clazzB = new classB with traitA
Copy the code

If class B inherits A class when it is declared, the dynamically mixed attributes must also inherit directly or indirectly from class A.

class classA{}
trait traitA extends classA {}
class classB extends classA {}
//---main--------//
// Dynamic mixin
 val clazzB = new classB with traitA
Copy the code

Either way, the goal is to avoid the diamond inheritance problem that occurs when dynamic mixing occurs.

Why not just set up A B→C→A inheritance?

What to make of strange practices? We need to put ourselves in the shoes of class A’s programmers and think in terms of dynamic mixing: there are cases where class A wants to provide optional services to its subclasses, and its subclasses only invoke functions through mixing attributes if necessary. Let’s substitute in a scenario to illustrate the problem:

The scene of a

Now you have a Computer with an Int property storage and a Disk property dedicated to the Computer. This feature has such a function: it can expand the storage capacity of their Computer. It takes effect when you load it onto a Computer instance when you need it.

class Computer(protected var storage: Int) {

  println("build a Computer.")
  protected def setStorage(storage: Int) :Unit = this.storage = this.storage + storage
  // The capacity of the computer can be viewed externally.
  def getStorage: Int = storage
}

trait Disk extends Computer {
  println("This computer's capability has been extended.")
  setStorage(1024)}Copy the code

For any Computer instance that incorporates Disk attributes, the storage attribute is expanded by 1024 units. This step is performed by executing the setStorage method when the Disk is initialized. We declare two instances of Computer simultaneously in the main program, one with Disk mixed in and one without.

val computer = new Computer(512) with Disk
println(s"computer's storage: ${computer.getStorage}")

val computer2 = new Computer(512)
println(s"computer2's storage: ${computer2.getStorage}")
Copy the code

Run the main program and observe the printing order of the screen:

build a Computer.
This computer's capability has been extended.
computer's storage: 1536
build a Computer.
computer2's storage: 512
Copy the code

Scenario 2

Define a HuaWeiComputer class. This class has a subclass MateBook.

//-------HuaWeiComputer------------//
class HuaWeiComputer{
  // Assume that huawei computers have the following two functions:
  def powerOn() :Unit ={
    print("Computer boot up")}def powerOff() :Unit ={
    print("Computer shutdown")}}//----------MateBoook------------//
class MateBook extends HuaWeiComputer {}
Copy the code

HuaWeiComputer HuaWeiComputer promises to provide additional repair service when any huawei-branded computer is damaged.

Two conditions are mentioned: The class belongs to HuaWeiComputer. The maintenance service is optional.

The HuaWeiComputer therefore provides an additional “after-sales service” feature:

trait afterSaleService extends HuaWeiComputer
{
  def post(info: String) :Unit ={
    println(S "Received your model number$info, we will provide you with additional after-sales service.")}}Copy the code

When a Huawei-branded computer malfunctions, it can tap into this feature in a dynamic way. Huawei computers that do not need maintenance functions should not be mixed with features, of course, it has no corresponding post method. Other PCS that are not HuaWeiComputer cannot use this function.

// Huawei computers that need repair functions can be mixed with features
val huaWei100 = new MateBook with afterSaleService
huaWei100.post("100")

// Huawei computers that do not require maintenance functions do not need to be mixed with features
val huaWei101 = new MateBook

// Computers that are not HuaWeiComputer cannot use this service (an error occurs)
val Apple100 = new MacBook with afterSaleSerive
Copy the code

A self type reference to a trait

Now there is a feature called Install that restricts its use to computers that implement (or mix in) the Disk feature. So what do you do?

trait Install extends Computer with Disk
{
    //....
}
Copy the code

This only means that the Install attribute inherits both the Computer and Disk attributes, not the Computer class with Disk attributes mixed in. At this point, we need the attribute’s own type reference: we restrict the class we want to Install to in the format this: XXXX => :

trait Install{
  this : Computer with Disk= >def bigData : Int = getStorage - 1300
}
Copy the code

It limits “the ability to install large files only to computers with large hard drives.” Let’s go back to the main program and observe the effect: Only Computer instances with Disk mixed in can mix Install and invoke its functionality, otherwise, a compilation error will be reported:

val computer = new Computer(512) with Disk with Install
println(s"computer's storage: ${computer.getStorage}")
println(computer.bigData)

// Since it does not mix Disk attributes, it cannot mix Install again.
val computer2 = new Computer(512) 
println(s"computer2's storage: ${computer2.getStorage}")
Copy the code

If you were to rewrite the Install feature from a Java perspective, its declaration would look something like this:

public interface Install<Computer extends Disk> {/ /... }
Copy the code

Review the four ways to create objects so far

  • newMethod: The most common way to build an instance.
  • applyMethod: its companion object encapsulates the constructor of the class as oneapplyMethod to get an instance by calling it.
  • Anonymous subclasses: Often used to override and use an abstract class or attribute directly.
  • Dynamic mixing: according to actual needs, innewObject in the process of mixing additionaltrait