Chapter 8 object oriented programming

1. Package

Package is a way to organize classes;

The main purpose of using packages is to ensure the uniqueness of classes;

For example, if you write a Test class in your code, then your colleagues may also write a Test class. If two classes with the same name appear, they will conflict and the code cannot be compiled.

1.1 import classes in package

Many ready-made classes have been provided in Java for us to use, such as

public class Test {
    public static void main(String[] args) {
        java.util.Date date = new java.util.Date();
        // Get a millisecond timestamp
        System.out.println(date.getTime());
    }
}

You can use java.util.Date to introduce the date class in the java.util package.

However, this writing method is more troublesome. You can use the import statement to import the package.

import java.util.Date;
public class Test {
    public static void main(String[] args) {
        Date date = new Date();
        // Get a millisecond timestamp
        System.out.println(date.getTime());
   }
}

If you need to use other classes in java.util, you can use import java.util. *.

  • Here, not all classes under util are imported at once, but which class is required for Java processing.
import java.util.*;
public class Test {
    public static void main(String[] args) {
        Date date = new Date();
        // Get a millisecond timestamp
        System.out.println(date.getTime());
   }
}

However, we prefer to explicitly specify the class name to be imported, otherwise it is still prone to conflict.

import java.util.*;
import java.sql.*;
public class Test {
    public static void main(String[] args) {
    // There is a class such as Date in util and sql. At this time, ambiguity will occur and compilation error will occur
        Date date = new Date();
        System.out.println(date.getTime());
   }
}
// Compilation error
Error:(5, 9) java: yes Date Ambiguous reference to
 java.sql Classes in java.sql.Date and java.util Classes in java.util.Date All match

In this case, you need to use the full class name

import java.util.*;
import java.sql.*;
public class Test {
    public static void main(String[] args) {
        java.util.Date date = new java.util.Date();
        System.out.println(date.getTime());
   }
}

Note: import is very different from #include in C + +. C + + must #include to introduce other file contents, but Java does not.

import is just for the convenience of writing code. import is more similar to C + + namespace and using.

1.2 static import

Use import static to import static methods and fields in a package.

  • It is not recommended to use.
import static java.lang.System.*;
public class Test {
    public static void main(String[] args) {
        out.println("hello");
    }
}

In this way, it is more convenient to write some code, such as:

import static java.lang.Math.*;
public class Test {
    public static void main(String[] args) {
        double x = 30;
        double y = 40;
        // Static import is more convenient to write
        // double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
        double result = sqrt(pow(x, 2) + pow(y, 2));
        System.out.println(result);
   }
}

1.3 putting classes into packages

Basic rules:

  • Add a package statement at the top of the file to specify which package the code is in.
  • The package name needs to be specified as a unique name as far as possible, usually in the inverted form of the company's domain name (e.g. com.baidu.demo1).
  • The package name should match the code path. For example, if you create a package of com.baidu.demo1, there will be a corresponding path com/baidu/demo1 to store the code.
  • If a class does not have a package statement, the class is placed in a default package.

Difference between package and import:

  • Package: "package" refers to the package where the class is located.
  • Import: "import", which indicates the required classes in the class.

1.4 access control of package

We have learned about public and private in the class. Members in private can only be used inside the class. If a member does not contain public and private keywords, this member can be used in other classes inside the package, but not in classes outside the package.

The following code gives an example. Demo1 and Demo2 are in the same package, and Test is in other packages.

Demo01.java

package com.baidu.demo;

public class Demo1 {
    int value = 0;
}

Demo02.java

package com.baidu.demo;

public class Demo2 {
    public static void Main(String[] args) {
        Demo1 demo = new Demo1();
        System.out.println(demo.value);
    }
}

// The execution result can access the value variable
10

Test.java

import com.baidu.demo.Demo1;

public class Test {
    public static void main(String[] args) {
        Demo1 demo = new Demo1();
        System.out.println(demo.value);
    }
}

// Compilation error
Error:(6, 32) java: value stay com.baidu.demo.Demo1 Is not public; It cannot be accessed from an external package

1.5 common system packages

  1. java.lang: basic classes commonly used in the system (String, Object). This package is automatically imported from JDK1.1;
  2. java.lang.reflect: java reflection programming package;
  3. java.net: network programming development package;
  4. java.sql: support package for database development;
  5. java.util: a tool package provided by java. (set class, etc.) is very important;
  6. java.io: I/O programming development kit;

2. Succession

2.1 background

Classes created in code are mainly used to abstract some things in reality (including attributes and methods).

Sometimes there are some relationships between objective things, so there will be some relationships when expressed as classes and objects.

For example, design a class to represent animals

Note that we can create a separate java file for each class. The class name must match the. Java file name (case sensitive).

// Animal.java
public class Animal {
    public String name;
    
    public Animal(String name) {
    	this.name = name;
    }
    
    public void eat(String food) {
    	System.out.println(this.name + "I am eating" + food);
    }
}

// Cat.java
class Cat {
    public String name;
    
    public Cat(String name) {
    	this.name = name;
    }
    public void eat(String food) {
    	System.out.println(this.name + "I am eating" + food);
    }
}

// Bird.java
class Bird {
    public String name;
    
    public Bird(String name) {
    	this.name = name;
    }
    
    public void eat(String food) {
   		System.out.println(this.name + "I am eating" + food);
    }
    
    public void fly() {
    	System.out.println(this.name + "Flying ︿( ̄︶ ̄)︿");
    }
}

In this code, we found that there are a lot of redundant code.

After careful analysis, we find that Animal is related to Cat and Bird:

  • All three classes have the same eat method, and the behavior is exactly the same.
  • These three classes all have the same name attribute, and the meaning is exactly the same.
  • Logically, Cat and Bird are both Animal (is - a semantics).

At this point, we can let Cat and Bird inherit the Animal class respectively to achieve the effect of code reuse.

