抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

14.接口与继承

14.1 接口

14.1.1 接口

接口的创建:(与创建类相似)

1
2
3
4
5
6
7
//创建一个播放器接口
public interface Player{
public void play();
public void pause();
public void stop();
public void tune(); //接口只有方法声明,没有主体
}

Java接口是一系列方法的声明(没有主体),是一些方法特征的集合;

一个接口只有方法的特征没有方法的实现(不能使用 new来创建实例),因此这些方法可以在不同的地方被不同的类实现(继承),进而在不同的类中实现不同的功能。

接口就是一组抽象方法和常量值的集合。可以把接口看成是一种特殊的抽象类。

(1)其所有的方法都必须是抽象的(abstract)。

(2)其属性成员(若有)只能是final static的常量。

14.1.2 实现接口

实现一个接口与类的继承相似,用 implements 来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建一个MP3类,实现播放器功能
public MP3 implements Player{
public void play(){
System.out.println("播放");
}

public void pause(){
System.out.println("暂停");
}

public void stop(){
System.out.println("停止");
}

public void tune(){
System.out.println("调节音量");
}
}

14.2 对象转型

14.2.1 明确引用类型与对象类型的概念

引用和对象都是有类型的

1
Hero h = new Hero();

在这个例子,引用是 h,对象是 new Hero(),它们的类型均为 Hero

通常情况引用类型与对象类型是一样的,但也有不一样的时候

14.2.2 子类转父类(向上转型)

所谓的转型,是指当引用类型对象类型不一致的时候,才需要进行类型转换;
类型转换有时候会成功,有时候会失败

假如 ADHeroHero 的子类

1
2
3
Hero h = new Hero();
ADHero ad = new ADHero();

用图来表示其关系如下:

image-20210126224549229
1
h = ad;                      //向上转型

转型后的图:

image-20210126224655825

可以看到 h 经过 ad 最终指向的还是 Hero

因此,把 h 指向 ADHero 一定可以,因为 ADHero 继承了 Hero,Hero能实现的功能 ADHero 也可以

14.2.3 父类转子类(向下转型)

父类转子类,有的时候行,有的时候不行,所以必须进行强制转换。
强制转换的意思就是:转换有风险,风险自担。

假如 ADHeroSupportHero 的两个不同子类:

1
2
3
Hero h =new Hero();
ADHero ad = new ADHero();
Support s = new Support();
1
h = s;            // 向上转型,一定可以

向上转型一定可以,因为 h通过 s 最终还是指向 Hero

image-20210126225703371
1
ad = (ADHero)h;    //向下转型,不一定可以,需要强制转换

虽然 ad 指向的 ADHeroHero 的子类,眨眼看来 ad 最终也是指向 Hero 的,但实际上指向的是 ADHero,ADHero与Hero还是有区别的,ADHero有的方法Hero不一定有

而ad通过h最终指向的是 Hero,而不是 ADHero,因此会有风险(指向ADHero才不会有风险),只能强制转换。

image-20210126230408061

因为子类拥有的方法父类不一定有,因此 h 向下转型后,ad可能就没有了一些子类的方法

14.2.4 没有继承关系的两个类

没有继承关系的两个类,互相转换,一定会失败

14.2.5实现类转换成接口(向上转型)

假如 AD 是一个接口,ADHero 类继承了它

1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) {
ADHero ad = new ADHero();

AD adi = ad; //向上转型
}
}

14.2.6 转型练习

如下转换能否成功?如果不能,是哪一行会出错?为什么会出错?

1
2
3
4
5
6
7
8
9
10
11
public class Hero {
public String name;
protected float hp;

public static void main(String[] args) {
ADHero ad = new ADHero();
Hero h = ad;
AD adi = (AD) h;
APHero ap = (APHero) adi;
}
}
分析
第7行向上转型没问题
第8行,可以将h强制转换为AD,因为 AD 与 Hero 之间通过子类关联在了一起;因此 h 指向了AD,而通过 h -> ad -> ADHero -> AD,最终也是指向AD,虽有风险,但不会报错;
第9行,不能将 adi 强制转换为 APHero,因为 AD 与 APHero之间没有关联,因此ap本应该指向 APHero,但通过转换 ap -> adi -> AD,即最终指向了AD,因此不能进行转换,会报错
image-20210126234924701

