文章目录
1. 继承
1.1 为什么需要继承
在Java中,我们可以使用类来对现实世界的实体来进行描述,类经过实例化后产生的对象则可以用来表示现实世界中的实体,但是现实世界错综复杂,我们在设计程序时就需要考虑一些事物之间本身存在的联系
比如:狗和猫,它们都是动物
用Java语言来描述,则有:
//Dog.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("汪汪叫~~");
}
}
//Cat.java
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("喵喵叫~~");
}
}
通过观察上述代码,我们会发现猫和狗类中存在着大量重复,如:
对此,在面向对象思想中提出了继承的概念,专门来进行共性抽取,实现代码复用
1.2 继承的概念
继承机制:在保持原有类特性的基础上进行扩展,增加新的功能,这样新产生的类,称为派生类。
继承主要解决的问题是:共性的抽取,实现代码复用。
例如:狗和猫都是动物,那么我们就可以将它们共性的内容进行抽取,然后采用继承的思想来达到共用
上述图示中,Dog和Cat都继承了Animal类,其中:Animal类称为父类/基类/超类,Dog和Cat可以称为Animal的子类/派生类,继承之后,子类可以复用父类中的成员,子类在实现时只需要关心自己新增加的成员即可
1.3 继承的语法
在Java中如果要表示类之间的继承关系,需要借助extends
关键字,具体如下:
修饰符 class 子类 extends 父类 {
//...
}
在此,我们对上述猫和狗类通过继承方式重新设计程序:
//Animal.java
package demo1;
public class Animal {
String name;
int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + "正在吃饭!");
}
public void sleep() {
System.out.println(name + "正在睡觉!");
}
}
//Cat.java
package demo1;
public class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
void mew() {
System.out.println("喵喵叫~~");
}
}
//Dog.java
package demo1;
public class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
void bark() {
System.out.println("汪汪叫~~");
}
}
//TestAnimal.java
package demo1;
public class TestAnimal {
public static void main(String[] args) {
Dog dog = new Dog("小七",2);
Cat cat = new Cat("小米",2);
//dog访问的eat()和sleep()是从Animal中继承下来的
System.out.println(dog.name);
dog.eat();
dog.sleep();
dog.bark();
System.out.println(cat.name);
cat.eat();
cat.sleep();
cat.mew();
}
}
//执行结果
小七
小七正在吃饭!
小七正在睡觉!
汪汪叫~~
小米
小米正在吃饭!
小米正在睡觉!
喵喵叫~~
注:
- 子类会将父亲中的成员变量或成员方法继承到子类中;
- 子类继承父类之后,一般要添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了。
1.4 父类成员访问
在继承的体系中,子类将父类中的字段和方法都继承下来了,那在子类中能否直接访问父类中继承下来的成员呢?
1.4.1 子类中访问父类的成员变量
-
子类和父类不存在同名成员变量
//Base.java public class Base { int a; int b; } //Derived.java public class Derived extends Base { int c; public void method() { a = 10; //访问从父类中继承下来的a b = 20; //访问从父类中继承下来的b c = 30; //访问子类自己的c } }
-
子类和父类成员变量同名
//Base.java public class Base { int a; int b; int c; } //Derived.java public class Derived extends Base { int a; //与父类中成员a同名,且类型相同 char b; //与父类中成员b同名,但类型不同 public void method() { a = 100; //访问子类自己的a b = 110; //访问子类自己的b c = 130; //子类没有c,所以访问从父类继承下来的c //d = 140; //编译失败,因为子类和父类都没有定义d System.out.println("b = " + b); } public static void main(String[] args) { Derived derived = new Derived(); derived.method(); } } //执行结果 b = x
-
在子类方法中 或者 通过子类对象访问成员时:
- 如果访问的成员变量子类中有,则优先访问子类自己的成员变量;
- 如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译失败;
- 如果访问的成员变量与父类中成员变量同名,则优先访问子类自己的。
注:成员变量访问遵循就近原值,自己有的优先自己,如果没有则向父类中找
1.4.2 子类访问父类中的成员方法
-
成员方法名不同
//Base.java package demo3; public class Base { public void methodA() { System.out.println("Base中的methodA()"); } } //Derived.java package demo3; public class Derived extends Base{ public void methodB() { System.out.println("Derived中的methodB方法"); } public void methodC() { methodB(); //访问子类自己的methodB() methodA(); //访问父类的methodA() //methodD(); //编译失败,没有找到methodD() } public static void main(String[] args) { Derived derived = new Derived(); derived.methodC(); } } //执行结果 Derived中的methodB方法 Base中的methodA()
注:成员方法没有同名时,在子类方法中或者通过子类对象访问方法,则优先访问子类自己的,若没有再到父类中找,如果父类中也没有则报错
-
成员方法名相同
//Base.java package demo3; public class Base { public void methodA() { System.out.println("Base中的methodA()"); } public void methodB() { System.out.println("Base中的methodB()"); } } //Derived.java package demo3; public class Derived extends Base{ public void methodA(int a) { System.out.println("Derived中的methodA()"); } public void methodB() { System.out.println("Derived中的methodB()"); } public void methodC() { methodA(); //没有传参,访问父类的methodA() methodA(10); //传参,访问子类的methodA() methodB(); //访问子类自己的methodB(),无法访问父类中的methodB() } public static void main(String[] args) { Derived derived = new Derived(); derived.methodC(); } } //执行结果 Base中的methodA() Derived中的methodA() Derived中的methodB()
注:
- 通过子类对象访问父类与子类不同名方法时,优先在子类中找,找到则访问,否则在父类中寻找访问,若父类中也没有则编译报错;
- 通过子类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(构成重载),则根据调用方法时传递的参数选择合适的方法访问,如果都没有则报错。
1.5 super关键字
当我们需要在子类方法中调用父类方法时,可以使用super
关键字来进行访问
代码示例:
//Base.java
package demo3;
public class Base {
int a;
int b;
public void methodA() {
System.out.println("Base中的methodA()");
}
public void methodB() {
System.out.println("Base中的methodB()");
}
}
//Derived.java
package demo3;
public class Derived extends Base{
int a;
char b;
public void methodA(int a) {
System.out.println("Derived中的methodA()");
}
public void methodB() {
System.out.println("Derived中的methodB()");
}
public void methodC() {
//对同名的成员变量,直接访问时,访问的都是子类的
a = 100; //等价于:this.a = 100; 且this是当前对象的引用
b = 101; //等价于:this.b = 101;
//通过super关键字访问父类的成员变量
super.a = 200;
super.b = 250;
methodA(); //没有传参,访问父类的methodA()
methodA(10); //传参,访问子类的methodA()
methodB(); //访问子类自己的methodB()
super.methodB(); //访问父类的methodB()
}
public static void main(String[] args) {
Derived derived = new Derived();
derived.methodC();
}
}
//执行结果
Base中的methodA()
Derived中的methodA()
Derived中的methodB()
Base中的methodB()
注:
- 只能在非静态方法中使用
- 在子类方法中,访问父类的成员变量和方法
super()
还可用于调用父类的构造方法
1.6 子类构造方法
子类对象在构建时,需要先调用父类构造方法,然后再执行子类的构造方法
代码示例:
public class Base {
public Base() {
System.out.println("Base()");
}
}
public class Derived extends Base {
public Derived() {
//super(); //子类构造方法中默认会调用父类的无参构造方法:super();
//用户没有写时编译器会自动添加,并且super()必须是子类构造方法中第一条语句,且只能出现一次
System.out.println("Derived()");
}
}
public class Test() {
public static void main(String args[]) {
Derived derived = new Derived();
}
}
//执行结果
Base()
Derived()
在构建子类对象时,先要调用父类的构造方法,将从父类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整
注:
- 若父类显示定义无参或者默认的构造方法,在子类构造方法的第一行默认有隐含的super()调用,即调用父类的构造方法
- 如果父类构造方法是带有参数的,此时需要用户为子类显示定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败
- 在子类构造方法中,super()调用父类构造时,必须是子类构造函数中第一条语句
- super()只能在子类构造方法中出现一次,并且不能和this同时出现
1.7 super 和 this
super和this都可以在成员方法中被用来访问成员变量和调用其他的成员方法,而它们有什么区别呢?
[相同点]
- 都是Java中的关键字
- 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
- 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
[不同点]
-
this是当前对象的引用,当前对象即调用实例方法的对象,而super相当于是子类对象中从父类继承下来部分成员的引用
-
在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
-
在构造方法中,this(…)用于调用本类构造方法,super(…)用于调用父类构造方法,两种调用不能同时在构造方法中出现
-
构造方法中一定会存在super(…)的调用,用户没有写编译器则会自动增加,但是this(…)用户不写则没有
2. 再谈初始化
通过代码来回忆一下实例代码块和静态代码块在没有继承关系时的执行顺序:
package demo5;
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("构造方法执行");
}
{
System.out.println("实例代码块执行");
}
static {
System.out.println("静态代码块执行");
}
}
public class TestDemo {
public static void main(String[] args) {
Person person1 = new Person("悟空",25);
System.out.println("==================");
Person person2 = new Person("曹操",35);
}
}
//执行结果
静态代码块执行
实例代码块执行
构造方法执行
==================
实例代码块执行
构造方法执行
注:
- 静态代码块先执行,并且只执行一次,在类加载阶段执行
- 当有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后构造方法执行
若在继承关系上执行,结果又是怎么样的?
代码示例:
package demo6;
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:静态代码块执行");
}
}
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 TestDemo {
public static void main(String[] args) {
Student s1 = new Student("张三丰",100);
System.out.println("==================");
Student s2 = new Student("杨过",25);
}
}
//执行结果
Person:静态代码块执行
Student:静态代块块执行
Person:实例代码块执行
Person:构造方法执行
Student:实例代码块执行
Student:构造方法执行
==================
Person:实例代码块执行
Person:构造方法执行
Student:实例代码块执行
Student:构造方法执行
通过分析执行结果,可以得出以下结论:
- 父类静态代码块优先于子类静态代码块执行,且是最早执行的
- 父类实例代码块和父类构造方法紧接着执行
- 父类的执行完后轮到子类的实例代码块和构造代码块执行
- 第二次实例化子类对象是,父类和子类的静态代码块都不会再执行
3. 访问限定符引用
在类和对象中,为了执行封装特性,Java中引入了访问修饰限定符,主要限定:类或者类中成员能否在类外或者其他包中被访问
那在父类中不同访问权限的成员,在子类中的可见性又是什么样的呢?这里通过代码来演示:
package extend01;
public class B {
private int a;
protected int b;
public int c;
int d;
}
/
package extend01;
public class D extends B{
//同一个包中的子类
public void method() {
//super.a = 10; //编译错误,父类private成员在相同包d的子类中不可见
super.b = 20; //父类protected成员在相同包的子类中可以直接访问
super.c = 30; //父类protected成员在相同包的子类中可以直接访问
super.d = 40; //父类中默认访问权限修饰的成员在相同包的子类中可以直接访问
}
}
/
package extend02;
import extend01.B;
//不同包中的子类
public class C extends B {
public void method() {
//super.a = 10; //编译报错,父类中private成员在不同包的子类中不可见
super.b = 20; //父类中protected修饰的成员在不同包的子类中可以直接访问
super.c = 30; //父类中public修饰的成员在不同包的子类中可以直接访问
//super.d = 40; //父类中默认访问权限修饰的成员在不同包的子类中不能直接访问
}
}
/
package extend02;
//不同包中的类
public class TestC {
public static void main(String[] args) {
C c = new C();
c.method();
//System.out.println(c.a); //编译报错,父类中private成员在不同包的其他类中不可见
//System.out.println(c.b); //父类中protected修饰的成员在不同包的其他类中不能直接访问
System.out.println(c.c); //父类中public修饰的成员在不同包的其他类中可以直接访问
//System.out.println(c.d); //父类中默认访问权限修饰的成员在不同包的其他类中不能直接访问
}
}
注:父类中private成员变量虽然在子类中不能直接访问,但是也继承到子类中了
4. 继承方式
Java中支持以下几种继承方式:
注:Java中不支持多继承一般不建议出现超过三层的继承关系,如果继承层次太多,就需要考虑对代码进行重构。如果想从语法上进行限制继承,可以使用final
关键字
5. final关键字
final关键字可以用来修饰变量、成员方法以及类
-
修饰变量或字段,表示常量(即不能修改)
final int a = 10; a = 20; //编译出错,a为常量不能修改
-
修饰类:表示此类不能被继承
final public class Animal { //... } public class Dog extends Animal { //... } //编译出错,无法对Animal进行继承
-
修饰方法:表示该方法不能被
重写
-
6. 继承与组合
组合:将一个类的实例作为另一个类的字段
继承表示对象之间是is-a的关系,比如:狗是动物,猫是动物
组合表示对象之间是has-a的关系,比如:汽车
比如汽车,它与其轮胎、发动机、方向盘、车载系统等构成的关系就是组合,因为汽车是由这些部件组成的:
代码示例:
//轮胎类
class Tire {
//...
}
//发动机类
class Engine {
//...
}
//车载系统类
class VehicleSystem {
//...
}
class Car {
private Tire tire; //可以复用轮胎中的属性和方法
private Engine engine; //可以复用发动机中的属性和方法
private VehicleSystem vs; //可以复用车载系统中的属性和方法
}
class Benz extend Car {
//将汽车中包含的:轮胎、发动机、车载系统全部继承下来
}
组合和继承都可以实现代码复用,使用哪个需要根据应用场景来选择,一般建议:能用组合尽量用组合!!