继承和多态
在继承的关系中,“子类就是一个父类”。也就是说,子类可以被当做父类看待。
例如父类是员工,子类是讲师,那么“讲师就是一个员工”。关系:is-a。
实现继承的格式:
定义父类的格式:(一个普通的类定义)
public class 父类名称 {
// ...
}
定义子类的格式:
public class 子类名称 extends 父类名称 {
// ...
}
在父子类的继承关系当中,如果成员变量重名,则创建子类对象时,访问有两种方式:
直接通过子类对象访问成员变量:
等号左边是谁,就优先用谁,没有则向上找。
间接通过成员方法访问成员变量:
该方法属于谁,就优先用谁,没有则向上找。
例如:
// 等号左边是谁,就优先用谁
System.out.println(zi.num); // 优先子类,200
//System.out.println(zi.abc); // 到处都没有,编译报错!
System.out.println("===========");
// 这个方法是子类的,优先用子类的,没有再向上找
zi.methodZi(); // 200
// 这个方法是在父类当中定义的,
zi.methodFu(); // 100
super关键字的用法有三种:
在父子类的继承关系当中,创建子类对象,访问成员方法的规则:创建的对象是谁,就优先用谁,如果没有则向上找。
无论是成员方法还是成员变量,如果没有都是向上找父类,绝对不会向下找子类的。
在继承关系当中,方法的名称一样,参数列表也一样。
方法的覆盖重写特点:创建的是子类对象,则优先用子类方法。例如:
public class Demo01ExtendsMethod {
public static void main(String[] args) {
Zi zi = new Zi();
// 创建的是new了子类对象,所以优先用子类方法
zi.method();
}
}
//父类:
public class Fu {
public void method() {
System.out.println("父类重名方法执行!");
}
}
//子类:
public class Zi extends Fu {
public void method() {
System.out.println("子类重名方法执行!");
}
}
方法覆盖重写的注意事项:
必须保证父子类之间方法的名称相同,参数列表也相同。
@Override:写在方法前面,用来检测是不是有效的正确覆盖重写。
子类方法的返回值必须【小于等于】父类方法的返回值范围。
小扩展提示:java.lang.Object类是所有类的公共最高父类(祖宗类)
子类方法的权限必须【大于等于】父类方法的权限修饰符。
小扩展提示:public > protected > (default) > private 备注:(default)不是关键字default,而是什么都不写,留空。
继承关系中,父子类构造方法的访问特点:
总结:
例如:
//父类:
public class Fu {
int num = 10;
public void method() {
System.out.println("父类方法");
}
}
//子类:
public class Zi extends Fu {
int num = 20;
public Zi() {
super();
}
public void methodZi() {
System.out.println(super.num); // 父类中的num
}
public void method() {
super.method(); // 访问父类中的method
System.out.println("子类方法");
}
}
super关键字用来访问父类内容,而this关键字用来访问本类内容。用法也有三种:
在第三种用法当中要注意:
A. this(...)调用也必须是构造方法的第一个语句,唯一一个。
B. super和this两种构造调用,不能同时使用。
例如:
this(123); // 本类的无参构造,调用本类的有参构造
//this(1, 2); // 错误写法!
public Zi(int n) {
this(1, 2);
}
public Zi(int n, int m) {
}
抽象方法:就是加上abstract关键字,然后去掉大括号,直接分号结束。
抽象类:抽象方法所在的类,必须是抽象类才行。在class之前写上abstract即可。
一个抽象类不一定含有抽象方法,只要保证抽象方法所在的类是抽象类,即可。没有抽象方法的抽象类,也不能直接创建对象,在一些特殊场景下有用途。
抽象类的子类也可以是一个抽象类
如何使用抽象类和抽象方法:
//抽象类
public abstract class Animal {
// 这是一个抽象方法,代表吃东西,但是具体吃什么(大括号的内容)不确定。
public abstract void eat();
// 这是普通的成员方法
// public void normalMethod() {
//
// }
}
//子类继承抽象类
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}
//测试类
public class DemoMain {
public static void main(String[] args) {
// Animal animal = new Animal(); // 错误写法!不能直接创建抽象类对象
Cat cat = new Cat();
cat.eat();
}
}
换成了关键字interface之后,编译生成的字节码文件仍然是:.java --> .class。
接口就是多个类的公共规范。
接口是一种引用数据类型,最重要的内容就是其中的:抽象方法。
public interface 接口名称 {
// 接口内容
}
接口包含的内容
如果是Java 7,那么接口中可以包含的内容有:
常量
抽象方法
public abstract 返回值类型 方法名称(参数列表);
注意事项:1. 接口当中的抽象方法,修饰符必须是两个固定的关键字:public abstract2. 这两个关键字修饰符,可以选择性地省略。3. 方法的三要素,可以随意定义。
如果是Java 8,还可以额外包含有:
默认方法:使用 default 修饰,不可省略,供子类调用或者子类重写。接口当中的默认方法,可以解决接口升级的问题。
在原有接口中添加新的方法而不需要在接口实现类中重写,相当于被接口实现类继承
静态方法:使用 static 修饰,供接口直接调用。 代码如下:
public interface InterFaceName {
public default void method() {
// 执行语句
}
public static void method2() {
// 执行语句
}
}
如果是Java 9,还可以额外包含有:
私有方法:使用 private 修饰,供接口中的默认方法或者静态方法调用。 代码如下:
public interface InterFaceName {
private void method() {
// 执行语句
}
}
私有方法的使用:只有默认方法可以调用。不能被实现类或别人使用。
私有静态方法:默认方法和静态方法可以调用。 例如:
/*
问题描述:
我们需要抽取一个共有方法,用来解决两个默认方法之间重复代码的问题。
但是这个共有方法不应该让实现类使用,应该是私有化的。
解决方案:
从Java 9开始,接口当中允许定义私有方法。
1. 普通私有方法,解决多个默认方法之间重复代码问题
格式:
private 返回值类型 方法名称(参数列表) {
方法体
}
2. 静态私有方法,解决多个静态方法之间重复代码问题
格式:
private static 返回值类型 方法名称(参数列表) {
方法体
}
*/
public interface MyInterfacePrivateA {
public default void methodDefault1() {
System.out.println("默认方法1");
methodCommon();
}
public default void methodDefault2() {
System.out.println("默认方法2");
methodCommon();
}
private void methodCommon() {
System.out.println("AAA");
System.out.println("BBB");
System.out.println("CCC");
}
}
public class 实现类名称 implements 接口名称 {
// ...
}
接口的实现类必须覆盖重写(实现)接口中所有的抽象方法。
实现:去掉abstract关键字,加上方法体大括号。
创建实现类的对象,进行使用。
注意事项:
如果实现类并没有覆盖重写接口中所有的抽象方法,那么这个实现类自己就必须是抽象类。
public class Demo01Interface {
public static void main(String[] args) {
// 错误写法!不能直接new接口对象使用。
// MyInterfaceAbstract inter = new MyInterfaceAbstract();
// 创建实现类的对象使用
MyInterfaceAbstractImpl impl = new MyInterfaceAbstractImpl();
impl.methodAbs1();
impl.methodAbs2();
}
}
使用接口的时候,需要注意:
接口是没有静态代码块或者构造方法的。
一个类的直接父类是唯一的,但是一个类可以同时实现多个接口。
格式:
public class MyInterfaceImpl implements MyInterfaceA, MyInterfaceB {
// 覆盖重写所有抽象方法
}
接口中,存在同名的静态方法并不会冲突,原因是只能通过各自接口名访问静态方法。
子接口重写默认方法时,default关键字可以保留。
子类重写默认方法时,default关键字不可以保留。
注意事项:1. 多个父接口当中的抽象方法如果重复,没关系。2. 多个父接口当中的默认方法如果重复,那么子接口必须进行默认方法的覆盖重写,【而且带着default关键字】。
//定义父接口
interface A {
public default void method(){
System.out.println("AAAAAAAAAAAAAAAAAAA");
}
}
interface B {
public default void method(){
System.out.println("BBBBBBBBBBBBBBBBBBB");
}
}
//定义子接口
interface D extends A,B{
@Override
public default void method() {
System.out.println("DDDDDDDDDDDDDD");
}
}
public static final修饰。 父类名称 对象名 = new 子类名称();
或者:接口名称 对象名 = new 实现类名称();
多态的成员变量和成员方法访问
在多态的代码当中,成员方法的访问规则是:
看new的是谁,就优先用谁,没有则向上找。
口诀:编译看左边,运行看右边。
对比一下:
成员变量:编译看左边,运行还看左边。
成员方法:编译看左边,运行看右边。
例如:
public class Demo02MultiMethod {
public static void main(String[] args) {
Fu obj = new Zi(); // 多态
obj.method(); // 父子都有,优先用子
obj.methodFu(); // 子类没有,父类有,向上找到父类
// 编译看左边,左边是Fu,Fu当中没有methodZi方法,所以编译报错。
// obj.methodZi(); // 错误写法!
}
}