At this time, inherited classes such as Animal are called parent classes, base classes or superclasses. For classes such as Cat and Bird, we are called subclasses and derived classes. Similar to the real son inheriting his father's property, the subclass will also inherit the fields and methods of the parent class to achieve the effect of code reuse.

2.2 grammar rules

Basic grammar

class Subclass extends Parent class {
    
}
  • Use extends to specify the parent class;
  • A subclass in Java can only inherit one parent class (while languages such as C++/Python support multiple inheritance);
  • The subclass will inherit all public fields and methods of the parent class;
  • The private fields and methods of the parent class cannot be accessed in the child class;
  • Subclass instances also contain instances of the parent class. You can use the super keyword to get a reference to the parent class instance.
    • Three uses of super (cannot appear in static methods):
      1. super(): call the constructor of the parent class. Similar to this, it must be placed in the first line of the constructor.
      2. super.func();: Call the normal method of the parent class;
      3. super.data;: Call the member attribute of the parent class;

The above code can be improved using inheritance. At this time, we let Cat and Bird inherit from the Animal class, so Cat does not have to write the name field and eat method when defining.

class Animal {
    public String name;
    
    public Animal(String name) {
    	this.name = name;
    }
    
    public void eat(String food) {
    	System.out.println(this.name + "I am eating" + food);
    }
}

class Cat extends Animal {
    public Cat(String name) {
        // Use super to call the constructor of the parent class
        super(name);
    }
}
class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }
    
    public void fly() {
    	System.out.println(this.name + "Flying ︿( ̄︶ ̄)︿");
    }
}

public class Test {
    public static void main(String[] args) {
        Cat cat = new Cat("Xiao Hei");
        cat.eat("Cat food");
        
        Bird bird = new Bird("round");
        bird.fly();
    }
}

Extensions originally means "extension" in English, and the inheritance of the class we write can also be understood as "extension" on the code based on the parent class.

For example, the Bird class we wrote extends the fly method based on Animal.

If the child class and parent class have fields with the same name, when using the variable name. Name, the fields of the child class are preferred. If you want to use the fields with the same name of the parent class, you should use super.name;

class Animal{
    public String name;
    public int age;
    public Animal(String name){
        this.name = name;
    }
    public void eat(){
        System.out.println("eat()");
    }
}

class Bird extends Animal{
    public String wing;

    public String name;
    public Bird(String name){
        super(name);
    }

    public void fly(){
        System.out.println(super.name+"fly()");
    }
}

public class test01 {
    public static void main(String[] args) {
        Bird b = new Bird("hello");
        System.out.println(b.name);
        b.fly();
    }
}

If we change the name to private, the subclass cannot be accessed at this time.

class Bird extends Animal {
    public Bird(String name) {
    	super(name);
    }
    
    public void fly() {
    	System.out.println(this.name + "Flying ︿( ̄︶ ̄)︿");
    }
}

// Compilation error
Error:(19, 32) java: name stay Animal Medium is private access control 

2.3 protected keyword

Just now we found that if the field is set to private, the subclass cannot be accessed. However, setting it to public violates our original intention of "encapsulation".

The best of both worlds is the protected keyword.

  • The fields and methods modified by protected are inaccessible to the caller of the class.
  • For subclasses of a class and other classes in the same package, the fields and methods modified by protected are accessible.
// Animal.java
public class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    public void eat(String food) {
    	System.out.println(this.name + "I am eating" + food);
    }
}

// Bird.java
public class Bird extends Animal {
    public Bird(String name) {
    	super(name);
    }
    
    public void fly() {
        // For the protected field of the parent class, the child class can access it correctly
        System.out.println(this.name + "Flying ︿( ̄︶ ̄)︿");
    }
}

// Test.java and Animal.java are not in the same package
public class Test {
    public static void main(String[] args) {
        Animal animal = new Animal("Small animals");
        System.out.println(animal.name); // There is a compilation error at this time. name cannot be accessed
    }
}

Summary: there are four access permissions for fields and methods in Java.

  • private: it can be accessed inside the class, but not outside the class;
  • Default (also known as package access permission): it can be accessed inside a class. Classes in the same package can be accessed, but other classes cannot be accessed;
  • protected: it can be accessed inside the class, subclasses and classes in the same package can be accessed, and other classes cannot be accessed;
    • protected modified fields, when accessed in subclasses in different packages, can only be accessed through the super. Field name.
  • public: both inside the class and the caller of the class can access it;

When and which one?

We hope that the class should be "encapsulated" as much as possible, that is, hide the internal implementation details and only expose the necessary information to the caller of the class.

Therefore, we should use strict access rights as much as possible. For example, if a method can use private, try not to use public.

In addition, there is a simple and crude method: set all fields to private and all methods to public. However, this method is an abuse of access rights, or do you want students to seriously think about who to use the field methods provided by this class when writing code (whether it is used internally by the class, by the caller of the class, or by subclasses).

2.4 more complex inheritance relationships

In our example just now, only Animal, Cat and Bird are involved, but what if the situation is more complicated?

In this case, we may need to represent more kinds of cats~

Using inheritance at this time will involve more complex systems.

// Animal.java
public Animal {
...
}
// Cat.java
public Cat extends Animal {
...
}
// ChineseGardenCat.java
public ChineseGardenCat extends Cat {
...
}
// OrangeCat.java
public Orange extends ChineseGardenCat {
...
}
......

The inheritance method just now is called multi-layer inheritance, that is, subclasses can further derive new subclasses.

Always keep in mind that the classes we write are abstractions of real things. However, the projects we encounter in the company are often complex and may involve a series of complex concepts, which need to be represented by code. Therefore, there will be many classes written in our real projects, and the relationship between classes will be more complex.

But even so, we don't want the inheritance levels between classes to be too complex. Generally, we don't want more than three levels of inheritance. If there are too many inheritance levels, we need to consider refactoring the code.

