Home > Java > javaTutorial > Introduction to inheritance, polymorphism, overloading and rewriting in Java

Introduction to inheritance, polymorphism, overloading and rewriting in Java

高洛峰
Release: 2017-01-19 13:54:10
Original
1693 people have browsed it

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 based on an existing type, by adding new methods or redefining existing methods (will be discussed below, this This method is called overwriting) to generate a new type. 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: Class defined using interface.

The following inheritance rules exist between these three types:

Classes can inherit (extends) classes, can inherit (extends) abstract classes, and can inherit (implements) interfaces.
Abstract classes can inherit (extends) classes, can inherit (extends) abstract classes, and can inherit (implements) interfaces.
Interfaces can only inherit (extends) 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:

Classes and abstract classes can only inherit at most one class, or inherit at most one abstract class, and these two situations are mutually exclusive. That is to say, 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 can partially, completely or completely not implement the abstract method of the parent abstract class or the interface 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 kind 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 method:

public class Sub { 
  private Parent p = new Parent(); 
  
  public void doSomething() { 
    // 复用Parent类的方法 
    p.method(); 
    // other code 
  } 
} 
  
class Parent { 
  public void method() { 
    // do something here 
  } 
}
Copy after login

Of course, in order to make the code more efficient, we can also initialize the original type (such as Parent p) when we need to use it.

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 affect the original code. The code brings new bugs, and there is no need to re-test because of 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 take a look at the following example to help understand:

//汽车接口 
interface Car { 
  // 汽车名称 
  String getName(); 
  
  // 获得汽车售价 
  int getPrice(); 
} 
  
// 宝马 
class BMW implements Car { 
  public String getName() { 
    return "BMW"; 
  } 
  
  public int getPrice() { 
    return 300000; 
  } 
} 
  
// 奇瑞QQ 
class CheryQQ implements Car { 
  public String getName() { 
    return "CheryQQ"; 
  } 
  
  public int getPrice() { 
    return 20000; 
  } 
} 
  
// 汽车出售店 
public class CarShop { 
  // 售车收入 
  private int money = 0; 
  
  // 卖出一部车 
  public void sellCar(Car car) { 
    System.out.println("车型:" + car.getName() + " 单价:" + car.getPrice()); 
    // 增加卖出车售价的收入 
    money += car.getPrice(); 
  } 
  
  // 售车总收入 
  public int getMoney() { 
    return money; 
  } 
  
  public static void main(String[] args) { 
    CarShop aShop = new CarShop(); 
    // 卖出一辆宝马 
    aShop.sellCar(new BMW()); 
    // 卖出一辆奇瑞QQ 
    aShop.sellCar(new CheryQQ()); 
    System.out.println("总收入:" + aShop.getMoney()); 
  } 
}
Copy after login

Running results:

Car model: BMW Unit price: 300000
Car model: CheryQQ Unit price: 20000
Total income: 320000

继承是多态得以实现的基础。从字面上理解,多态就是一种类型(都是Car类型)表现出多种状态(宝马汽车的名称是BMW,售价是300000;奇瑞汽车的名称是CheryQQ,售价是2000)。将一个方法调用同这个方法所属的主体(也就是对象或类)关联起来叫做绑定,分前期绑定和后期绑定两种。下面解释一下它们的定义:

前期绑定:在程序运行之前进行绑定,由编译器和连接程序实现,又叫做静态绑定。比如static方法和final方法,注意,这里也包括private方法,因为它是隐式final的。
后期绑定:在运行时根据对象的类型进行绑定,由方法调用机制实现,因此又叫做动态绑定,或者运行时绑定。除了前期绑定外的所有方法都属于后期绑定。

多态就是在后期绑定这种机制上实现的。多态给我们带来的好处是消除了类之间的耦合关系,使程序更容易扩展。比如在上例中,新增加一种类型汽车的销售,只需要让新定义的类继承Car类并实现它的所有方法,而无需对原有代码做任何修改,CarShop类的sellCar(Car car)方法就可以处理新的车型了。新增代码如下:

// 桑塔纳汽车 
class Santana implements Car { 
  public String getName() { 
    return "Santana"; 
  } 
  
  public int getPrice() { 
    return 80000; 
  } 
}
Copy after login

重载(overloading)和重写(overriding)