14.2.7 instanceof 语句

instanceof Hero 判断一个引用所指向的对象,是否是以下两种,返回 true 或 false

  • Hero类型
  • Hero的子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package charactor;

public class Hero {
public String name;
protected float hp;

public static void main(String[] args) {
ADHero ad = new ADHero();
APHero ap = new APHero();

Hero h1= ad;
Hero h2= ap;

//判断引用h1指向的对象,是否是ADHero类型
System.out.println(h1 instanceof ADHero); //true

//判断引用h2指向的对象,是否是APHero类型
System.out.println(h2 instanceof APHero); //true

//判断引用h1指向的对象,是否是Hero的子类型
System.out.println(h1 instanceof Hero); //true
}
}

14.3 重写

子类可以继承父类的对象方法

在继承后,重复提供该方法,就叫做方法的重写

又叫覆盖 override

父类Item有一个方法,叫做effect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package property;

public class Item {
String name;
int price;

public void buy(){
System.out.println("购买");
}
public void effect() {
System.out.println("物品使用后,可以有效果");
}

}

子类LifePotion继承Item,同时也提供了方法effect

1
2
3
4
5
6
7
8
9
package property;

public class LifePotion extends Item{

public void effect(){ //重写effect()方法
System.out.println("血瓶使用后,可以回血");
}

}

调用重写的方法
调用就会执行重写的方法,而不是从父类的方法

重写的优点:

如果没有重写这样的机制,也就是说LifePotion这个类,一旦继承了Item,所有方法都不能修改了。

但是LifePotion又希望提供一点不同的功能,为了达到这个目的,只能放弃继承Item,重新编写所有的属性和方法,然后在编写effect的时候,做一点小改动.

这样就增加了开发时间和维护成本

14.4 多态

14.4.1操作符的多态

同一个操作符在不同情境下,具备不同的作用:

  • 如果+号两侧都是整型,那么+代表 数字相加
  • 如果+号两侧,任意一个是字符串,那么+代表字符串连接

14.4.2 类的多态

多态: 都是同一个类型,调用同一个方法,却能呈现不同的状态(实际是子类重写了父类的方法,导致子类的方法与父类的方法不同,子类之间的方法也不同)

类的多态即父类引用指向不同的子类对象,调用同一个方法时出现不同的效果

假设 Hero是 ADHeroAPHero 的父类

1
2
Hero h1 = new ADHero();
Hero h2 = new APHero();