If you want to restrict inheritance from syntax, you can use the final keyword.

2.5 final keyword

We once learned that when the final keyword modifies a variable or field, it represents a constant (which cannot be modified).

final int a = 10;
a = 20; // Compilation error

The final keyword can also modify a class, which means that the modified class cannot be inherited.

final public class Animal {
	...
}

public class Bird extends Animal {
	...
}

// Compilation error
Error:(3, 27) java: Unable to start from the final com.bit.Animal Inherit

The function of the final keyword is to restrict classes from being inherited.

"Limitation" means "Inflexibility". In programming, flexibility is often not a good thing, and flexibility may mean more error prone.

When a class decorated with final is inherited, it will compile and report an error. At this time, it can remind us that such inheritance is contrary to the original intention of the class design.

The String class we usually use is decorated with final and cannot be inherited.

3. Combination

Similar to inheritance, composition is also a way to express the relationship between classes, which can also achieve the effect of code reuse.

For example, it represents a school:

public class Student {
...
}

public class Teacher {
...
}

public class School {
    public Student[] students;
    public Teacher[] teachers;
}

Composition does not involve special syntax (keywords such as extensions), but only takes an instance of one class as a field of another class.

This is one of the common ways we design classes.

Combinatorial representation has - a semantics

In the example just given, we can understand that a school "contains" several students and teachers.

Inheritance represents is - a semantics

In the above example of "animals and cats", we can understand that a cat is also an animal.

We should pay attention to the difference between the two semantics.

4. Polymorphism

4.1 upward transformation

In the example just now, we wrote the following code:

Bird bird = new Bird("round");

This code can also be written like this:

Bird bird = new Bird("round");
Animal bird2 = bird;

// Or write it in the following way
Animal bird2 = new Bird("round");

At this time, bird2 is a reference to a parent class (Animal) and points to an instance of a child class (Bird). This writing is called upward transformation.

Upward transformation can be understood in combination with is - a semantics.

For example, if I ask my daughter-in-law to feed Yuanyuan, I can say, "daughter-in-law, have you fed the bird?" or "daughter-in-law, have you fed the parrot?".

Because Yuanyuan is indeed a parrot and a bird~~

Why is "upward transformation"?

In object-oriented programming, for some complex scenarios (many classes, very complex inheritance relationships), the program will draw a UML diagram to represent the relationship between classes. At this time, the parent class is usually drawn above the child class, so we call it "upward transformation", which means turning to the parent class.

Note: the rules of UML diagrams are not discussed in detail in class. Interested students can have a look by themselves

Timing of upward Transformation:

  • Direct assignment
  • Method transmission parameter
  • Method return

As we have demonstrated, the other two methods are not fundamentally different from direct assignment.

Method transmission parameter

public class Test {
    public static void main(String[] args) {
        Bird bird = new Bird("round");
        feed(bird);
    }
    
    public static void feed(Animal animal) {
   		animal.eat("millet");
    }
}
// results of enforcement
 Yuanyuan is eating millet

At this time, the type of formal parameter Animal is Animal (base class), which actually corresponds to the instance of Bird (parent class).

Method return

public class Test {
    public static void main(String[] args) {
    	Animal animal = findMyAnimal();
	}
    public static Animal findMyAnimal() {
        Bird bird = new Bird("round");
    	return bird;
    }
}

At this time, the method findMyAnimal returns a reference of Animal type, but actually corresponds to the instance of Bird.

4.2 dynamic binding

What happens when a method with the same name appears in the subclass and parent class?

Modify the previous code slightly, add the eat method with the same name to the Bird class, and add different logs in the two eat.

// Animal.java
public class Animal {
    protected String name;
    
    public Animal(String name) {
    	this.name = name;
    }
    
    public void eat(String food) {
        System.out.println("I am a small animal");
        System.out.println(this.name + "I am eating" + food);
    }
}

// Bird.java
public class Bird extends Animal {
    public Bird(String name) {
    	super(name);
    }
    
    public void eat(String food) {
        System.out.println("I am a bird");
        System.out.println(this.name + "I am eating" + food);
    }
}

// Test.java
public class Test {
    public static void main(String[] args) {
        Animal animal1 = new Animal("round");
        animal1.eat("millet");
        Animal animal2 = new Bird("flat ");
        animal2.eat("millet");
    }
}
// results of enforcement
 I am a small animal
 Yuanyuan is eating millet
 I am a bird
 Bian Bian is eating millet    

At this point, we find that:

  • Although both animal1 and animal2 are references of Animal type, animal1 points to instances of Animal type and animal2 points to instances of Bird type.
  • Call eat methods for animal1 and animal2 respectively. It is found that animal1.eat() actually calls the methods of the parent class, while animal2.eat() actually calls the methods of the child class.

Therefore, in Java, which code (which is the code of the parent class method or the code of the subclass method) is executed by calling a class method, depends on whether the reference refers to the parent class or the subclass object. This process is determined by the program runtime (not the compilation time), so it is called dynamic binding, also known as runtime binding.

Static binding: also known as compile time polymorphic overloaded method - which function you call is deduced according to the type and number of parameters you give.

4.3 method rewriting

For the eat method just now:

The subclass implements the method with the same name as the parent class, and the type and number of parameters are exactly the same. This situation is called override / override.

Notes on rewriting

  1. Rewriting and overloading are completely different. Don't confuse (think about it, what are the overload rules?);

  2. Ordinary methods can be rewritten, but static methods modified by static cannot be rewritten;

  3. The access permission of the method overriding the subclass cannot be lower than that of the parent class (private and final modified methods cannot be overridden).

  4. The return value type of the overridden method may not be the same as that of the parent class (but it is recommended to write it the same, except in special cases).

    // Examples of different return values
    class Animal{
        public String name;
        public int age;
        public Animal(String name){
            this.name = name;
        }
        public Animal eat(){
            System.out.println("eat()");
            return null;
        }
    }
    
    class Dog extends Animal{
        public Dog(String name){
            super(name);
        }
        @Override
        public Dog eat(){
            System.out.println(this.name+"eat()");
            return null; 
        }
    }
    // At this point, the return values of the two eat() methods form a covariant type.
    