重载和重写都是针对方法的概念,在弄清楚这两个概念之前,我们先来了解一下什么叫方法的型构(英文名是signature,有的译作“签名”,虽然它被使用的较为广泛,但是这个翻译不准确的)。型构就是指方法的组成结构,具体包括方法的名称和参数,涵盖参数的数量、类型以及出现的顺序,但是不包括方法的返回值类型,访问权限修饰符,以及abstract、static、final等修饰符。比如下面两个就是具有相同型构的方法:

public void method(int i, String s) { 
  // do something 
} 
  
public String method(int i, String s) { 
  // do something 
}
Copy after login

而这两个就是具有不同型构的方法:

public void method(int i, String s) { 
  // do something 
} 
  
public void method(String s, int i) { 
  // do something 
}
Copy after login

了解完型构的概念后我们再来看看重载和重写,请看它们的定义:

重写,英文名是overriding,是指在继承情况下,子类中定义了与其基类中方法具有相同型构的新方法,就叫做子类把基类的方法重写了。这是实现多态必须的步骤。
重载,英文名是overloading,是指在同一个类中定义了一个以上具有相同名称,但是型构不同的方法。在同一个类中,是不允许定义多于一个的具有相同型构的方法的。

我们来考虑一个有趣的问题:构造器可以被重载吗?答案当然是可以的,我们在实际的编程中也经常这么做。实际上构造器也是一个方法,构造器名就是方法名,构造器参数就是方法参数,而它的返回值就是新创建的类的实例。但是构造器却不可以被子类重写,因为子类无法定义与基类具有相同型构的构造器。

重载、覆盖、多态与函数隐藏

经常看到C++的一些初学者对于重载、覆盖、多态与函数隐藏的模糊理解。在这里写一点自己的见解,希望能够C++初学者解惑。

要弄清楚重载、覆盖、多态与函数隐藏之间的复杂且微妙关系之前,我们首先要来回顾一下重载覆盖等基本概念。

首先,我们来看一个非常简单的例子,理解一下什么叫函数隐藏hide。

#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;
     //下面一句错误,故屏蔽掉
  //d.fun();error C2660: &#39;fun&#39; : function does not take 0 parameters
  d.fun(1);
     Derive *pd =new Derive();
     //下面一句错误,故屏蔽掉
     //pd->fun();error C2660: &#39;fun&#39; : function does not take 0 parameters
     pd->fun(1);
     delete pd;
  return 0;
}
Copy after login

/*在不同的非命名空间作用域里的函数不构成重载,子类和父类是不同的两个作用域。
在本例中,两个函数在不同作用域中,故不够成重载,除非这个作用域是命名空间作用域。*/
在这个例子中,函数不是重载overload,也不是覆盖override,而是隐藏hide。

接下来的5个例子具体说明一下什么叫隐藏

例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();//正确,派生类没有与基类同名函数声明,则基类中的所有同名重载函数都会作为候选函数。
     d.fun(1);//正确,派生类没有与基类同名函数声明,则基类中的所有同名重载函数都会作为候选函数。
     return 0;
}
Copy after login

例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:
     //新的函数版本,基类所有的重载版本都被屏蔽,在这里,我们称之为函数隐藏hide
  //派生类中有基类的同名函数的声明,则基类中的同名函数不会作为候选函数,即使基类有不同的参数表的多个版本的重载函数。
     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);
     //下面一句错误,故屏蔽掉
     //d.fun();error C2660: &#39;fun&#39; : function does not take 0 parameters
     return 0;
}
Copy after login

例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基类的其中一个函数版本,同样基类所有的重载版本都被隐藏hide
  //派生类中有基类的同名函数的声明,则基类中的同名函数不会作为候选函数,即使基类有不同的参数表的多个版本的重载函数。
     void fun(){cout << "Derive::fun()" << endl;}
     void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
     Derive d;
     d.fun();
     //下面一句错误,故屏蔽掉
     //d.fun(1);error C2660: &#39;fun&#39; : function does not take 1 parameters
     return 0;
}
Copy after login

例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();//正确
     d.fun(1);//正确
     return 0;
}
/*
输出结果
Derive::fun()
Base::fun(int i)
Press any key to continue
*/
Copy after login

例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();//正确
     d.fun(1);//正确
     d.fun(1,2);//正确
     return 0;
}
/*
输出结果
Base::fun()
Base::fun(int i)
Derive::fun(int i,int j)
Press any key to continue
*/
Copy after login

好了,我们先来一个小小的总结重载与覆盖两者之间的特征

