多态也称动态绑定、后期绑定、运行时绑定。
封装通过合并特征和行为来创建新的数据类型。实现隐藏则通过将细节私有化把接口和实现分离开来。多态的作用则是消除类型之间的耦合关系。
继承允许将对象视为自己本身的类型或者基类来加以处理。多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要他们从一个基类导出。
类的生命周期:
加载 loading
验证 verification
准备 preparation
解析 resolution
初始化 initialization
使用 using
卸载 unloading
8.1再论向上转型
不管导出类的存在,只和基类打交道,这正是多态允许的。
8.2转机
将一个方法的调用同一个方法的主题关联起来叫做绑定。
java中除了static方法和final方法之外都是动态绑定的。
方法的重写Overriding和重载Overloading是Java多态性的不同表现。重写Overriding是父类与子类之间多态性的一种表现, 重载Overloading是一个类中多态性的一种表现。如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写 (Overriding)。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被“屏蔽”了。如果在一个类中定义了多个同名的方 法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overloading)。
8.3构造器和多态
尽管构造器不具有多态性(实际上是隐式的static方法),但是还是非常有必要了解构造器怎样通过多态在复杂层次结构中运作。
8.4协变反悔类型
导出类中被覆盖的方法可以返回基类方法的返回类型的某种导出类型。协变返回类型允许返回更具体的类型。
8.5用继承进行设计
Java语言的三大特性:继承、封装和多态。
继承:复用类的一种方法,可以简省很多代码;
封装:通过合并特征和行为来创建新的数据类型。【这种“数据类型”跟Java本身提供的8大“基本数据类型”的地位完全相同。Java通过封装这种方式来扩充数据类型。】
多态:消除创新的数据类型之间的耦合关系。
2、前期绑定:static和final方法都是前期绑定(在编译时绑定和执行);
3、后期绑定:Java中除了static和final方法,都是后期绑定(前面提到过,private方法属于final方法)。
4、多态性:
只有后期绑定的方法具有多态性【其他的,如:前期绑定方法、域(字段)等不具有多态性】;
换句话说就是,多态性是面向对象的特性(三大特性之一)。而Java语言并不是一门完全的或者说纯粹的面向对象语言,像静态的这些就是反对象的东西。
5、构造器初始化
一定会调用父类的构造器,先为父类初始化,获取父类的对象(子对象),再进行自身初始化。
6、转型:
向上转型:会丢失新方法;
向下转型:需要作类型检查。
例子:
1 package lkl;
2
3 public class Cycle {
4 public int wheels (){
5 return 0;
6 }
7 }
8
9 public class Tricycle extends Cycle{
10 public int wheels(){
11 return 3;
12 }
13 }
14
15 public class BiCycle extends Cycle{
16 public int wheels(){
17 return 2;
18 }
19 }
20
21 public class Unicycle extends Cycle{
22 public int wheels(){
23 return 1;
24 }
25 }
26
27 public class RideTest {
28 public int wheels(Cycle cy){
29 return cy.wheels();
30 }
31 public static void main(String[] args){
32 RideTest rt = new RideTest();
33 ///根据传入的不同子类表现不同的行为,称为多态
34 System.out.println(rt.wheels(new Unicycle()));
35 System.out.println(rt.wheels(new BiCycle()));
36 System.out.println(rt.wheels(new Tricycle()));
多态
——“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过“私有化(private)”将接口和实现分离开来。多态的作用:消除类型之间的耦合关系。又名:动态绑定dynamic binding/后期绑定late binding或运行时绑定run-time binding
1. 向上转型:将对某个对象的引用视为对其基类型引用的做法被称作“向上转型”,不用管导出类的存在,编写的代码只是与基类打交道。
2. 方法调用绑定:前期绑定(early binding)程序执行前进行绑定---由编译器和链接程序实现。后期绑定(late binding)在运行时,根据对象的类型进行绑定。
3. 扩展性:多态——我们所做的代码修改,不会对程序中其他不应 影响的部分产生破坏。让程序员“将改变的事物与未变的事物分离开来”。
4. 缺陷:会重载“私有方法”。在导出类中,对于基类的private方法,最好用一个不同的名字。
5. 抽象类和抽象方法:包含抽象方法的类叫做“抽象类(abstract class)”,如果一个类包含一个或多个抽象方法,该类必须被限制为是抽象的,(否则,编译器会报错)。
6. 构造器和多态:要理解构造器怎样通过多态在复杂的层次结构中运作。
1) 构造器的调用顺序:a)调用基类构造器。b)按声明顺序调用成员的初始状态设置模块c)调用导出类构造器的主体。
2) 继承与清除:对于属性,处理的顺序和声明的顺序相反。对于基类,应该首先对其导出类进行清除,然后才是基类。
3) 构造器内部的多态方法的行为:一条规则——用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法。
7. 用继承进行设计:用继承表达行为间的差异,并用属性表达状态上的变化。
纯继承与扩展:向上转型存在的问题——不能调用那些新增方法。
向下转型与运行期类型标识:向下转型,若失败,则返回一个classCastException异常。运行期类型识别(RTTI),运行期间对类型进行检查的行为。
OOP语言中,多态是封装、继承之后的第三种基本特征。
封装:通过合并特征和行为来创建新的数据类型,“实现隐藏”通过细节“私有化”把接口和实现分离。
继承:以复用接口方式从已有类型用extends关键字创建新类型,并允许向上转型。
多态:消除类型之间的耦合关系(分离做什么和怎么做),基于继承的向上转型功能,允许同一种类型同一行为有不同的表现。
8.1再论向上转型
8.1.1忘记对象类型
不管导出类的存在,编写的代码(方法)只是针对基类类型。不需要为每个导出类型都写各自的代码,这正是多态所允许的。
8.2转机
程序运行时接受的是基类类型,但是它如何知道具体类型是哪一个从而调用正确的方法呢?我们需要了解绑定机制。
8.2.1方法调用绑定
把方法调用同方法主体关联起来称为绑定。
前期绑定:程序执行前绑定(由编译器和连接程序实现),C语言中方法调用都是前期绑定。
后期绑定:又叫动态绑定,运行时绑定,在运行时根据对象的类型绑定对应的方法主体。
Java中默认就是动态绑定,无需手动设置。特殊:static方法和final(private也是final)方法不存在多态性,不是动态绑定。
8.2.2产生正确行为
动态绑定使得多态中的基类对象可以正确执行相应的导出类对象方法。
8.2.3可扩展性
多态使得扩展新类型和扩展基类不会对已有代码(调用基类方法的代码)产生影响。它可以让程序员“将改变的事物与不变的事物分离开”。
8.2.4缺陷:不可以覆盖private方法
基类中private方法在子类中可以用相同的方法名和签名,但是它是一个全新的方法,不会按照我们想要的子类方法来执行。
调用的时候,按照基类方法的访问权限来决定是否可以调用。
子类是否会覆盖父类方法,按照子类是否可以访问到父类该方法来决定是否可以覆盖。
8.2.5缺陷:域与静态方法
多态特性(动态绑定)只是针对方法的。域和静态方法不具有这种特性。
如:父类和子类都有一个域 public String str; 在Super s = new Sub(); s.str 取出的是Super里的而不是Sub里的。 不过一般情况不会存在这种把域设置为public并且想用子类覆盖它的情况。
静态方法也不会有多态性。
8.3构造器和多态
构造器是隐式static方法,不具有多态特性。
8.3.1构造器的调用顺序
为什么编译器强制每个导出类的构造器必须调用基类构造器呢:因为构造器有个特殊的任务,检查对象是否被正确构造。导出类构造器只能访问它自己的成员,不能访问基类的成员(通常是private成员)。只有基类构造器才具有相应的知识和权限对自己的元素进行初始化。而导出类成员的初始化有可能会用到基类成员,因此导出类初始化在基类之后。
8.3.2继承与清理
通过组合和继承方式创建新类时,通常情况都是不需要担心对象的清理问题。
但是如果的确需要做清理时,必须非常小心谨慎:在使用完之后按照创建逆序清理,即sub.dispose()然后super.dispose()来清理。
更加复杂的情况:不知道什么时候使用结束,需要自己定义引用计数,然后再清理。
8.3.3构造器内部的多态方法行为
在调用子类构造器的过程中,会先调用父类构造器,此时子类构造器还没调用完成,子类对象也没有执行初始化,如果在父类构造器里调用多态方法,那么这个方法是可以产生多态行为特征的,但是由于子类构造器没有执行完,因此子类的初始化还没完成,多态方法里对子类成员变量的获取只能拿到默认值0,false,null
对象初始化过程(注意与类的加载过程区分):
1.给导出类对象分配内存空间,并初始化为0,false,null
2.调用父类构造器,并执行多态方法,拿到的是子类0,false,null的域
3.按照声明顺序调用成员变量的初始化
4.调用子类构造器主体
此处虽然逻辑没什么问题,但是行为的确错误了,所以在写构造器的时候,我们要尽可能用简单的方法使对象进入正常状态,如果可以的话避免调用其他方法。
8.4协变返回类型
导出类重写父类方法,方法的返回类型(区分返回值)可以是父类返回类型的某一个导出类。
8.5用继承进行设计
就创建新类型而言,不要只想到继承,应该优先考虑组合,它比继承具有更大的灵活性,可以动态的改变类型,而继承在编译时类型已经确定了。
8.5.1纯继承与扩展
纯粹的继承:基类接口与导出类完全一致,是is-a关系
扩展:导出类除了基类接口外,还有其他方法,是is-like-a关系,但是这些扩展方法不能以基类引用去调用
8.5.2向下转型与运行时类型识别
向上转型是安全的:基类不会有大于导出类的接口
向下转型需要确保类型的正确性:Java中类型转换(括号强转)都会进行类型检查,不正确抛出ClassCastException。这种在运行期间对类型进行检查的行为称作“运行时类型识别”(RTTI)。
8.6总结
多态意味着“不同的形式”。在OOP里,我们持有基类的相同接口,使用该接口的不同形式:不同版本的动态绑定。
1)向上转型
class TV{
public static void show(TV tv){
System.out.println("TV");
}
}
public class LeTV extends TV{
public static void main(String[] args) {
LeTV letv = new LeTV();
TV.show(letv);
}
}
看到没有,程序正常运行。本来参数类型是TV类型,但是子类传进去也可以。这种将LeTV转为父类TV的行为,就是向上转型。
向上转型是安全的,子类必须继承父类的方法(这是继承的定义),自己可以拥有自己的属性和方法,从一个特定的类向一个通用的类转换,是安全的。反过来有普通到特殊却有不对应的风险。
(2)多态
先看例子:
class TV{
public void show(){
System.out.println("TV");
}
}
class LeTV extends TV{
public void show(){
System.out.println("LeTV");
}
}
class MiTV extends TV{
public void show(){
System.out.println("MiTV");
}
}
class SanTV extends TV{
public void show(){
System.out.println("SanTV");
}
}
public class EveryTV {
public static void tvshow(LeTV tv){
tv.show();
}
public static void tvshow(MiTV tv){
tv.show();
}
public static void tvshow(SanTV tv){
tv.show();
}
public static void main(String[] args) {
tvshow(new LeTV());
tvshow(new MiTV());
tvshow(new SanTV());
}
}
程序没什么问题,每个方法都有对应的类型。但是除了这三个子类,还有几十,几百个TV子类,那么tvshow方法岂不是也要写上对应的几十甚至几百个?
然后,多态出现了。
public class EveryTV {
public static void tvshow(TV tv){
tv.show();
}
public static void main(String[] args) {
tvshow(new LeTV());
tvshow(new MiTV());
tvshow(new SanTV());
}
}
程序简洁了很多,并且运行结果和上面的一样。美好了许多。再看一看,这和向上转型很像。
多态,可以理解成多种形式,多种状态。多态也叫动态绑定、后期绑定、运行时绑定。
谈到绑定就要联系前期绑定(前期绑定国内的搜索真的是惨不忍睹,把编程思想的话全部抄进去,不会去真正理解前期绑定是什么!)。
what is Early and Late Binding?(来自stackoverflow)。
The short answer is that early (or static) binding refers to compile time binding and late (or dynamic) binding refers to runtime binding (for example when you use reflection)——byChristian Hagelid.
简单说前期绑定是编译期的绑定,后期绑定是运行时的绑定。
书上指出C的方法全部都是前期绑定,如果上面多态的例子是前期绑定的话,编译时就要绑定,但是tv这个引用只有一个,即使你传进去的是LeTV,它也不知道是要执行TV的show方法还是其他类的show方法。
正因为有了多态,后期绑定的存在,会根据对象来执行对应的方法。
因为多态的存在,我们可以添加多个新的子类而不用去担心要写多少tvshow方法。
向上转型的写法:
public static void main(String[] args) {
TV letv = new LeTV();
TV mitv = new MiTV();
TV santv = new SanTV();
tvshow(letv);
tvshow(mitv);
tvshow(santv);
}
各种各样的TV还是TV,可以向上转型,因为多态,又会调用各自的对象的show方法。
书上的原话讲得很好,我照抄一下,多态是一种让程序员“将改变的事物和未变的事物分离开来”的重要技术。
就上述而言,各自的TV的show方法有自己的重写,但是EveryTV这个类里面的tvshow方法却不用变动。就是多态的用处所在。
是不是觉得没什么用?
List al = new ArrayList();
List ll = new LinkedList();
用处就在这里,我先用al也行,ll也行,以后要改可以改,不然很多东西都要改过。
(3)缺陷
1)“覆盖”私有方法
书上举的例子有点牵强,方法不算重写。
public class TV{
private void show(){
System.out.println("TV");
}
public static void main(String[] args) {
TV tv = new LeTV();
tv.show();
}
}
class LeTV extends TV{
public void show(){
System.out.println("LeTV");
}
}
学了多态之后,一看结果是LeTV吧。错了,LeTV根本没有重写show方法,父类的show方法是private的,不可见,不可继承。假的“覆写”。
2)域与静态方法
对域,field概念模糊的,可以看看:java中的域是什么?
public class TV{
public int price = 10;
public int getprice(){
return price;
}
public static String getString(){
return "tv";
}
}
class LeTV extends TV{
public int price = 20;
public int getprice(){
return price;
}
public int getsuperprice(){
return super.price;
}
public static String getString(){
return "letv";
}
public static void main(String[] args) {
TV tv = new LeTV();
System.out.println(tv.price+" getprice:"+tv.getprice()+tv.getString());
LeTV letv = new LeTV();
System.out.println(letv.price+" getprice:"+letv.getprice()+" getsuperprice:"+letv.getsuperprice()
+letv.getString());
}
}
按照多态,tv.price的结果应该为20,但是结果却是10,因为域访问操作是由编译器解析,不是多态,多态是后期绑定,也就是运行期间。
而tv.getString方法也一样,并没有打印出letv,而是tv,因为方法是静态方法,只和类有关,与具体的对象不关联。
这是运用多态时要注意的两个地方。
刚开始看这个多态特性的时候难看懂,也不知道这东西有什么用,其实程序敲多了,其义自现。你会发现你写的东西导出都在用它。
)构造器和多态
这个问题其实前面写过了,构造器实际上是static方法,只不过是隐式声明,所以构造器并没有多态性。
但是需要知道加载的顺序。
class GrandFather{
GrandFather(){
print();
}
private int print(){
System.out.println("g");
return 1;
}
}
class Father extends GrandFather{
Father(){
print();
}
private int print(){
System.out.println("f");
return 1;
}
}
public class Son extends Father{
Son(){
print();
}
private int print(){
System.out.println("s");
return 1;
}
public static void main(String[] args) {
Son s = new Son();
}
}
其实new出子类的时候,需要调用父类构造器,递归下去。
所以输出结果为g,f,s。
2)继承与清理
虽然有GC,但是书上还是用了一次引用技术的方法来模拟清理对象。
class Count{
private int reference;
private static int counter;
private final long id = counter++;//注意一下,虽然是final,或许会觉得它不是不可变
//为什么还给他赋值呢,对,但是现在值没确定,我们给他赋值之后就不会再变了。
Count(){
System.out.println("count"+id);
}
public void addReference(){
reference ++;
}
protected void dispose(){
if(--reference == 0){
System.err.println("dispose count"+id);
}
}
}
public class Rubbish {
private Count count;
private static int counter;
private final long id = counter++;
Rubbish(Count count){
System.out.println("Rubbish"+id);
this.count = count;
this.count.addReference();
}
protected void dispose(){
System.out.println("dispose Rubbish"+id);
count.dispose();
}
public static void main(String[] args) {
Count count = new Count();
Rubbish rubbish[] = {new Rubbish(count),new Rubbish(count),
new Rubbish(count),new Rubbish(count)};
for(Rubbish r:rubbish){
r.dispose();
}
}
}
每new一个对象的时候,计数器counter计算count对象的数量,id为final是我们确定之后不希望被改变,reference是引用计数,每每对象增加一个,便会加一,当引用都没有的时候,我们也要将计算引用的这个对象清理。
原来GC里面的引用计数法是这样的一个原理。
3)用继承进行设计
TV大变身:
class TV{
public String getString(){
return "tv";
}
public TV change() {
// TODO Auto-generated method stub
return new TV();
}
}
class SanTV extends TV{
public String getString(){
return "santv";
}
}
public class LeTV extends TV{
public String getString(){
return "letv";
}
public SanTV change(){
return new SanTV();
}
public static void main(String[] args) {
TV letv = new LeTV();
System.out.println(letv.getString());
TV newtv = letv.change();
System.out.println(newtv.getString());
}
}
之前犯了一个错误,TV类里面是没有change方法的,我直接用了
TV newtv = letv.change();
发现报错,TV没有定义change方法,我子类不是可以有自己的新方法吗,为什么会报错?
后面搞明白了,父类引用指向子类对象,其实一开始调用的是父类的方法,由于多态的存在,后期绑定之后,才会结合具体的重写方法。但是我现在父类方法都没定义,肯定报错。为了验证,修改代码。
public class LeTV extends TV{
public String getString(){
return "letv";
}
public static void main(String[] args) {
TV letv = new LeTV();
System.out.println(letv.getString());
TV newtv = letv.change();
System.out.println(newtv.getString());
}
}
现在子类没有重写change方法,默认就是继承父类的change方法,这样的输出结果就是letv,tv。
其实上面这种转换是一种特殊的模式——状态模式。
其实TV可以看成List,两个具体的TV可以看成LinkedList和ArrayList。两种状态可以灵活的切换。
多态就讲到这里了——不同的形式。
要真正的理解,实需多敲敲代码,写完之后自己会发现自己是用了多态。
1. 生成一个派生类的对象,并将其向上转型为其基类的引用。
2. 在基类中定义一些方法,并在派生类中覆盖这些方法。
3. 用上述基类的引用调用这些方法。
结果:
实际调用的是派生类中的方法。
好处:
可以编写一些只与基类有关的代码,当应用于不同的派生类的对象时,这些方法不需要做
任何改动就能使用。这些代码就属于“不变的部分”。
举例:
Shape s = new Circle();
s.draw();/*实际调用的是Circle类中覆盖的那个draw方法*/
之所以能够达到这种效果,是因为Java内部的方法调用机制实现了后期绑定(也称运行时绑定),
即在运行时根据对象的类型绑定到具体的方法的实现。
调用同样的“接口”,具体的行为却会不同,所以叫“多态”。
publicclass D extends C{
public D(){
System.out.println("D-init");
}
publicstaticvoid main(String []args){
C c = new D();
System.out.println(c.getClass().getName());
c.f2();//编译出错,无法访问到f2()
}
}
class C {
public C(){
System.out.println("C-init");
}
privatevoid f2(){
System.out.println("C-private-2");
}
}
===========================================================
publicclass D {
public D(){
System.out.println("D-init");
}
privatevoid f2(){
System.out.println("D-private-2");
}
publicstaticvoid main(String []args){
D d = new C();
System.out.println(d.getClass().getName());
d.f2();
}
}
class C extends D{
public C(){
System.out.println("C-init");
}
publicvoid f1(){
System.out.println("C-public");
}
}
output:
D-init
C-init
com.keyword._static.C
D-private-2
首先我们知道子类的对象可以赋值给父类的引用,这是多态的一个体现。
但是为什么我们在D d = new C();之后,可以访问到父类的私有函数?
理论上来说,我们new 的是子类,也就是在堆区开辟的是子类的空间,而D d 只是一个引用,指向该子类对象在堆区地址。但是根据private私有的特性,我们认为是说到底这个子类的对象仍旧无法访问到父类中的private属性,哪怕编译时不报错,运行时也会出错。但是这里却正常的运行了,也成功访问到了父类的private属性,所以提出一个问题,这是怎么做到的?
解决这个问题也许不是来的那么直观。
其实现在假设new D()也就是创建一个父类对象,所需内存空间为1MB,那么在创建其子类对象new C();所需的内存空间必然大于1MB,先假设是1.5MB。
为何会多出一些空间,因为从构造链可以看出,初始化一个子类实例,必然会先初始化其父类实习,那么这个new 的父类实例放在哪里,其实放在子类的堆区内存中,也就是说,子类对象所在的内存空间,不但包含自己的信息,也有其父类的信息,而且是每个子类对象独立享有各自父类对象实例。
而D d = new C();这部操作,进行一个向上转型,也就是窄化操作,说白点就是一个本身指在子类对象范围以及父类非private对象范围的堆区可移动指针,重新使他只能指在父类对象范围内。
那么,这种条件下,是否能访问到private属性,我个人认为取决于运行的外部环境了,我觉得是堆栈环境,不知道这么说是否恰当。
在第二份示例代码中,main函数放在父类中,也就是说入口在父类,比如说当前运行的环境是父类环境,D d = new C();这部操作后,移动指针,指向父类的内容区间, 加上外部环境,我将其理解成,在父类环境中访问父类私有属性,也就是在自己家用自己的私有物品。那自然是可以访问的。
而第一份示例代码中,main函数放在子类中,也就是外部环境不是父类,那么哪怕堆栈指针移动到了父类的内容区间,也是无法访问到父类的private的,这是由private本身性质所决定的。
多态
在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本类型。
多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展程序。
再论向上转型
代码
//: polymorphism/music/Note.java
// Notes to play on musical instruments.
package polymorphism.music;
public enum Note {
MIDDLE_C, C_SHARP, B_FLAT; // Etc.
} ///:~
//: polymorphism/music/Instrument.java
package polymorphism.music;
import static net.mindview.util.Print.*;
class Instrument {
public void play(Note n) {
print("Instrument.play()");
}
}
///:~
//: polymorphism/music/Wind.java
package polymorphism.music;
// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {
// Redefine interface method:
public void play(Note n) {
System.out.println("Wind.play() " + n);
}
} ///:~
//: polymorphism/music/Music.java
// Inheritance & upcasting.
package polymorphism.music;
public class Music {
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // Upcasting
}
} /* Output:
Wind.play() MIDDLE_C
*///:~
Music.tune()方法接受一个Instrument引用,同时也接受任何导出自Instrument的类。例如Wind是Instrument的导出类,那么当Wind引用传递到tune()方法式,就会出现这种情况,而不需要任何类型转换。这么做是允许的——因为Wind从Instrument继承而来,所以Instrument接口必定存在于Wind中,从Wind向上转型到Instrument可能会“缩小”接口,但不会比Instrument的全部接口更窄。
忘记对象类型
为什么所有人都故意忘记对象的类型呢?在进行向上转型的时候,就会产生这样的情况 ;如果让tune()方法接受一个Wind引用作为自己的参数,似乎会更加直观。但是这样会引发一个重要的问题:如果那样做的话,就需要为Instrument的每种类型都编写一个新的tune方法。假设这样,我们再加入个新的类型的时候就要大量的增加代码。
举个例子
//: polymorphism/music/Music2.java
// Overloading instead of upcasting.
package polymorphism.music;
import static net.mindview.util.Print.*;
class Stringed extends Instrument {
public void play(Note n) {
print("Stringed.play() " + n);
}
}
class Brass extends Instrument {
public void play(Note n) {
print("Brass.play() " + n);
}
}
public class Music2 {
public static void tune(Wind i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Stringed i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Brass i) {
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
Stringed violin = new Stringed();
Brass frenchHorn = new Brass();
tune(flute); // No upcasting
tune(violin);
tune(frenchHorn);
}
} /* Output:
Wind.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
*///:~
这样做是可以,但是有一个缺点:必须为添加每一个新的Instrument类编写特定类型的方法。这意味着在开始的时候就需要更多的编程,同时在你以后添加类似tune()的新方法或者添加自Instrument导出的新类,仍需要做大量的工作。此外,如果我们忘记重载某个方法,编译器不会放回任何错误信息,这样关于类型的整个处理过程将变得难以控制。
如果我们只写这样一个简单方法,它仅接受基类作为参数,而不是那些特殊的导出类。这样做情况会变好吗?也就是说,如果我们不管导出类的存在,编写的代码只是与基类打交道,会不会更好?而这正是多态所允许的。
转机
观察一段代码
public static void tune(Instrument i) {
// ...
i.play(Note.MIDLE_C);
}
它接受的是Instrument引用,那么在这种情况下,编译器是怎么知道这个Instrument引用的指向是Wind对象,而不是Brass对象或者其他呢,实际上,编译器是无法得知的,而这就涉及到了绑定。
方法调用绑定
将一个方法调用同一个方法主体关联起来被称为绑定。如在程序执行前进行绑定,就叫做前期绑定。这是面向过程的语言中不需要选择就默认的绑定方式。例如,C++只有一种方法调用,那就是前期绑定。
但是这并不足以结果上面代码的困惑,解决的办法是后期绑定,它的含义是在运行时根据对象的类型进行绑定,后期绑定也叫做动态绑定或运行时绑定。在这里,编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。后期绑定机制随编译语言的不同而有所不同,不管怎样都必须在对象中安置某种”类型信息”。
Java除了Static方法和final方法(private方法属于final)之外,其他所有的方法都是后期绑定,这意味着通常情况下,我们不必判定是否进行后期绑定————它会自动发生。
将某个方法声明为final方法会有效的“关闭”动态绑定,这样,编译器就会为final方法调用生成更有效的代码。
产生正确的行为
一旦知道Java中所有方法都是通过动态绑定来实现多态这个事实后,我们就可以编写只与基类打交道的程序代码了,并且这些代码对所有的导出类都可以正确运行。
//: polymorphism/shape/Shape.java
package polymorphism.shape;
public class Shape {
public void draw() {}
public void erase() {}
} ///:~
//: polymorphism/shape/Circle.java
package polymorphism.shape;
import static net.mindview.util.Print.*;
public class Circle extends Shape {
public void draw() { print("Circle.draw()"); }
public void erase() { print("Circle.erase()"); }
} ///:~
//: polymorphism/shape/Triangle.java
package polymorphism.shape;
import static net.mindview.util.Print.*;
public class Triangle extends Shape {
public void draw() { print("Triangle.draw()"); }
public void erase() { print("Triangle.erase()"); }
} ///:~
//: polymorphism/shape/Square.java
package polymorphism.shape;
import static net.mindview.util.Print.*;
public class Square extends Shape {
public void draw() { print("Square.draw()"); }
public void erase() { print("Square.erase()"); }
} ///:~
//: polymorphism/shape/RandomShapeGenerator.java
// A "factory" that randomly creates shapes.
package polymorphism.shape;
import java.util.*;
public class RandomShapeGenerator {
private Random rand = new Random(47);
public Shape next() {
switch(rand.nextInt(3)) {
default:
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
} ///:~
//: polymorphism/Shapes.java
// Polymorphism in Java.
import polymorphism.shape.*;
public class Shapes {
private static RandomShapeGenerator gen =
new RandomShapeGenerator();
public static void main(String[] args) {
Shape[] s = new Shape[9];
// Fill up the array with shapes:
for(int i = 0; i < s.length; i++)
s[i] = gen.next();
// Make polymorphic method calls:
for(Shape shp : s)
shp.draw();
}
} /* Output:
Triangle.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Circle.draw()
*///:~
这段代码就是典型的多态应用了。
可扩展性
由于有了多态机制,我们可以根据自己的需求对系统添加任意多的新类型,而不需要修改代码。在一个良好的OOP程序中,大多数或者所有的方法都是只与基类接口通信的。这样的程序是可扩展的,因为我们可以从通用的基类继承出新的数据类型,从而新添一些功能,那些操作基类接口的方法不需要任何的改动就可以应用于新类。
我们再来看看Instrument这个例子。
//: polymorphism/music3/Music3.java
// An extensible program.
package polymorphism.music3;
import polymorphism.music.Note;
import static net.mindview.util.Print.*;
class Instrument {
void play(Note n) { print("Instrument.play() " + n); }
String what() { return "Instrument"; }
void adjust() { print("Adjusting Instrument"); }
}
class Wind extends Instrument {
void play(Note n) { print("Wind.play() " + n); }
String what() { return "Wind"; }
void adjust() { print("Adjusting Wind"); }
}
class Percussion extends Instrument {
void play(Note n) { print("Percussion.play() " + n); }
String what() { return "Percussion"; }
void adjust() { print("Adjusting Percussion"); }
}
class Stringed extends Instrument {
void play(Note n) { print("Stringed.play() " + n); }
String what() { return "Stringed"; }
void adjust() { print("Adjusting Stringed"); }
}
class Brass extends Wind {
void play(Note n) { print("Brass.play() " + n); }
void adjust() { print("Adjusting Brass"); }
}
class Woodwind extends Wind {
void play(Note n) { print("Woodwind.play() " + n); }
String what() { return "Woodwind"; }
}
public class Music3 {
// Doesn't care about type, so new types
// added to the system still work right:
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void tuneAll(Instrument[] e) {
for(Instrument i : e)
tune(i);
}
public static void main(String[] args) {
// Upcasting during addition to the array:
Instrument[] orchestra = {
new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
}
} /* Output:
Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C
*///:~
事实上,不需要改动tune()方法,所有的新类都能与原有的类一起正确运行,即使tune()是单独存放在某个文件中,并且Instrument接口中添加了其他的新方法,tune()也不需要再编写就能正确运行。
可以看到,tune()方法完全忽略了它周围代码所发生的变化,依旧正常运行,这正是我们期望多态所具有的特性。多态是一项让程序员“将改变的事物与未变的事物分离开来”的重要技术。
若干个对象共享
例如Frog对象拥有其自己的对象,并且知道他们的存活多久,因为Frog对象知道何时调用dispose()去释放其对象。然而,如果这些成员对象中存在于其他一个或多个对象共享的情况,问题将不再简单,不再能简单的调用dispose()了。在这种情况下,我们也许需要引用计数来跟踪依旧访问着共享对象的数量。
//: polymorphism/ReferenceCounting.java
// Cleaning up shared member objects.
import static net.mindview.util.Print.*;
class Shared {
private int refcount = 0;
private static long counter = 0;
private final long id = counter++;
public Shared() {
print("Creating " + this);
}
public void addRef() { refcount++; }
protected void dispose() {
if(--refcount == 0)
print("Disposing " + this);
}
public String toString() { return "Shared " + id; }
}
class Composing {
private Shared shared;
private static long counter = 0;
private final long id = counter++;
public Composing(Shared shared) {
print("Creating " + this);
this.shared = shared;
this.shared.addRef();
}
protected void dispose() {
print("disposing " + this);
shared.dispose();
}
public String toString() { return "Composing " + id; }
}
public class ReferenceCounting {
public static void main(String[] args) {
Shared shared = new Shared();
Composing[] composing = { new Composing(shared),
new Composing(shared), new Composing(shared),
new Composing(shared), new Composing(shared) };
for(Composing c : composing)
c.dispose();
}
} /* Output:
Creating Shared 0
Creating Composing 0
Creating Composing 1
Creating Composing 2
Creating Composing 3
Creating Composing 4
disposing Composing 0
disposing Composing 1
disposing Composing 2
disposing Composing 3
disposing Composing 4
Disposing Shared 0
*///:~
static long counter跟踪所创建的Shared的实例的数量,id是final的,因为我们不希望它的值在对象生命周期中被改变。
在将一个共享对象附着到类上时必须记住调用addRef(),但是dispose()方法跟踪引用数,并决定何时执行清理。
构造器内部的多态方法行为
如果在一个构造器的内部调用正在构造的对象的某个动态绑定方法是会发生什么?
在一般的方法内部,动态绑定的调用是在运行时才决定的,因为对象无法知道它是属于方法所在的那个类,还是属于那个类的导出类。
如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义。然而,这个调用效果可能相当难于预料,因为被覆盖的方法在对象被完全构造之前就被调用。这可能会造成一些难于发现的错误。
简单来说就是在基类的构造器中调用基类中函数,这个函数也在导出类中被覆盖,当在导出类调用基类构造器的时候,那么因为一个动态绑定的方法调用会向外深入继承层次结构内部,它可以调用导出类里的方法。那么可能会调用某个方法,而这个方法所操作的成员可能还未进行初始化。
//: polymorphism/PolyConstructors.java
// Constructors and polymorphism
// don't produce what you might expect.
import static net.mindview.util.Print.*;
class Glyph {
void draw() { print("Glyph.draw()"); }
Glyph() {
print("Glyph() before draw()");
draw();
print("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
print("RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw() {
print("RoundGlyph.draw(), radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
} /* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*///:~
Glyph.draw()方法设计将会被覆盖,这种覆盖是在RoundGlyph中发生的。但是Glyph构造器会调用这个方法,结果导致了对RoundGlyph.draw()的调用,这似乎是我们的目的,但是输出的结果却不是我们预计的,我们发现当Glyph()的构造器调用draw()方法时,radius不是默认初始值1,而是0。
我们先来看看初始化的实际过程:
在其他任何食物发生之前,将分配给对象的存储空间初始化为二进制的零。
如以前说的一样会调用基类构造器。此时,调用被覆盖的draw()方法(要在调用RoundGlyph构造器之前调用),由于步骤1的缘故,我们此时会发现radius的值为0。
按照声明的顺序调用成员的初始化方法。
调用导出类的构造器主体。
这样做有一个优点,那就是所有东西都至少初始化为零。而不是仅仅留作垃圾。对象的引用通常为null,所以如果忘记为该引用进行初始化,就会在运行时出现异常。
因此,编写构造器的时候有一条准则:“用尽可能简单的方法使对象进入正常状态,可以的话,避免调用其他方法。”在构造器唯一能够安全调用的那些方法是基类中的final方法。
用继承进行设计
多态并不是用来使得任何东西都可以被继承的,因为这反倒会加重我们的设计负担,使事情变得不必要的复杂起来。
更好的方式是选择“组合”,尤其是不能十分确定应该使用哪一种方式时。组合更加灵活,因为它可以动态选择类型(因此也就选择了行为);相反,继承在编译时需要知道确切类型。
//: polymorphism/Transmogrify.java
// Dynamically changing the behavior of an object
// via composition (the "State" design pattern).
import static net.mindview.util.Print.*;
class Actor {
public void act() {}
}
class HappyActor extends Actor {
public void act() { print("HappyActor"); }
}
class SadActor extends Actor {
public void act() { print("SadActor"); }
}
class Stage {
private Actor actor = new HappyActor();
public void change() { actor = new SadActor(); }
public void performPlay() { actor.act(); }
}
public class Transmogrify {
public static void main(String[] args) {
Stage stage = new Stage();
stage.performPlay();
stage.change();
stage.performPlay();
}
} /* Output:
HappyActor
SadActor
*///:~
在这里,Stage对象包含一个对Actor的引用,而Actor被初始化为HappyActor对象。这意味着performPlay()会产生某种特殊的行为。而当我们替换actor的对象引用的话,就可以改变我们的行为了。
一条准则是:“用继承表达行为的差异,并用字段来表达状态上的变化。”
纯继承与扩展
采用“纯粹”的方式("is-a")来创建继承层次结构似乎是最好的方式。也就是说,只有基类已经建立的方法才可以在导出类中被覆盖。也可以说是导出类“只有”覆盖基类的函数没有其他的函数。
因为一个类的接口已经确定了它应该是什么,继承可以确保所有的导出类具有基类的接口,且绝不会少。“纯粹”的继承,导出类将具有和基类一样的接口。
也可以认为这是一种纯替代,因为导出类可以完全代替基类,而在使用它们时,可以不需要知道关于子类的任何额外信息。也就是说,基类可以接收发送给导出类的任何信息,因为二者具有完全相同的接口。我们只需知道从导出类向上转型,永远不需要知道正在处理的对象的确切类型。这都是通过多态来处理的。
但是导出类往往不是只单纯的只具有从基类继承的接口,它还有自己的额外函数,我们可以称为是(is-like-a)(像一个)关系,因为导出类就像是一个基类——它有着相同的接口,但是它还具有由额外方法实现的其他特性。
”像一个“关系也是有缺点的。导出类中接口的扩展部分不能被基类访问。因此,一旦我们向上转型,就不能调用那些新方法。在这种情况下,我们一般是重新查看对象的确切类型,以便我们能够访问该类型所扩充的方法。
向下转型与运行时类型识别
由于向上转型会丢失具体的类型信息,所以我们想,通过向下转型来获取类型信息。然后我们知道向上转型是安全的,因为基类的接口是不会大于导出类的接口的。因此,我们通过基类的接口发送的信息保证都能被接受。但是对于向下转型,我们无法知道我们它将转型是哪种类型。
在Java中,所有转型都会得到检查!所以即使我们只是进行一次普通的加括弧形式的类型转型,在进入运行时仍会进行检查,如果不是正确的类型,会返回一个ClassCastException(类转型异常)。这种在运行时期对类型进行检查的行为称作“运行时类型识别”(RTTI)。
//: polymorphism/RTTI.java
// Downcasting & Runtime type information (RTTI).
// {ThrowsException}
class Useful {
public void f() {}
public void g() {}
}
class MoreUseful extends Useful {
public void f() {}
public void g() {}
public void u() {}
public void v() {}
public void w() {}
}
public class RTTI {
public static void main(String[] args) {
Useful[] x = {
new Useful(),
new MoreUseful()
};
x[0].f();
x[1].g();
// Compile time: method not found in Useful:
//! x[1].u();
((MoreUseful)x[1]).u(); // Downcast/RTTI
((MoreUseful)x[0]).u(); // Exception thrown
}
} ///:~
1.在子类中的方法如果覆盖了父类的方法,它的返回值可以是父类的返回方法的返回类型的子类。例如:
package test;
class R{
}
class T extends R{
}
class Father{
public R f(){
return new R();
}
}
public class p164 extends Father{
public T f(){
return new T();
}
}
2.向下转型:
父类的子类可以自由的进行向上转型,使用父类来代表子类,这是因为,父类不会大于子类的接口。但是,如果想将父类强转成子类,也就是进行向下转型,就有可能产生类转型异常,也就是ClassCastException.例如,以下代码中,如果作为父类的f想要转成子类的类型p164,如果是使用Useful test=new p167();这种方式来创建的,那么强转可以成功,而且可以调用子类中的父类没有的扩展方法。但是,如果就是一个父类的创建方式,Useful test=new Userful();那么这种港式不能强转为子类,会报出ClassCastException。不管是不是调用的子类中的父类没有的扩展方法,都会报错。
package test;
class Useful{
public void f(){
System.out.println("father f()");
}
public void g(){
System.out.println("father g()");
}
}
public class p167 extends Useful{
public void f(){
System.out.println("son f()");
}
public void g(){
System.out.println("son g()");
}
public void v(){
System.out.println("son v()");
}
public static void main(String args[]){
Useful test=new p167();
((p167)test).f();
}
}
OOP语言中,多态是封装、继承之后的第三种基本特征。
封装:通过合并特征和行为来创建新的数据类型,“实现隐藏”通过细节“私有化”把接口和实现分离。
继承:以复用接口方式从已有类型用extends关键字创建新类型,并允许向上转型。
多态:消除类型之间的耦合关系(分离做什么和怎么做),基于继承的向上转型功能,允许同一种类型同一行为有不同的表现。
8.1再论向上转型
8.1.1忘记对象类型
不管导出类的存在,编写的代码(方法)只是针对基类类型。不需要为每个导出类型都写各自的代码,这正是多态所允许的。
8.2转机
程序运行时接受的是基类类型,但是它如何知道具体类型是哪一个从而调用正确的方法呢?我们需要了解绑定机制。
8.2.1方法调用绑定
把方法调用同方法主体关联起来称为绑定。
前期绑定:程序执行前绑定(由编译器和连接程序实现),C语言中方法调用都是前期绑定。
后期绑定:又叫动态绑定,运行时绑定,在运行时根据对象的类型绑定对应的方法主体。
Java中默认就是动态绑定,无需手动设置。特殊:static方法和final(private也是final)方法不存在多态性,不是动态绑定。
8.2.2产生正确行为
动态绑定使得多态中的基类对象可以正确执行相应的导出类对象方法。
8.2.3可扩展性
多态使得扩展新类型和扩展基类不会对已有代码(调用基类方法的代码)产生影响。它可以让程序员“将改变的事物与不变的事物分离开”。
8.2.4缺陷:不可以覆盖private方法
基类中private方法在子类中可以用相同的方法名和签名,但是它是一个全新的方法,不会按照我们想要的子类方法来执行。
调用的时候,按照基类方法的访问权限来决定是否可以调用。
子类是否会覆盖父类方法,按照子类是否可以访问到父类该方法来决定是否可以覆盖。
8.2.5缺陷:域与静态方法
多态特性(动态绑定)只是针对方法的。域和静态方法不具有这种特性。
如:父类和子类都有一个域 public String str; 在Super s = new Sub(); s.str 取出的是Super里的而不是Sub里的。 不过一般情况不会存在这种把域设置为public并且想用子类覆盖它的情况。
静态方法也不会有多态性。
8.3构造器和多态
构造器是隐式static方法,不具有多态特性。
8.3.1构造器的调用顺序
为什么编译器强制每个导出类的构造器必须调用基类构造器呢:因为构造器有个特殊的任务,检查对象是否被正确构造。导出类构造器只能访问它自己的成员,不能访问基类的成员(通常是private成员)。只有基类构造器才具有相应的知识和权限对自己的元素进行初始化。而导出类成员的初始化有可能会用到基类成员,因此导出类初始化在基类之后。
8.3.2继承与清理
通过组合和继承方式创建新类时,通常情况都是不需要担心对象的清理问题。
但是如果的确需要做清理时,必须非常小心谨慎:在使用完之后按照创建逆序清理,即sub.dispose()然后super.dispose()来清理。
更加复杂的情况:不知道什么时候使用结束,需要自己定义引用计数,然后再清理。
8.3.3构造器内部的多态方法行为
在调用子类构造器的过程中,会先调用父类构造器,此时子类构造器还没调用完成,子类对象也没有执行初始化,如果在父类构造器里调用多态方法,那么这个方法是可以产生多态行为特征的,但是由于子类构造器没有执行完,因此子类的初始化还没完成,多态方法里对子类成员变量的获取只能拿到默认值0,false,null
对象初始化过程(注意与类的加载过程区分):
1.给导出类对象分配内存空间,并初始化为0,false,null
2.调用父类构造器,并执行多态方法,拿到的是子类0,false,null的域
3.按照声明顺序调用成员变量的初始化
4.调用子类构造器主体
此处虽然逻辑没什么问题,但是行为的确错误了,所以在写构造器的时候,我们要尽可能用简单的方法使对象进入正常状态,如果可以的话避免调用其他方法。
8.4协变返回类型
导出类重写父类方法,方法的返回类型(区分返回值)可以是父类返回类型的某一个导出类。
8.5用继承进行设计
就创建新类型而言,不要只想到继承,应该优先考虑组合,它比继承具有更大的灵活性,可以动态的改变类型,而继承在编译时类型已经确定了。
8.5.1纯继承与扩展
纯粹的继承:基类接口与导出类完全一致,是is-a关系
扩展:导出类除了基类接口外,还有其他方法,是is-like-a关系,但是这些扩展方法不能以基类引用去调用
8.5.2向下转型与运行时类型识别
向上转型是安全的:基类不会有大于导出类的接口
向下转型需要确保类型的正确性:Java中类型转换(括号强转)都会进行类型检查,不正确抛出ClassCastException。这种在运行期间对类型进行检查的行为称作“运行时类型识别”(RTTI)。
8.6总结
多态意味着“不同的形式”。在OOP里,我们持有基类的相同接口,使用该接口的不同形式:不同版本的动态绑
、将某个对象的引用视为其基类对象的引用的做法被称作“向上转型”(upcasting)
2、向上转型使得方法简单,它只接受基类引用作为参数,不去考虑子类的特殊性,每种子类具体的的操作,由多态的动态绑定来实现
3、将方法调用同方法主体关联起来叫做绑定(binding),在编译期进行的绑定叫做前期绑定(early binding),运行时进行的绑定叫做后期绑定(late binding)或动态绑定(dynamic binding)或运行时绑定(run-time binding),面向过程的语言(比如C)只能进行前期绑定,Java中除了static和final方法(private方法属于final)外,全是后期绑定
4、多态是由后期绑定实现的
5、注意私有方法的重载是很特别的
[@more@]
6、类中只要有一个抽象方法(abstract method)就必须定义为抽象类(abstract class),否则有编译错误,抽象类不能派生实例,如果派生也会有编译错误,继承自抽象类的子类,如果已经为每个抽象方法提供了定义,就可以成为普通类,否则就仍然是抽象类
7、构造函数的调用顺序:a.递归调用基类构造函数,从层次的根类直至当前类;b.内部组合类按照声明顺序调用构造函数;c.调用本类的构造函数;注意其中的b.是在c.之前的,即先生成内部组合类的实例,然后才正式开始构建本类实例
8、重载子类的dispose()方法时,务必记住调用基类的dispose()方法,否则基类的清除动作就不会发生,即在dispose()方法中一定要显式调用super.dispose()
9、 a. 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。
b. 如前所述的那样,调用基类构造器。此时,调用被重载的draw()方法(是的,是在调用RoundGlyph 构造器之前调用的),由于步骤(1)的缘故,我们此时会发现radius 的值为0。
c. 按照声明的顺序调用成员的初始化代码。
d. 调用导出类的构造器主体。
10、编写构造函数的规则:用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其它方法;在构造函数中调用动态绑定的方法,可能发生严重错误
11、用继承表达行为间的差异,用属性表达状态上的变化(比如state模式)
12、对父类对象进行向下转型后调用子类方法,如果父类中没有此方法,将会产生编译期错误
13、多态必须是动态绑定的,否则就不是多态
、类的继承
①类的继承的概念
java类的继承可用下面的语句来表示
class 父类 //定义父类
{ }
class 子类 extends 父类 //用extends关键字实现类的继承
{ }
example:
package projiect; class Study { String name; } class Hard extends Study { String result; } public class test1 { public static void main(String[] args) { // TODO Auto-generated method stub Hard s=new Hard(); s.name="我"; s.result="爱学习"; System.out.println(s.name+s.result); } }
结果:我爱学习
当然还有多层继承(父类A)——(父类B)——(父类C)——子类,可用写成如下:
class A
{ }
class B extends A
{ }
class C extends B
{ }
②super关键字的使用
example:
package projiect; class Person { String name; int age; public String talk() { return "我是:"+this.name+",今年:"+this.age+"岁"; } } class Student extends Person { String school; public Student(String name,int age,String school) { super.name=name; super.age=age; System.out.print(super.talk()); this.school=school; } } public class test1 { public static void main(String[] args) { // TODO Auto-generated method stub Student s=new Student("小明",23,"UCAS"); System.out.println(",学校:"+s.school); } }
结果:我是:小明,今年:23岁,学校:UCAS
如果要限制子类继承父类的话,在父类中,可以用private申请私有变量。
二、类的多态性
直接用一个范例介绍多态性
package projiect; class Person { public void talk1() { System.out.print("我是小明"); } public void talk2() { System.out.print(",老师让我滚出去!"); } } class Student extends Person { public void talk3() { System.out.print("我是小明"); } public void talk2() { System.out.print(",我让老师滚出去!"); } } public class test1 { public static void main(String[] args) { // TODO Auto-generated method stub Person p=new Person(); //优先用父类Person里面的函数 p.talk1(); p.talk2(); } }
结果为:我是小明,老师让我滚出去!
如果把Person p=new Person();改为Person p=new Student();//这是优先利用子类Student中的函数。
结果为:我是小明,我让老师滚出去!