Example of method permission: change eat of subclass to private

// Animal.java
public class Animal {
    public void eat(String food) {
    	...
    }
}

// Bird.java
public class Bird extends Animal {
    // Change the eat of the subclass to private
    private void eat(String food) {
    	...
    }
}

// Compilation error
Error:(8, 10) java: com.bit.Bird Medium eat(java.lang.String)Cannot overwrite com.bit.Animal Medium
eat(java.lang.String)
Attempting to assign lower access rights; Previously public

In addition, overridden methods can be explicitly specified using the @ Override annotation.

// Bird.java
public class Bird extends Animal {
    @Override
    private void eat(String food) {
    	...
    }
}

With this annotation, we can check some legitimacy. For example, if you accidentally misspell the method name (for example, write it as aet), the compiler will find that there is no aet method in the parent class, and an error will be compiled, indicating that rewriting cannot be formed.

We recommend explicitly adding the @ Override annotation when overriding methods in code.

The details of annotations will be described in later chapters.

Summary: the difference between overloading and rewriting.

Experience dynamic binding and method rewriting

The dynamic binding and method rewriting described above use the same code example.

In fact, method rewriting is a rule at the Java syntax level, and dynamic binding is the underlying implementation of method rewriting. The two essentially describe the same thing, but with different emphases.

4.4 understanding polymorphism

After understanding upward transformation, dynamic binding and method rewriting, we can design programs in the form of polymorphism.

We can write some code that only focuses on the parent class, which can be compatible with various subclasses at the same time.

Code example: print multiple shapes

class Shape {
    public void draw() {
    	// Don't do anything
    }
}

class Cycle extends Shape {
    @Override
    public void draw() {
        System.out.println("○");
    }
}

class Rect extends Shape {
    @Override
    public void draw() {
    	System.out.println("□");
    }
}

class Flower extends Shape {
    @Override
    public void draw() {
    	System.out.println("♣");
    }
}


/I am the dividing line//
// Test.java
public class Test {
    public static void main(String[] args) {
        Shape shape1 = new Flower();
        Shape shape2 = new Cycle();
        Shape shape3 = new Rect();
        drawMap(shape1);
        drawMap(shape2);
        drawMap(shape3);
    }
    
    // Print a single drawing
    public static void drawShape(Shape shape) {
    	shape.draw();
    }
}

In this code, the code above the split line is written by the implementer of the class, and the code below the split line is written by the caller of the class.

When the caller of the class writes the drawMap method, the parameter type is shape (parent class). At this time, he does not know or pay attention to the instance of which type (subclass) the current shape reference refers to. At this time, the drawing method called by the shape reference may have many different manifestations (related to the corresponding instance of the shape). This behavior is called polymorphism.

Polymorphism, as the name suggests, is "a reference that can show many different forms"

For example, Tang Laoshi keeps two parrots (round and flat) and a child (nuclear bomb). My daughter-in-law calls them "Sons". Then I said to my daughter-in-law, "go feed your son.". Then if the "son" here refers to a parrot, my daughter-in-law will feed bird food; If the "son" here refers to a nuclear bomb, my daughter-in-law will feed steamed bread.

So how to determine what the "son" here specifically refers to? That's according to the "context" between me and my daughter-in-law.

The same is true of polymorphism in code. Whether a reference refers to a parent object or a subclass object (there may be multiple) is also determined according to the code of the context.

PS: you can infer your family status at home according to the tone of Tang Laoshi's speech.

What are the benefits of using polymorphism?

  1. The cost of using classes by class callers is further reduced.
  • Encapsulation is to make the caller of a class do not need to know the implementation details of the class.
  • Polymorphism allows the caller of a class not to know what the type of the class is, but to know that the object has a method.

Therefore, polymorphism can be understood as a further encapsulation, which further reduces the use cost of class callers.
This also fits the original intention of "managing code complexity" in < < code encyclopedia > >.

  1. It can reduce the "loop complexity" of the code and avoid using a lot of if - else
    For example, what we need to print now is not one shape, but multiple shapes. If not based on polymorphism, the implementation code is as follows:
public static void drawShapes() {
    Rect rect = new Rect();
    Cycle cycle = new Cycle();
    Flower flower = new Flower();
    String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
    
    for (String shape : shapes) {
        if (shape.equals("cycle")) {
        	cycle.draw();
        } else if (shape.equals("rect")) {
        	rect.draw();
        } else if (shape.equals("flower")) {
        	flower.draw();
        }
    }
}

If polymorphism is used, there is no need to write so many if - else branch statements, and the code is simpler.

public static void drawShapes() {
    // We created an array of Shape objects
    Shape[] shapes = {new Cycle(), 
                      new Rect(), 
                      new Cycle(),
    				  new Rect(), 
                      new Flower()
                     };
    for (Shape shape : shapes) {
    	shape.draw();
    }
}

What is "circle complexity"?

Cyclomatic complexity is a way to describe the complexity of a piece of code. If a piece of code is plain and straightforward, it is relatively simple and easy to understand. If there are many conditional branches or loop statements, it is considered more complex to understand.

Therefore, we can simply and roughly calculate the number of conditional statements and loop statements in a piece of code, which is called "cycle complexity". If the cyclomatic complexity of a method is too high, refactoring needs to be considered.

Different companies have different specifications for the cyclomatic complexity of code. Generally, it will not exceed 10.

  1. More scalable
    If you want to add a new shape, the cost of code change using polymorphism is also relatively low.
class Triangle extends Shape {
    @Override
    public void draw() {
    	System.out.println("△");
    }
}