重载overload的特征:

n 相同的范围(在同一个类中);
n 函数名相同参数不同;
n virtual 关键字可有可无。

覆盖override是指派生类函数覆盖基类函数,覆盖的特征是:

n 不同的范围(分别位于派生类与基类);
n 函数名和参数都相同;
n 基类函数必须有virtual 关键字。(若没有virtual 关键字则称之为隐藏hide)

如果基类有某个函数的多个重载(overload)版本,而你在派生类中重写(override)了基类中的一个或多个函数版本,或是在派生类中重新添加了新的函数版本(函数名相同,参数不同),则所有基类的重载版本都被屏蔽,在这里我们称之为隐藏hide。所以,在一般情况下,你想在派生类中使用新的函数版本又想使用基类的函数版本时,你应该在派生类中重写基类中的所有重载版本。你若是不想重写基类的重载的函数版本,则你应该使用例4或例5方式,显式声明基类名字空间作用域。
事实上,C++编译器认为,相同函数名不同参数的函数之间根本没有什么关系,它们根本就是两个毫不相关的函数。只是C++语言为了模拟现实世界,为了让程序员更直观的思维处理现实世界中的问题,才引入了重载和覆盖的概念。重载是在相同名字空间作用域下,而覆盖则是在不同的名字空间作用域下,比如基类和派生类即为两个不同的名字空间作用域。在继承过程中,若发生派生类与基类函数同名问题时,便会发生基类函数的隐藏。当然,这里讨论的情况是基类函数前面没有virtual 关键字。在有virtual 关键字关键字时的情形我们另做讨论。
继承类重写了基类的某一函数版本,以产生自己功能的接口。此时C++编绎器认为,你现在既然要使用派生类的自己重新改写的接口,那我基类的接口就不提供给你了(当然你可以用显式声明名字空间作用域的方法,见[C++基础]重载、覆盖、多态与函数隐藏(1))。而不会理会你基类的接口是有重载特性的。若是你要在派生类里继续保持重载的特性,那你就自己再给出接口重载的特性吧。所以在派生类里,只要函数名一样,基类的函数版本就会被无情地屏蔽。在编绎器中,屏蔽是通过名字空间作用域实现的。

所以,在派生类中要保持基类的函数重载版本,就应该重写所有基类的重载版本。重载只在当前类中有效,继承会失去函数重载的特性。也就是说,要把基类的重载函数放在继承的派生类里,就必须重写。

这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,具体规则我们也来做一小结:

n 如果派生类的函数与基类的函数同名,但是参数不同。此时,若基类无virtual关键字,基类的函数将被隐藏。(注意别与重载混淆,虽然函数名相同参数不同应称之为重载,但这里不能理解为重载,因为派生类和基类不在同一名字空间作用域内。这里理解为隐藏)
n 如果派生类的函数与基类的函数同名,但是参数不同。此时,若基类有virtual关键字,基类的函数将被隐式继承到派生类的vtable中。此时派生类vtable中的函数指向基类版本的函数地址。同时这个新的函数版本添加到派生类中,作为派生类的重载版本。但在基类指针实现多态调用函数方法时,这个新的派生类函数版本将会被隐藏。
n 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏。(注意别与覆盖混淆,这里理解为隐藏)。
n 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数有virtual关键字。此时,基类的函数不会被“隐藏”。(在这里,你要理解为覆盖哦^_^)。

插曲:基类函数前没有virtual关键字时,我们要重写更为顺口些,在有virtual关键字时,我们叫覆盖更为合理些,戒此,我也希望大家能够更好的理解C++中一些微妙的东西。费话少说,我们举例说明吧。

例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);
 //下面一句错误,故屏蔽掉
 //pb->fun(1,2);virtual函数不能进行overload,error C2661: &#39;fun&#39; : 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;
}
/*
Copy after login

输出结果


Derive::fun()
Derive::fun(int i)

Derive::fun()
Derive::fun(int i)
Derive::fun(int i,int j)
Press any key to continue
*/

例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;
}
Copy after login

例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;
}
Copy after login


例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;
}
Copy after login

例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;
}
Copy after login

例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(&#39;a&#39;);//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(&#39;a&#39;);//Derive::fun(char c)    
     //overload
     pd->fun(0.01);//Derive::fun(double d)    
  
     delete pb;
     delete pd;
     return 0;
}
Copy after login


