目录
继承
为什么需要继承
继承的概念
继承的语法
父类成员的访问
子类中访问父类的成员变量
1.子类和父类不存在同名的成员变量
2.子类和父类成员变量同名
子类中访问父类的成员方法
1.成员方法名字不同
2.成员方法名字相同
super关键字
子类构造方法
super和this
再谈初始化
protected关键字
继承方式
final关键字
继承与组合
Java中使用类对现实世界中实体进行描述,类经过实例化之后的产物对象,则可以用来表示现实世界的实体,但是现实世界错综复杂,事物之间可能有一些关联,那再设计程序时就需要考虑。
比如:狗和猫,他们都是动物。(每个动物都有共性,可以抽取出来它们的共性)
使用java语言对狗和猫进行描述,设计出:
//创建一个狗类
public class Dog {
String name;
int age;
float weight;
public void eat(){
System.out.println(name + "正在吃饭");
}
public void sleep(){
System.out.println(name + "正在睡觉");
}
void bark(){
System.out.println(name + "汪汪汪");
}
}
//创建一个猫类
public class Cat {
String name;
int age;
float weight;
public void eat(){
System.out.println(name + "正在吃饭");
}
public void sleep(){
System.out.println(name + "正在睡觉");
}
void mew(){
System.out.println(name + "喵喵喵~~~");
}
}
通过狗类和猫类,我们发现大量代码出现重复,如图所示:
那如何能实现共性抽取呢?面向对象的思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用。 (继承可以看作is-a关系,比如Dog is a animal)
继承(inheritance)机制:是面向对象程序设计使代码可以复用的重要手段,它允许程序员在保持原有类的个性的基础上进行扩展,增加新功能,这样产生的新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。继承解决的主要问题是:共性抽取,实现代码复用
例如:狗和猫都是动物,那么我们就可以将共性进行抽取,然后采用继承的思想来达到共用。
上述图示中,Dog和Cat都继承了Animal类,其中:Animal类称为父类/基类/超类,Dog和Cat可以称为Animal的子类/派生类,继承之后,子类可以复用父类中的成员,子类在实现时只需关心自己新增加的成员即可。
从继承概念中可以看出继承的最大作用是:实现代码复用,后面也应用于多态。
在java中如果要表示类的继承,需要用到extends关键字,具体如下:
修饰符 class 子类名称 extends 父类名称 {
//...
}
对之前的Dog类和Cat类通过继承重新设计:
//Animal.java
public class Animal {
String name;
int age;
public void eat(){
System.out.println(name + "正在吃饭");
}
public void sleep(){
System.out.println(name + "正在睡觉");
}
}
//Dog.java
public class Dog extends Animal {
void bark() {
System.out.println(name + "汪汪汪");
}
}
//Cat.java
public class Cat extends Animal {
void mew() {
System.out.println(name + "喵喵喵");
}
}
public class TestExtend {
public static void main(String[] args) {
Dog dog = new Dog();
//dog类中并没有定义任何成员变量,name和age属性肯定是从父类Animal中继承下来的
System.out.println(dog.name);
System.out.println(dog.age);
//dog访问的eat和sleep方法也是从Animal中继承下来的
dog.eat();
dog.sleep();
//bark是子类新增加的方法
dog.bark();
}
}
注意:
1.子类会将父类中的成员变量或者成员方法继承到子类中了
2.子类继承父类后,建议添加自己特有的成员,体现出与基类的不同,否则没有继承的必要
在继承体系中,子类将父类中的方法和字段继承下来了,那在子类中能否直接访问父类中继承下来的成员呢?
class Base {
int a;
int b;
}
public class Derived extends Base {
int c;
public void method() {
a = 10;//访问从父类继承下来的a
b = 20;//访问从父类继承下来的b
c = 30;//访问子类自己的c
}
}
class Base {
int a;
int b;
int c;
}
public class Derived extends Base {
int a;//与父类中成员a同名,而且类型相同
char b;//与父类中成员b同名,而且类型不同
public void method() {
a = 100;//使用的是子类新增的a
b = 101;//使用的是子类新增的b
c = 102;//使用从父类继承下来的c
}
}
通过上述栗子,我们可以得出以下规律:
1.如果访问的成员变量子类中有,则优先访问自己的成员变量
2.如果访问的成员变量子类中没有,则访问从父类继承下来的,如果父类也没有,则报错
3.如果访问的成员变量与父类中的成员变量同名,则优先访问自己的
简:成员变量的访问遵循就近原则,自己有则优先访问自己的,如果没有则在父类中找
总结:成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,则优先访问自己的,自己没有时再到父类中找,如果父类中也没有则报错
class Base {
public void methodA() {
System.out.println("Base中的methodA()");
}
public void methodB() {
System.out.println("Base中的methodB()");
}
}
public class Derived extends Base {
public void methodA(int a) {
System.out.println("Derived 中的methodA(int) 方法");
}
public void methodB() {
System.out.println("Derived 中的methodB() 方法");
}
public void methodC() {
methodA();//没有传参,访问父类中的methodA()
methodA(20);//有传参,访问子类中的methodA(int)
methodB();//直接访问,则永远访问到的都是子类中的methodB(),永远无法访问到基类的
}
public static void main(String[] args) {
Derived d = new Derived();
d.methodC();
}
}
总结:通过派生类对象访问父类与子类相同名的方法时,如果父类和子类同名方法的参数列表不同,根据调用方法时传递的参数选择合适的方法进行访问,如果没有则报错。
那么有的人会问,如果成员的访问遵循就近原则,那么如果想访问父类中同名的成员应该怎么办?
这就需要super关键字
使用场景:子类和父类中可能存在相同名称的成员,需要在子类方法中访问与父类同名的成员,这时就需要super,该关键字的主要作用:在子类中访问父类的成员。
class Parent {
int value = 10;
public void methodA() {
System.out.println("Parent中的methodA()");
}
}
class Child extends Parent {
int value = 20;
public void methodA() {
System.out.println("Child中的methodA()");
}
void printValues() {
System.out.println("Child value: " + value); // 子类字段
System.out.println("Parent value: " + super.value); // 父类字段
methodA();//子类方法
super.methodA();//父类方法
}
public static void main(String[] args) {
Child c = new Child();
c.printValues();
}
}
执行结果:
在子类方法中,如果想要明确访问父类中的成员时,借助super关键字即可。
注意事项:
1.只能在非静态的方法中使用
2.在子类方法中,调用父类的成员变量和方法
父子父子,先有父再有子,即:子类对象构造时,需要先调用基类构造犯法,然后执行子类的构造方法。
class Base {
public Base() {
System.out.println("Base()");
}
}
public class Derived extends Base {
public Derived() {
//super();//注意子类构造方法中默认会调用基类的无参构造方法:super();
//用户没有写时,编译器会自动添加,而且super()必须是子类构造方法中的第一条语句
//并且只出现一次
System.out.println("Derived()");
}
public static void main(String[] args) {
Derived d = new Derived();
}
}
执行结果:
在子类构造方法中,并没有写任何关于基类构造的代码,但是在构造子类对象时,先执行基类的构造方法,然后执行子类的构造方法,因为:子类对象中成员是由两部分组成的,基类继承下来的以及子类新增加的部分。父子父子必是先有父后有子,所以在构建子类对象的时候,先要调用基类的构造方法将从基类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整。
注意:
1.若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类的构造方法
2.如果父类构造方法是带有参数的,此时需要用户为子类显式定义的构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败
3.在子类的构造方法中,super(..)调用父类构造时,必须是子类构造方法中的第一条语句
4.super(...)只能在子类构造中出现一次,并且不能和this同时出现
super和this都可以在成员方法中用来访问:成员变量和调用其他成员方法,都可以作为构造方法的第一条语句,那它们之间有什么区别呢?
相同点:
1.都是Java的关键字
2.只能在类的非静态方法中使用,用来访问非静态成员方法和字段
3.在构造方法中使用时,必须是构造方法中的第一条语句,并且不能同时存在
不同点:
1.this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来的部分成员的引用
2.在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类的方法与属性
3.在构造方法中:this(...)用来调用本类的构造方法,super(...)用来调用父类的构造方法,两种调用不能同时在构造方法中出现
4.构造方法中一定会存在super(...)的调用,用户没有写编译器也会增加,而this(..)不写则没有
还记得之前讲过的代码块吗?我们来简要回顾一下几个重要的代码块:实例代码块和静态代码块。
我们之前讲过在没有继承关系下的执行顺序。
1.静态代码块先执行,并且只执行一次,在类的加载阶段执行
2.当有对象创建时,才会执行实例代码块,实例代码块执行完成后,构造方法执行
那么如果现在有继承关系,那它们的执行顺序又是什么?让我们看看下面的代码:
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Person:构造方法执行");
}
{
System.out.println("Person:实例代码块执行");
}
static {
System.out.println("Person:静态代码块执行");
}
}
public class Student extends Person {
public Student(String name, int age) {
super(name, age);
System.out.println("Student:构造方法执行");
}
{
System.out.println("Student:实例代码块执行");
}
static {
System.out.println("Student:静态代码块执行");
}
}
public class Test1 {
public static void main(String[] args) {
Student s1 = new Student("zhangsan",19);
System.out.println("-----------------------------");
Student s2 = new Student("lisi",22);
}
}
执行结果:
通过分析结果,得出以下结论:
1.父类静态代码块优先于子类静态代码块执行,而且是最早执行
2.父类实例代码块和父类构造方法紧接着执行
3.子类实例代码块和子类构造方法紧接着执行
4.第二次实例化子类对象时,父类盒子类的静态代码块都将不会执行
成员可见性: 使用
protected
关键字修饰的成员(字段或方法)可以被同一个包内的其他类访问,以及继承自该类的子类访问。访问权限范围:
protected
修饰的成员在同一个包内是可见的,同时也对继承关系中的子类可见,即使子类位于不同的包内。使用举例:
package com.example; // 包名 public class Parent { protected int value; protected void printValue() { System.out.println("Value: " + value); } } package com.example; // 同一个包 public class Child extends Parent { void accessParent() { value = 10; // 访问父类字段 printValue(); // 调用父类方法 } } package otherpackage; // 不同包 import com.example.Parent; public class OtherChild extends Parent { void accessParent() { value = 20; // 访问父类字段 printValue(); // 调用父类方法 } }
在Java中有以下几种继承方式:
注意:java不支持多继承
我们写的类是现实事物的抽象,而我们真正在公司中所遇到的项目往往业务比较复杂,也会涉及到一系列复杂的概念,都需要我们用代码表示,所以在实际项目中写的类比较多,类之间的关系也十分复杂
但是即使如此,我们并不希望类之间的继承层次太复杂,一般我们不希望超出三层的继承关系,如果继承层数过多,就考虑对代码进行重构了。
如果想从语法上限制继承,就可以使用final关键字
final关键字可以用来修饰变量,成员方法和类。
1.修饰变量和字段,表示常量(即不能修改)
final int a = 10;
a = 20;//编译出错
2.修饰类,表示此类不能被继承
final public class Animal {
...
}
public class Bird extends Animal {
...
}
//编译出错
我们平时用的String字符串类,就是用final修饰的,不能被继承
3.修饰方法:表示该方法不能被重写(后面介绍)
和继承类似,组合也是一种表达类之间关系的方式,也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如extends关键字),仅仅是将一个类的实例作为另一个类的字段
继承表示对象之间是is-a的关系,比如:狗是动物,猫是动物。
组合表示对象之间是has-a的关系,比如:汽车有轮胎,汽车有发动机。
举个例子:汽车和其轮胎,发动机,方向盘,车载系统等的关系就应该是组合,因为汽车是由这些部件组成的。
//轮胎类
class Tire {
//...
}
//发动机类
class Engine {
//...
}
//车载系统类
class VehicleSystem {
//...
}
class Car {
private Tire tire;//可以复用轮胎中的属性和方法
private Engine engine;//可以复用发动机中的属性和方法
private VehicleSystem vs;//可以复用车载系统类中的属性和方法
//。。。
}
public class Benz extends Car {
//将汽车中的轮胎,发动机,车载系统全部继承下来
}
组合和继承都可以实现代码的复用,应该使用继承还是组合,需要根据应用场景来选择。一般建议:能用组合尽量用组合。