For the caller of the class (drawShapes method), just create an instance of the new class, and the change cost is very low.

If polymorphism is not used, the if - else in drawShapes must be modified to a certain extent, and the change cost is higher.

4.5 downward transformation

Upward transformation means that a child object is transformed into a parent object, and downward transformation means that a parent object is transformed into a child object. Compared with upward transformation, downward transformation is less common, but it also has certain uses.

// Animal.java
public class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
    public void eat(String food) {
        System.out.println("I am a small animal");
        System.out.println(this.name + "I am eating" + food);
    }
}

// Bird.java
public class Bird extends Animal {
    public Bird(String name) {
    	super(name);
    }
    public void eat(String food) {
        System.out.println("I am a bird");
        System.out.println(this.name + "I am eating" + food);
    }
    public void fly() {
    	System.out.println(this.name + "Flying");
    }
}

Next is the familiar operation

Animal animal = new Bird("round");
animal.eat("millet");

// results of enforcement
 Yuanyuan is eating millet

Next, let's try to make Yuanyuan fly

animal.fly();
// Compilation error
 can't find fly method

matters needing attention

During compilation, the type of Animal is Animal. At this time, the compiler only knows that there is an eat method in this class and no fly method.

Although animal actually refers to a Bird object, the compiler uses the type of animal to see which methods are available.

For codes such as animal = new bird ("circle").

  • The compiler checks which methods exist and looks at the type Animal.
  • Whether to execute the methods of the parent class or the methods of the child class depends on the type of Bird.

So if you want to achieve the effect just now, you need to transform downward.

// (Bird) indicates cast
Bird bird = (Bird)animal;
bird.fly();

// results of enforcement
 The circle is flying

However, such downward transformation is sometimes unreliable, such as:

Animal animal = new Cat("kitten");
Bird bird = (Bird)animal;
bird.fly();

// The execution result throws an exception
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Bird
at Test.main(Test.java:35)

animal essentially refers to a Cat object, which cannot be converted into a Bird object, and an exception will be thrown at runtime.

Therefore, in order to make the downward transformation safer, we can first determine whether animal is essentially a Bird instance, and then convert.

Animal animal = new Cat("kitten");
if (animal instanceof Bird) {
    Bird bird = (Bird)animal;
    bird.fly();
}

instanceof can determine whether a reference is an instance of a class. If yes, return true. At this time, it is safer to make a downward transformation.

4.6 super keyword

In the previous code, due to the rewriting mechanism, the methods of subclasses are called. What if you need to call the parent method inside the child class? You can use the super keyword.

super means to get a reference to the parent class instance. Two common uses are involved:

  1. super is used to call the constructor of the parent class (this code has been written earlier);

    public Bird(String name) {
    	super(name);
    }
    
  2. Use super to call the normal methods of the parent class;

    public class Bird extends Animal {
        public Bird(String name) {
        	super(name);
        }
        
        @Override
        public void eat(String food) {
            // Modify the code so that the child calls the interface of the parent class
            super.eat(food);
            System.out.println("I am a bird");
            System.out.println(this.name + "I am eating" + food);
        }
    }
    

In this code, if you directly call eat (without super) in the eat method of the subclass, it is considered to call the eat of the subclass itself (that is, recursion). Adding the super keyword is the method of calling the parent class.

Note that the functions of super and this are somewhat similar, but note the differences:

4.7 call the method of rewriting in a construction method (a pit).

A code with a hole.

We create two classes. B is the parent class and D is the child class. The func method is rewritten in D, and func is called in the construction method of B.

class B {
    public B() {
        // do nothing
        func();
    }
    public void func() {
    	System.out.println("B.func()");
    }
}

class D extends B {
    private int num = 1;
    @Override
    public void func() {
    	System.out.println("D.func() " + num);
    }
}

public class Test {
    public static void main(String[] args) {
    	D d = new D();
    }
}
// results of enforcement
D.func() 0
  • While constructing the D object, the construction method of B will be called;
  • The func method is invoked in the construction method of B, which triggers dynamic binding and calls to func in D.
  • At this time, the D object itself has not been constructed, and num is in an uninitialized state with a value of 0.

Conclusion: "make objects into working state in a way as simple as possible", try not to call the method in the constructor. If this method is rewritten by child class, it will trigger dynamic binding, but at this time the subclass object has not been constructed yet, some hidden but extremely difficult problems may appear.

4.8 summary

Polymorphism is a difficult part of object-oriented programming. We will further understand the use of polymorphism in the following abstract classes and interfaces. The focus is on the coding benefits of polymorphism.

On the other hand, if you put aside Java, polymorphism is actually a broader concept, which is not necessarily related to the syntax of "inheritance".

  • "Dynamic polymorphism" in C + + is similar to Java polymorphism. But C + + also has a "static polymorphism" (template), which has nothing to do with the inheritance system.
  • Polymorphism in Python embodies the "duck type" and has nothing to do with the inheritance system.
  • There is no concept of "inheritance" in Go language, which can also represent polymorphism.

No matter what programming language, the core of polymorphism is to make the caller not pay attention to the specific type of object. This is an important way to reduce users' use cost.

5. Abstract class

5.1 grammar rules

In the example of printing graphics just now, we found that the draw method in the parent Shape does not seem to work in practice. The main graphics are drawn by
The draw ing method of various subclasses of Shape. For a method that does not actually work, we can design it as an abstract method. The class containing the abstract method is called an abstract class.

abstract class Shape {
	abstract public void draw();
}
  • Add the abstract keyword before the draw method to indicate that it is an abstract method. At the same time, abstract methods have no method body (without {}, concrete code cannot be executed).
  • For a class containing abstract methods, the abstract keyword must be added to indicate that it is an abstract class.