例7-1和例8-1很好理解,我把这两个例子放在这里,是让大家作一个比较摆了,也是为了帮助大家更好的理解:
n 例7-1中,派生类没有覆盖基类的虚函数,此时派生类的vtable中的函数指针指向的地址就是基类的虚函数地址。
n 例8-1中,派生类覆盖了基类的虚函数,此时派生类的vtable中的函数指针指向的地址就是派生类自己的重写的虚函数地址。
在例7-2和8-2看起来有点怪怪,其实,你按照上面的原则对比一下,答案也是明朗的:
n 例7-2中,我们为派生类重载了一个函数版本:void fun(double d) 其实,这只是一个障眼法。我们具体来分析一下,基类共有几个函数,派生类共有几个函数:
类型
基类
派生类

Vtable部分
void fun(int i)
指向基类版的虚函数void fun(int i)

静态部分

void fun(double d)


我们再来分析一下以下三句代码
Base *pb = new Derive();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)

这第一句是关键,基类指针指向派生类的对象,我们知道这是多态调用;接下来第二句,运行时基类指针根据运行时对象的类型,发现是派生类对象,所以首先到派生类的vtable中去查找派生类的虚函数版本,发现派生类没有覆盖基类的虚函数,派生类的vtable只是作了一个指向基类虚函数地址的一个指向,所以理所当然地去调用基类版本的虚函数。最后一句,程序运行仍然埋头去找派生类的vtable,发现根本没有这个版本的虚函数,只好回头调用自己的仅有一个虚函数。

这里还值得一提的是:如果此时基类有多个虚函数,此时程序编绎时会提示”调用不明确”。示例如下

#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: &#39;fun&#39; : ambiguous call to overloaded function
     delete pb;
     return 0;
}
Copy after login

好了,我们再来分析一下例8-2。

n 例8-2中,我们也为派生类重载了一个函数版本:void fun(double d) ,同时覆盖了基类的虚函数,我们再来具体来分析一下,基类共有几个函数,派生类共有几个函数:

Type
Base class
Derived class

Vtable part
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 rewritten virtual function address.

Let’s analyze the following three lines of code

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. In the second sentence, it is natural to call the virtual function of the derived class. Function version, the third sentence, hey, it feels weird again. In fact, C++ programs are very stupid. When running, they immersed themselves in the vtable table of the derived class and took a look at it. Damn, I didn’t even think about it. I really can’t figure out the version I want. Why don’t the base class pointers just go around and look for it? Haha, it turns out that my eyesight is limited. The base class is so old. It must be presbyopia. Its eyes can only see It’s the non-Vtable part (i.e. the static part) and the Vtable part that you want to manage. The void fun (double d) of the derived class is so far away that you can’t see it! Besides, the derived class has to take care of everything. Don’t the derived class not Do you have some rights of your own? Hey, stop arguing, each mind his own business^_^

Alas! Are you going to sigh? The base class pointer can make polymorphic calls, but it can never make derived classes. Overloaded call (refer to Example 6)~~~

Let’s take a look at Example 9 again.
The effect of this example is the same as Example 6, with the same approach but the same goal. 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. More The realization of state is achieved by overriding the virtual virtual function of the base class by the derived class. 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 version of the base class. , 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 object's The actual type 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 override base class in the derived class. Note that the function prototype and base class at this time Keep it consistent, that is, the same name and 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 linked, and polymorphism is dynamically linked. 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 name space of the current class. Valid within the domain, inheritance will lose the overloading feature. Of course, if the base class pointer calls a virtual function at this time, 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 that the base class pointer actually points to, so 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.