类的多态的条件:

  1. 父类(接口)引用指向子类对象

  2. 调用的方法有[重写](#14.3 重写)

14.4.3 使用类多态的好处

如果不使用多态,例如:

假设英雄要使用血瓶和魔瓶,就需要为Hero设计两个方法
useLifePotion、useMagicPotion;
除了血瓶和魔瓶还有很多种物品,那么就需要设计很多很多个方法,比如
usePurityPotion、useGuard、useInvisiblePotion等等等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package charactor;

import property.LifePotion;
import property.MagicPotion;

public class Hero {
public String name;
protected float hp;

public void useLifePotion(LifePotion lp){
lp.effect();
}
public void useMagicPotion(MagicPotion mp){
mp.effect();
}

public static void main(String[] args) {

Hero garen = new Hero();
garen.name = "盖伦";

LifePotion lp =new LifePotion();
MagicPotion mp =new MagicPotion();

garen.useLifePotion(lp);
garen.useMagicPotion(mp);

}

}

这个时候采用多态来解决这个问题:

设计一个方法叫做useItem,其参数类型是Item

  • 如果是使用血瓶,调用该方法
  • 如果是使用魔瓶,还是调用该方法
  • 无论英雄要使用什么样的物品,只需要一个方法 即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package charactor;

import property.Item;
import property.LifePotion;
import property.MagicPotion;

public class Hero {
public String name;
protected float hp;

public void useItem(Item i){ //设计一个方法叫做useItem,其参数类型是Item
i.effect();
}

public static void main(String[] args) {

Hero garen = new Hero();
garen.name = "盖伦";

LifePotion lp =new LifePotion();
MagicPotion mp =new MagicPotion();

garen.useItem(lp); //使用血瓶lp
garen.useItem(mp); //使用魔瓶mp

}

}

14.5 隐藏

与重写类似:

  • 重写,是子类覆盖父类的对象方法
  • 隐藏,就是子类覆盖父类的类方法

如何隐藏呢:

父类有一个类方法 :battleWin

1
2
3
4
5
6
7
8
9
10
11
package charactor;

public class Hero {
public String name;
protected float hp;

public static void battleWin(){ //类方法,静态方法
System.out.println("hero battle win");
}

}

创建一个子类,隐藏父类方法 battleWin()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package charactor;

public class ADHero extends Hero implements AD{

//隐藏父类的battleWin方法
public static void battleWin(){
System.out.println("ad hero battle win");
}

public static void main(String[] args) {
Hero.battleWin(); // 调用父类的方法
ADHero.battleWin();//调用子类的方法
}

}

问:对于Hero h =new ADHero();

h是父类类型的引用,但是指向一个子类对象h.battleWin(); 会调用父类的方法?还是子类的方法?

答:当父类的引用指向一个子类对象时,执行的:

  • 对象方法是子类的对象方法(因为重写)
  • 类方法是父类的类方法(类方法不能被重写)

14.6 super关键字

super()用于子类的构造函数内部;

特点:

  1. 实例化一个父类的时候,父类的构造方法会被自动调用(根据实例化方式来选择调用有参或无参的构造方法);

  2. 实例化一个子类的时候,若没有写 super()语句,会默认调用父类无参构造方法。

  3. 并且,父类和子类的构造方法都会被调用,且父类的构造方法被调用

1
2
3
4
5
6
7
package superKeyWord;
//父类:
public class Hero{
public Hero(){
System.out.println("调用父类的构造方法");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package superKeyWord;

//子类
public class SuperTest extends Hero {
public SuperTest(){
System.out.println("调用子类构造方法");
}

public static void main(String[] args){
new Hero(); //创建父类对象
System.out.println("---------------");
new SuperTest(); //创建子类对象
}

}

/*输出:
调用父类的构造方法
---------------
调用父类的构造方法
调用子类构造方法
*/

14.6.1 super调用父类带参构造方法

super() 相当于一个父类的对象,在子类中使用就类似于创建了一个父类的对象,会调用父类带参的构造方法

superthis 类似,this是当前类的对象,super是父类的对象,两者都只能在方法内部使用

1
2
3
4
5
6
7
8
9
10
package superKeyWord;

//父类:
public class Hero{
public Hero(String name){ //带参的构造方法

System.out.println("调用父类的构造方法"+name);

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package superKeyWord;

public class SuperTest extends Hero {

// super("h2"); //super只能在方法内部使用

public SuperTest(){

super("h2"); //正确,super在方法内使用

System.out.println("调用子类构造方法");
}

public static void main(String[] args){
new Hero("h1");
System.out.println("---------------");
new SuperTest();
}

}

/*输出:
调用父类的构造方法h1
---------------
调用父类的构造方法h2
调用子类构造方法
*/

14.6.2 super调用父类属性

1
2
3
4
5
6
7
8
//父类
package superKeyWord;

public class Hero{

int moveSpeed = 100;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//子类

package superKeyWord;

public class SuperTest extends Hero {

int moveSpeed = 200;

public void getMoveSpeed1(){
System.out.println(super.moveSpeed); //打印父类moveSpeed
}

public void getMoveSpeed2() {
System.out.println(this.moveSpeed); //打印子类moveSpeed
}

public static void main(String[] args){

SuperTest h = new SuperTest();
h.getMoveSpeed1(); //输出:100
h.getMoveSpeed2(); //输出:200
}

}

14.6.3 super调用父类方法

当子类重写了父类的方法后,super调用的依然是父类原本的方法

14.7 Object类

Object类是所有类的父类,即声明一个类的时候,默认就继承了Object

14.7.1 Object提供的一些方法

  • toString():

    返回当前对象的字符串表达
    通过 System.out.println()打印对象就是打印该对象的toString()返回值

    1
    2
    3
    4
    5
    6
    Hero h = new Hero();

    //下面两行等效
    System.out.println(h);
    System.out.println(h.toString());

  • finalize():

    当一个对象没有任何引用指向的时候,它就满足垃圾回收的条件

    当它被垃圾回收的时候,它的finalize() 方法就会被调用。

    finalize() 不是开发人员主动调用的方法,而是由虚拟机JVM调用

    1
    2
    3
    4
    Hero h;

    h = new Hero();
    h = new Hero();

    执行第四行的时候,第三行的 Hero() 对象没了引用指向,就满足垃圾回收条件

  • equals():

    equals() 用于判断两个对象的内容是否相同,假设,当两个英雄的hp相同的时候,我们就认为这两个英雄相同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package charactor;

    public class Hero {
    public String name;
    protected float hp;

    public boolean equals(Object o){
    if(o instanceof Hero){
    Hero h = (Hero) o;
    return this.hp == h.hp;
    }
    return false;
    }

    public static void main(String[] args) {
    Hero h1= new Hero();
    h1.hp = 300;
    Hero h2= new Hero();
    h2.hp = 400;
    Hero h3= new Hero();
    h3.hp = 300;

    System.out.println(h1.equals(h2));
    System.out.println(h1.equals(h3));
    }
    }
  • ==

    这不是Object的方法,但是用于判断两个对象是否相同,
    更准确的讲,用于判断两个引用,是否指向了同一个对象

  • hashCode():

    返回对象的哈希值

  • 线程同步方法

    1
    2
    3
    wait()
    notify()
    notifyAll()
  • getClass:

    会返回一个对象的类对象

14.8 final修饰

final修饰类,方法,基本类型变量,引用的时候分别有不同的意思。

  • 修饰类:表示该类不能被继承

    1
    2
    3
    public final class Hero{   //该类不能被继承

    }
  • 修饰方法:表示该方法不能被重写

    1
    2
    3
    public final void useItem(){

    }
  • 修饰基本变量:表示该变量只能被赋值一次

    1
    2
    3
    final int a;
    a = 1;
    a = 2; //报错
  • 修饰引用:表示该引用只有一次指向对象的机会

    1
    2
    3
    final Hero h;
    h = new Hero();
    h = new Hero(); //报错

可以用 final 修饰变量使其作为常量使用

常量指的是可以公开,直接访问,不会变化的值

14.9 抽象类

抽象类

在类中声明一个方法,这个方法没有实现体,是一个“空”方法

这样的方法就叫抽象方法,使用修饰符abstract

注意:

  • 当一个类有抽象方法的时候,该类必须被声明为抽象类

  • 子类继承抽象类后,必须重写抽象方法(如果有的话),赋予其具体功能

  • 抽象类可以没有抽象方法,可以有实体方法,但是有抽象方法时必须声明为抽象类

  • 抽象类不能直接进行实例化,即不能创建抽象类的对象

1
2
3
4
5
6
7
8
9
10
11
12
package charactor;

public abstract class Hero { //抽象类

String name;
float hp;
float armor;
int moveSpeed;

public abstract void attack();// 抽象方法attack

}

抽象类与接口区别

  1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
  2. 接口中除了 staticfinal 变量,不能有其他变量,而抽象类中则不一定。
  3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过 extends 关键字扩展多个接口。
  4. 接口方法默认修饰符是 public,抽象方法可以有 publicprotecteddefault 这些修饰符(抽象方法就是为了被重写所以不能使用 private 关键字修饰!)。
  5. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
抽象类 接口
抽象类也是类,子类只能继承一个类 子类可以实现多个接口
可以是public,protected,package,private 只能是public
静态、非静态 静态
final或非final的属性 final的

另外,抽象类和接口都可以有实体方法。 接口中的实体方法,叫做[默认方法](#14.11 默认方法)

14.10 内部类

内部类分为四种:

  • 非静态内部类

    非静态内部类,只有一个外部类对象存在的时候,才有意义。也就是说,要调用非静态内部类的属性和方法,必须要先创建一个外部类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Hero{
    String name;

    class BattleScore{ //内部类
    int score;
    public void kill(){

    }
    }
    }

    调用内部类的途径:

    1
    2
    Hero h = new Hero();  //先创建外部类
    BattleScore bs = h.new BattleScore();
  • 静态内部类

    在一个类里面声明一个静态内部类,静态内部类的实例化不需要一个外部类的实例为基础,可以直接实例化。

    另外,静态内部类不能直接访问外部类的对象属性

    语法:

    1
    new 外部类.静态内部类();

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Hero{
    String name;

    static class BattleScore{ //静态内部类
    int score;
    public void kill(){

    //静态内部类不能直接访问外部类的对象属性
    System.out.println(name + "kille a Hero");//报错
    }
    }
    }

    实例化:

    1
    Hero.BattleScore bs = new Hero.BattleScore();
  • 匿名类

    匿名类指的是在创建某个类的对象的同时实例化它,使代码更加简洁精练
    通常情况下,要使用一个接口或者抽象类,都必须创建一个子类,为了快速使用,直接实例化一个抽象类,并“当场”实现其抽象方法。
    既然实现了抽象方法,那么就是一个新的类,只是这个类,没有命名。这样的类,叫做匿名类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package charactor;

    public abstract class Hero {
    String name;
    float hp;
    float armor;
    int moveSpeed;

    public abstract void attack(); //抽象方法

    public static void main(String[] args) {

    Hero h = new Hero(){
    //当场实现attack方法
    public void attack() {
    System.out.println("新的进攻手段");
    }
    };
    h.attack();
    //通过打印h,可以看到h这个对象属于Hero$1这么一个系统自动分配的类名

    System.out.println(h);
    }

    }

    注意:在匿名类中使用外部的局部变量,外部的局部变量必须修饰为final,但在jdk8中,已经不需要强制修饰成final了,因为编译器偷偷的帮你加上了看不见的final

  • 本地类

    本地类可以理解为有名字的匿名类

    • 内部类与匿名类不一样的是,内部类必须声明在成员的位置,即与属性和方法平等的位置。
    • 本地类和匿名类一样,直接声明在代码块里面,可以是主方法,for循环里等等地方
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package charactor;

    public abstract class Hero {
    String name;
    float hp;
    float armor;
    int moveSpeed;

    public abstract void attack();

    public static void main(String[] args) {

    //在主方法里声明本地类
    //与匿名类的区别在于,本地类有了自定义的类名
    class SomeHero extends Hero{
    public void attack() {
    System.out.println( name+ " 新的进攻手段");
    }
    }

    SomeHero h =new SomeHero();
    h.name ="地卜师";
    h.attack();
    }

    }

14.11 默认方法

默认方法是JDK8新特性,指的是接口也可以提供具体方法了,而不像以前,只能提供抽象方法

例如:

1
2
3
4
5
6
7
8
public interface Mortal {
public void die(); //抽象方法

default public void revive() { //具体方法

System.out.println("本英雄复活了");
}
}

为什么会有默认方法?

假设没有默认方法这种机制,那么如果要为Mortal增加一个新的方法revive,那么所有实现了Mortal接口的子类,都需要做改动(都要在子类中考虑重写revive的具体方法)。

但是引入了默认方法后,原来的类,不需要做任何改动,并且还能得到这个默认方法

通过这种手段,就能够很好的扩展新的类,并且做到不影响原来的类

评论