matters needing attention:

  1. Abstract classes cannot be instantiated directly.

    Shape shape = new Shape();
    
    // Compilation error
    Error:(30, 23) java: Shape It's abstract; Cannot instantiate
    
  2. Abstract methods cannot be private;

    abstract class Shape {
    	abstract private void draw();
    }
    
    // Compilation error
    Error:(4, 27) java: Illegal modifier combination: abstract and private
    
  3. Abstract classes can contain other non abstract methods or fields. The rules of this non abstract method are the same as those of ordinary methods. It can be overridden or called directly by subclasses.

    abstract class Shape {
        abstract public void draw();
        
        void func() {
        	System.out.println("func");
        }
    }
    
    class Rect extends Shape {
    	...
    }
    
    public class Test {
        public static void main(String[] args) {
            Shape shape = new Rect();
            shape.func();
        }
    }
    
    // results of enforcement
    func
    
  4. If an abstract class a inherits an abstract class B, the abstract class A may not implement the abstract methods of the abstract parent class B. When class A is inherited by an ordinary class, the abstract methods in the two abstract classes a and B must be overridden.

    abstract class B{
        public int a;
    
        public abstract void drow();
    
        public void func(){
            System.out.println("General test methods");
        }
    }
    
    abstract class A extends B{
        public abstract void funcA();
    }
    
    public class Test extends A{
        @Override
        public void funcA(){
            System.out.println("This is A Abstract methods of classes");
    
        }
        @Override
        public void drow(){
            System.out.println("This is B Abstract methods of classes");
        }
    
        public static void main(String[] args) {
            A a = new A() {
                @Override
                public void funcA() {
                    System.out.println("This is A Abstract methods of classes");
                }
    
                @Override
                public void drow() {
    
                }
            };
            a.funcA();
            a.func();
            a.drow();
            System.out.println(a.a);
        }
    }
    
    
  5. Abstract classes and methods cannot be final decorated.

5.2 functions of abstract classes

Abstract classes exist in order to be inherited.

The abstract class itself cannot be instantiated. If you want to use it, you can only create subclasses of the abstract class. Then let the subclass override the abstract methods in the abstract class.

Some students may say that ordinary classes can also be inherited and ordinary methods can also be rewritten. Why do you have to use abstract classes and abstract methods?

That's true, but using abstract classes is equivalent to one more check of the compiler.

The scenario of using abstract classes is like the above code. The actual work should not be completed by the parent class, but by the child class. If you accidentally misuse the parent class at this time, you will not report an error using the ordinary class compiler. However, if the parent class is an abstract class, an error will be prompted when instantiating, so that we can find the problem as soon as possible.

The meaning of many grammars is to "prevent errors". For example, the final we used is similar. If you don't modify the created variables, you can't change them
Is it equivalent to a constant? But in addition, final can let the compiler remind us in time when it is accidentally modified.

Making full use of compiler verification is very meaningful in practical development.

6. Interface

Interfaces are a further step of abstract classes. Abstract classes can also contain non abstract methods and fields, while the methods contained in interfaces are abstract methods, and fields can only contain static constants.

6.1 grammar rules

In the example of printing graphics just now, our parent class Shape does not contain other non abstract methods, and can also be designed as an interface.

interface IShape {
    public abstract void draw(); // Abstract method

    default public void func(){
        System.out.println("Method implementation 11");
    }

    default public void func2(){
        System.out.println("Method implementation 22");
    }

    public static void funcStatic(){
        System.out.println("Static method");
    }
}

class Rect implements IShape{
    @Override
    public void draw() {
        System.out.println("♦");
    }

    @Override
    public void func() {
        System.out.println("Override the default method in the interface");
    }
}
public class Test01 {
    public static void main(String[] args) {
        IShape iShape = new Rect();

        iShape.draw();
        iShape.func();
        iShape.func2();
    }

}
  • Use interface to define an interface;

  • Ordinary methods in the interface cannot have the implementation of specific methods. If necessary, they can only be modified by the keyword default and cannot be rewritten;

  • There can be static modified methods in the interface, which cannot be overridden;

  • All methods in the interface are public, so public can be omitted;

  • The abstract method in the interface is public abstract by default, so abstract can be omitted;

  • The interface cannot be instantiated through the keyword new;

  • Class inherits the interface through the keyword implements. At this time, the meaning of expression is no longer "extension", but "implementation";

  • When a class implements an interface, it must rewrite the abstract method in the interface, and public must be added before rewriting the method.

  • When calling, you can also create an interface reference corresponding to an instance of a subclass.

Extensions vs. implementations

Extension refers to the further expansion of functions when certain functions already exist.

Implementation means that there is nothing at present and needs to be constructed from scratch.

The interface can only contain abstract methods. For fields, the interface can only contain final static, which must be initialized.

interface IShape {
    void draw();
    public static final int num = 10;
}

The public, static and final keywords can be omitted. The omitted num still represents the static constant of public.

Tips:

  1. When we create an interface, the name of the interface usually starts with the capital letter I.
  2. Interface naming generally uses the word of "adjective".
  3. Ali coding specification stipulates that methods and attributes in the interface should not be decorated with any symbols to keep the code concise.

An incorrect code

interface IShape {
	abstract void draw() ; // Even if you do not write public, it is also public
}
class Rect implements IShape {
    void draw() {
    	System.out.println("□") ; // error permission is more strict, so it cannot be overridden.
    }
}

6.2 implementation of multiple interfaces

Sometimes we need to make a class inherit from multiple parent classes at the same time. This is achieved in some programming languages through multiple inheritance.

However, Java only supports single inheritance, and a class can only extend one parent class (ordinary class or abstract class). However, you can use implements to implement multiple interfaces at the same time, which can also achieve the similar effect of multiple inheritance.

Now we represent a group of animals by classes:

class Animal {
    protected String name;
    
    public Animal(String name) {
    	this.name = name;
    }
}

In addition, we provide a group of interfaces, which respectively mean "can fly", "can run" and "can swim".

interface IFlying {
	void fly();
}

interface IRunning {
	void run();
}