Finally, to clarify, virtual virtual functions can also be overloaded, but overloading can only be valid 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");
What follows this code is often the question, that is, what exactly is this line of code? How many String objects have been created? 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.
我们可以把上面这行代码分成String str、=、"abc"和new String()四部分来看待。String str只是定义了一个名为str的String类型的变量,因此它并没有创建对象;=是对变量str进行初始化,将某个对象的引用(或者叫句柄)赋值给它,显然也没有创建对象;现在只剩下new String("abc")了。那么,new String("abc")为什么又能被看成"abc"和new String()呢?我们来看一下被我们调用了的String的构造器:
Java代码
public String(String original) {
//other code ...
}
大家都知道,我们常用的创建一个类的实例(对象)的方法有以下两种:
使用new创建对象。
调用Class类的newInstance方法,利用反射机制创建对象。
我们正是使用new调用了String类的上面那个构造器方法创建了一个对象,并将它的引用赋值给了str变量。同时我们注意到,被调用的构造器方法接受的参数也是一个String对象,这个对象正是"abc"。由此我们又要引入另外一种创建String对象的方式的讨论——引号内包含文本。
这种方式是String特有的,并且它与new的方式存在很大区别。
Java代码
String str="abc";
毫无疑问,这行代码创建了一个String对象。
Java代码
String a="abc";
String b="abc";
那这里呢?答案还是一个。
Java代码
String a="ab"+"cd";
再看看这里呢?答案仍是一个。有点奇怪吗?说到这里,我们就需要引入对字符串池相关知识的回顾了。
在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
我们再回头看看String a="abc";,这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,它的判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。因此,我们不难理解前面三个例子中头两个例子为什么是这个答案了。
对于第三个例子:
Java代码
String a="ab"+"cd";
由于常量的值在编译的时候就被确定了。在这里,"ab"和"cd"都是常量,因此变量a的值在编译时就可以确定。这行代码编译后的效果等同于:
Java代码
String a="abcd";
因此这里只创建了一个对象"abcd",并且它被保存在字符串池里了。
现在问题又来了,是不是所有经过“+”连接后得到的字符串都会被添加到字符串池中呢?我们都知道“==”可以用来比较两个变量,它有以下两种情况:
如果比较的是两个基本类型(char,byte,short,int,long,float,double,boolean),则是判断它们的值是否相等。
如果表较的是两个对象变量,则是判断它们的引用是否指向同一个对象。
下面我们就用“==”来做几个测试。为了便于说明,我们把指向字符串池中已经存在的对象也视为该对象被加入了字符串池:
Java代码

public class StringTest {
  public static void main(String[] args) {
    String a = "ab";// 创建了一个对象,并加入字符串池中
    System.out.println("String a = \"ab\";");
    String b = "cd";// 创建了一个对象,并加入字符串池中
    System.out.println("String b = \"cd\";");
    String c = "abcd";// 创建了一个对象,并加入字符串池中
  
    String d = "ab" + "cd";
    // 如果d和c指向了同一个对象,则说明d也被加入了字符串池
    if (d == c) {
      System.out.println("\"ab\"+\"cd\" 创建的对象 \"加入了\" 字符串池中");
    }
    // 如果d和c没有指向了同一个对象,则说明d没有被加入字符串池
    else {
      System.out.println("\"ab\"+\"cd\" 创建的对象 \"没加入\" 字符串池中");
    }
  
    String e = a + "cd";
    // 如果e和c指向了同一个对象,则说明e也被加入了字符串池
    if (e == c) {
      System.out.println(" a +\"cd\" 创建的对象 \"加入了\" 字符串池中");
    }
    // 如果e和c没有指向了同一个对象,则说明e没有被加入字符串池
    else {
      System.out.println(" a +\"cd\" 创建的对象 \"没加入\" 字符串池中");
    }
  
    String f = "ab" + b;
    // 如果f和c指向了同一个对象,则说明f也被加入了字符串池
    if (f == c) {
      System.out.println("\"ab\"+ b  创建的对象 \"加入了\" 字符串池中");
    }
    // 如果f和c没有指向了同一个对象,则说明f没有被加入字符串池
    else {
      System.out.println("\"ab\"+ b  创建的对象 \"没加入\" 字符串池中");
    }
  
    String g = a + b;
    // 如果g和c指向了同一个对象,则说明g也被加入了字符串池
    if (g == c) {
      System.out.println(" a + b  创建的对象 \"加入了\" 字符串池中");
    }
    // 如果g和c没有指向了同一个对象,则说明g没有被加入字符串池
    else {
      System.out.println(" a + b  创建的对象 \"没加入\" 字符串池中");
    }
  }
}
Copy after login

运行结果如下:
String a = "ab";
String b = "cd";
"ab"+"cd" 创建的对象 "加入了" 字符串池中
a +"cd" 创建的对象 "没加入" 字符串池中
"ab"+ b 创建的对象 "没加入" 字符串池中
a + b 创建的对象 "没加入" 字符串池中
从上面的结果中我们不难看出,只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中,对此我们不再赘述。
但是有一种情况需要引起我们的注意。请看下面的代码:
Java代码

