14.接口与继承
14.1 接口
14.1.1 接口
接口的创建:(与创建类相似)
1 | //创建一个播放器接口 |
Java接口是一系列方法的声明(没有主体),是一些方法特征的集合;
一个接口只有方法的特征没有方法的实现(不能使用 new来创建实例),因此这些方法可以在不同的地方被不同的类实现(继承),进而在不同的类中实现不同的功能。
接口就是一组抽象方法和常量值的集合。可以把接口看成是一种特殊的抽象类。
(1)其所有的方法都必须是抽象的(abstract)。
(2)其属性成员(若有)只能是final static的常量。
14.1.2 实现接口
实现一个接口与类的继承相似,用 implements
来实现
1 | // 创建一个MP3类,实现播放器功能 |
14.2 对象转型
14.2.1 明确引用类型与对象类型的概念
引用和对象都是有类型的
1 | Hero h = new Hero(); |
在这个例子,引用是 h
,对象是 new Hero()
,它们的类型均为 Hero
通常情况引用类型与对象类型是一样的,但也有不一样的时候
14.2.2 子类转父类(向上转型)
所谓的转型,是指当引用类型和对象类型不一致的时候,才需要进行类型转换;
类型转换有时候会成功,有时候会失败
假如 ADHero
是 Hero
的子类
1 | Hero h = new Hero(); |
用图来表示其关系如下:
1 | h = ad; //向上转型 |
转型后的图:
可以看到 h
经过 ad
最终指向的还是 Hero
因此,把 h
指向 ADHero
一定可以,因为 ADHero
继承了 Hero
,Hero能实现的功能 ADHero 也可以
14.2.3 父类转子类(向下转型)
父类转子类,有的时候行,有的时候不行,所以必须进行强制转换。
强制转换的意思就是:转换有风险,风险自担。
假如 ADHero
和 Support
是 Hero
的两个不同子类:
1 | Hero h =new Hero(); |
1 | h = s; // 向上转型,一定可以 |
向上转型一定可以,因为 h
通过 s
最终还是指向 Hero
1 | ad = (ADHero)h; //向下转型,不一定可以,需要强制转换 |
虽然 ad
指向的 ADHero
是 Hero
的子类,眨眼看来 ad
最终也是指向 Hero 的,但实际上指向的是 ADHero,ADHero与Hero还是有区别的,ADHero有的方法Hero不一定有
而ad通过h最终指向的是 Hero,而不是 ADHero,因此会有风险(指向ADHero才不会有风险),只能强制转换。
因为子类拥有的方法父类不一定有,因此 h
向下转型后,ad
可能就没有了一些子类的方法
14.2.4 没有继承关系的两个类
没有继承关系的两个类,互相转换,一定会失败
14.2.5实现类转换成接口(向上转型)
假如 AD
是一个接口,ADHero
类继承了它
1 | public class Test { |
14.2.6 转型练习
如下转换能否成功?如果不能,是哪一行会出错?为什么会出错?
1 | public class Hero { |
分析 |
---|
第7行向上转型没问题 |
第8行,可以将h强制转换为AD,因为 AD 与 Hero 之间通过子类关联在了一起;因此 h 指向了AD,而通过 h -> ad -> ADHero -> AD,最终也是指向AD,虽有风险,但不会报错; |
第9行,不能将 adi 强制转换为 APHero,因为 AD 与 APHero之间没有关联,因此ap本应该指向 APHero ,但通过转换 ap -> adi -> AD,即最终指向了AD,因此不能进行转换,会报错 |
14.2.7 instanceof 语句
instanceof Hero
判断一个引用所指向的对象,是否是以下两种,返回 true 或 false
- Hero类型
- Hero的子类
1 | package charactor; |
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是
ADHero
与APHero
的父类
1
2 Hero h1 = new ADHero();
Hero h2 = new APHero();
类的多态的条件:
-
父类(接口)引用指向子类对象
-
调用的方法有[重写](#14.3 重写)
14.4.3 使用类多态的好处
如果不使用多态,例如:
假设英雄要使用血瓶和魔瓶,就需要为Hero设计两个方法
useLifePotion、useMagicPotion;
除了血瓶和魔瓶还有很多种物品,那么就需要设计很多很多个方法,比如
usePurityPotion、useGuard、useInvisiblePotion等等等等
1 | package charactor; |
这个时候采用多态来解决这个问题:
设计一个方法叫做useItem,其参数类型是Item
- 如果是使用血瓶,调用该方法
- 如果是使用魔瓶,还是调用该方法
- 无论英雄要使用什么样的物品,只需要一个方法 即可
1 | package charactor; |
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()
用于子类的构造函数内部;
特点:
-
实例化一个父类的时候,父类的构造方法会被自动调用(根据实例化方式来选择调用有参或无参的构造方法);
-
实例化一个子类的时候,若没有写
super()
语句,会默认调用父类无参构造方法。 -
并且,父类和子类的构造方法都会被调用,且父类的构造方法先被调用
1 | package superKeyWord; |
1 | package superKeyWord; |
14.6.1 super调用父类带参构造方法
super()
相当于一个父类的对象,在子类中使用就类似于创建了一个父类的对象,会调用父类带参的构造方法
super
与 this
类似,this是当前类的对象,super是父类的对象,两者都只能在方法内部使用
1 | package superKeyWord; |
1 | package superKeyWord; |
14.6.2 super调用父类属性
1 | //父类 |
1 | //子类 |
14.6.3 super调用父类方法
当子类重写了父类的方法后,super调用的依然是父类原本的方法
14.7 Object类
Object类是所有类的父类,即声明一个类的时候,默认就继承了Object
14.7.1 Object提供的一些方法
-
toString():
返回当前对象的字符串表达
通过 System.out.println()打印对象就是打印该对象的toString()返回值1
2
3
4
5
6Hero h = new Hero();
//下面两行等效
System.out.println(h);
System.out.println(h.toString());
-
finalize():
当一个对象没有任何引用指向的时候,它就满足垃圾回收的条件
当它被垃圾回收的时候,它的finalize() 方法就会被调用。
finalize() 不是开发人员主动调用的方法,而是由虚拟机JVM调用
1
2
3
4Hero 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
26package 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
3wait()
notify()
notifyAll() -
getClass:
会返回一个对象的类对象
14.8 final修饰
final修饰类,方法,基本类型变量,引用的时候分别有不同的意思。
-
修饰类:表示该类不能被继承
1
2
3public final class Hero{ //该类不能被继承
} -
修饰方法:表示该方法不能被重写
1
2
3public final void useItem(){
} -
修饰基本变量:表示该变量只能被赋值一次
1
2
3final int a;
a = 1;
a = 2; //报错 -
修饰引用:表示该引用只有一次指向对象的机会
1
2
3final Hero h;
h = new Hero();
h = new Hero(); //报错
可以用
final
修饰变量使其作为常量使用常量指的是可以公开,直接访问,不会变化的值
14.9 抽象类
抽象类
在类中声明一个方法,这个方法没有实现体,是一个“空”方法
这样的方法就叫抽象方法,使用修饰符abstract
注意:
当一个类有抽象方法的时候,该类必须被声明为抽象类
子类继承抽象类后,必须重写抽象方法(如果有的话),赋予其具体功能
抽象类可以没有抽象方法,可以有实体方法,但是有抽象方法时必须声明为抽象类
抽象类不能直接进行实例化,即不能创建抽象类的对象
1 | package charactor; |
抽象类与接口区别
- 接口的方法默认是
public
,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。 - 接口中除了
static
、final
变量,不能有其他变量,而抽象类中则不一定。 - 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过
extends
关键字扩展多个接口。 - 接口方法默认修饰符是
public
,抽象方法可以有public
、protected
和default
这些修饰符(抽象方法就是为了被重写所以不能使用private
关键字修饰!)。 - 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
抽象类 | 接口 |
---|---|
抽象类也是类,子类只能继承一个类 | 子类可以实现多个接口 |
可以是public,protected,package,private | 只能是public |
静态、非静态 | 静态 |
final或非final的属性 | final的 |
另外,抽象类和接口都可以有实体方法。 接口中的实体方法,叫做[默认方法](#14.11 默认方法)
14.10 内部类
内部类分为四种:
-
非静态内部类
非静态内部类,只有一个外部类对象存在的时候,才有意义。也就是说,要调用非静态内部类的属性和方法,必须要先创建一个外部类
1
2
3
4
5
6
7
8
9
10public class Hero{
String name;
class BattleScore{ //内部类
int score;
public void kill(){
}
}
}调用内部类的途径:
1
2Hero h = new Hero(); //先创建外部类
BattleScore bs = h.new BattleScore(); -
静态内部类
在一个类里面声明一个静态内部类,静态内部类的实例化不需要一个外部类的实例为基础,可以直接实例化。
另外,静态内部类不能直接访问外部类的对象属性
语法:
1
new 外部类.静态内部类();
例:
1
2
3
4
5
6
7
8
9
10
11
12public 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
25package 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
26package 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 | public interface Mortal { |
为什么会有默认方法?
假设没有默认方法这种机制,那么如果要为Mortal增加一个新的方法revive,那么所有实现了Mortal接口的子类,都需要做改动(都要在子类中考虑重写revive的具体方法)。
但是引入了默认方法后,原来的类,不需要做任何改动,并且还能得到这个默认方法
通过这种手段,就能够很好的扩展新的类,并且做到不影响原来的类