引语
高级语言为了避免重复写代码,那么就可以使用父子一样的继承关系使新的子类拥有和父类一样的特性,同时也能拥有自己特有的属性。但是java的继承特点是什么。
继承的定义和特点
继承是使用已存在的类的定义作为基础建立新类的技术。
在这种关系中,已有类我们称之为父类,这个新类我们称之为子类。
继承的特点
- 子类拥有父类除构造器外所有非private的属性和方法,但不能选择性地继承父类(即:必须继承父类的除构造器外所有非private的成员变量和方法,要使用其他类的private方法的唯一办法是定义内部类)
- 新类的定义可以增加新的数据或新的功能(即子类可以对父类进行扩展)
- 子类可以用自己的方式实现父类的方法(即方法的重写)
继承存在的缺陷
- 父类变,子类就必须变
- 继承破坏了封装(对于父类而言,它的实现细节对与子类来说都是透明的)
- 继承是一种强耦合关系
通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。
但同时当我们使用继承的时候,我们需要确信使用继承确实是有效可行的办法。那么到底要不要使用继承呢?
Think in java》中提供了解决办法:
问一问自己是否需要从子类向父类进行向上转型。如果必须向上转型,则继承是必要的,但是如果不需要,则应当好好考虑自己是否需要继承。
关于必须继承父类的除构造器外所有非private的成员变量和方法的说明如下:
class Animal{…..定义动物的特性…..};
class Snake extends Animal{……定义蛇除去动物的特性自己所拥有的特性……};
class Cat extends Animal{……定义猫除去动物的特性自己所拥有的特性……};
在上例中,在你定义猫类(Cat)时你无法将猫类定义为继承蛇类(Snake),就将蛇类的定义分为两部分:动物类,再继承后自己的类。 解决了不能选择性地继承父类的问题。
实际上继承者是被继承者的特殊化,它除了拥有被继承者的特性外,还拥有自己独有得特性。
例如猫有抓老鼠、爬树等其他动物没有的特性。
同时在继承关系中,继承者完全可以替换被继承者,反之则不可以,
例如我们可以说猫是动物,但不能说动物是猫就是这个道理,
其实对于这个我们将其称之为“向上转型”,下面介绍。
继承涉及到的三个关键点:构造器、protected关键字、向上转型。
构造器
通过前面我们知道子类可以继承父类的属性和方法,除了那些private的外还有一样是子类继承不了的—构造器
对于构造器而言,它只能够被子类调用,而不能被继承。调用父类的构造方法我们使用super()即可
- 对于子类,其构造器的正确初始化是在构造器中调用父类构造器来完成初始化,否则会报错。
- 于子类如果没有显示的调用父类的构造器,编译器会默认给子类调用父类的构造器。
父类
public class Person {
protected String name;
protected int age;
protected String sex;
Person(){
System.out.println("Person Constrctor...");
}
}
子类
public class Chinese extends Person{
private String language="speak chinese";
Chinese(){//这里没有调用super()但是在构造时它会默认调用父类的构造方法(即便父类没有显示的写出其默认构造方法但依然会调用)
System.out.println("Chinese Constructor...");
}
public static void main(String[] args) {
Chinese husband = new Chinese();
}
}
执行结果
Person Constrctor...
Chinese Constructor...
通过这个示例可以看出,构建过程是从父类“向外”扩散的,也就是从父类开始向子类一级一级地完成构建。
而且我们并没有显示的引用父类的构造器,编译器会默认给子类调用父类的构造器。
但是,这个默认调用父类的构造器是有前提的:父类有默认构造器。
如果父类没有默认构造器,我们就要必须显示的使用super()来调用父类构造器,否则编译器会报错:无法找到符合父类形式的构造器。
修改示例如下
父类没有默认构造器了
public class Person {
protected String name;
protected int age;
protected String sex;
Person(String name){
System.out.println("Person Constrctor..."+name);
}
}
子类
public class Chinese extends Person{
private String language="speak chinese";
Chinese(){
super("zhangsan");//如果不显示的调用父类构造器,这个构造方法直接报错:Implicit super constructor Person() is undefined. Must explicitly invoke another constructor
System.out.println("Chinese Constructor...");
}
public static void main(String[] args) {
Chinese husband = new Chinese();
}
}
子类初始化时一定要调用父类的构造方法,不调用行吗?
不行,子类继承了父类的各种属性,而构造方法则相当于把父类给实例化出来,如果你子类实例化的时候不调用父类的构造方法,相当于子类压根就没有父亲,又怎么谈得上继承呢?
另外,抽象类是例外,因为抽象类是没有构造方法的,无法实例化
protected关键字
private访问修饰符,对于封装而言,是最好的选择,但这个只是基于理想的世界。
有时候我们需要这样的需求:我们需要将某些事物尽可能地对这个世界隐藏,但是仍然允许子类(包括和父类不在同包)的成员来访问它们。这个时候就需要使用到protected。
对于protected而言,它指明就类用户而言,他是private,但是对于任何继承与此类的子类而言或者其他任何位于同一个包的类而言,他却是可以访问的。
诚然尽管可以使用protected访问修饰符来限制父类属性和方法的访问权限,但是最好的方式还是将属性保持为private(我们应当一致保留更改底层实现),通过protected方法来控制类的继承者的访问权限。
父类在test包下,构造函数必须为protecet,否则不同包的子类构造函数无法调用
package tet;
public class Person {
protected String name;
protected int age;
protected String sex;
protected Person(){
System.out.println("Person Constrctor...");
}
public int getAge() {
return age;
}
protected void setAge(int age) {
this.age = age;
}
}
子类在test1包下,因为需要使用父类的方法,所以初始化时必须在构造函数中调用父类的构造方法。
package tet1;
import tet.Person;
public class Chinese extends Person{
private String language="speak chinese";
Chinese(){
super();
System.out.println("Chinese Constructor...");
}
public static void main(String[] args) {
Chinese zhangsan = new Chinese();
zhangsan.setAge(12);
System.out.println(zhangsan.getAge());
}
}
运行结果
Person Constrctor...
Chinese Constructor...
12
向上转型
继承是is-a的相互关系,猫继承与动物,所以我们可以说猫是动物,或者说猫是动物的一种。这样将猫看做动物就是向上转型
父类的display方法
package tet;
public class Person {
protected String name;
protected int age;
protected String sex;
protected Person(){
System.out.println("Person Constrctor...");
}
Person(String name){
System.out.println("Person Constrctor..."+name);
}
public void display(){
System.out.println("Person Display...");
}
}
子类可以向上转型,但是转型后就不能调用子类专有的属性或方法
package tet;
public class Chinese extends Person{
private String language="speak chinese";
Chinese(){
super("zhangsan");
System.out.println("Chinese Constructor...");
}
public void show(){
System.out.println("Person show...");
}
public static void main(String[] args) {
Person person = new Chinese();
person.display();
person.show();//无法调用,会报错
}
}
如果在增加Japan并继承Person,chinese和Japan都重写display方法,那么当我们具体调用的时候可以如下
public static void main(String[] args) {
Person person1 = new Chinese();
Person person2 = new Japan();
person1.display();//它会自动的调用Chinese类的display方法
person2.display();//它会自动的调用Japan类的display方法
}
可以看出,向上转型体现了类的多态性,增强了程序的简洁性,当然我可以吧person改为接口类,这样更加便捷。
将子类转换成父类,在继承关系上面是向上移动的,所以一般称之为向上转型。由于向上转型是从一个叫专用类型向较通用类型转换,所以它总是安全的,唯一发生变化的可能就是属性和方法的丢失。这就是为什么编译器在“未曾明确表示转型”或“未曾指定特殊标记”的情况下,仍然允许向上转型的原因。
向下转型
子类转型成父类是向上转型,反过来说,父类转型成子类就是向下转型。但是,向下转型可能会带来一些问题:我们可以说猫是动物,但不能动物就是猫。来看下面的例子:
A类:
package a.b;
public class A {
void aMthod() {
System.out.println("A method");
}
}
A的子类B:
package a.b;
public class B extends A {
void bMethod1() {
System.out.println("B method 1");
}
void bMethod2() {
System.out.println("B method 2");
}
}
C类:
package a.b;
public class C {
public static void main(String[] args) {
A a1 = new B(); // 向上转型
a1.aMthod(); // 调用父类aMthod(),a1遗失B类方法bMethod1()、bMethod2()
B b1 = (B) a1; // 向下转型,编译无错误,运行时无错误
b1.aMthod(); // 调用父类A方法
b1.bMethod1(); // 调用B类方法
b1.bMethod2(); // 调用B类方法
A a2 = new A();
B b2 = (B) a2; // 向下转型,编译无错误,运行时将出错
b2.aMthod();
b2.bMethod1();
b2.bMethod2();
}
}
从上面的代码我们可以得出这样一个结论:向下转型需要使用强制转换。运行C程序,控制台将输出:
Exception in thread “main” java.lang.ClassCastException: a.b.A cannot be cast to a.b.B at
a.b.C.main(C.java:14)
A method
A method
B method 1
B method 2
为什么前一句向下转型代码可以,而后一句代码却出错?
这是因为a1指向一个子类B的对象,所以子类B的实例对象b1当然也可以指向a1。而a2是一个父类对象,子类对象b2不能指向父类对象a2。
那么如何避免在执行向下转型时发生运行时ClassCastException异常?
使用instanceof就可以了。我们修改一下C类的代码:
A a2 = new A();
if (a2 instanceof B) {
B b2 = (B) a2;
b2.aMthod();
b2.bMethod1();
b2.bMethod2();
}
这样处理后,就不用担心类型转换时发生ClassCastException异常了,因为a2不是B类的实例,所以后面的代码不会被执行。