1、继承的概述
继承是Java面向对象的三大特征之一,是比较重要的一部分,与后面的多态有着直接的关系。继承很好理解,子承父业嘛!在Java中继承就是子类继承父类的一些特征和行为,使得子类对象(实例)具有父类相同的特征和行为,当然子类也可以有自己的特征与行为。
继承的目的:如果多个子类中存在相同的特征与行为,那么就可以将这些内容抽取到父类中,而父类中的一些特征和行为可以被子类继承下来使用,不再需要再在子类中重复定义了。这里以动物为例:Animal类是所有动物的父类,它应该有动物的一些共同特征和行为,而猫、狗等动物也都具有这些特征和行为,所有它们可以通过继承来实现。
如图所示:
将猫、狗类中的共同属性和行为抽取出来,
其中,多个类可以称为子类,也叫派生类;多个类抽取出来的这个类称为父类、超类(superclass)或者基类。
继承描述的是事物之间的所属关系,这种关系是:is-a
的关系。例如,图中猫属于动物,狗也属于动物。可见,父类更通用,子类更具体。我们通过继承,可以使多种事物之间形成一种关系体系。
①、Java继承的特点:
- Java只支持单继承 ,不支持多继承(如A继承B,A继承C),但支持多层继承(如A继承B,B继承C) 。
- 子类拥有父类非private的属性,方法。(其实子类继承父类后,仍然认为获取了父类的private结构,只是因为封装的影响,使得子类不能直接调用父类的结构而已)
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
- 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系)。
②、继承的好处:
- 减少代码的冗余,提高代码的复用性。
- 便于功能的扩展。
- 为后面多态的使用提供了前提。
③、类的继承格式:
在Java中通过 extends
关键字来实现继承关系,形式如下:
[修饰符] class 父类 {
属性、方法
}
[修饰符] class 子类 extends 父类 {
属性、方法
}
④、类的继承举例:
/*
* 定义动物类Animal,做为父类
*/
public class Animal {
// 定义name属性
public String name;
// 定义age属性
public int age;
// 定义动物的吃东西方法
public void eat() {
System.out.println("动物具有吃的行为...");
}
}
//定义猫类Cat 继承 动物类Animal
class Cat extends Animal {
// 定义一个猫抓老鼠的方法catchMouse
public void catchMouse() {
System.out.println("猫抓老鼠");
}
}
//定义猫类Dog 继承 动物类Animal
class Dog extends Animal {
// 定义一个狗会看门的方法watchDoor
public void watchDoor() {
System.out.println("狗会看门");
}
}
//定义测试类
class Main {
public static void main(String[] args) {
// 创建一个猫类对象
Cat cat = new Cat();
// 为该猫类对象的name属性进行赋值
cat.name = "TomCat";
// 为该猫类对象的age属性进行赋值
cat.age = 3;
// 调用该猫继承来的eat()方法
cat.eat();
// 调用该猫的catchMouse()方法
cat.catchMouse();
Dog dog = new Dog();
dog.name = "JackDog";
dog.age = 2;
dog.eat();
dog.watchDoor();
}
}
注意:我们知道,子类可以继承父类的所有属性和方法,并直接使用,但是私有(private)的例外,私有的属性不能直接访问(也可以理解为私有的属性不能被继承,官方文档中的说法);如果直接使用父类用private修饰的属性或方法在编译时就会报错。虽然子类不能直接进行访问父类的私有属性,但可以通过继承的getter/setter方法进行访问。如图所示:
既然提到Java的继承,肯定离不开this
,super
关键字和构造器
的使用,下面来介绍一下。
2、构造器
构造器也叫构造方法,构造器的作用是用于创建并初始化对象。
当我们在使用new
关键字创建对象时,如A a = new A(); 此时使用了无参构造器创建了一个对象,对象创建完成后它的成员变量也会被初始化,默认是相对数据类型的默认值。如果我们需要赋别的值,需要挨个为它们再赋值,太麻烦了。我们能不能在new对象时,直接为当前对象的某个或所有成员变量直接赋值呢。答案是可以使用有参的构造器。
注意:构造器只为实例变量初始化,不为静态类变量初始化
①、构造器的语法格式
构造器又称为构造方法或构造函数,那是因为它长的很像方法。但是和方法还有有所区别的。
[修饰符] 构造器名(){
// 实例初始化代码
}
[修饰符] 构造器名(参数列表){
// 实例初始化代码
}
构造器创建的注意事项:
- 构造器名必须与它所在的类名必须相同
- 它没有返回值,所以不需要返回值类型,甚至不需要void
- 如果你不提供构造器,系统会给出默认无参数构造器,并且该构造器的修饰符默认与类的修饰符相同
- 如果你提供了构造器,系统将不再提供无参数构造器,除非你自己定义。
- 构造器是可以重载的,既可以定义参数,也可以不定义参数。
- 构造器的修饰符只能是权限修饰符,不能被其他任何修饰
简单的举例代码如下:
public class Father {
private String name;
private int age;
// 无参构造
public Father() {}
// 有参构造
public Father(String name,int age) {
this.name = name;
this.age = age;
}
//getter,setter方法省略...
}
3、方法重载与重写
这里就给出它两的定义与区别,具体的可以去问度娘。
方法重载(Overload):指在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可,与修饰符和返回值类型无关。
参数列表不同:指的是参数个数不同,数据类型不同,数据类型顺序不同。
方法重写(Override):子类中定义与父类中相同的方法,一般方法体不同,用于改造并覆盖父类的方法。当子类继承了父类的某个方法之后,发现这个方法并不能满足子类的实际需求,那么可以通过方法重写,覆盖父类的方法。
重写的具体规则如下:
- 必须保证父子类之间方法的名称相同,参数列表也相同。
- 子类方法的返回值类型必须与父类方法的返回值类型相同或者为父类方法返回值类型的子类类型。
- 子类方法的访问权限必须不能小于父类方法的访问权限。(public > protected > 缺省 > private)
- 子类方法 抛出的异常不能大于父被重写的异常
注意事项:
- 静态方法不能被重写,方法重写指的是实例方法重写,静态方法属于类的方法不能被重写,而是隐藏。
- 私有等在子类中不可见的方法不能被重写
- final方法不能被重写
4、this和super关键字
4.1、this关键字
①、this关键字的含义:表示当前对象的引用或正在创建的对象。可用于调用本类的属性、方法、构造器。
②、this使用位置:
- this在实例初始化相关的代码块和构造器中:表示正在创建的那个实例对象,即正在new谁,this就代表谁
- this在非静态实例方法中:表示调用该方法的对象,即谁在调用,this就代表谁。
- this不能出现在静态代码块和静态方法中
③、this使用格式:
(1) this.成员变量名:
- 当方法的局部变量与当前对象的成员变量重名时,就可以在成员变量前面加this.,如果没有重名问题,就可以省略this.
- this.成员变量会先从本类声明的成员变量列表中查找,如果未找到,会去从父类继承的在子类中仍然可见的成员变量列表中查找
(2) this.成员方法:
- 调用当前对象的成员方法时,都可以加"this.",也可以省略,实际开发中都省略
- 当前对象的成员方法,先从本类声明的成员方法列表中查找,如果未找到,会去从父类继承的在子类中仍然可见的成员方法列表中查找
(3) this()或this(实参列表):
- 只能调用本类的其他构造器
- 必须在构造器的首行
- 如果一个类中声明了n个构造器,则最多有 n - 1个构造器中使用了"this(【实参列表】)",否则会发生递归调用死循环
this关键字的简单举例如下所示(注:这里只举例this调用构造器,调用其它的下面会举例):
/**
* 通过this调用构造器测试
*/
public class Demo {
public static void main(String[] args) {
Father father = new Father("张三", 30);
father.show();
}
}
class Father {
public String name;
public int age;
//无参构造器
public Father() {
System.out.println("调用了Father无参构造器");
}
//name属性的有参构造器
public Father(String name) {
this();
this.name = name;
System.out.println("调用了Father的name属性有参构造器");
}
//全参构造器
public Father(String name, int age) {
this(name);
this.name = name;
this.age = age;
System.out.println("调用了Father的全参构造器");
}
// 定义show方法
public void show() {
System.out.println("name:" + this.name + ",age:" + this.age);
}
}
4.2、super关键字
①、super的含义:super是用于在当前类中访问父类的一个特殊关键字,可用于调用父类的属性、方法、构造器,它不是对象的引用。(区别this :super不能单独使用赋值给一个变量)
②、super使用的前提
- 通过super引用父类的xx,都是在子类中仍然可见的
- 不能在静态代码块和静态方法中使用super
③、super的使用格式:
(1) super.成员变量:在子类中访问父类的成员变量,如果是当子类的成员变量与父类的成员变量重名时,必定是调用父类的成员变量。
(2) super.成员方法:在子类中调用父类的成员方法,如果是当子类重写了父类的成员方法时,必定是调用父类的成员方法。
(3) super()或super(实参列表):在子类的构造器首行,用于表示调用父类的哪个实例初始化方法
super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现。
super关键字的简单举例:
public class Demo {
public static void main(String[] args) {
Son son = new Son();
son.show();
}
}
class Father {
String name = "Father";
int age = 40;
public Father() {
System.out.println("调用了父类的无参构造器");
}
public Father(String name, int age) {
this.name = name;
this.age = age;
}
public void show() {
System.out.println("父类的Show方法");
}
}
class Son extends Father {
String name = "Son";
int age = 20;
public Son() {
//调用父类构造器,可以省略,系统会默认添加
super();
}
public void show() {
System.out.println("子类的Show方法");
System.out.println("本类属性:" + this.name + "," + this.age);
//调用父类方法
super.show();
System.out.println("父类属性:" + super.name + "," + super.age);
}
}
注意:这个默认调用父类的构造器(super()
)是有前提的:父类必须有默认构造器。如果父类没有默认构造器(父类写了有参的构造器,那么默认的无参构造器就会被隐藏),我们就要必须显示的使用super()来调用父类构造器,或者显示的写出父类的无参构造器,否则编译器会报错:无法找到符合父类形式的构造器。
4.3、this、super的小结
- 在使用this、super调用构造器的时候,this、super语句必须放在构造方法的第一行,否则编译会报错。
- 由于this、super关键字调用构造器的时候都必须出现在第一行,所以它两不能同时出现。
- 不能在子类中使用父类构造方法名来调用父类构造方法,因为父类的构造方法不被子类继承。
- 调用父类的构造方法的唯一途径是使用 super 关键字,如果子类中没显式调用,则编译器自动调用 super(),也就是说会一直调到Object类,因为Object是所有类的父类。
- 子类会默认使用super()调用父类默认构造器,如果父类没有默认构造器,则子类必须显式的使用super()来调用父类构造器。
- super和this都不能出现在静态方法和静态代码块中,因为super和this都是存在于对象中的
4.4、this、super的练习
①、父类,子类及子类方法中存在同名变量时
public class Test {
public static void main(String[] args) {
Son son = new Son();
System.out.println(son.a);//20
System.out.println(son.b);//11
son.test();
son.method(30);
son.fun(13);
}
}
class Father {
int a = 10;
int b = 11;
}
class Son extends Father {
int a = 20;
public void test() {
//子类与父类的属性同名,子类对象中就有两个a
System.out.println("父类的a:" + super.a);//10 直接从父类局部变量找
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("子类的a:" + a);//20 先找局部变量找,没有再从本类成员变量找
//子类与父类的属性不同名,是同一个b
System.out.println("b = " + b);//11 先找局部变量找,没有再从本类成员变量找,没有再从父类找
System.out.println("b = " + this.b);//11 先从本类成员变量找,没有再从父类找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}
public void method(int a) {
//子类与父类的属性同名,子类对象中就有两个成员变量a,此时方法中还有一个局部变量a
System.out.println("父类的a:" + super.a);//10 直接从父类局部变量找
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("局部变量的a:" + a);//30 先找局部变量
}
public void fun(int b) {
System.out.println("b = " + b);//13 先找局部变量
System.out.println("b = " + this.b);//11 先从本类成员变量找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}
}
②、父子类中找方法1
public class Test {
public static void main(String[] args) {
Son s = new Son();
System.out.println(s.getNum());//10 没重写,先找本类,没有,找父类
Daughter d = new Daughter();
System.out.println(d.getNum());//20 重写了,先找本类
}
}
class Father {
protected int num = 10;
public int getNum() {
return num;
}
}
class Son extends Father {
private int num = 20;
}
class Daughter extends Father {
private int num = 20;
public int getNum() {
return num;
}
}
③、父子类中找方法2
public class Test {
public static void main(String[] args) {
Son s = new Son();
s.test();
Daughter d = new Daughter();
d.test();
}
}
class Father {
protected int num = 10;
public int getNum() {
return num;
}
}
class Son extends Father {
private int num = 20;
public void test() {
System.out.println(getNum());//10 本类没有找父类
System.out.println(this.getNum());//10 本类没有找父类
System.out.println(super.getNum());//10 本类没有找父类
}
}
class Daughter extends Father {
private int num = 20;
public int getNum() {
return num;
}
public void test() {
System.out.println(getNum());//20 本类有,先找本类
System.out.println(this.getNum());//20 本类有,先找本类
System.out.println(super.getNum());//10 重写了,直接找父类
}
}
5、继承带来的问题
- 子类与父类存在严重的耦合关系。
- 继承破坏了父类的封装性。
- 子类继承父类的属性和方法,也就说明可以从子类中恶意修改父类的属性和方法。
所以能不使用继承关系就尽量不要使用继承。
6、何时使用继承
- 子类需要额外增加属性,而不仅仅是属性值的改变。
- 子类需要增加自己独有的行为方式(包括增加新的方法或重写父类的方法)。