interface ISwimming {
	void swim();
}

Next, we create several specific animals;

Cats can run.

class Cat extends Animal implements IRunning {
    public Cat(String name) {
    	super(name);
    }
    
    @Override
    public void run() {
    	System.out.println(this.name + "Running on four legs");
    }
}

Fish can swim.

class Fish extends Animal implements ISwimming {
    public Fish(String name) {
    	super(name);
    }
    
    @Override
    public void swim() {
    	System.out.println(this.name + "He is swimming with his tail");
    }
}

Frogs can run and swim (amphibians).

class Frog extends Animal implements IRunning, ISwimming {
    public Frog(String name) {
    	super(name);
    }
    
    @Override
    public void run() {
    	System.out.println(this.name + "Jumping forward");
    }
    
    @Override
    public void swim() {
    	System.out.println(this.name + "He is kicking his legs to swim");
    }
}

Tip: use ctrl + i in IDEA to quickly implement the interface.

There is also a magical animal, water, land and air, called "duck".

class Duck extends Animal implements IRunning, ISwimming, IFlying {
    public Duck(String name) {
    	super(name);
    }
    
    @Override
    public void fly() {
    	System.out.println(this.name + "Flying with wings");
    }
    
    @Override
    public void run() {
    	System.out.println(this.name + "He is running on two legs");
    }
    
    @Override
    public void swim() {
    	System.out.println(this.name + "Floating on the water");
    }
}

The above code shows the most common usage in Java object-oriented programming: a class inherits a parent class and implements multiple interfaces at the same time.

The meaning of inheritance expression is is - a semantics, while the meaning of interface expression is xxx.

A cat is an animal that can run.

Frog is also an animal. It can run and swim.

Duck is also an animal. It can run, swim and fly.

What are the benefits of this design? Keep the benefits of polymorphism in mind and let the program forget the type. With an interface, users of a class do not have to focus on specific types, but only on whether a class has certain capabilities.

For example, now implement a method called "walking".

public static void walk(IRunning running) {
    System.out.println("I took my partner for a walk");
    running.run();
}

Inside the walk method, we don't care what kind of animal it is, as long as the parameters can run.

Cat cat = new Cat("kitten");
walk(cat);

Frog frog = new Frog("Little frog");
walk(frog);

// results of enforcement
 I took my partner for a walk
 The kitten is running on four legs
 I took my partner for a walk
 The little frog is jumping forward

Even the parameter can not be "animal", as long as it can run!

class Robot implements IRunning {
    private String name;
    
    public Robot(String name) {
    	this.name = name;
    }
    
    @Override
    public void run() {
    	System.out.println(this.name + "Running on wheels");
    }
}

Robot robot = new Robot("robot");
walk(robot);

// results of enforcement
 The robot is running on wheels

6.3 interface application examples

Three common interfaces:

Comparable,

Comparator,

Cloneable

The example just now is more abstract. Let's take another more practical example.

Sort an array of objects

Given a student class

class Student {
    private String name;
    private int score;
    
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
    
    @Override
    public String toString() {
    	return "[" + this.name + ":" + this.score + "]";
    }
}

Give a student object array and sort the elements in the object array (in descending order of scores).

Student[] students = new Student[] {
    new Student("Zhang San", 95),
    new Student("Li Si", 96),
    new Student("Wang Wu", 97),
    new Student("Zhao Liu", 92),
};

According to our previous understanding, we have a ready-made sort method for arrays. Can we use this method directly?

Arrays.sort(students);
System.out.println(Arrays.toString(students));

// Run error, throw exception
Exception in thread "main" java.lang.ClassCastException: Student cannot be cast to
java.lang.Comparable

After careful consideration, it is not difficult to find that, unlike ordinary integers, two integers can be directly compared, and the size relationship is clear. How to determine the size relationship between two student objects? We need to specify additional.

If it is a custom data type for size comparison, be sure to implement the compareTo method in the comparable interface.

Big disadvantage: the class is very invasive. Once it is written, it can't be changed easily.

Let our Student class implement the Comparable interface and the compareTo method.

class Student implements Comparable {
    private String name;
    private int score;
    
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
    
    @Override
    public String toString() {
    	return "[" + this.name + ":" + this.score + "]";
    }
    
    @Override
    public int compareTo(Object o) {
        Student s = (Student)o;
        if (this.score > s.score) {
        	return -1;
        } else if (this.score < s.score) {
        	return 1;
        } else {
        	return 0;
        }
    }
}

In the sort method, the compareTo method will be called automatically. The parameter of compareTo is Object. In fact, what is passed in is an Object of Student type.

Then compare the size relationship (in scores) between the current object and the parameter object.

  • If the current object should be placed before the parameter object, a number less than 0 is returned;
  • If the current object should be placed after the parameter object, a number greater than 0 is returned;
  • If the current object and the parameter object are in no order, return 0;

Execute the program again, and the results will meet expectations.

// results of enforcement
[[Wang Wu:97], [Li Si:96], [Zhang San:95], [Zhao Liu:92]]

Note: for the sort method, each object of the array that needs to be passed in is "comparable", which requires the ability of compareTo. By overriding the compareTo method, you can define comparison rules.

In order to further understand the interface, we can try to implement a sort method to complete the sorting process just now (using bubble sorting)

public static void sort(Comparable[] array) {
    for (int bound = 0; bound < array.length; bound++) {
        for (int cur = array.length - 1; cur > bound; cur--) {
            if (array[cur - 1].compareTo(array[cur]) > 0) {
                // Explain that the order does not meet the requirements, and exchange the position of two variables
                Comparable tmp = array[cur - 1];
                array[cur - 1] = array[cur];
                array[cur] = tmp;
            }
        }
    }
}

Execute the code again

sort(students);
System.out.println(Arrays.toString(students));

// results of enforcement
[[Wang Wu:97], [Li Si:96], [Zhang San:95], [Zhao Liu:92]]

