接口
概述
接口,是Java语言中一种引用类型,是方法的集合,如果说类的内部封装了成员变量、构造方法和成员方法,那么接口的内部主要就是封装了方法,包含抽象方法(JDK 7及以前),默认方法和静态方法(JDK 8),私有方法
(JDK 9)。
接口中所有的方法都是抽象的(JDK7及以前)。它定义了一类行为的结合,这些行为的实现,由其实现类来实现。不同实现类有不同的方法实现,因此,提供了代码实现的灵活性。
接口中,所有的方法,缺省都是abstract的,因此,不需要显示的在方法的前面加上abstract,另外,在接口中,所有的方法都是public的。因此,接口中所有方法默认都是public abstract的(JDK7及以前)。
在实现类中,对于接口中抽象方法的实现,它的访问权限不能低于接口中的方法的访问权限。这个对于派生类和基类也是一样,在子类中覆盖了父类中的方法的时候,那么所提供的方法的访问权限,要么和基类中相应的方法的访问权限等同,要么就比它高。
接口的更重要的作用是制定模块与模块之间通讯的协议。
在接口中声明方法时,不能使用native、static、final、synchronized、private(JDK7及以前)、protected等修饰符。
接口中可以有数据成员(成员变量),这些成员默认都是public static final的。因此,可以通过接口直接调用静态成员变量的。
一个接口可以继承另外一个接口。
Java中不允许类的多继承,但是允许接口的多继承。
Java中一个类可以实现多个接口。一个类在继承另外一个类的同时,可以实现多个接口。
注意,在继承的同时实现多个接口,要注意,extends和implements它们的先后顺序。extends要放在implements的前面。
和public类一样,public接口也必须定义在与接口同名的文件中。同一个文件中,只能有一个接口是public接口。
接口也可以有缺省的接口,缺省的接口也只是在同一个包中才能被访问。
通常接口需要在模块之间或者类之间引用,因此,通常将接口定义为public的。
接口的定义,它与定义类方式相似,但是使用 interface 关键字。它也会被编译成.class文件,但一定要明确它并
不是类,而是另外一种引用数据类型。
引用数据类型:数组,类,接口。
接口的使用,它不能创建对象,但是可以被实现( implements ,类似于被继承)。一个实现接口的类(可以看做是接口的子类),需要实现接口中所有的抽象方法,创建该类对象,就可以调用方法了,否则它必须是一个抽象
类。
public interface 接口名称 {
// 抽象方法
// 默认方法
// 静态方法
// 私有方法
}
含有抽象方法
抽象方法:使用 abstract 关键字修饰,可以省略,没有方法体。该方法供子类实现使用。
public interface InterFaceName {
public abstract void method();
}
含有默认方法和静态方法
默认方法:使用 default 修饰,不可省略,供子类调用或者子类重写。
静态方法:使用 static 修饰,供接口直接调用。
public interface InterFaceName {
public default void method() {
// 执行语句
}
public static void method2() {
// 执行语句
}
}
含有私有方法和私有静态方法
私有方法:使用 private 修饰,供接口中的默认方法调用。
私有静态方法:供接口中静态方法和私有方法或者default方法调用。
public interface InterFaceName {
private void method() {
// 执行语句
}
}
接口的实现
类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类
似继承,格式相仿,只是关键字不同,实现使用 implements 关键字。
非抽象子类实现接口:
- 必须重写接口中所有抽象方法。否则子类也是抽象类。
- 继承了接口的默认方法,即可以直接调用,也可以重写。
class 类名 implements 接口名 {
// 重写接口中抽象方法【必须】
// 重写接口中默认方法【可选】
}
抽象方法的实现
public interface LiveAble {
// 定义抽象方法
public abstract void eat();
public abstract void sleep();
}
public class Animal implements LiveAble {
@Override
public void eat() {
System.out.println("吃东西");
}
@Override
public void sleep() {
System.out.println("晚上睡");
}
}
public class InterfaceDemo {
public static void main(String[] args) {
// 创建子类对象
Animal a = new Animal();
// 调用实现后的方法
a.eat();
a.sleep();
}
}
输出结果:
吃东西
晚上睡
默认方法的实现
可以继承,可以重写,二选一,但是只能通过实现类的对象来调用。
//1、继承默认方法
public interface LiveAble {
public default void fly(){
System.out.println("天上飞");
}
}
public class Animal implements LiveAble {
// 继承,什么都不用写,直接调用
}
public class InterfaceDemo {
public static void main(String[] args) {
// 创建子类对象
Animal a = new Animal();
// 调用默认方法
a.fly();
}
}
输出结果:
天上飞
//2、重写默认方法
public interface LiveAble {
public default void fly(){
System.out.println("天上飞");
}
}
public class Animal implements LiveAble {
@Override
public void fly() {
System.out.println("自由自在的飞");
}
}
public class InterfaceDemo {
public static void main(String[] args) {
// 创建子类对象
Animal a = new Animal();
// 调用重写方法
a.fly();
}
}
输出结果:
自由自在的飞
静态方法的实现
静态与.class 文件相关,只能使用接口名调用,不可以通过实现类的类名或者实现类的对象调用。
public interface LiveAble {
public static void run(){
System.out.println("跑起来~~~");
}
}
public class Animal implements LiveAble {
// 无法重写静态方法
}
public class InterfaceDemo {
public static void main(String[] args) {
// Animal.run(); // 【错误】无法继承方法,也无法调用
LiveAble.run(); //
}
}
输出结果:
跑起来~~~
私有方法的实现
私有方法:只有默认方法可以调用。
私有静态方法:默认方法和静态方法可以调用。
如果一个接口中有多个默认方法,并且方法中有重复的内容,那么可以抽取出来,封装到私有方法中,供默认方法
去调用。从设计的角度讲,私有的方法是对默认方法和静态方法的辅助。
public interface LiveAble {
default void func(){
func1();
func2();
}
private void func1(){
System.out.println("跑起来~~~");
}
private void func2(){
System.out.println("跑起来~~~");
}
}
/*
在任何版本的Java中,接口都能定义抽象方法。
格式:
public abstract 返回值类型 方法名称(参数列表);
注意事项:
1. 接口当中的抽象方法,修饰符必须是两个固定的关键字:public abstract,即接口中的抽象方法修饰符默认就是public abstract
2. 这两个关键字修饰符,可以选择性地省略。(初学者不推荐。)
3. 方法的三要素,可以随意定义。
*/
public interface MyInterfaceAbstract {
// 这是一个抽象方法
public abstract void methodAbs1();
// 这也是抽象方法
abstract void methodAbs2();
// 这也是抽象方法
public void methodAbs3();
// 这也是抽象方法
void methodAbs4();
}
public class MyInterfaceAbstractImpl implements MyInterfaceAbstract {
@Override
public void methodAbs1() {
System.out.println("这是第一个方法!");
}
@Override
public void methodAbs2() {
System.out.println("这是第二个方法!");
}
@Override
public void methodAbs3() {
System.out.println("这是第三个方法!");
}
@Override
public void methodAbs4() {
System.out.println("这是第四个方法!");
}
}
/*
接口就是多个类的公共规范。
接口是一种引用数据类型,最重要的内容就是其中的:抽象方法。
如何定义一个接口的格式:
public interface 接口名称 {
// 接口内容
}
备注:换成了关键字interface之后,编译生成的字节码文件仍然是:.java --> .class。
如果是Java 7,那么接口中可以包含的内容有:
1. 常量
2. 抽象方法
如果是Java 8,还可以额外包含有:
3. 默认方法
4. 静态方法
如果是Java 9,还可以额外包含有:
5. 私有方法
接口使用步骤:
1. 接口不能直接使用,必须有一个“实现类”来“实现”该接口。
格式:
public class 实现类名称 implements 接口名称 {
// ...
}
2. 接口的实现类必须覆盖重写(实现)接口中所有的抽象方法。
实现:去掉abstract关键字,加上方法体大括号。
3. 创建实现类的对象,进行使用。
注意事项:
如果实现类并没有覆盖重写接口中所有的抽象方法,那么这个实现类自己就必须是抽象类。
*/
public class Demo01Interface {
public static void main(String[] args) {
// 错误写法!不能直接new接口对象使用。
// MyInterfaceAbstract inter = new MyInterfaceAbstract();
// 创建实现类的对象使用
MyInterfaceAbstractImpl impl = new MyInterfaceAbstractImpl();
impl.methodAbs1();
impl.methodAbs2();
}
}
/*
从Java 8开始,接口里允许定义默认方法。
格式:
public default 返回值类型 方法名称(参数列表) {
方法体
}
备注:接口当中的默认方法,可以解决接口升级的问题。
*/
public interface MyInterfaceDefault {
// 抽象方法
public abstract void methodAbs();
// 新添加了一个抽象方法
// public abstract void methodAbs2();
// 新添加的方法,改成默认方法
//public 可以省略的,默认就是public的。default这个不能够省略。
public default void methodDefault() {
System.out.println("这是新添加的默认方法");
}
}
public class MyInterfaceDefaultA implements MyInterfaceDefault {
@Override
public void methodAbs() {
System.out.println("实现了抽象方法,AAA");
}
}
public class MyInterfaceDefaultB implements MyInterfaceDefault {
@Override
public void methodAbs() {
System.out.println("实现了抽象方法,BBB");
}
@Override
public void methodDefault() {
System.out.println("实现类B覆盖重写了接口的默认方法");
}
}
/*
1. 接口的默认方法,可以通过接口实现类对象,直接调用。
2. 接口的默认方法,也可以被接口实现类进行覆盖重写。
*/
public class Demo02Interface {
public static void main(String[] args) {
// 创建了实现类对象
MyInterfaceDefaultA a = new MyInterfaceDefaultA();
a.methodAbs(); // 调用抽象方法,实际运行的是右侧实现类。
// 调用默认方法,如果实现类当中没有,会向上找接口,这样接口中新增加了方法,
//对于实现类A和B都不需要改动,即可直接调用默认的方法,向上找接口中的内容。这就是解决了接口升级的问题。
//后续学习lambda和函数式编程,接口的默认方法可以拼接函数模型。
a.methodDefault(); // 这是新添加的默认方法
System.out.println("==========");
MyInterfaceDefaultB b = new MyInterfaceDefaultB();
b.methodAbs();
b.methodDefault(); // 实现类B覆盖重写了接口的默认方法
}
}
/*
从Java 8开始,接口当中允许定义静态方法。
格式:
public static 返回值类型 方法名称(参数列表) {
方法体
}
public可以省略,static不可省略
提示:就是将abstract或者default换成static即可,带上方法体。
*/
public interface MyInterfaceStatic {
public static void methodStatic() {
System.out.println("这是接口的静态方法!");
}
}
public class MyInterfaceStaticImpl implements MyInterfaceStatic {
}
/*
注意事项:不能通过接口实现类的对象来调用接口当中的静态方法。
正确用法:通过接口名称,直接调用其中的静态方法。
格式:
接口名称.静态方法名(参数);
*/
public class Demo03Interface {
public static void main(String[] args) {
// 创建了实现类对象
MyInterfaceStaticImpl impl = new MyInterfaceStaticImpl();
// 错误写法!
// impl.methodStatic();
// 直接通过接口名称调用静态方法
MyInterfaceStatic.methodStatic();
}
}
/*
问题描述:
我们需要抽取一个共有方法,用来解决两个默认方法之间重复代码的问题。
但是这个共有方法不应该让实现类使用,应该是私有化的。
解决方案:
从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 MyInterfacePrivateAImpl implements MyInterfacePrivateA {
public void methodAnother() {
// 直接访问到了接口中的默认方法,这样是错误的!default供实现类对象调用或者实现类重写。
//如果实现类中能够调用接口中的default,那么也是需要通过类似super方式,但是super是指向父类的,而不是接口的。所以不行。
// methodCommon();错误,private只能在接口内部使用。
}
}
public interface MyInterfacePrivateB {
public static void methodStatic1() {
System.out.println("静态方法1");
methodStaticCommon();
}
public static void methodStatic2() {
System.out.println("静态方法2");
methodStaticCommon();
}
private static void methodStaticCommon() {
System.out.println("AAA");
System.out.println("BBB");
System.out.println("CCC");
}
}
public class Demo04Interface {
public static void main(String[] args) {
MyInterfacePrivateB.methodStatic1();
MyInterfacePrivateB.methodStatic2();
// 错误写法!private的只能在接口内部使用
// MyInterfacePrivateB.methodStaticCommon();
}
}
/*
接口当中也可以定义“成员变量”,但是必须使用public static final三个关键字进行修饰。即接口中成员变量默认就是public static final
从效果上看,这其实就是接口的【常量】。
格式:
public static final 数据类型 常量名称 = 数据值;
备注:
一旦使用final关键字进行修饰,说明不可改变。
注意事项:
1. 接口当中的常量,可以省略public static final,注意:不写也照样是这样。
2. 接口当中的常量,必须进行赋值;不能不赋值。因为是常量,必须定义时赋初值,否则后续没有办法给常量重新赋值的。
3. 接口中常量的名称,使用完全大写的字母,用下划线进行分隔。(推荐命名规则)
*/
public interface MyInterfaceConst {
// 这其实就是一个常量,一旦赋值,不可以修改
public static final int NUM_OF_MY_CLASS = 12;
}
public class Demo05Interface {
public static void main(String[] args) {
// 访问接口当中的常量
System.out.println(MyInterfaceConst.NUM_OF_MY_CLASS);
}
}
//软件生产一直希望达到硬件生产的形式。各个组件不同厂商生产,但是彼此兼容,能够协同工作。
//制定接口,两个模块之间彼此协商,制定接口。一个模块只需要通过接口去获得另外一个模块的功能即可。而另外这个模块要做的就是去实现这个接口中暴露出来的方法。
//接口,肯定是一个人去实现这个接口,另外一个人去调用这个接口。
interface VideoCard{
void Display();
String getName();
}
class Dmeng implements VideoCard{
String name;
public Dmeng(){
name = "Dmeng's videocard";
}
public void setName(String name){
this.name = name;
}
public void Display(){
System.out.println("Dmeng's videocard is working");
}
public String getName(){
return name;
}
}
class MainBoard{
String strCPU;
VideoCard vc;
void setCPU(String strCPU){
this.strCPU = strCPU;
}
void setVideoCard(VideoCard vc){
this.vc = vc;
}
void run(){
System.out.println(strCPU);
System.out.println(vc.getName());
System.out.println(vc.Display());
System.out.println("MainBoard is running");
}
}
//Mainboard就是使用接口的人。Dmeng就是实现接口的人。可以有不同的厂商实现这个接口。而在接口使用人这里不用去关心到底谁实现的,如何去实现的。
//因为vc的类型是VideoCard,是一个接口,因此,它不能直接实例化一个对象的,应该用它的实现类产生一个对象,然后将这个对象赋给vc这个变量,这就是应用接口的好处。
//对于主板厂商来说,只需要根据标准的显卡接口完成主板的制作,至于这个显卡最终由谁来生产是由帝盟生产还是其它的厂商生产,我们不需要关心。我们获取到这个标准的显卡接口之后,就可知道其实现类必然要实现其中的方法。只需要利用接口的这个变量vc去调用实现类的对象中的方法即可。不要认为是这个接口实例化了一个对象。
class Computer{
public static void main(String[] args){
Dmeng d = new Dmeng();
MainBoard m = new MainBoard();
m.setCPU("Intel's CPU");
m.setVideoCard(d);
m.run();
}
}
接口的多实现
继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。并且,一个类能继承一个父类,同时实现多个接口。
class 类名 [extends 父类名] implements 接口名1,接口名2,接口名3... {
// 重写接口中抽象方法【必须】
// 重写接口中默认方法【不重名时可选】
}
抽象方法
接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次。
interface A {
public abstract void showA();
public abstract void show();
}
interface B {
public abstract void showB();
public abstract void show();
}
public class C implements A,B{
@Override
public void showA() {
System.out.println("showA");
}
@Override
public void showB() {
System.out.println("showB");
}
@Override
public void show() {
System.out.println("show");
}
}
默认方法
接口中,有多个默认方法时,实现类都可继承使用。如果默认方法有重名的,必须重写一次。
interface A {
public default void methodA(){}
public default void method(){}
}
interface B {
public default void methodB(){}
public default void method(){}
}
public class C implements A,B{
@Override
public void method() {
System.out.println("method");
}
}
静态方法
接口中,存在同名的静态方法并不会冲突,原因是只能通过各自接口名访问静态方法。
优先级的问题
当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类就近选择执
行父类的成员方法。
interface A {
public default void methodA(){
System.out.println("AAAAAAAAAAAA");
}
}
class D {
public void methodA(){
System.out.println("DDDDDDDDDDDD");
}
}
class C extends D implements A {
// 未重写methodA方法
}
public class Test {
public static void main(String[] args) {
C c = new C();
c.methodA();
}
}
输出结果:
DDDDDDDDDDDD
public interface MyInterface {
public default void method() {
System.out.println("接口的默认方法");
}
}
public class Fu {
public void method() {
System.out.println("父类方法");
}
}
public class Zi extends Fu implements MyInterface {
}
/*
使用接口的时候,需要注意:
1. 接口是没有静态代码块或者构造方法的。
2. 一个类的直接父类是唯一的,但是一个类可以同时实现多个接口。
格式:
public class MyInterfaceImpl implements MyInterfaceA, MyInterfaceB {
// 覆盖重写所有抽象方法
}
3. 如果实现类所实现的多个接口当中,存在重复的抽象方法,那么只需要覆盖重写一次即可。
4. 如果实现类没有覆盖重写所有接口当中的所有抽象方法,那么实现类就必须是一个抽象类。
5. 如果实现类所实现的多个接口当中,存在重复的默认方法,那么实现类一定要对冲突的默认方法进行覆盖重写。
6. 一个类如果直接父类当中的方法,和接口当中的默认方法产生了冲突,优先用父类当中的方法。
*/
public class Demo01Interface {
public static void main(String[] args) {
Zi zi = new Zi();
zi.method();
}
}
public interface MyInterfaceA {
// 错误写法!接口不能有静态代码块
// static {
//
// }
// 错误写法!接口不能有构造方法
// public MyInterfaceA() {
//
// }
public abstract void methodA();
public abstract void methodAbs();
public default void methodDefault() {
System.out.println("默认方法AAA");
}
}
public interface MyInterfaceB {
// 错误写法!接口不能有静态代码块
// static {
//
// }
// 错误写法!接口不能有构造方法
// public MyInterfaceA() {
//
// }
public abstract void methodB();
public abstract void methodAbs();
public default void methodDefault() {
System.out.println("默认方法BBB");
}
}
public class MyInterfaceImpl /*extends Object*/ implements MyInterfaceA, MyInterfaceB {
@Override
public void methodA() {
System.out.println("覆盖重写了A方法");
}
@Override
public void methodB() {
System.out.println("覆盖重写了B方法");
}
@Override
public void methodAbs() {
System.out.println("覆盖重写了AB接口都有的抽象方法");
}
@Override
public void methodDefault() {
System.out.println("对多个接口当中冲突的默认方法进行了覆盖重写");
}
}
public abstract class MyInterfaceAbstract implements MyInterfaceA, MyInterfaceB {
@Override
public void methodA() {
}
@Override
public void methodAbs() {
}
@Override
public void methodDefault() {
}
}
接口的多继承【了解】
一个接口能继承另一个或者多个接口,这和类之间的继承比较相似。接口的继承使用 extends 关键字,子接口继
承父接口的方法。如果父接口中的默认方法有重名的,那么子接口需要重写一次。
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");
}
}
子接口重写默认方法时,default关键字可以保留。
子类重写默认方法时,default关键字不可以保留。
接口中,无法定义成员变量,但是可以定义常量,其值不可以改变,默认使用public static final修饰。
接口中,没有构造方法,不能创建对象。
接口中,没有静态代码块。
public interface MyInterfaceA {
public abstract void methodA();
public abstract void methodCommon();
public default void methodDefault() {
System.out.println("AAA");
}
}
public interface MyInterfaceB {
public abstract void methodB();
public abstract void methodCommon();
public default void methodDefault() {
System.out.println("BBB");
}
}
/*
这个子接口当中有几个方法?答:4个。
methodA 来源于接口A
methodB 来源于接口B
methodCommon 同时来源于接口A和B
method 来源于我自己
*/
public interface MyInterface extends MyInterfaceA, MyInterfaceB {
public abstract void method();
@Override
public default void methodDefault() {
}
}
public class MyInterfaceImpl implements MyInterface {
@Override
public void method() {
}
@Override
public void methodA() {
}
@Override
public void methodB() {
}
@Override
public void methodCommon() {
}
}
/*
1. 类与类之间是单继承的。直接父类只有一个。
2. 类与接口之间是多实现的。一个类可以实现多个接口。
3. 接口与接口之间是多继承的。
注意事项:
1. 多个父接口当中的抽象方法如果重复,没关系。
2. 多个父接口当中的默认方法如果重复,那么子接口必须进行默认方法的覆盖重写,【而且带着default关键字】。
*/
public class Demo01Relations {
}
在Java 9+版本中,接口的内容可以有:
1. 成员变量其实是常量,格式:
[public] [static] [final] 数据类型 常量名称 = 数据值;
注意:
常量必须进行赋值,而且一旦赋值不能改变。
常量名称完全大写,用下划线进行分隔。
2. 接口中最重要的就是抽象方法,格式:
[public] [abstract] 返回值类型 方法名称(参数列表);
注意:实现类必须覆盖重写接口所有的抽象方法,除非实现类是抽象类。
3. 从Java 8开始,接口里允许定义默认方法,格式:
[public] default 返回值类型 方法名称(参数列表) { 方法体 }
注意:默认方法也可以被覆盖重写,默认方法不能够在实现类中被调用,只能通过实现类的对象进行调用。
4. 从Java 8开始,接口里允许定义静态方法,格式:
[public] static 返回值类型 方法名称(参数列表) { 方法体 }
注意:应该通过接口名称进行调用,不能通过实现类对象调用接口静态方法
5. 从Java 9开始,接口里允许定义私有方法,格式:
普通私有方法:private 返回值类型 方法名称(参数列表) { 方法体 }
静态私有方法:private static 返回值类型 方法名称(参数列表) { 方法体 }
注意:private的方法只有接口自己才能调用,不能被实现类或别人使用。
java中没有define这样的预处理语句。
在java中使用final定义常量。它的值在运行的时候,是不能够被修改的。
对于final声明的常量,必须在声明的同时进行初始化。
很多书上写,对于final声明的常量,必须在声明的同时进行初始化,实际上,可以不在声明的时候,进行初始化。可以将初始化工作放到构造函数中进行。这里指的常量是成员变量而言。
为了节省内存,通常将常量声明为静态的static。
因为常量在运行的时候不能被改变,那么可以将常量定义为static的。让所有对象共享同一个常量。而不是让每个对象都有一份这个常量的拷贝。
由于现在定义为静态的常量(成员变量),初始化放在构造函数中是不行的,对于静态的常量(成员变量),它也是只属于某个类,并不属于某个具体的对象。而当使用类名.静态常量名,这样调用的时候,不会导致构造函数的调用,因此,将静态常量这个成员变量放到构造方法中初始化,结果就是没有初始化。所以不行。
因此,当一个常量定义为static的时候,就要在声明的同时对其进行初始化。
多态
概述
多态是继封装、继承之后,面向对象的第三大特性。
生活中,比如跑的动作,小猫、小狗和大象,跑起来是不一样的。再比如飞的动作,昆虫、鸟类和飞机,飞起来也
是不一样的。可见,同一行为,通过不同的事物,可以体现出来的不同的形态。多态,描述的就是这样的状态。
多态: 是指同一行为,具有多个不同表现形式。
多态的前提:
- 继承或者实现【二选一】
- 方法的重写【意义体现:不重写,无意义】
- 父类引用指向子类对象【格式体现】
多态,实质就是父类型的引用指向子类型的对象。
多态体现的格式:
父类类型 变量名 = new 子类对象;
变量名.方法名();
父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
Fu f = new Zi();
f.method();
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写
后方法。
public abstract class Animal {
public abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
public class Test {
public static void main(String[] args) {
// 多态形式,创建对象
Animal a1 = new Cat();
// 调用的是 Cat 的 eat
a1.eat();
// 多态形式,创建对象
Animal a2 = new Dog();
// 调用的是 Dog 的 eat
a2.eat();
}
}
多态的好处
实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展
性与便利。
public abstract class Animal {
public abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
public class Test {
public static void main(String[] args) {
// 多态形式,创建对象
Cat c = new Cat();
Dog d = new Dog();
// 调用showCatEat
showCatEat(c);
// 调用showDogEat
showDogEat(d);
/*
以上两个方法, 均可以被showAnimalEat(Animal a)方法所替代
而执行效果一致
*/
showAnimalEat(c);
showAnimalEat(d);
}
public static void showCatEat (Cat c){
c.eat();
}
public static void showDogEat (Dog d){
d.eat();
}
public static void showAnimalEat (Animal a){
a.eat();
}
}
由于多态特性的支持,showAnimalEat方法的Animal类型,是Cat和Dog的父类类型,父类类型接收子类对象,当
然可以把Cat对象和Dog对象,传递给方法。
当eat方法执行时,多态规定,执行的是子类重写的方法,那么效果自然与showCatEat、showDogEat方法一致,
所以showAnimalEat完全可以替代以上两方法。
不仅仅是替代,在扩展性方面,无论之后再多的子类出现,我们都不需要编写showXxxEat方法了,直接使用
showAnimalEat都可以完成。
所以,多态的好处,体现在,可以使程序编写的更简单,并有良好的扩展。
Java编程思想:
多态就是不同形式。面向对象中,你有相同的脸(基类中通用的接口)和使用那个脸的不同的形式:动态绑定方法的不同的版本。
如果没有数据抽象(封装)和继承,那么我们不可能创建理解,创建一个多态的例子。多态是一个不能被孤立看待的一个特征,相反,应该在一个大的类关系的场景中,多态存在的,人们经常被其它的Java中的非面向对象的特征所困扰,例如,方法重载,有时候被当作面向对象的特征,不要被愚弄,如果不是晚绑定,那么它就不是多态。对于方法的重载,它不是晚绑定,它是早绑定。
早绑定,编译期间的,也就是编译的时候,java编译器就知道参数是什么样的,如何进行调用的。
晚绑定,就是运行的时候,Java才知道如何执行,编译的时候,Java是不知道的。也就是只有运行的时候,new出了对象之后,才知道父类型的引用变量到底指向的是哪个子类的对象。这个在编译的时候,编译器只是看等号两边的类型是否是同一颗继承树上的,来判断语法是否正确,但是不知道这个引用变量到底指向哪个对象,因为对象还没创建呢。
方法重载,不是晚绑定,不是面向对象的特征,多态不是由它来决定的。多态是晚绑定的。如果不是晚绑定,就一定不是多态。
要使用多态,你必须扩展视角包括不仅仅是单独的类成员和方法,而且应该考虑不同类之间的关系。
因为多态能够实现更快的开发,代码的更好的组织,以及维护性。
public class Fu {
public void method() {
System.out.println("父类方法");
}
public void methodFu() {
System.out.println("父类特有方法");
}
}
public class Zi extends Fu {
@Override
public void method() {
System.out.println("子类方法");
}
}
/*
代码当中体现多态性,其实就是一句话:父类引用指向子类对象。
格式:
父类名称 对象名 = new 子类名称();
或者:
接口名称 对象名 = new 实现类名称();
*/
public class Demo01Multi {
public static void main(String[] args) {
// 使用多态的写法
// 左侧父类的引用,指向了右侧子类的对象
Fu obj = new Zi();
obj.method();
obj.methodFu();
}
}
public class Fu /*extends Object*/ {
int num = 10;
public void showNum() {
System.out.println(num);
}
public void method() {
System.out.println("父类方法");
}
public void methodFu() {
System.out.println("父类特有方法");
}
}
public class Zi extends Fu {
int num = 20;
int age = 16;
@Override
public void showNum() {
System.out.println(num);
}
@Override
public void method() {
System.out.println("子类方法");
}
public void methodZi() {
System.out.println("子类特有方法");
}
}
/*
访问成员变量的两种方式:
1. 直接通过对象名称访问成员变量:看等号左边是谁,优先用谁,没有则向上找。
2. 间接通过成员方法访问成员变量:看该方法属于谁,优先用谁,没有则向上找。
*/
public class Demo01MultiField {
public static void main(String[] args) {
// 使用多态的写法,父类引用指向子类对象
Fu obj = new Zi();
System.out.println(obj.num); // 父:10
// System.out.println(obj.age); // 错误写法!
System.out.println("=============");
// 子类没有覆盖重写,就是父:10
// 子类如果覆盖重写,就是子:20
obj.showNum();
}
}
/*
在多态的代码当中,成员方法的访问规则是:
看new的是谁,就优先用谁,没有则向上找。
口诀:编译看左边,运行看右边。
对比一下:
成员变量:编译看左边,运行还看左边。
成员方法:编译看左边,运行看右边。
*/
public class Demo02MultiMethod {
public static void main(String[] args) {
Fu obj = new Zi(); // 多态
obj.method(); // 父子都有,优先用子
obj.methodFu(); // 子类没有,父类有,向上找到父类
// 编译看左边,左边是Fu,Fu当中没有methodZi方法,所以编译报错。
// obj.methodZi(); // 错误写法!
}
}
引用类型转换
多态的转型分为向上转型与向下转型两种:
向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。当父类引用指向一个子类对象时,便是向上转型。
父类类型 变量名 = new 子类类型();
如:Animal a = new Cat();
向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。
一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。
子类类型 变量名 = (子类类型) 父类变量名;
如:Cat c =(Cat) a;
为什么要转型
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥
有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子
类特有的方法,必须做向下转型。
为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验。
变量名 instanceof 数据类型
如果变量属于该数据类型,返回true。
如果变量不属于该数据类型,返回false。
public abstract class Animal {
public abstract void eat();
}
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫吃鱼");
}
// 子类特有方法
public void catchMouse() {
System.out.println("猫抓老鼠");
}
}
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗吃SHIT");
}
public void watchHouse() {
System.out.println("狗看家");
}
}
/*
向上转型一定是安全的,没有问题的,正确的。但是也有一个弊端:
对象一旦向上转型为父类,那么就无法调用子类原本特有的内容。
解决方案:用对象的向下转型【还原】。
*/
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;
}
}
/*
如何才能知道一个父类引用的对象,本来是什么子类?
格式:
对象 instanceof 类名称
这将会得到一个boolean值结果,也就是判断前面的对象能不能当做后面类型的实例。
*/
public class Demo02Instanceof {
public static void main(String[] args) {
Animal animal = new Dog(); // 本来是一只狗
animal.eat(); // 狗吃SHIT
// 如果希望掉用子类特有方法,需要向下转型
// 判断一下父类引用animal本来是不是Dog
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.watchHouse();
}
// 判断一下animal本来是不是Cat
if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.catchMouse();
}
giveMeAPet(new Dog());
}
public static void giveMeAPet(Animal animal) {
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.watchHouse();
}
if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.catchMouse();
}
}
}
注意,引用变量指向谁,就会调用谁的方法。P虽然声明的是父类型的引用,但是程序运行起来之后,指向的是子类型的对象,因此,就会调用子类型对象的方法。
子类中没有显示定义这个方法了(覆盖父类的方法),那么由于从父类继承过来了sing这个方法了,因此,通过子类对象调用方法时就会调用继承过来的方法(父类中的方法)。
class Parent{
/*
public void sing(){
System.out.println("parent is singing");
}
*/
}
class Child extends Parent{
public void sing(){
System.out.println("child is singing");
}
}
public class PolyTest{
public static void main(String[] args){
Parent p = new Child();
p.sing();//报错
}
}
在Parent中找sing方法。找不到,报错。
因为,编译的时候,jvm是看引用变量的类型当中是否有对应的方法来进行编译的,也就是静态的,因为还没运行,没有对象呢,自然不知道引用变量到底指向哪个对象的。
也就是说p虽然是指向子类对象的引用,但是p这个变量的类型是Parent,而Parent中现在没有sing方法,也就是说,对于多态,父类型的引用变量,指向子类型的对象,然后调用了子类对象的某个方法,那么就要求,这个方法必须在父类中也要存在,因为这个p本身是父类型的。那么什么类型的才能使用什么类型的方法,虽然实际指向的是子类的对象。不能说父类中没有这个方法,子类中有,然后进行调用,这样调用不了的,编译的时候,看到的p的类型就是Parent类型的,因此首先就是检查父类中是否有这个方法的。如果有这个方法,然后在执行的时候,创建了对象了,父类型引用指向了子类型对象了(动态绑定),再去子类对象中调用不管是继承过来的还是重写的那个sing方法。如果子类中没有重写这个方法,那么此时就会向上找,找父类中的该方法进行调用。
class Parent{
public void sing(){
System.out.println("parent is singing");
}
}
class Child extends Parent{
/*
public void sing(){
System.out.println("child is singing");
}
*/
}
public class PolyTest{
public static void main(String[] args){
Parent p = new Child();
//子类没有重写,子类对象调用,当前子类中没有,因此,向上找父类,调用父类中的sing,
//如果子类中重写了,那么就调用子类中的方法
p.sing();
}
}
编译的时候,肯定是看什么类型的引用变量,那么就调用什么类型里面的方法,是这种规则去编译检查的。因为编译的时候,还不能确定p到底指向的是什么对象(因为编译时new方法是不会真正执行去创建对象的),也就是说,编译的时候,都是字面上看到的,都是静态的,而new的对象是在运行的时候才会创建的。只有运行的时候,才知道p指向的具体的对象是谁。
父类型的引用转换为子类型的引用,向下类型转换。转换的原则是,这个引用变量实际指向的是什么类型的子类对象,那么就向下转换为什么类型的。
转换之后,那么就只能调用子类型中的方法了,父类型中的特有的方法就调用不了了。因为,引用变量到底能够调用什么方法,主要是看它的类型的,看它类型中有什么方法的。
向下类型转换的另外一个目的,就是可以调用子类中特有的方法,在父类中没有的方法了。否则不转换的话,直接调用子类中特有的方法,编译会报错。
class Animal{
public void sing(){
System.out.println("animal is singing");
}
}
class Dog extends Animal{
public void sing(){
System.out.println("dog is singing");
}
}
class Cat extends Animal{
public void sing(){
System.out.println("cat is singing");
}
}
public class PolyTest{
public static void main(String[] args){
Animal a = new Dog();
Dog dog = (Dog)a;
dog.sing();
Animal b = new Cat();
Cat cat = (Cat)b;
cat.sing();
//error,编译是能够通过的,因为,编译的时候,不知道b到底是指向谁呢,只有运行的时候,才会知道。
//运行时错误。
//因此,向下类型转换,就是这个引用变量指向哪个子类对象,那么就能够转换为那个子类的引用。指向谁,就转成谁。
//通过上面可以看到,编译时候没错,运行时报错,因此,多态是一个运行期的行为,而不是编译期的行为。也就是java程序,只有到真正运行的时候,才知道这个b到底是指向的什么对象。编译的时候,只是知道b是Animal类型的,但是具体指向谁是不知道的,因为右边的new对象,还没有开辟内存创建对象呢。
//编译的时候,只是知道b是Animal类型的,而d是Dog类型的,在同一个继承树上的上下继承关系,因此,能够向下类型转换。
//当运行的时候,知道b真正指向的是Cat的对象,所以,类型转换错误。
Dog d = (Dog)b;
d.sing();//error
}
指向谁,就调用谁的方法。类型转换的时候,就转换成谁。
class Animal{
public void sing(){
System.out.println("animal is singing");
}
}
class Dog extends Animal{
public void sing(){
System.out.println("dog is singing");
}
}
class Cat extends Animal{
public void sing(){
System.out.println("cat is singing");
}
}
public class PolyTest{
public static void main(String[] args){
Animal animal = new Animal();
Cat cat = (Cat)animal;//编译通过,运行报错。 //因为Cat是Animal的一个子类,因此,在编译的时候,编译器只是知道这些信息,但是animal这个变量到底指向的是谁,只有在执行的时候才能够确定下来,而在执行的时候,jvm发现,animal指向的是一个Animal类型的对象而不是指向的Cat类型的对象,那么强制转为Cat类型,就肯定错误的了,就例如,将动物强制转换为猫,就肯定不对了。指向谁才能转换成谁,例如,指向Cat才能转换为Cat。现在指向的是Animal,就不能转为Cat。
}
}
一共两种类型转换:
a、向上类型转换(upcast):比如Cat类型转为Animal类型,即子类型转为父类型。对于向上类型转换,不需要显示指定。
b、向下类型转换(downcast):比如将Animal类型转为Cat类型,即将父类型转为子类型。对于向下类型转换,必须显示指定(必须使用强制类型转换)。
Animal animal = new Cat()//这就是向上类型转换。
Cat cat = new Cat()
Animal animal = cat
animal.sing()//这种也是向上类型转换。
多态中,子类就是父类,这种说法是对的。但是父类就是子类,这种说法就不对,因为父类引用必须是指向子类对象,才能说父类就是子类。
向下类型转换的时候,要看父类型的引用到底指向的是谁,如果父类型的引用就是指向的父类型的对象,那么就不能向下类型转换了。编译通过,运行的时候就错误。
Animal a = new Animal();
Cat cat = (Cat) a;
cat.sing();
这里调用a.sing()和调用cat.sing()效果一样的,因为都指向的Cat类型的对象。但是为何还要使用向下类型转换呢?原因是,父类没有的,子类可以添加,也就是子类中可能具有父类没有的方法,因此,这个时候,就只能向下转换为子类引用,然后才能调用子类特有的方法。如果不向下类型转换的话,那么这个引用变量的类型就是父类型的,而调用方法的时候,jvm是根据变量的类型中有什么方法来进行调用的,这样就只能调用父类型中有的方法了。因此,就需要进行向下类型转换才行。
强制类型转换,是在子类中具有子类特有的方法,父类中没有这个方法的时候,才有使用的必要。
多态是运行期的行为,不是编译期的行为,此前的例子,都是在编译的时候,眼睛直接能够看到引用变量指向的是谁,下面的例子是在编译的时候,我们眼睛看不出指向的是谁,只有运行的时候才知道。
class A{
public void method(){
System.out.println("A");
}
}
class B extends A{
public void method(){
System.out.println("B");
}
}
class C extends A{
public void method(){
System.out.println("C");
}
}
class D extends A{
public void method(){
System.out.println("D");
}
}
public class PolyTest{
public static void main(String[] args){
A a = null;
if(args[0].equals("1")){
a = new B();
}else if(args[0].equals("2")){
a = new C();
}else if(args[0].equals("3")){
a = new D();
}
a.method();
}
}
这就是晚绑定,也就是编译的时候,不知道,只有执行的时候,才能确定跟哪个对象绑定。编译的时候,不知道a到底指向哪个对象。
上面的程序,目前来说,无法实现向下类型转换,因为要向下类型转换,必须明确知道a指向的是谁,但是现在a到底指向谁只有运行的时候才知道。这个需要反射学完才能解决。这个向下类型转换只有运行期的时候才能知道,而反射正好是一个运行期的东西。
实例变量是静态绑定的即早绑定在编译阶段就已经确定了的所以编译阶段发现var在子类中没有定义那么就会向上找发现父类中有定义并且能继承那么就绑定
父类型的引用变量指向子类型的对象。
当将子类对象引用传递给声明为父类的对象变量,如果子类有的方法,那么就调用子类的方法,如果子类中没有这个方法,那么就调用父类的这个方法。
有的书将方法的重载也看成是多态,叫做编译时多态。在编译时刻根据传递变量不同,调用相应的方法。
将这里的多态,叫做运行时多态。在运行时,根据传递对象引用的不同,调用相应对象的方法。
因为new操作是在运行时刻才会产生对象的,而编译的时候是不会产生对象的。
子类对象是父类的实例,反之不对。
案例
public interface USB {
public abstract void open(); // 打开设备
public abstract void close(); // 关闭设备
}
// 键盘就是一个USB设备
public class Keyboard implements USB {
@Override
public void open() {
System.out.println("打开键盘");
}
@Override
public void close() {
System.out.println("关闭键盘");
}
public void type() {
System.out.println("键盘输入");
}
}
// 鼠标就是一个USB设备
public class Mouse implements USB {
@Override
public void open() {
System.out.println("打开鼠标");
}
@Override
public void close() {
System.out.println("关闭鼠标");
}
public void click() {
System.out.println("鼠标点击");
}
}
public class Computer {
public void powerOn() {
System.out.println("笔记本电脑开机");
}
public void powerOff() {
System.out.println("笔记本电脑关机");
}
// 使用USB设备的方法,使用接口作为方法的参数
public void useDevice(USB usb) {
usb.open(); // 打开设备
if (usb instanceof Mouse) { // 一定要先判断
Mouse mouse = (Mouse) usb; // 向下转型
mouse.click();
} else if (usb instanceof Keyboard) { // 先判断
Keyboard keyboard = (Keyboard) usb; // 向下转型
keyboard.type();
}
usb.close(); // 关闭设备
}
}
public class DemoMain {
public static void main(String[] args) {
// 首先创建一个笔记本电脑
Computer computer = new Computer();
computer.powerOn();
// 准备一个鼠标,供电脑使用
// Mouse mouse = new Mouse();
// 首先进行向上转型
USB usbMouse = new Mouse(); // 多态写法
// 参数是USB类型,我正好传递进去的就是USB鼠标
computer.useDevice(usbMouse);
// 创建一个USB键盘
Keyboard keyboard = new Keyboard(); // 没有使用多态写法
// 方法参数是USB类型,传递进去的是实现类对象
computer.useDevice(keyboard); // 正确写法!也发生了向上转型
// 使用子类对象,匿名对象,也可以
// computer.useDevice(new Keyboard()); // 也是正确写法
computer.powerOff();
System.out.println("==================");
method(10.0); // 正确写法,double --> double
method(20); // 正确写法,int --> double
int a = 30;
method(a); // 正确写法,int --> double
}
public static void method(double num) {
System.out.println(num);
}
}