JAVA中 表示继承 的关键字是 extends
extends 英文含义是扩展,意味着子类不是父类的子集,而是父类的拓展。
子类叫:subClass
父类叫:supperclass
为什么要有继承?
当然是根据实际应用总结而来的。
考虑以下场景:
场景:有一个Animal 类,再写一个Cat类、Dog类,你会发现Cat类、Dog类都是动物类Animal的子概念,但又不是具体的动物。比如Cat类,有各种各样的猫,Dog类有各种各样的狗,不是一个具体的狗。而是一个概念,对某一类的动物的归纳。然后,不管的Dog还是Cat, 都可以写 属性 name 名字、weight 重量、sex 性别 ,也都可以有 吃饭eat(), 叫唤speak()等方法。
一开始确实是 Animal、Cat、Dog 3个类都写了相同的 属性,方法。后来发现,这样做存在代码冗余。完全没必要。因为这几个类是有联系的。有一些属性和方法是公共的。比如,所有的Cat、Dog 都是动物。动物类就是 最基础的类。如果在基础的类里面写公共的内容,然后其他的类继承,就可以减少代码冗余。
public class Dog extends Animal{ }
public class Cat extends Animal{ }
上面的代码 演示了 类的继承的写法,使用extends 关键字。
上一节讲了继承的由来,是因为某些类有公共的属性和方法,抽取到了一个基础的类。
这样就带来了一些新的问题。
虽然Animal是基类,但是有些东西是Animal 自己的,不想给子类Dog或Cat继承,要怎么办?
这个是实际问题。JAVA设计者提供了权限这个工具来解决这个问题。当某个类的成员使用 private修饰时,意味着只能在本类中访问,即使你是儿子,继承了我,也不能访问。对于某些不想让子类访问的属性或方法,就可以使用private修饰。事实上子类是可以拿到父类所有的属性和方法,只是由于权限原因,不可使用。
有些属性我的兄弟可以访问,但是跨包就不能访问,同时,我还希望跨包的子类还能访问,因为是我儿子,继承了。就可以使用protected权限进行修饰这个资源。
现在,你发现,权限不仅仅是解决封装性,还解决继承性的某些问题。
对于子类Dog或者Cat来说,继承了父类,也就有了父类中可以拿来用的属性和方法,但是我不满意怎么办?或者说我部分不满意又怎么办?
Ans:完全不满意,可以对父类的方法进行重写。部分不满意,也是需要重写,但是可以使用super关键字调用父类的方法,再加上自己的实现,这样实现了即有自己的逻辑,又用了父类的逻辑。
关于方法的重写,见下面。
因为有句老话青出于蓝而胜于蓝,子类一般都是有自己独有的属性或者方法的。自己的属性和方法比父类多,就相当于是对父类进行了拓展。
即 子类继承父类,除了永远父类的东西,还可以定义属于自己的属性、方法。
JAVA中,规定一个类可以被很多个子类继承。这也是继承的由来。
但是,java规定,一个类只能有1个直接父类。父类的父类不是自己的直接父类,而是爷爷类,这样是允许的。
其实,有些语言是支持 多继承的,即多个直接父类,但是这样会带来很多问题,详细的可以百度了解为啥不支持多继承。
JAVA中的类的继承性 是单继承
但是类是支持多层继承,即 子类继承父类,父类继承爷爷类,爷爷类继承太爷爷类 等
这个很符合人类社会规律,人只有1个生物学父亲,父亲也只有1个生物学父亲。
JAVA 中有一个祖宗类。叫做Object
比如人类的共同祖先。
扯远了,Object是 JAVA 设计者 将所有的类的公共属性和方法抽取的一个基类,为了方便我们使用。
所有类都会默认继承Object 类。不用extends 写出来,JVM会帮我们干这个。
这个Object 类在 java.lang.Object 位置。去看看源码吧。
Object类有很多的方法,每一个类都可以使用。具体Object都有哪些常用的方法,后续再讲。
方法的重写与方法的重载不同。
前面讲的方法的重载是指同一个类的同名方法,由于参数的不同,实现了方法的重载。
而重写,则不是同一个类的同名方法。而与继承有关。子类继承了父类,就拥有父类的所有方法。
如果子类中定义一个和父类一样的方法,方法名、参数列表都一样,那么就是方法的重写。
因为子类就算不定义和父类同名的方法,也是可以调用的。因为继承过来的。
但是子类又定义了,就是说对父类的这个方法不满意,或者部分不满意,进行了重写。
子类重写的方法必须和父类的方法 名称、参数列表一致
原因: 重写就是重新写一遍,当然要求名字,参数一样;名字不一样就是另一个方法了,没必要叫重写。参数类别不一样,那就是重载了。
子类重写的方法返回值类型不能大于父类被重写的方法返回值类型
原因:这个和多态有关。就算是重写也要保证父类变量即父类对象可以接收子类方法的返回值,所以有这样的规定。
子类重写的方法的权限,不能小于父类被重写的方法。即要保证父类是可以访问的。
这个也和多态有关,因为可以用父类型接收子类型,使用多态进行调用,必须要有权限。
子类不能重写父类中声明为private权限的方法
这个就是权限存在的意义,private权限子类不能使用。认为子类就没有这个方法,不能进行重写。不能重写是因为子类没有继承到。或者说继承到不能使用,等价于没有。 子类不能重写也不代表不能定义,可以定义这样的方法,但是这个方法不是重写而是一个普通方法。
子类重写方法抛出的异常不能大于父类被重写的方法的异常
这个还是和多态有关。
关于static,static方法是不能被重写的
这个是static的特性。static 的出现使得 归属权发生了变化。具体见后面对static 解析。
super就算超级的意思。
它的出现是由于继承出现,有了子、父类的概念。前面讲到子类对父类的方法部分不满意时,子类中即想要调用父类的方法,又想要又自己的逻辑。就是重写需求的一种。
其中,实现调用父类的方法,就JAVA设计者 提供了super 关键字给我们使用。
前面的this 指代的是当前对象,是一个对象。
这里的super 指代的是当前类的父类。
那么,super是否指代的是父类的一个对象呢?如果指定的是一个类,类名是不能直接调用方法、属性的(除static外)。
所以super一定也是一个对象。但是我们并没有new 一个父类的对象出来,这个父类的对象是哪里来的呢?
研究这个问题,必须研究java是如果实现类与类的继承。
先不看结果,自己来推理。
已知条件:子类只要继承父类,就可以使用父类的属性方法(非私有)。
这说明什么,通过继承让2个原本不相干的类产生了联系,子类能够使用父类的东西,我能想到可能有3种实现方案:
1、将父类的代码拷贝一份,放在子类中,子类自然就可以使用这些属性和方法。这样无需创建父类对象。
2、创建子类对象时,先调用父类的构造函数创建一个父类对象,父类对象会存在堆空间里面,有个首地址可以找到。然后创建子对象,将子对象的内容也放到同一片堆空间中,这个时候,这个对象即是父类对象,又是子类对象。因为包含了2个类的东西。
3、创建子类时,同时创建一个父类对象,这个父类对象与子类进行关联,当需要使用父类中方法时,去找这个父类对象,然后真正调用这个父类对象的方法。
检验一下:
这是知乎上面一位答主的代码,我就借用来看看。可以看到,this和super的hashCode是相同的,使用未重写的equals方法判断得出this与super的内存地址是相同的。这说明 this与super是同一个对象。即同一片内存空间的2个引用。
这张图片直接用 == 判断了this与super的内存地址,也说明了 this 与 super 是同一个对象。
这么来看,实现方案3就是错误的,因为没有单独的父类对象存在。只有一个对象存在。
第二种方案是最接近真实答案的方案,具体的实现我现在水平还不够,哈哈。
通过查阅资料以及自己猜测,差不多就是 创建子类对象时,会先调用父类的构造方法创建一个对象,这个对象不是属于父类,而是属于子类,只不过父类的内容会放到这个对象的存储区域,和子类对象的内容待在同一片内存空间。
由于同一部分空间包含了2部分的内容,this和super就是用来区分里面的内容,到底是 使用父类继承的,还是使用子类自身的。
前面讲到this是当前对象,也讲到super也是一个对象,因为也是通过对象点的方式进行属性和方法的调用。又有super 和 this的内存空间地址一致,是同一片空间的引用。这里是有矛盾的?或者说,同一个人的2个名字有2个名字A和B,当叫做A时,表现一种性格,当叫做B时,表现另一种性格。JAVA 是如果 来区分的?
查阅资料,得到结论是this 是真正的对象,而super只是一个关键字,或者说是this对象的一部分。this对象中包含父类对象的那一部分的内容给了super。这样super才能像对象一样调用属性和方法。
这里还需要深入探讨。但是有些结论我们已经知道了。
this和super指向同一片内存空间
this是一个对象,而super是一个关键字
this指代当前对象,super指代当前对象的父类对象,但是并不存在父类对象。super可以调用父类中的方法、属性、构造函数等
【待我研究透彻再来完善这部分内容】
1、当父子类的属性相同时,由于属性不存在重写。所以子类中默认调用的就是子类自己的属性,当属性同名时,又想调用父类的属性
就需要使用 super点属性名 的方式进行调用。
2、使用 super点方法名 的方式调用父类的方法
3、使用 super(参数列表) 调用父类的构造方法
注意:
super(参数列表)调用父类的构造方法 必须放在子类构造器的首行; 就是说 最多用一次。
类的多个构造器中,至少有1个构造器使用 super(参数列表) 调用父类的构造器 , 不调用父类构造器,就没法实现继承。
类的构造器中,this(参数列表) 和 super(参数列表) 只能 二者选其一。
因为子类的构造函数 默认会调用super(),这是继承实现原理会干的事,默认调了,你2个方法都用的话就会出现 死循环调用。
子类继承父类以后,就获取了父类中声明的属性和方法。创建子类的对象在堆空间中就会加载所有父类声明的属性。
所有的父类是指 子类的父类,爷爷类,太爷爷类。。。Object类 这些直接或者间接继承的类
实现:
子类调用父类的构造器,通过子类自己的构造器中默认调用super()
父类的构造器又会调用自己的构造器中的super()
一直调到Object 类为止。
注意:上一步super就分析了,虽然会调用父类的构造器,但是并不会创建父类的对象。只会实例化子类自己的对象。
父类的东西都保存在这个对象里面。
没有继承就没有多态!
前面讲到 子类继承了父类,实例化子类对象时,父类对象的属性和方法也会保存在这个对象里面。
就可以说 子类的对象其实也是父类这种类型的。
// Animal 类 默认继承 Object类 public class Animal { public String type = "Animal"; public void speak(){ System.out.println(this.type); } } // Dog 继承 Animal 类 public class Dog extends Animal{ public String type = "Dog"; @Override public void speak(){ System.out.println(this.type); } } // JapanDog 继承Dog类 public class JapanDog extends Dog{ public String type = "JapanDog"; @Override public void speak(){ System.out.println(this.type); } } // 继承关系 JapanDog->Dog->Animal->Object // 多态写法 new 子类对象时,这个对象的类型除了可以是自己本身的类型,也可以 是任意父类的类型。 JapanDog japanDog = new JapanDog(); Dog dog = new JapanDog(); Animal aniDog = new JapanDog(); Object obj = new JapanDog();
如上面的代码都是正确的,在多态写法中,new 子类对象时,这个对象的类型除了可以是自己本身的类型,也可以 是任意父类的类型。
为什么可以这样? 因为子类这个对象不仅仅有自己类的东西,还有全部父类的东西。
写法总结:new 子类对象时,这个对象的类型除了可以是自己本身的类型,也可以 是任意父类的类型。当赋值的是父类对象的类型时,就是多态的写法。
这也是为什么要使用多态的原因。
Object obj = new JapanDog();
当使用这种写法时,父类是Object,obj对象 只能使用Object 类有的属性和方法。由于Object没有speak方法,虽然这是一个子类对象,obj也不能直接调用。需要进行对象强转。
Animal aniDog = new JapanDog();
同理,当使用Animal 类型接收 JapanDog子类对象时,这个对象是 Animal 型,Animal 类里面有speak方法,aniDog就可以调用speak方法,而且这个方法被子类 Dog 重写了,会调用Dog里面的speak方法,再看,JapanDog 里面也重写了Dog里面的speak方法,最终调用的是 JapanDog 类的speak方法。
如果只是 Dog 类重写了,而JapanDog 没有重写,就会调用JapanDog 里面的speak方法。
这说明,当使用父类接收子类对象时,只能调用父类对象中存在的方法和属性。属性不存在重写,属性一定是调用父类的。
但是方法被重写了,就会调用子类中被重写的方法。神奇的事发生了,父类对象接收的子类对象,调用的是父类方法名,实际执行的确实子类重写的方法!
Dog dog = new JapanDog();
如果是上面这样写,子类对象就被Dog类接收了。dog对象实际是JapanDog对象,但是现在是一个Dog对象。只能调用Dog类里面的属性和方法。后面就和 Animal 接收是一样的。看方法是不是被重写了。
总结:1、用什么类型接收,就只能调用什么类型里面有的东西。
2、如果这个接收的类型里面的方法,被重写了,就是调用最终被重写的这个方法。
3、多态是基于继承来说的,没有继承就没有多态。
1、多态写法是 子类对象可以被任意父类接收。
2、多态调用方法是最终调用被重写的方法。
见下面的例子
// Animal 类 默认继承 Object类 public class Animal { public String type = "Animal"; public void speak(){ System.out.println(this.type); } } // cat 继承 Animal 类 public class Cat extends Animal{ public String type = "cat"; @Override public void speak(){ System.out.println(this.type); } } // Dog 继承 Animal 类 public class Dog extends Animal{ public String type = "Dog"; @Override public void speak(){ System.out.println(this.type); } } // JapanDog 继承Dog类 public class JapanDog extends Dog{ public String type = "JapanDog"; @Override public void speak(){ System.out.println(this.type); } }
// ============================================== // 使用, 思考一下 传入 一个dog 对象,一个cat对象,一个JapanDog对象 public static void aniSpeak(Animal animal){ animal.speak(); } // 这样调用 JapanDog japanDog = new JapanDog(); Dog dog = new JapanDog(); Animal aniDog = new JapanDog(); Cat cat = new Cat(); aniSpeak(japanDog); aniSpeak(dog); aniSpeak(aniDog); aniSpeak(cat);
上面的代码就一个anispeak 函数,主要参数列表是 Animal 类型。
这个类型可以接收Animal的任意之类。里面调用的是Animal里面的speak方法。
只要子类重写了这个speak方法,传入子类的对象,被Animal接收,最终调用的就是子类里面自己重新的方法。
实现了同样的代码,传入不同的类对象,调用类对象自己的逻辑。
超越了if else对动物种类的判断,如果增加动物类型,只需要增加一个动物类,重写这个speak方法就可以了。
上面的例子,你应该还发现了一个点,那就是 多态实现了一种规则和标注。规则就是 父类Animal中的方法名称。
见下面的代码:
public static void aniSpeak(Animal animal){ animal.speak(); }
可以认为,凡是动物叫,就是调用speak方法。具体的动物怎么叫,还是调用子类自己的speak方法里面的逻辑。
而且我们发现,父类Animal里面的speak的逻辑根本就不需要。只是需要这个名字。这个名字就是大家统一重写的名字。这个名字就是一个规则,或者说是标准,规范了。不同的人,只要继承这个类,实现这个方法就可以不关心内部逻辑,使用统一使用Animal定义的方法名进行使用。
后面介绍的抽象类,接口,就是发现父类的需要被重写的方法,或者说 当定义规则方法时,不需要函数体,即具体实现而改进来的。
instanceof 关键字用来检查 某个对象是否为某个类的对象。
JapanDog japanDog = new JapanDog(); Dog dog = new JapanDog(); Animal aniDog = new JapanDog(); Cat cat = new Cat(); System.out.println(cat instanceof Animal); System.out.println(aniDog instanceof Animal); System.out.println(aniDog instanceof Dog); System.out.println(dog instanceof Animal); System.out.println(japanDog instanceof Dog); System.out.println(japanDog instanceof Animal);
以上代码判断的 instanceof 输出都是 true
这也意味着 子类的类型可以是子类本身,也可以是任意父类。
但是,子类对象一旦被确定的类型接收,或者说赋值给了确定了类型对象,
编译时只能识别这个类型的属性和方法,内存中实际是加载了这个对象,这个对象不仅仅有自己的东西,还包括了所有父类的东西。
有实际上是包含了所有父类以及自己的属性和方法。这个对手可以被任意父类以及自己本身的类别接收。
当赋值给了一种指定类型时,如
Animal aniDog = new JapanDog();
这个aniDog对象是可以被转换为其他类型的。如
Dog aniDog2dog = (Dog) aniDog; JapanDog aniDog2JapanDog = (JapanDog) aniDog;
语法就是 小括号,里面是需要强制的类型。
1、什么类型都可以转换吗?
ans: 只能强制转换为 这个对象自身的类型或者它的任意父类类型。
2、如何判断能否强制转换?
ans: 使用instanceof 关键字先判断一下。如果为false,则不能强制转换,否则出错。