public class StringStaticTest {
  // 常量A
  public static final String A = "ab";
  
  // 常量B
  public static final String B = "cd";
  
  public static void main(String[] args) {
    // 将两个常量用+连接对s进行初始化
    String s = A + B;
    String t = "abcd";
    if (s == t) {
      System.out.println("s等于t,它们是同一个对象");
    } else {
      System.out.println("s不等于t,它们不是同一个对象");
    }
  }
}
Copy after login

这段代码的运行结果如下:
s等于t,它们是同一个对象
这又是为什么呢?原因是这样的,对于常量来讲,它的值是固定的,因此在编译期就能被确定了,而变量的值只有到运行时才能被确定,因为这个变量可以被不同的方法调用,从而可能引起值的改变。在上面的例子中,A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:
Java代码
String s=A+B;
等同于:
Java代码
String s="ab"+"cd";
我对上面的例子稍加改变看看会出现什么情况:
Java代码

public class StringStaticTest {
  // 常量A
  public static final String A;
  
  // 常量B
  public static final String B;
  
  static {
    A = "ab";
    B = "cd";
  }
  
  public static void main(String[] args) {
    // 将两个常量用+连接对s进行初始化
    String s = A + B;
    String t = "abcd";
    if (s == t) {
      System.out.println("s等于t,它们是同一个对象");
    } else {
      System.out.println("s不等于t,它们不是同一个对象");
    }
  }
}
Copy after login


它的运行结果是这样:
s不等于t,它们不是同一个对象
只是做了一点改动,结果就和刚刚的例子恰好相反。我们再来分析一下。A和B虽然被定义为常量(只能被赋值一次),但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。
由于字符串池中对象的共享能够带来效率的提高,因此我们提倡大家用引号包含文本的方式来创建String对象,实际上这也是我们在编程中常采用的。
接下来我们再来看看intern()方法,它的定义如下:
Java代码
public native String intern();
这是一个本地方法。在调用这个方法时,JAVA虚拟机首先检查字符串池中是否已经存在与该对象值相等对象存在,如果有则返回字符串池中对象的引用;如果没有,则先在字符串池中创建一个相同值的String对象,然后再将它的引用返回。
我们来看这段代码:
Java代码

public class StringInternTest {
  public static void main(String[] args) {
    // 使用char数组来初始化a,避免在a被创建之前字符串池中已经存在了值为"abcd"的对象
    String a = new String(new char[] { &#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;d&#39; });
    String b = a.intern();
    if (b == a) {
      System.out.println("b被加入了字符串池中,没有新建对象");
    } else {
      System.out.println("b没被加入字符串池中,新建了对象");
    }
  }
}
Copy after login

运行结果: 
b没被加入字符串池中,新建了对象
如果String类的intern()方法在没有找到相同值的对象时,是把当前对象加入字符串池中,然后返回它的引用的话,那么b和a指向的就是同一个对象;否则b指向的对象就是JAVA虚拟机在字符串池中新建的,只是它的值与a相同罢了。上面这段代码的运行结果恰恰印证了这一点。 
最后我们再来说说String对象在JAVA虚拟机(JVM)中的存储,以及字符串池与堆(heap)和栈(stack)的关系。我们首先回顾一下堆和栈的区别: 
栈(stack):主要保存基本类型(或者叫内置类型)(char、byte、short、int、long、float、double、boolean)和对象的引用,数据可以共享,速度仅次于寄存器(register),快于堆。
堆(heap):用于存储对象。
我们查看String类的源码就会发现,它有一个value属性,保存着String对象的值,类型是char[],这也正说明了字符串就是字符的序列。 
当执行String a="abc";时,JAVA虚拟机会在栈中创建三个char型的值'a'、'b'和'c',然后在堆中创建一个String对象,它的值(value)是刚才在栈中创建的三个char型值组成的数组{'a','b','c'},最后这个新创建的String对象会被添加到字符串池中。如果我们接着执行String b=new String("abc");代码,由于"abc"已经被创建并保存于字符串池中,因此JAVA虚拟机只会在堆中新创建一个String对象,但是它的值(value)是共享前一行代码执行时在栈中创建的三个char型值值'a'、'b'和'c'。 
说到这里,我们对于篇首提出的String str=new String("abc")为什么是创建了两个对象这个问题就已经相当明了了。

更多Java中继承、多态、重载和重写介绍相关文章请关注PHP中文网!

Related labels:
source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template