继承的概念
在我们抽象化类的时候,我们会发现某一些类会存在一些共同点,但是同时又会有一些不同点,例如下面这两个类
class Dog{
public String name;
public String color;
public void eat(){
System.out.println(name + "正在吃饭");
}
public void bark(){
System.out.println(name + "汪汪叫");
}
}
class Cat{
public String name;
public String color;
public void eat(){
System.out.println(name + "正在吃饭");
}
public void meow(){
System.out.println(name + "喵喵叫");
}
}
那么这个时候如果这样的类一多,比如上面我又来一个牛类、羊类、马类等等类似的。那么就会多出一大段描述名字、颜色和吃饭行为的代码,那么这样代码的冗余度就会比较高。那么有没有一种方式可以将这种共性提取,让这些对象共享这些相同的属性呢?
那么实际上继承就能帮我们完成这一点,它能够提取一些类的共通属性,从而降低代码的冗余度。用专业的话说,继承解决的核心问题就是:共性的抽取,实现代码复用
继承的语法
我们依旧以上面的代码为例,可以看到,上面的这个猫咪类和狗狗类其中含有非常多的共性,例如:name
,color
,eat()
,那么实际上我们就可以将这些共性提取出来,作为一个新的类,如下所示
class Animal {
public String name;
public String color;
public void eat() {
System.out.println(name + "正在吃饭");
}
}
那么这个时候共性的抽取就完成了,如何实现代码的复用呢?这个时候就要通过关键字extends
来实现,语法如下
class 类1 extends 类2 {
}
那么此时,类1
将会继承类2
的属性,简单地说,就是类2
直接拥有了类1
的属性。并且此时类2
一般被称作为父类,而类1
就被称作为子类
直接通过例子来看
我们上面已经完成了共性的抽取,创建了一个Animal
类作为父类等待被继承,那么接下来就是让刚刚的Cat
类和Dog
类作为子类来继承这个类,实现代码的复用
class Animal {
public String name;
public String color;
public void eat() {
System.out.println(name + "正在吃饭");
}
}
class Dog extends Animal{
public void bark(){
System.out.println(name + "汪汪叫");
}
}
class Cat extends Animal{
public void meow(){
System.out.println(name + "喵喵叫");
}
}
此时Dog
类和Cat
类就拥有了父类Animal
类的属性:name
、color
、eat()
那么下面的这段代码,在上面代码的基础上就是可以正常运行的
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "旺财";
dog.eat();
}
}
使用继承时应该注意:
- 子类应该拥有自己特有的成员,不然继承就失去了意义
- 虽然继承支持多代继承,但是一般情况下不推荐超过三代,这点在继承方式中也有提到
父类成员访问
无同名情况
实际上无重名的情况就可以看作是父类和子类合并了,可以直接正常的访问,那么也就没什么好多说的,我们就重点看同名的情况
同名情况
先看下面代码(为了方便理解,父类命名为Father,子类命名为Son)
class Father{
int a = 10;
}
class Son extends Father{
int a = 20;
}
public class Test{
public static void main(String[] args) {
Son test = new Son();
System.out.println(test.a);
}
}
//打印 20
我们可以看到,我们用test
访问父子类中同名变量的时候,优先访问的是子类中的变量
也就是说成员变量的访问遵循的原则是:子类优先原则,也就是先从子类中找,然后如果找不到就去父类中找,如果还找不到就会报错
那么成员方法的访问原则是否也是这样的呢?直接上代码
class Father{
public void a(){
System.out.println("父类方法");
}
}
class Son extends Father{
public void a(){
System.out.println("子类方法");
}
}
public class Test{
public static void main(String[] args) {
Son test = new Son();
test.a();
}
}
//打印 子类方法
会发现成员方法的访问原则和成员变量相同,都是子类优先原则
这里要注意,这里说的方法同名是方法签名一样,假如方法签名不同但是方法名字相同,那么就看作是重载,会优先访问符合传参要求的方法
class Father{
static int a = 10;
public void a(){
System.out.println("父类方法");
}
}
class Son extends Father{
//加了一个参数
public void a(int a){
System.out.println("子类方法");
}
}
public class Test{
public static void main(String[] args) {
Son test = new Son();
test.a();
}
}
//打印 父类方法
那么有没有办法在父子类成员同名的情况下去访问父类成员呢?当然有,就是使用super
关键字
super
关键字
在同名情况下,如果实在还想要访问父类的成员,那么就只能通过在子类的成员方法里使用super
关键字去访问父类方法。super
能够帮助我们访问直接父类中的成员,什么叫做直接父类,就是只继承了一次的两个类中,父类就被称作是子类的直接父类
class Father{
int a = 10;
}
class Son extends Father{
int a = 20;
public void test(){
System.out.println(super.a);
System.out.println(a);
}
}
public class Test{
public static void main(String[] args) {
Son son = new Son();
son.test();
}
}
//依次打印 20 10
此处在该成员方法中,没有借助super
而直接用名字访问的a
变量,实际上就可以看作是this.a
,借助this
访问成员的访问优先级也和上面的重名访问的访问优先级一致,也是子类优先原则
拓展:下面看一下在多层继承的时候super
是如何访问父类成员的
class GrandFather{
String b = "父父类的成员a";
}
class Father extends GrandFather{
String a = "父类的成员a";
}
class Son extends Father{
public void test(){
System.out.println(super.a);
System.out.println(super.b);
}
}
public class Test{
public static void main(String[] args) {
Son son = new Son();
}
}
//依次打印 父类的成员a 父父类的成员b
所以不重名的情况下,super
就可以正常访问所有父类中的成员
但是这个时候可能有人说:super
不是只能访问直接父类成员吗?你这个跨级了怎么访问的?
这里注意:实际上每一个继承关系中都会存在一个隐藏的super
去指向自己的直接父类,所以在这里并不是这个Son
中的super
直接访问了父父类GrandFather
的成员a
,而是Son
中的super
在直接父类Father
中找不到a
,于是借助了直接父类Father
中的隐藏super
去访问它的直接父类GrandFather
中的成员a
,所以从本质上来讲super
依旧是只能访问直接父类的
那下面看一下重名的情况,代码又是如何运行的
class GrandFather{
String a = "父父类的成员a";
}
class Father extends GrandFather{
String a = "父类的成员a";
}
class Son extends Father{
public void test(){
System.out.println(super.a);
}
}
public class Test{
public static void main(String[] args) {
Son son = new Son();
son.test();
}
}
//打印 父类的成员a
从上面的解释我们也可以知道,由于super
指代的是直接父类,所以如果直接父类中找得到成员就访问它,如果找不到就会调用直接父类中的super
来找,一直找不到就一直循环这个过程,直到找到最后一个父类,如果到了最后一个父类还找不到则报错。
super
的几个注意点:
super
只能在非静态方法中使用,这点和this
相同super
本质上就是一个关键字,不是父类对象的引用。和this
不同,this
代表的是当前对象的引用(可以自己打印看看,this
可以正常打印但是super
不能打印)super
只能用于访问父类成员,不可以用来访问子类成员- 平常在代码中可以通过将
this
和super
标识出来提高代码可读性 - 如果存在多层父类,
super
先从直接父类开始找成员,如果找不到就调用直接父类中的super
来找,一直找不到就一直循环这个过程,直到找到顶级父类为止(此时还找不到则报错)
再谈构造方法
先看一段代码
class Father{
public Father(){
System.out.println("父类构造方法");
}
}
class Son extends Father{
public Son(){
System.out.println("子类构造方法");
}
}
public class Test{
public static void main(String[] args) {
Son son = new Son();
}
}
//依次打印 父类构造方法 子类构造方法
会发现父类的构造方法优先于子类运行
那么接下来我们再看一段代码,来再更加细致的看一下两个构造方法的运行次序
class Father{
A a = new A();
public Father(){
System.out.println("父类构造方法");
}
}
class Son extends Father{
B b = new B();
public Son(){
System.out.println("子类构造方法");
}
}
public class Test{
public static void main(String[] args) {
Son son = new Son();
}
}
class A{
public A(){
System.out.println("测试运行次序1");
}
}
class B{
public B(){
System.out.println("测试运行次序2");
}
}
//依次打印 测试运行次序1 父类构造方法 测试运行次序2 子类构造方法
可以看到,父类一定是先构造完才会构造子类的,那么理由实际上也很简单:由子类创建的对象,是继承着父类的属性的,所以在创建的时候一定会先构造父类的成员,然后再进行子类的构造
假如说我的父类构造方法有参数的话,那就没办法让它自行执行了,那么我们要怎么传参让它执行呢?(接下来这部分请注意括号内部是否有省略号)
这个时候就可以联想起来我们之前使用this(...)
来在无参数的构造方法中调用有参数的构造方法,那么再联系上面的知识,很容易就可以想到实际上可以通过super(...)
来调用父类含参构造方法
class Father{
public Father(int x){
System.out.println("含参的父类构造方法");
}
}
class Son extends Father{
public Son(){
super(10);
System.out.println("子类构造方法");
}
}
public class Test{
public static void main(String[] args) {
Son son = new Son();
}
}
那么这个super(...)
的书写方法和this(...)
也是一样的,都需要在第一行写。同时这也说明了两件事:
- 倘若我们的子类构造方法中要调用含参的父类构造方法,那么这个子类构造方法就无法调用其他的含参子类构造方法了,因为不可能
this(...)
和super(....)
都位于第一行 super(...)
只能在子类构造方法中出现一次,因为一定要在第一行
实际上,如果你在子类构造方法第一行不写super(...)
,编译器就会给你写一个隐藏的无参数super()
在子类构造方法的开头位置,并且这个隐藏的super()
不占据行数,即使你写了this(...)
在第一行的位置上也不会报错
并且还有一个注意点:位于子类构造方法内部的super()/super(...)
不影响构造的顺序,只会影响调用构造方法的类型,一定是父类先构造,不要以为super()/super(...)
在子类构造方法里就是要开始运行子类构造方法才开始构造父类
假如我们只定义了一个含参父类构造方法,那么就说明我们一定要去用super()
传参,否则就无法构造一个带有继承关系的对象。也就是说,假如我们只定义了一个含参父类构造方法,那么我们就一定要写出一个子类构造方法去传参,不能只通过编译器帮你写的隐藏构造方法了
class Father{
public Father(int x){
System.out.println("含参的父类构造方法");
}
}
class Son extends Father{
//会报错,因为隐藏的super()没有传参数
}
public class Test{
public static void main(String[] args) {
Son son = new Son();
}
}
this
和super
的异同点
上面讨论了有关super
关键字的内容,会发现它和this
有一些相似之处,但同时也有一些不同,因此这里梳理一下它们之间的异同点
相同点
- 都是Java中的关键字
- 都只能在类的非静态方法中使用,用来访问非静态的成员方法和成员变量
- 在构造方法中显性调用时,必须是构造方法中的第一条语句
不同点
this
代表的是当前调用成员方法对象的引用,但是super
就只是一个用于访问父类成员的关键字,不代表任何成员的引用this
在继承中可以用于访问子类和父类的所有成员,并且优先指代子类,但是super
只能用于访问父类成员- 构造方法中一定会有
super()
,没有写编译器也会给你写一个隐藏的无参数super()
,但是this()
不写就没有 - 构造方法中,
super()
用于调用父类构造方法,this()
用于调用子类构造方法
再谈初始化
之前我们讲过了没有继承关系下各个代码块以及构造方法的运行顺序,那么倘若是在继承关系下
这些代码块以及构造方法的运行顺序又是怎么样的呢?我们先上代码
class Father {
static {
System.out.println("父类静态代码块");
}
{
System.out.println("父类实例代码块");
}
public Father(){
System.out.println("父类构造方法");
}
}
class Son extends Father{
static {
System.out.println("子类静态代码块");
}
{
System.out.println("子类实例代码块");
}
public Son(){
System.out.println("子类构造方法");
}
}
public class Test{
public static void main(String[] args) {
Son son = new Son();
}
}
//依次输出 父类静态代码块 子类静态代码块 父类实例代码块 父类构造方法 子类实例代码块 子类构造方法
那么就可以看出,静态代码块的优先级是高于其他代码块和构造方法的,并且是一个独立的父子优先级
其他的优先根据父子优先级运行,而父子类内部的优先级则为实例代码优先运行,构造方法是最后运行
并且注意,静态代码块和无继承关系的时候一样,只会在第一次创建对象的时候执行一次,后面不再执行
protected
关键字
在类和对象章节,我们谈及了封装的概念,并且提到了访问限定符的概念,那么我们先重新看一下这些限定符的限定范围
范围 | private | default | protected | public |
---|---|---|---|---|
同一个包中的同一个类 | 可访问 | 可访问 | 可访问 | 可访问 |
同一个包中的不同类 | x | 可访问 | 可访问 | 可访问 |
不同包中的子类 | x | x | 可访问 | 可访问 |
不同包中的非子类 | x | x | x | 可访问 |
注意:其中的default
实际上就是不加任何修饰符
那个时候我们跳过了protected
这个限定符,因为不知道子类的概念。现在我们学习了继承,就可以来重新理解以下protected
这个限定符的限定范围了
package Package1;
public class Test1 {
private int a = 0;
int b = 0;
protected int c = 0;
public int d = 0;
}
//========================================================
package Package2;
import Package1.Test1;
class Test2 extends Test1 {
public Test2(){
Test1 test = new Test1();
test.a = 0;
test.b = 0;
test.c = 0;
//上面三个代码全都报错
test.d = 0;
}
}
在三个报错代码中,前面两个test.a
和test.b
很明显是由于访问不到从而导致的报错。但是为什么test.c = 0
这段代码也会报错呢?此时Test2
也确实是Test1
的子类,难道还有别的问题?
答案是:确实在访问方式上有一些问题,实际上倘若我们要想访问由protected
修饰的成员,那么就必须通过super
关键字来访问。这也是为什么用protected
修饰的成员如果没有继承关系无法访问的原因,因为没有继承关系就没有指向父类的super
,那么也就无法访问到了
那么上面代码如果想要访问到c
,则正确写法应该是
package Package2;
import Package1.Test1;
class Test2 extends Test1 {
public Test2(){
Test1 test = new Test1();
super.c = 0;
}
}
关于访问限定符的使用方法,如果是简单粗暴的做法那就为:成员变量为private
,成员方法为public
但是还是应该在写代码的时候认真思考:这个成员的调用者是谁?并以此为基础来赋予访问操作限定符
继承方式
Java中只支持三种继承方式:单继承、多层继承、不同类继承一个类,但是不支持多继承
虽然继承允许一直无限制的多层继承下去,但是在实际运用中不推荐使用超过三层的继承关系,如果遇到要考虑是否应该重构代码
如果想要让某一个类无法被继续继承下去,可以使用final
关键字
final
关键字
当我们不想让一个类被继承的时候,在前面加上final
关键字即可
使用例
final class Test1{
}
class Test extends Test1{
//报错 显示final类无法继承
}
实际上final
并不仅仅可以用来修饰类,还可以用来修饰方法和变量
修饰变量时,代表这个值为常量不可修改
public class Test {
public static void main(String[] args) {
//常量的命名推荐全大写
final int A = 0;
A = 10;
//报错 final修饰的量不能修改
}
}
那么我们看一下下面的这段代码
public class Test {
public static void main(String[] args) {
final int[] arr = new int[10];
//那么请问下面两段代码哪一个有问题
arr[] = new int[5];//1
arr[0] = 0;//2
}
}
如果我们当时对于引用类型及引用变量有了一定理解,那么这里很容易就能看出1号代码是有问题的
为什么?
在一号代码中,因为我们用final
修饰的变量是arr
,而arr
是一个引用变量,存储的是引用,那么我们用final
修饰了这个引用变量后,就不能改变里面存储的引用了,但是这里又尝试将一个新的数组的引用给arr
,那就肯定是有问题的
但是在二号代码中,由于final
并没有修饰我们的数组,所以数组里面的值你可以随便改,那么2号代码就是没有问题的
关于final
修饰方法,我们这里暂时知道用final
修饰的方法无法重写,有关重写的知识在多态中讲解
组合
组合和继承一样,都是用来表示类之间的关系的,但是组合表示的是包含的关系。
组合也能够实现代码的复用,但是组合并没有什么特殊的语法,那么要怎么才能实现组合呢?
我们通过一个例子来说明
class GPU{
}
class CPU{
}
class Computer{
private GPU gpu;
private CPU cpu;
}
上面写了一个电脑类,而电脑应该是包含GPU
和CPU
的,所以运用组合来表达这三个类之间的关系
而组合实际上就是在大类中创建小类类型的变量,那么就可以通过这个变量来访问小类里面的成员,从而实现代码的复用。以这个例子来说,我们在Computer
中,创建了CPU
和GPU
类型的变量gpu
和cpu
,那么之后我们就可以通过gpu
和cpu
来访问GPU
和CPU
里面的成员了
简单的说,继承描述的关系是,什么是什么(例如,猫是动物,狗是动物)。而组合描述的关系是,什么有什么(例如电脑有CPU、GPU,汽车有轮子、发动机)
在实际使用中,要使用继承还是组合是要根据实际情况来选择的,但是一般推荐能用组合就用组合