比如:无论老师还是助教,他们都是员工(实现员工接口或继承员工类),他们都有共同的行为那就是工作(只是工作的具体内容不一样),这时候
无论右边new的时候换成哪个子类对象,等号左边调用方法都不会变化
这就是多态!
(1)为什么要转型
eg:当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥 有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子 类特有的方法,必须做向下转型。
(2) 多态的转型分为向上转型与向下转型两种:
向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。
向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。
例如:
public class Demo01Main {
public static void main(String[] args) {
// 对象的向上转型,就是:父类引用指向之类对象。
Animal animal = new Cat(); // 本来创建的时候是一只猫
animal.eat(); // 猫吃鱼
// animal.catchMouse(); // 错误写法!
// 向下转型,进行“还原”动作
Cat cat = (Cat) animal;
cat.catchMouse(); // 猫抓老鼠
// 下面是错误的向下转型
// 本来new的时候是一只猫,现在非要当做狗
// 错误写法!编译不会报错,但是运行会出现异常:
// java.lang.ClassCastException,类转换异常
Dog dog = (Dog) animal;
}
}
ClassCastException 类型转换异常:
例如:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse 【运行报错】
}
}
这段代码可以通过编译,但是运行时,却报出了ClassCastException ,类型转换异常!这是因为,明明创建了 Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系,不符合类型转换的定义。 为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验,格式如下:
对象 instanceof 类名称
这将会得到一个boolean值结果,也就是判断前面的对象能不能当做后面类型的实例。
例如:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
if (a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse(); // 调用的是 Cat 的 catchMouse
} else if (a instanceof Dog){
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse
}
}
}
学习了继承后,我们知道,子类可以在父类的基础上改写父类内容,比如,方法重写。那么我们能不能随意的继承 API中提供的类,改写其内容呢?显然这是不合适的。为了避免这种随意改写的情况,Java提供了 final 关键字, 用于修饰不可改变内容。
final关键字代表最终、不可改变的。
常见四种用法:
重写被 final 修饰的方法,编译时就会报错。 Tips:对于基本类型来说,不可变说的是变量当中的数据不可改变
对于引用类型来说,不可变说的是变量当中的地址值不可改变
例如:
public class Demo01Final {
public static void main(String[] args) {
int num1 = 10;
System.out.println(num1); // 10
num1 = 20;
System.out.println(num1); // 20
// 一旦使用final用来修饰局部变量,那么这个变量就不能进行更改。
// “一次赋值,终生不变”
final int num2 = 200;
System.out.println(num2); // 200
// num2 = 250; // 错误写法!不能改变!
// num2 = 200; // 错误写法!
// 正确写法!只要保证有唯一一次赋值即可
final int num3;
num3 = 30;
// 对于基本类型来说,不可变说的是变量当中的数据不可改变
// 对于引用类型来说,不可变说的是变量当中的地址值不可改变
Student stu1 = new Student("赵丽颖");
System.out.println(stu1);
System.out.println(stu1.getName()); // 赵丽颖
stu1 = new Student("霍建华");
System.out.println(stu1);
System.out.println(stu1.getName()); // 霍建华
System.out.println("===============");
final Student stu2 = new Student("高圆圆");
// 错误写法!final的引用类型变量,其中的地址不可改变
// stu2 = new Student("赵又廷");
System.out.println(stu2.getName()); // 高圆圆
stu2.setName("高圆圆圆圆圆圆");
System.out.println(stu2.getName()); // 高圆圆圆圆圆圆
}
}
修饰符 final 返回值类型 方法名称(参数列表) {
// 方法体
}
对于类、方法来说,abstract关键字和final关键字不能同时使用,因为矛盾。
不能使用一个final类来作为父类
对于成员变量来说,如果使用final关键字修饰,那么这个变量也照样是不可变。
例如:
public class Person {
private final String name/* = "鹿晗"*/;
public Person() {
name = "关晓彤";
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
// public void setName(String name) {
// this.name = name;
// }
}
Java中有四种权限修饰符:
public > protected > (default) > private
同一个类(我自己) √ √ √ √
同一个包(我邻居) √ √ √ NO
不同包子类(我儿子) √ √ NO NO
不同包非子类(陌生人) √ NO NO NO
(default)并不是关键字“default”,而是根本不写。
什么是内部类?
如果一个事物的内部包含另一个事物,那么这就是一个类内部包含另一个类。例如:身体和心脏的关系。又如:汽车和发动机的关系。
内部类仍然是一个独立的类,在编译之后会内部类会被编译成独立的.class文件,但是前面冠以外部类的类名 和$符号 。 比如,Person$Heart.class
分类:
修饰符 class 外部类名称 {
修饰符 class 内部类名称 {
// ...
}
// ...
}
注意:内用外,随意访问;外用内,需要内部类对象。
【外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();】例如:
public class Demo01InnerClass {
public static void main(String[] args) {
Body body = new Body(); // 外部类的对象
// 通过外部类的对象,调用外部类的方法,里面间接在使用内部类Heart
body.methodBody();
System.out.println("=====================");
// 按照公式写:
Body.Heart heart = new Body().new Heart();
heart.beat();
}
}
public class Body { // 外部类
public class Heart { // 成员内部类
// 内部类的方法
public void beat() {
System.out.println("心脏跳动:蹦蹦蹦!");
System.out.println("我叫:" + name); // 正确写法!
}
}
// 外部类的成员变量
private String name;
// 外部类的方法
public void methodBody() {
System.out.println("外部类的方法");
new Heart().beat();//外部类的方法,里面间接在使用内部类Heart
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// 如果出现了重名现象,那么格式是:外部类名称.this.外部类成员变量名
public class Outer {
int num = 10; // 外部类的成员变量
public class Inner /*extends Object*/ {
int num = 20; // 内部类的成员变量
public void methodInner() {
int num = 30; // 内部类方法的局部变量
System.out.println(num); // 局部变量,就近原则
System.out.println(this.num); // 内部类的成员变量
System.out.println(Outer.this.num); // 外部类的成员变量
}
}
}
如果一个类是定义在一个方法内部的,那么这就是一个局部内部类。
“局部”:只有当前所属的方法才能使用它,出了这个方法外面就不能用了。
定义格式:
定义格式:
修饰符 class 外部类名称 {
修饰符 返回值类型 外部类方法名称(参数列表) {
class 局部内部类名称 {
// ...
}
}
}
局部内部类,如果希望访问所在方法的局部变量,那么这个局部变量必须是【有效final的】。
备注:从Java 8+开始,只要局部变量事实不变,那么final关键字可以省略。
原因:
public > protected > (default) > private
定义一个类的时候,权限修饰符规则:
内部类的简化写法。它的本质是一个 带具体实现的父类或者父接口的 匿名的 子类对象。 开发中,最常用到的内部类就是匿名内部类了。
匿名内部类必须继承一个父类或者实现一个父接口。
匿名内部类的定义格式:
接口名称 对象名 = new 父类名或者接口名称() {
// 覆盖重写所有抽象方法
};
对格式“new 接口名称() {...}”进行解析:
另外还要注意几点问题:
匿名对象:匿名对象就是只有右边的对象,没有左边的名字和赋值运算符。格式
new 类名称();
注意事项:匿名对象只能使用唯一的一次,下次再用不得不再创建一个新对象。
使用建议:如果确定有一个对象只需要使用唯一的一次,就可以用匿名对象。
例如:
public class DemoMain {
public static void main(String[] args) {
// MyInterface obj = new MyInterfaceImpl();
// obj.method();
// MyInterface some = new MyInterface(); // 错误写法!
// 使用匿名内部类,但不是匿名对象,对象名称就叫objA
MyInterface objA = new MyInterface() {
@Override
public void method1() {
System.out.println("匿名内部类实现了方法!111-A");
}
@Override
public void method2() {
System.out.println("匿名内部类实现了方法!222-A");
}
};
objA.method1();
objA.method2();
System.out.println("=================");
// 使用了匿名内部类,而且省略了对象名称,也是匿名对象
new MyInterface() {
@Override
public void method1() {
System.out.println("匿名内部类实现了方法!111-B");
}
@Override
public void method2() {
System.out.println("匿名内部类实现了方法!222-B");
}
}.method1();
// 因为匿名对象无法调用第二次方法,所以需要再创建一个匿名内部类的匿名对象
new MyInterface() {
@Override
public void method1() {
System.out.println("匿名内部类实现了方法!111-B");
}
@Override
public void method2() {
System.out.println("匿名内部类实现了方法!222-B");
}
}.method2();
}
}
基本类型可以作为成员变量、作为方法的参数、作为方法的返回值,那么当然引用类型也是可以的。
例如:创建一个英雄角色
class Weapon {
String name; // 武器名称
int hurt; // 伤害值
}
class Armour {
String name;// 装备名称
int protect;// 防御值
}
class Role {
int id;
int blood;
String name;
// 添加武器属性
Weapon wp;
// 添加盔甲属性
Armour ar;
// 提供get/set方法
public Weapon getWp() {
return wp;
}
public void setWeapon(Weapon wp) {
this.wp = wp;
}
public Armour getArmour() {
return ar;
}
public void setArmour(Armour ar) {
this.ar = ar;
}
// 攻击方法
public void attack(){
System.out.println("使用"+ wp.getName() +", 造成"+wp.getHurt()+"点伤害");
}
// 穿戴盔甲
public void wear(){
// 增加防御,就是增加blood值
this.blood += ar.getProtect();
System.out.println("穿上"+ar.getName()+", 生命值增加"+ar.getProtect());
}
}
public class Test {
public static void main(String[] args) {
// 创建Weapon 对象
Weapon wp = new Weapon("屠龙刀" , 999999);
// 创建Armour 对象
Armour ar = new Armour("麒麟甲",10000);
// 创建Role 对象
Role r = new Role();
// 设置武器属性
r.setWeapon(wp);
// 设置盔甲属性
r.setArmour(ar)
// 攻击
r.attack();
// 穿戴盔甲
r.wear();
}
}
输出结果:
使用屠龙刀,造成999999点伤害
穿上麒麟甲 ,生命值增加10000
类作为成员变量时,对它进行赋值的操作,实际上,是赋给它该类的一个对象。
接口作为成员变量时,对它进行赋值的操作,实际上,是赋给它该接口的一个子类对象。
例如: 接口是对方法的封装,对应游戏当中,可以看作是扩展游戏角色的技能。所以,如果想扩展更强大技能,我们在 Role 中,可以增加接口作为成员变量,来设置不同的技能。
// 法术攻击
public interface FaShuSkill {
public abstract void faShuAttack();
}
public class Role {
FaShuSkill fs;
public void setFaShuSkill(FaShuSkill fs) {
this.fs = fs;
}
// 法术攻击
public void faShuSkillAttack(){
System.out.print("发动法术攻击:");
fs.faShuAttack();
System.out.println("攻击完毕");
}
}
public class Test {
public static void main(String[] args) {
// 创建游戏角色
Role role = new Role();
// 设置角色法术技能
role.setFaShuSkill(new FaShuSkill() {
@Override
public void faShuAttack() {
System.out.println("纵横天下");
}
});
// 发动法术攻击
role.faShuSkillAttack();
// 更换技能
role.setFaShuSkill(new FaShuSkill() {
@Override
public void faShuAttack() {
System.out.println("逆转乾坤");
}
});
// 发动法术攻击
role.faShuSkillAttack();
}
}
/*输出结果:
发动法术攻击:纵横天下
攻击完毕
发动法术攻击:逆转乾坤
攻击完毕
*/