What is polymorphism? What is its implementation mechanism? What is the difference between overloading and rewriting? These are the four very important concepts we are going to review this time: inheritance, polymorphism, overloading and overwriting.
inheritance
Simply put, inheritance is to generate a new type based on an existing type by adding new methods or redefining existing methods (as discussed below, this method is called rewriting). Inheritance is one of the three basic characteristics of object-oriented - encapsulation, inheritance, and polymorphism. Every class we write when using JAVA is inheriting, because in the JAVA language, the java.lang.Object class is The most fundamental base class (or parent class or super class) of all classes. If a newly defined class we define does not explicitly specify which base class it inherits from, then JAVA will default to it inheriting from the Object class.
We can divide classes in JAVA into the following three types:
Class: A class defined using class and does not contain abstract methods.
Abstract class: A class defined using abstract class, which may or may not contain abstract methods.
Interface: A class defined using interface.
The following inheritance rules exist between these three types:
Classes can extend classes, abstract classes, and implements interfaces.
Abstract classes can inherit (extends) classes, they can inherit (extends) abstract classes, and they can inherit (implements) interfaces.
Interfaces can only extend interfaces.
Please note that the different keywords extends and implements used in each inheritance case in the above three rules cannot be replaced at will. As we all know, after an ordinary class inherits an interface, it must implement all the methods defined in this interface, otherwise it can only be defined as an abstract class. The reason why I don't use the term "implementation" for the implements keyword here is because conceptually it also represents an inheritance relationship, and in the case of the abstract class implements interface, it does not have to implement this interface definition. Any method, so it is more reasonable to use inheritance.
The above three rules also comply with the following constraints:
Both classes and abstract classes can only inherit at most one class, or at most one abstract class, and these two situations are mutually exclusive, that is, they either inherit a class or an abstract class.
When classes, abstract classes and interfaces inherit interfaces, they are not restricted by the number. In theory, they can inherit an unlimited number of interfaces. Of course, for a class, it must implement all methods defined in all interfaces it inherits.
When an abstract class inherits an abstract class or implements an interface, it may partially, completely or completely not implement the abstract methods of the parent abstract class or the interfaces defined in the parent class interface.
When a class inherits an abstract class or implements an interface, it must implement all abstract methods of the parent abstract class or all interfaces defined in the parent class interface.
The benefit that inheritance brings to our programming is the reuse (reuse) of original classes. Just like the reuse of modules, the reuse of classes can improve our development efficiency. In fact, the reuse of modules is the superimposed effect of the reuse of a large number of classes. In addition to inheritance, we can also use composition to reuse classes. The so-called combination is to define the original class as an attribute of the new class, and achieve reuse by calling the methods of the original class in the new class. If there is no included relationship between the newly defined type and the original type, that is to say, from an abstract concept, the things represented by the newly defined type are not one of the things represented by the original type, such as yellow people It is a type of human being, and there is a relationship between them, including and being included, so at this time, combination is a better choice to achieve reuse. The following example is a simple example of the combination:
public class Sub { private Parent p = new Parent(); public void doSomething() { // Reuse the method p.method() of the Parent class; // other code } } class Parent { public void method() { // do something here } }
Of course, in order to make the code more efficient, we can also initialize it when we need to use the original type (such as Parent p).
Using inheritance and combination to reuse original classes is an incremental development model. The advantage of this method is that there is no need to modify the original code, so it will not bring new bugs to the original code. , and there is no need to re-test due to modifications to the original code, which is obviously beneficial to our development. Therefore, if we are maintaining or transforming an original system or module, especially when we do not have a thorough understanding of them, we can choose the incremental development model, which can not only greatly improve our development efficiency, but also Avoid risks caused by modifications to original code.
Polymorphism
Polymorphism is another important basic concept. As mentioned above, it is one of the three basic characteristics of object-oriented. What exactly is polymorphism? Let’s first look at the following example to help understand:
//Car interface interface Car { // Car name String getName(); // Get the car price int getPrice(); } // BMW class BMW implements Car { public String getName() { return "BMW"; } public int getPrice() { return 300000; } } // CheryQQ class CheryQQ implements Car { public String getName() { return "CheryQQ"; } public int getPrice() { return 20000; } } // Car sales shop public class CarShop { // Car sales income private int money = 0; // Sell a car public void sellCar(Car car) { System.out.println("Car model: " + car.getName() + " Unit price: " + car.getPrice()); // Increase the income from car sales money += car.getPrice(); } // Total income from car sales public int getMoney() { return money; } public static void main(String[] args) { CarShop aShop = new CarShop(); // Sell a BMW aShop.sellCar(new BMW()); // Sell a BMW Chery QQ aShop.sellCar(new CheryQQ()); System.out.println("Total revenue: " + aShop.getMoney()); } }
Running results:
Model: BMW Unit price: 300,000
Model: CheryQQ Unit price: 20,000
Total revenue: 320,000
Inheritance is the basis for realizing polymorphism. Literally understood, polymorphism is a type (both Car type) showing multiple states (the name of BMW is BMW and the price is 300,000; the name of Chery is CheryQQ and the price is 2,000). Associating a method call with the subject (that is, the object or class) to which the method belongs is called binding, which is divided into two types: early binding and late binding. Their definitions are explained below:
Early binding: binding before the program is run, implemented by the compiler and linker, also called static binding. For example, static methods and final methods. Note that private methods are also included here because they are implicitly final.
Late binding: binding according to the type of the object at runtime, implemented by the method calling mechanism, so it is also called dynamic binding, or runtime binding. All methods except early binding are late binding.
Polymorphism is implemented on the mechanism of late binding. The benefit that polymorphism brings to us is that it eliminates the coupling relationship between classes and makes the program easier to expand. For example, in the above example, to add a new type of car for sale, you only need to let the newly defined class inherit the Car class and implement all its methods without making any modifications to the original code. The sellCar(Car car) of the CarShop class Method can handle new car models. The new code is as follows:
// Santana Car class Santana implements Car { public String getName() { return "Santana"; } public int getPrice() { return 80000; } }
Overloading and overriding
Overloading and rewriting are both concepts for methods. Before clarifying these two concepts, let's first understand what the method's structure is (the English name is signature, some are translated as "signature", although it is used More widely, but this translation is not accurate). Structure refers to the composition structure of a method, specifically including the name and parameters of the method, covering the number, type and order of appearance of the parameters, but does not include the return value type of the method, access modifiers, and modifications such as abstract, static, and final. symbol. For example, the following two methods have the same structure:
public void method(int i, String s) { // do something } public String method(int i, String s) { // do something }
These two are methods with different configurations:
public void method(int i, String s) { // do something } public void method(String s, int i) { // do something }
After understanding the concept of Gestalt, let's take a look at overloading and rewriting. Please look at their definitions:
Overriding, the English name is overriding, means that in the case of inheritance, a new method with the same structure as the method in the base class is defined in the subclass, which is called the subclass overriding the method of the base class. This is a necessary step to achieve polymorphism.
Overloading, the English name is overloading, refers to defining more than one method with the same name but different structures in the same class. In the same class, it is not allowed to define more than one method with the same type.
Let's consider an interesting question: Can constructors be overloaded? The answer is of course yes, we often do this in actual programming. In fact, the constructor is also a method. The constructor name is the method name, the constructor parameters are the method parameters, and its return value is an instance of the newly created class. However, the constructor cannot be overridden by a subclass, because a subclass cannot define a constructor with the same type as the base class.
Overloading, overriding, polymorphism and function hiding
It is often seen that some beginners of C++ have a vague understanding of overloading, overwriting, polymorphism and function hiding. I will write some of my own opinions here, hoping to help C++ beginners clear up their doubts.
Before we understand the complex and subtle relationship between overloading, overwriting, polymorphism and function hiding, we must first review basic concepts such as overloading and coverage.
First, let's look at a very simple example to understand what function hiding is.
#include <iostream>using namespace std;class Base{public: void fun() { cout << "Base::fun()" << endl; }};class Derive : public Base{public: void fun(int i ) { cout << "Derive::fun()" << endl; }};int main(){ Derive d; //The following sentence is wrong, so it is blocked //d.fun();error C2660: 'fun' : function does not take 0 parameters d.fun(1); Derive *pd =new Derive(); //The following sentence is wrong, so it is blocked //pd->fun();error C2660: 'fun' : function does not take 0 parameters pd->fun(1); delete pd; return 0;}
/*Functions in different non-namespace scopes do not constitute overloading. Subclasses and parent classes are two different scopes.
In this example, the two functions are in different scopes, so they are not overloaded unless the scope is a namespace scope. */
In this example, the function is not overloaded or overridden, but hidden.
The next five examples explain specifically what hiding is.
Example 1
#include <iostream>using namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d; d.fun();//Correct, the derived class does not have a function declaration with the same name as the base class, then all overloaded functions with the same name in the base class will be used as candidate functions. d.fun(1);//Correct, the derived class does not have a function declaration with the same name as the base class, then all overloaded functions with the same name in the base class will be used as candidate functions. return 0;}
Example 2
#include <iostream>using namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: //New function version, all overloaded versions of the base class are blocked, here, we call it hide function hide //If there is a declaration of a function with the same name of the base class in the derived class, the function with the same name in the base class will not be used as a candidate function, even if the base class has multiple versions of overloaded functions with different parameter lists. void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;} void fun2(){cout << "Derive::fun2()" << endl ;}};int main(){ Derive d; d.fun(1,2); //The following sentence is wrong, so it is blocked //d.fun();error C2660: 'fun' : function does not take 0 parameters return 0;}
Example 3
#include <iostream>using namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: //Override one of the function versions of the override base class. Similarly, all overloaded versions of the base class are hidden. //If there is a declaration of a function with the same name of the base class in the derived class, the function with the same name in the base class will not be used as a candidate function, even if the base class has multiple versions of overloaded functions with different parameter lists. void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d; d.fun(); //The following sentence is wrong, so it is blocked //d.fun(1);error C2660: 'fun' : function does not take 1 parameters return 0;}
Example 4
#include <iostream>using namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: using Basic::fun; void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d; d.fun();//Correct d.fun(1); //Correct return 0;}/*Output result Derive::fun()Base::fun(int i)Press any key to continue*/
Example 5
#include <iostream>using namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: using Basic::fun; void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d; d .fun();//Correct d.fun(1);//Correct d.fun(1,2);//Correct return 0;}/*Output result Base::fun()Base::fun(int i)Derive::fun(int i,int j)Press any key to continue*/
Okay, let’s first make a small summary of the characteristics between overloading and overwriting.
Characteristics of overload:
n the same scope (in the same class);
n The function name is the same but the parameters are different;
n The virtual keyword is optional.
Override means that a derived class function covers a base class function. The characteristics of override are:
n different scopes (located in derived classes and base classes respectively);
n The function name and parameters are the same;
n Base class functions must have the virtual keyword. (If there is no virtual keyword, it is called hidden hide)
If the base class has multiple overloaded versions of a function, and you override (override) one or more function versions in the base class in the derived class, or add new ones in the derived class function version (same function name, different parameters), then all overloaded versions of the base class are blocked, which we call hidden here. So, in general, when you want to use a new function version in a derived class and want to use the function version of the base class, you should override all overloaded versions in the base class in the derived class. If you do not want to override the overloaded function version of the base class, you should use Example 4 or Example 5 to explicitly declare the base class namespace scope.
In fact, the C++ compiler believes that there is no relationship between functions with the same function name and different parameters. They are simply two unrelated functions. It's just that the C++ language introduced the concepts of overloading and overwriting in order to simulate the real world and allow programmers to deal with real-world problems more intuitively. Overloading is under the same namespace scope, while overriding is under different namespace scopes. For example, base class and derived class are two different namespace scopes. During the inheritance process, if a derived class has the same name as a base class function, the base class function will be hidden. Of course, the situation discussed here is that there is no virtual keyword in front of the base class function. We will discuss the situation when there is a virtual keyword keyword separately.
An inherited class overrides a function version of the base class to create its own functional interface. At this time, the C++ compiler thinks that since you now want to use the rewritten interface of the derived class, the interface of my base class will not be provided to you (of course you can use the method of explicitly declaring the namespace scope, see [C++ Basics] Overloading, overwriting, polymorphism and function hiding (1)). It ignores that the interface of your base class has overloading characteristics. If you want to continue to maintain the overloading feature in the derived class, then give the interface overloading feature yourself. Therefore, in a derived class, as long as the function name is the same, the function version of the base class will be ruthlessly blocked. In the compiler, masking is implemented through namespace scope.
Therefore, to maintain the overloaded version of the base class function in the derived class, you should override all the overloaded versions of the base class. Overloading is only valid in the current class, and inheritance will lose the characteristics of function overloading. In other words, if you want to put the overloaded function of the base class in the inherited derived class, you must rewrite it.
"Hidden" here means that the function of the derived class blocks the base class function with the same name. Let us also make a brief summary of the specific rules:
n If the function of the derived class has the same name as the function of the base class, but the parameters are different. At this time, if the base class does not have the virtual keyword, the functions of the base class will be hidden. (Be careful not to confuse it with overloading. Although the function with the same name and different parameters should be called overloading, it cannot be understood as overloading here because the derived class and the base class are not in the same namespace scope. This is understood as hiding)
n If the function of the derived class has the same name as the function of the base class, but the parameters are different. At this time, if the base class has the virtual keyword, the functions of the base class will be implicitly inherited into the vtable of the derived class. At this time, the function in the derived class vtable points to the function address of the base class version. At the same time, this new function version is added to the derived class as an overloaded version of the derived class. But when the base class pointer implements polymorphic calling function method, this new derived class function version will be hidden.
n If the function of the derived class has the same name as the function of the base class, and the parameters are also the same, but the base class function does not have the virtual keyword. At this time, the functions of the base class are hidden. (Be careful not to confuse it with coverage, which is understood here as hiding).
n If the function of the derived class has the same name as the function of the base class, and the parameters are also the same, but the base class function has the virtual keyword. At this time, the functions of the base class will not be "hidden". (Here, you have to understand it as coverage ^_^).
Interlude: When there is no virtual keyword in front of the base class function, we need to rewrite it more smoothly. When there is the virtual keyword, it is more reasonable to call it override. Don't do this. I also hope that everyone can better understand C++. Something subtle. Without further ado, let’s illustrate with an example.
Example 6
#include <iostream>using namespace std; class Base{public: virtual void fun() { cout << "Base::fun()" << endl; }//overload virtual void fun(int i) { cout << "Base::fun(int i)" << endl; }//overload}; class Derive : public Base{public: void fun() { cout << "Derive::fun()" << endl; }//override void fun(int i) { cout << "Derive::fun(int i)" << endl; }//override void fun(int i,int j){ cout<< "Derive::fun (int i,int j)" <<endl;}//overload}; int main(){ Base *pb = new Derive(); pb->fun(); pb->fun(1); //The following sentence is wrong, so it is blocked //pb->fun(1,2); virtual function cannot be overloaded, error C2661: 'fun' : no overloaded function takes 2 parameters cout << endl; Derive *pd = new Derive(); pd->fun(); pd->fun(1); pd->fun(1,2);//overload delete pb; delete pd; return 0;}/*
Output results
Derive::fun()
Derive::fun(int i)
Derive::fun()
Derive::fun(int i)
Derive::fun(int i,int j)
Press any key to continue
*/
Example 7-1
#include <iostream> using namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }}; class Derive : public Base{}; int main(){ Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) delete pb; return 0;}
Example 7-2
#include <iostream> using namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }}; class Derive : public Base{public: void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } }; int main(){ Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) pb->fun((double)0.01);//Base::fun(int i) delete pb; return 0;}
Example 8-1
#include <iostream> using namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }}; class Derive : public Base{public: void fun(int i){ cout <<"Derive::fun(int i)"<< endl; }}; int main(){ Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) delete pb; return 0;}
Example 8-2
#include <iostream> using namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }}; class Derive : public Base{public: void fun(int i){ cout <<"Derive::fun(int i)"<< endl; } void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } }; int main(){ Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) pb->fun((double) 0.01);//Derive::fun(int i) delete pb; return 0;}
Example 9
#include <iostream> using namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }};class Derive : public Base{public: void fun(int i){ cout <<"Derive::fun(int i)"<< endl; } void fun(char c){ cout <<"Derive::fun(char c)"<< endl; } void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } };int main(){ Base *pb = new Derive(); pb->fun(1); //Derive::fun(int i) pb->fun('a');//Derive::fun(int i) pb->fun((double)0.01);//Derive::fun(int i) Derive *pd =new Derive(); pd->fun(1);//Derive::fun(int i) // overload pd->fun('a');//Derive::fun(char c) //overload pd->fun(0.01);//Derive::fun(double d) delete pb; delete pd; return 0;}
Examples 7-1 and 8-1 are easy to understand. I put these two examples here for everyone to make a comparison and to help everyone understand better:
n In Example 7-1, the derived class does not cover the virtual function of the base class. At this time, the address pointed by the function pointer in the vtable of the derived class is the virtual function address of the base class.
n In Example 8-1, the derived class overrides the virtual function of the base class. At this time, the address pointed to by the function pointer in the vtable of the derived class is the address of the derived class's own overridden virtual function.
Examples 7-2 and 8-2 look a bit strange. In fact, if you compare them according to the above principles, the answer will be clear:
n In Example 7-2, we overloaded a function version for the derived class: void fun(double d) In fact, this is just a cover-up. Let's analyze it specifically. There are several functions in the base class and several functions in the derived class:
type base class derived class
Vtable section
void fun(int i)
Points to the base class version of the virtual function void fun(int i)
static part
void fun(double d)
Let’s analyze the following three lines of code again:
Base *pb = new Derive();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
The first sentence is the key. The base class pointer points to the object of the derived class. We know that this is a polymorphic call. The second sentence follows. The base class pointer at runtime is found to be a derived class object based on the type of the runtime object, so first Go to the vtable of the derived class to find the virtual function version of the derived class. It is found that the derived class does not cover the virtual function of the base class. The vtable of the derived class only makes a pointer to the address of the virtual function of the base class, so it is natural to call the base class. Class version of the virtual function. In the last sentence, the program is still looking for the vtable of the derived class, and finds that there is no virtual function of this version at all, so it has to go back and call its own virtual function.
It is also worth mentioning here that if the base class has multiple virtual functions at this time, the program will prompt "Unclear call" when compiling the program. Examples are as follows
#include <iostream> using namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; } virtual void fun(char c){ cout < <"Base::fun(char c)"<< endl; }}; class Derive : public Base{public: void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } }; int main(){ Base *pb = new Derive(); pb->fun(0.01);//error C2668: 'fun' : ambiguous call to overloaded function delete pb; return 0;}
Okay, let’s analyze Example 8-2 again.
n In Example 8-2, we also overloaded a function version for the derived class: void fun(double d), and also covered the virtual function of the base class. Let’s analyze it in detail. There are several functions in the base class, and the derived class has several functions. The class has several functions:
type base class derived class
Vtable section
void fun(int i)
void fun(int i)
static part
void fun(double d)
From the table, we can see that the function pointer in the vtable of the derived class points to its own overridden virtual function address.
Let’s analyze the following three lines of code again:
Base *pb = new Derive();
pb->fun(1);//Derive::fun(int i)
pb->fun((double)0.01);//Derive::fun(int i)
There is no need to say more about the first sentence. The second sentence is to call the virtual function version of the derived class as a matter of course. The third sentence, hey, it feels weird again. In fact, C++ programs are very stupid. When running, they immerse themselves in In the vtable table of the derived class, I just looked at it and realized that there is no version I want. I really can’t figure it out. , why don't the base class pointers look around and look for it? Haha, it turns out that the eyesight is limited. The base class is so old, so it must be presbyopia. All its eyes can see is its own non-Vtable part ( That is, the static part) and the Vtable part you want to manage, the void of the derived class fun(double d) is so far away, you can’t see it! Besides, the derived class has to take care of everything, doesn’t the derived class have some power of its own? Hey, no more arguing, everyone can take care of themselves^_^
Alas! Are you going to sigh? The base class pointer can make polymorphic calls, but it can never make overloaded calls to derived classes (refer to Example 6)~~~
Let’s look at Example 9 again,
The effect of this example is the same as that of Example 6, with the same purpose. I believe that after you understand the above examples, this is also a little Kiss.
summary:
Overloading selects the function version to be called based on the parameter list of the function, while polymorphism selects the virtual function version to be called based on the actual type of the runtime object. Polymorphism is implemented through derived classes to base classes. The virtual virtual function is implemented by overriding it. If the derived class does not override the virtual virtual function of the base class, the derived class will automatically inherit the virtual virtual function of the base class. Function version. At this time, no matter whether the object pointed to by the base class pointer is a base type or a derived type, the virtual virtual function of the base class version will be called; if the derived class overrides the virtual virtual function of the base class, it will be called at runtime according to The actual type of the object is used to select the virtual virtual function version to be called. For example, if the object type pointed to by the base class pointer is a derived type, the virtual virtual function version of the derived class will be called, thus achieving polymorphism.
The original intention of using polymorphism is to declare the function as virtual in the base class, and to override the virtual virtual function version of the base class in the derived class. Note that the function prototype at this time is consistent with the base class, that is, the same name and the same name. Parameter type; if you add a new function version to the derived class, you cannot dynamically call the new function version of the derived class through the base class pointer. This new function version only serves as an overloaded version of the derived class. Still the same sentence, overloading is only valid in the current class. Whether you overload it in a base class or a derived class, the two are not related to each other. If we understand this, we can also successfully understand the output results in Examples 6 and 9.
Overloading is statically bound, polymorphism is dynamically bound. To further explain, overloading has nothing to do with the type of object that the pointer actually points to, and polymorphism is related to the type of object that the pointer actually points to. If a base class pointer calls an overloaded version of a derived class, the C++ compiler considers it illegal. The C++ compiler only thinks that the base class pointer can only call the overloaded version of the base class, and the overload only works in the namespace of the current class. Valid within the domain, inheritance will lose the overloading feature. Of course, if the base class pointer at this time calls a virtual function, Then it will also dynamically select the virtual virtual function version of the base class or the virtual virtual function version of the derived class to perform specific operations. This is determined by the type of object actually pointed by the base class pointer, so overloading and pointers The type of object that the pointer actually points to has nothing to do with it; polymorphism is related to the type of object that the pointer actually points to.
Finally, to clarify, virtual virtual functions can also be overloaded, but overloading can only be effective within the scope of the current namespace. How many String objects have been created?
Let's first look at a piece of code:
Java code
String str=new String("abc");
This code is often followed by the question, that is, how many String objects is created by this line of code? I believe everyone is familiar with this question, and the answer is well known, 2. Next, we will start from this question and review some JAVA knowledge related to creating String objects.
We can divide the above line of code into four parts: String str, =, "abc" and new String(). String str only defines a String type variable named str, so it does not create an object; = initializes the variable str and assigns a reference (or handle) to an object to it, and obviously does not create an object. ;Now only new String("abc") is left. So, why can new String("abc") be regarded as "abc" and new String()? Let's take a look at the String constructor we called:
Java code
public String(String original) {
//other code...
}
As we all know, there are two commonly used methods for creating instances (objects) of a class:
Use new to create objects.
Call the newInstance method of the Class class and use the reflection mechanism to create the object.
We used new to call the above constructor method of the String class to create an object and assign its reference to the str variable. At the same time, we noticed that the parameter accepted by the called constructor method is also a String object, and this object is exactly "abc". From this we have to introduce another way to create a String object - text contained within quotation marks.
This method is unique to String, and it is very different from the new method.
Java code
String str="abc";
There is no doubt that this line of code creates a String object.
Java code
String a="abc";
String b="abc";
What about here? The answer is still one.
Java code
String a="ab"+"cd";
What about here? The answer is still one. A little weird? At this point, we need to introduce a review of string pool related knowledge.
There is a string pool in the JAVA Virtual Machine (JVM), which stores many String objects and can be shared, so it improves efficiency. Since the String class is final, its value cannot be changed once created, so we don't have to worry about program confusion caused by sharing String objects. The string pool is maintained by the String class, and we can call the intern() method to access the string pool.
Let's look back at String a="abc";. When this line of code is executed, the JAVA virtual machine first searches in the string pool to see if such an object with the value "abc" already exists. Its judgment is based on The return value of the String class equals(Object obj) method. If there is, no new object will be created, and a reference to the existing object will be returned directly; if not, the object will be created first, then added to the string pool, and then its reference will be returned. Therefore, it is not difficult for us to understand why the first two of the previous three examples have this answer.
For the third example:
Java code
String a="ab"+"cd";
Because the value of the constant is determined at compile time. Here, "ab" and "cd" are constants, so the value of variable a can be determined at compile time. The compiled effect of this line of code is equivalent to:
Java code
String a="abcd";
Therefore, only one object "abcd" is created here, and it is saved in the string pool.
Now the question comes again, will all the strings obtained after "+" connection be added to the string pool? We all know that "==" can be used to compare two variables. It has the following two situations:
If two basic types (char, byte, short, int, long, float, double, boolean) are compared, it is judged whether their values are equal.
If the table compares two object variables, it is judged whether their references point to the same object.
Next we will use "==" to do a few tests. For ease of explanation, we regard pointing to an object that already exists in the string pool as the object being added to the string pool:
Java code
public class StringTest { public static void main(String[] args) { String a = "ab";// Create an object and add it to the string pool System.out.println("String a = /"ab/" ;"); String b = "cd";// An object is created and added to the string pool System.out.println("String b = /"cd/";"); String c = "abcd"; // An object is created and added to the string pool String d = "ab" + "cd"; // If d and c point to the same object, it means that d has also been added to the string pool if (d == c ) { System.out.println("/"ab/"+/"cd/" The created object/" is added to/" the string pool"); } // If d and c do not point to the same object, It means that d has not been added to the string pool else { System.out.println("/"ab/"+/"cd/" The created object/"is not added/" to the string pool"); } String e = a + "cd"; // If e and c Points to the same object, it means that e has also been added to the string pool if (e == c) { System.out.println(" a +/"cd/" The created object/"joined/" string pool middle"); } // If e and c do not point to the same object, it means that e has not been added to the string pool else { System.out.println(" a +/"cd/" created object/"not added/" to the string pool" ); } String f = "ab" + b; // If f and c point to the same object, it means that f has also been added to the string pool if (f == c) { System.out.println("/ Object created by "ab/"+ b /"Added/" to the string pool"); } // If f and c do not point to the same object, it means that f has not been added to the string pool else { System.out.println("/"ab/" + b created object/"not added/" to the string pool"); } String g = a + b; // If g and c point to the same object, it means that g has also been added to the string pool if ( g == c) { System.out.println(" a + b The created object/"added/" to the string pool"); } // If g and c do not point to the same object, it means that g has not been added to the string pool else { System.out.println(" a + b created object/"not added/" to the string pool"); } } }
The running results are as follows:
String a = "ab";
String b = "cd";
The object created by "ab"+"cd" is "joined" in the string pool
The object created by a + "cd" is "not added" to the string pool.
The object created by "ab"+ b is "not added" to the string pool.
The object created by a + b is "not added" to the string pool. From the above results, we can easily see that only new objects generated by using "+" connections between String objects created using quotation marks to include text will be added. in the string pool. For all "+" connection expressions that contain objects created in new mode (including null), the new objects they generate will not be added to the string pool, and we will not go into details about this.
But there is one situation that demands our attention. Please look at the code below:
Java code
public class StringStaticTest { // Constant A public static final String A = "ab"; // Constant B public static final String B = "cd"; public static void main(String[] args) { // Use two constants +Connect to initialize s String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s equals t, they are the same object"); } else { System.out.println("s is not equal to t, they are not the same object"); } } }
The result of running this code is as follows:
s is equal to t. They are the same object. Why? The reason is this. For a constant, its value is fixed, so it can be determined at compile time, while the value of a variable can only be determined at runtime, because this variable can be called by different methods, so May cause the value to change. In the above example, A and B are constants and their values are fixed, so the value of s is also fixed, and it is determined when the class is compiled. That is to say:
Java code
String s=A+B;
Equivalent to:
Java code
String s="ab"+"cd";
Let me change the above example slightly and see what happens:
Java code
public class StringStaticTest { // Constant A public static final String A; // Constant B public static final String B; static { A = "ab"; B = "cd"; } public static void main(String[] args) { // Initialize s by connecting the two constants with + String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s is equal to t, they are the same object"); } else { System.out.println("s is not equal to t, they are not the same object"); } } }
The result of its operation is this:
s is not equal to t. They are not the same object but have been slightly modified. The result is exactly the opposite of the example just now. Let’s analyze it again. Although A and B are defined as constants (can only be assigned once), they are not assigned immediately. Before the value of s is calculated, when they are assigned and what value they are assigned are all variables. Therefore, A and B behave like a variable before being assigned a value. Then s cannot be determined at compile time, but can only be created at runtime.
Since the sharing of objects in the string pool can improve efficiency, we encourage everyone to create String objects by including text in quotes. In fact, this is what we often use in programming.
Next let's take a look at the intern() method, which is defined as follows:
Java code
public native String intern();
This is a native method. When calling this method, the JAVA virtual machine first checks whether an object with a value equal to the object already exists in the string pool. If so, it returns a reference to the object in the string pool; if not, it first creates an object in the string pool. String object with the same value, and then returns its reference.
Let's look at this code:
Java code
public class StringInternTest { public static void main(String[] args) { // Use a char array to initialize a to avoid that an object with the value "abcd" already exists in the string pool before a is created String a = new String( new char[] { 'a', 'b', 'c', 'd' }); String b = a.intern(); if (b == a) { System.out.println("b was added to the string pool, no new object was created"); } else { System.out.println("b was not added to the string pool, no new object was created"); } } }
Running results:
b has not been added to the string pool and a new object has been created. If the intern() method of the String class does not find an object with the same value, it adds the current object to the string pool and then returns its reference, then b and a Points to the same object; otherwise, the object pointed to by b is newly created by the JAVA virtual machine in the string pool, but its value is the same as a. The running result of the above code just confirms this point.
Finally, let’s talk about the storage of String objects in the JAVA Virtual Machine (JVM), and the relationship between the string pool and the heap and stack. Let’s first review the difference between heap and stack:
Stack: Mainly saves basic types (or built-in types) (char, byte, short, int, long, float, double, boolean) and object references. Data can be shared, and its speed is second only to register. Faster than heap.
Heap: used to store objects.
When we look at the source code of the String class, we will find that it has a value attribute, which stores the value of the String object. The type is char[], which also shows that a string is a sequence of characters.
When executing String a="abc";, the JAVA virtual machine creates three char values 'a', 'b' and 'c' in the stack, and then creates a String object in the heap, its value (value ) is an array of three char values just created on the stack {'a', 'b', 'c'}. Finally, the newly created String object will be added to the string pool. If we then execute the String b=new String("abc"); code, since "abc" has been created and saved in the string pool, the JAVA virtual machine will only create a new String object in the heap, but its The value is the three char type values 'a', 'b' and 'c' created on the stack when the previous line of code was executed.
At this point, we are already quite clear on the question of why String str=new String("abc") raised at the beginning of this article creates two objects.