Exercise – comparing Comparable and Comparator

import java.util.Arrays;
import java.util.Comparator;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 17448
 * Date: 2021-11-22
 * Time: 11:20
 */

//  Comparable is very intrusive to classes. Once written, it doesn't dare to change it easily
//class Student implements Comparable<Student> {
//    public int age;
//    public String name;
//    public double score;
//
//    public Student(int age, String name, double score) {
//        this.age = age;
//        this.name = name;
//        this.score = score;
//    }
//
//    @Override
//    public String toString() {
//        return "Student{" +
//                "age=" + age +
//                ", name='" + name + '\'' +
//                ", score=" + score +
//                '}';
//    }
//
//    //Let sort know what sort is based on -- by default
//    @Override
//    public int compareTo(Student o) {
        // Whoever calls compareTo is this
        if (this.score > o.score) {
            return 1;
        } else if (this.score == o.score) {
            return 0;
        } else {
            return -1;
        }
//
//        return (int)(this.score - o.score);
//    }
//}


class Student {
    public int age;
    public String name;
    public double score;

    public Student(int age, String name, double score) {
        this.age = age;
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", score=" + score +
                '}';
    }
}

//  Comparator -- comparator -- is less intrusive to classes
class AgeComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age - o2.age;
    }
}

class ScoreComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return (int)(o1.score - o2.score);
    }
}
public class Test01 {

    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student(12, "bob", 98.9);
        students[1] = new Student(6, "cat", 88.9);
        students[2] = new Student(18, "dog", 9.1);

        System.out.println(Arrays.toString(students));
        AgeComparator ageComparator = new AgeComparator();

        Arrays.sort(students,ageComparator);
        System.out.println(Arrays.toString(students));

        ScoreComparator scoreComparator = new ScoreComparator();
        Arrays.sort(students,scoreComparator);
        System.out.println(Arrays.toString(students));
    }

    public static void main3(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student(12, "bob", 98.9);
        students[1] = new Student(6, "cat", 88.9);
//        if(students[0].compareTo(students[1]) > 0){
//
//        }

        //System.out.println(students[0].compareTo(students[2]));
        AgeComparator ageComparator = new AgeComparator();
        System.out.println(ageComparator.compare(students[0],students[1]));
    }

    public static void main2(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student(12, "bob", 98.9);
        students[1] = new Student(6, "cat", 88.9);
        students[2] = new Student(18, "dog", 99.1);

        System.out.println(Arrays.toString(students));
        Arrays.sort(students);
        System.out.println(Arrays.toString(students));

    }

    public static void main1(String[] args) {
        // Simple integer type sorting
        int[] array = {1, 21, 3, 14, 5, 16};
        System.out.println(Arrays.toString(array));
        Arrays.sort(array);
        System.out.println(Arrays.toString(array));
    }
}

Inheritance between interfaces

Interfaces can inherit an interface to achieve the effect of reuse. Use the extends keyword.

interface IRunning {
	void run();
}

interface ISwimming {
	void swim();
}

// Amphibians can run and swim
interface IAmphibious extends IRunning, ISwimming {
    
}

class Frog implements IAmphibious {
	...
}

Create a new interface through interface inheritance. IAmphibious means "amphibious". At this time, to implement the Frog class created by the interface, you need to implement both the run method and the swim method.

Inheritance between interfaces is equivalent to merging multiple interfaces together.

6.4 Clonable interface and deep copy

Some useful interfaces are built in Java, and Clonable is one of them.

There is a clone method in the Object class. Calling this method can create a "copy" of the Object. However, if you want to call the clone method legally, you must implement the Clonable interface first, otherwise you will throw a clonnotsupportedexception exception.

class Animal implements Cloneable {
private String name;
@Override
public Animal clone() {
Animal o = null;
try {
o = (Animal)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
}
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
Animal animal2 = animal.clone();
System.out.println(animal == animal2);
}
}
// Output results
// false

Shallow copy VS deep copy

Cloneable copies an object as a "shallow copy".

Observe the following codes:

public class Test {
    static class A implements Cloneable {
        public int num = 0;
        @Override
        public A clone() throws CloneNotSupportedException {
        	return (A)super.clone();
    	}
	}
    
    static class B implements Cloneable {
        public A a = new A();
        
        @Override
        public B clone() throws CloneNotSupportedException {
        	return (B)super.clone();
        }
    }
    
    public static void main(String[] args) throws CloneNotSupportedException {
        B b = new B();
        B b2 = b.clone();
        b.a.num = 10;
        System.out.println(b2.a.num);
    }
}
// results of enforcement
10

The b object copied through the clone only copies the b itself, but does not copy the internal a object. At this time, the a reference contained in b and b2 still points to the same object. When you modify one side, the other side will also change.

When we learn serialization in the future, we will tell you how to make deep copy.

7. Summary

Abstract classes and interfaces are common uses of polymorphism in Java. Both need to grasp the key points and recognize the difference between the two (important!!! Common interview questions).

Core difference: abstract classes can contain ordinary methods and fields. Such ordinary methods and fields can be directly used by subclasses (without rewriting), while interfaces cannot contain ordinary methods. Subclasses must override all abstract methods.

As the example of Animal written before. The Animal here contains an attribute such as name, which exists in any subclass. Therefore, the Animal here can only be used as an abstract class, not an interface.

class Animal {
    protected String name;
    
    public Animal(String name) {
    	this.name = name;
    }
}

Remind again:

The significance of abstract classes is to enable the compiler to better verify. We will not use classes like Animal directly, but use its subclasses. If you accidentally create an instance of Animal, the compiler will remind us in time.

I hope it can help you. If there is anything wrong, please give me advice. Thank you!

Tags: Java OOP Polymorphism abstract class inheritance

Posted on Mon, 22 Nov 2021 09:36:12 -0500 by gljaber