继承是面向对象的三大特征之一,也是实现软件复用的重要手段。Java的继承具有单继承的特点,每个子类只有一个直接父类。
理解继承
Java中子类继承父类的语法:
[修饰符] class SubClass extends SuperClass
{
// 类定义部分
}
按照这种关系,我们把SuperClass类称为父类,把SubClass称为子类。
因为父类包含的范围总比子类包含的范围要大,所以可以认为父类是大类,子类是小类。
extends在英语中是扩展的意思,而不是继承。这个关键字很好的体现了子类和父类的关系:子类是对父类的扩展,子类是一种特殊的父类。
1),编码一个父类就是一个泛化的过程,将不同的子类的共性抽象成父类的过程;
2),编码一个子类就是一个特化的过程,在原有的父类的基础上加上一些个性的过程。
总结一下就是父类放共性,子类放特性,继承是一种从一般到特殊的关系,提高了代码的复用性,让类与类之间产生关系,继承其实也就是多态的前提。
继承的特点
子类扩展了父类,将可以获得父类中所有的在访问权限内的属性和方法,值得注意的是:java中的子类不能获得父类的构造器。
java.lang.Object是所有类的父类,Object要么是直接父类要么是间接父类。Object是默认父类,当你继承了一个父类时,Object就被取消继承了,好比构造器一样。
/**
* 子类继承父类中在访问范围之内的属性和方法
*
* @author LinkinPark
*
*/
public class SubLinkin extends SuperLinkin
{
public static void main(String[] args)
{
SubLinkin linkin = new SubLinkin();
// 通过继承,子类获得了父类中的public修饰的name属性
System.out.println(linkin.name);
// 通过继承,子类获得了父类中的protected修饰的sayHi方法
linkin.sayHi();
// 控制台输出如下:
// LinkinPark
// SuperLinkin say Hi
}
}
class SuperLinkin
{
public SuperLinkin()
{
}
public String name = "LinkinPark";
protected void sayHi()
{
System.out.println("SuperLinkin say Hi");
}
}
前面的整理强调了,子类不可以获得父类的构造器,这里抛出三个问题:
1),若一个类的所有的构造方法使用private修饰,该类能不能有子类?明确的告诉你,不可以。我们来针对这种情况编码一段看下:
/**
* 子类继承父类,如果父类所有构造器private修饰,编译不过
*
* @author LinkinPark
*
*/
public class SubLinkin extends SuperLinkin
{
// 代码错误,编译不过。提示明确说SuperLinkin父类构造器不能访问。
}
class SuperLinkin
{
private SuperLinkin()
{
}
}
2),若一个类的所有的构造方法使用protected修饰,该类能不能有子类?明确的告诉你,可以的。
/**
* 子类继承父类,如果父类所有构造器protected修饰,编译通过
*
* @author LinkinPark
*
*/
public class SubLinkin extends SuperLinkin
{
// 编译通过
}
class SuperLinkin
{
protected SuperLinkin()
{
}
}
3),子类不能直接访问父类的私有成员,那如果现在我们就是想访问怎么办呢?
一般情况下,我们封装一个类,都会为一些私有属性提供对外暴露操作的入口的,所以这个时候在子类中可以调用父类中的非私有方法来间接访问父类的私有成员。
/**
* 子类继承父类,通过暴露方法访问私有属性
*
* @author LinkinPark
*
*/
public class SubLinkin extends SuperLinkin
{
public static void main(String[] args)
{
SubLinkin linkin = new SubLinkin();
// 父类中name属性用private修饰,这里不能访问,下面代码编译不过
System.out.println(linkin.name);
// 但是父类中提供了getXXX方法,可以调用该方法访问父类中的私有属性
System.out.println(linkin.getName());
}
}
class SuperLinkin
{
public SuperLinkin()
{
}
// 父类中属性私有化,但是有对外暴露的getXXX方法用于访问
private String name;
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
}
Java的单继承
父类的私有成员子类不能继承到,Java只支持单继承,不支持多继承,一个类有且只有一个直接父类,那么问题来了:Java为什么不支持多继承?
关于Java中为什么摒弃多继承采用单继承,我后面会用一篇博客专门整理,这里暂时先按如下解释理解。
多继承虽然能使子类同时拥有多个父类的特征,但是其缺点也是很显著的,主要有两方面:
1),如果在一个子类继承的多个父类中拥有相同名字的实例变量,子类在引用该变量时将产生歧义,无法判断应该使用哪个父类的变量
2),如果在一个子类继承的多个父类中拥有相同方法,子类中有没有覆盖该方法,那么调用该方法时将产生歧义,无法判断应该调用哪个父类的方法
重写父类的方法
子类扩展了父类,子类是一个特殊的父类。大部分时候,子类总是以父类为基础,额外增加新的属性和方法,但是有一种情况例外,子类需要重写父类的方法。
这里举一个鸟类和鸵鸟的例子,鸵鸟是鸟类中的一个特殊品种,所以鸵鸟类是鸟类的一个子类;但是鸟类有飞翔的功能,但是鸵鸟呢?
飞翔的行为显然不适合于鸵鸟,此时在鸵鸟类中就需要明确的重写鸟类的方法。
/**
* 子类继承父类,重写父类方法
*
* @author LinkinPark
*
*/
public class Ostrich extends Bird
{
// 子类方法前加上@Override能编译通过,表明是方法的覆写。
@Override
public void fly()
{
System.out.println("可怜的鸵鸟,只能在地上跑。。。");
}
// 如果需要在子类方法中调用父类被覆盖的实例方法,可以使用super来调用
public void birdFly()
{
super.fly();
}
public static void main(String[] args)
{
Ostrich bird = new Ostrich();
// 调用鸵鸟自己的飞翔方法
bird.fly();
// 调用父类的通用的飞翔方法
bird.birdFly();
// 可怜的鸵鸟,只能在地上跑。。。
// 一般的鸟儿都会在天上飞的。。。
}
}
/**
* 定义一个普通的父类鸟类,该类有一个飞翔的通用方法。
* 实际编码中,设计用来被继承的类,方法的访问修饰符一般都使用protected
*
* @author LinkinPark
*
*/
class Bird
{
public void fly()
{
System.out.println("一般的鸟儿都会在天上飞的。。。");
}
}
子类包含与父类同名方法的现象被称为方法重写,也就是方法覆盖。当父类的某个方法不适合于子类本身的特征行为时就当重写父类中的方法。
方法重写时应遵循的原则:
1),方法签名必须相同,方法签名=方法名+形参列表。当父类中某个方法不适合于子类时,子类出现父类一模一样的方法,所以这个时候方法签名肯定要一模一样。
这里有一个小技巧的,子类重写父类的某一个方法,就在这个子类方法前加上注解@Override,如果编译通过,表明是方法的重写,且重写成功。
2),子类方法的返回值类型比父类方法的返回值类型更小或相等。
父类是所有子类的公共部分的抽象和泛化,子类重写父类方法过程中肯定不能返回一个比父类还大的返回值。
3),子类方法声明抛出的异常应比父类方法申明抛出的异常更小或相等。
父类是所有子类的公共部分的抽象和泛化,子类重写父类方法过程中肯定不能抛出一个比父类还大的异常。
4),子类方法的访问权限应比父类方法更大或相等。
因为子类是一个特化的过程,没有父类那么通用和严格,所以子类方法的访问权限肯定是比父类更大。
5),如果父类方法具有private访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。
如果子类中定义了一个与父类private方法具有相同的方法名,相同的形参列表,相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法而已。
/**
* 子类继承父类,子类无法重写父类private修饰的方法
*
* @author LinkinPark
*
*/
public class SubLinkin extends SuperLinkin
{
// 使用注解必杀技Override,编译不过,因为父类中sayHi方法用private修饰,没法访问
// 这里sayHi方法只是重新定义了一个方法,随便自己定义方法签名,和父类中的sayHi没关系
// @Override
public static void sayHi()
{
System.out.println("父类用它优雅的姿态say hi");
}
}
class SuperLinkin
{
// 父类中sayHi方法用private修饰,子类不可以访问该方法
private void sayHi()
{
System.out.println("父类用它优雅的姿态say hi");
}
}
super关键字
如果需要在子类方法中调用父类被覆盖的实例方法,则可使用super限定来调用父类被覆盖的实例方法。比如前面鸵鸟和鸟类的例子中,鸵鸟的birdFly方法:
// 如果需要在子类方法中调用父类被覆盖的实例方法,可以使用super来调用
public void birdFly()
{
super.fly();
}
正如this不能出现在static修饰的方法中一样,super也不能出现在static修饰的方法中。
static修饰的方法是属于类的,该方法的调用者可能是一个类,而不是对象,因而super限定也就失去了意义。
如果在构造器中使用super,则super用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。
如果子类中包含了和父类同名的属性,那么在子类的实例方法中访问这个属性的时候,默认使用子类中的属性,也就是说子类的属性覆盖了父类的属性。值得注意的是:这里说
的覆盖并不是完全覆盖,系统在创建子类对象时,依然会为父类中定义的,被隐藏的变量分配内存空间。
如果子类中没有包含和父类同名的属性,在子类的实例方法中访问该属性的时候,则无须显式使用super或者父类名作为调用者。
如果在某个方法中访问名为a的属性,但是没有显式指定调用者,则系统查找a的顺序为:
1),查找该方法中是否有名为a的局部变量
2),查找当前类中是否包含了名为a的属性
3),查找a的直接父类中是否包含了a的属性,依次上溯a的所有父类,直到object
4),如果被覆盖的是类属性,在子类中可以通过父类名作为调用者来访问被覆盖的类的属性。
调用父类构造器
在继承操作中,对于子类对象的实例化:子类对象在实例化之前必须首先调用父类中的构造方法之后再调用自身的构造方法。
子类不会获得父类的构造器,但是子类构造器里可以调用父类构造器的初始化代码。
在一个构造器中调用另一个重载的构造器使用this调用来完成,在子类构造器中调用父类构造器使用super调用来完成。
使用super调用父类构造器必须出现在子类构造器执行体的第一行,所以this调用和super调用不会同时出现。
不管我们是否使用super调用来执行父类构造器中的初始化代码,子类构造器总会调用父类构造器一次。
子类构造器中调用父类构造器分如下几种情况:
1),子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类对象的构造器。
2),子类构造器执行体的第一行使用this显式调用本类中重载的构造器,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。
执行本来中的另外一个构造器时就会调用父类构造器。
3),子类构造器执行体中既没有super调用,也没有this调用,系统将会执行子类构造器之前,隐式调用父类无参数的构造器。
创建任何Java对象,最先执行的总是Java.lang.Object类的构造器。
/**
* 子类继承父类,子类调用父类构造器
*
* @author LinkinPark
*
*/
public class Wolf extends Animal
{
public Wolf()
{
super("狼人", 3);
System.out.println("Wolf无参数构造器。。。");
}
public static void main(String[] args)
{
new Wolf();
// Creature无参数构造器 。。。
// Animal带一个参数的构造器。。。
// Animal带两个参数的构造器。。。
// Wolf无参数构造器。。。
}
}
/**
* 第二父类,Wolf直接父类
*
* @author LinkinPark
*
*/
class Animal extends Creature
{
public Animal()
{
}
public Animal(String name)
{
System.out.println("Animal带一个参数的构造器。。。");
}
public Animal(String name, int age)
{
this(name);
System.out.println("Animal带两个参数的构造器。。。");
}
}
/**
* 第一父类,Wolf间接父类
*
* @author LinkinPark
*
*/
class Creature
{
public Creature()
{
System.out.println("Creature无参数构造器 。。。");
}
}
重载和重写
重载的英语是overload,重写的英语是override。
其实把重载和重写放在一起比较本身没有太大的意义,重载主要发生在同一个类的多个同名方法之间,而重写发生在子类和父类的同名方法之间。
它们之间的联系很少,完全不是一个东西,除了两者都是发生在方法之间,并要求方法名相同以外,并没有太大的相似之处。
当然,父类方法和子类方法之间也可能发生重载,因为子类会获得父类方法,如果子类定义了一个与父类方法有相同的方法名,但参数列表不同的方法,就会形成父类方法和子
类方法的重载。
No. |
区别点 |
重载(overload) |
重写(override) |
1 |
判断规则 |
两同一不同 |
一同两小一大 |
2 |
权限 |
没有权限要求 |
被重写的方法访问权限比父类更大 |
3 |
范围 |
发生在一个类之中 |
发生在继承关系中 |
4 |
术语 |
overload |
override |
5 |
多态 |
编译时多态 |
运行时多态 |