和类一样,接口也是java中一种重要的数据类型,用接口声明的变量称作接口变量。那么接口变量中可以存放怎样的数据呢?
-
接口属于引用型变量,接口变量中可以存放实现该接口的类的实例的引用。接口中可以没有成员变量,但是如果有成员变量,这些成员变量必须是public static final的,即公共的、静态的、不可变的常量,这是默认的 不必写出
eg:public interface MyInterface {
int a = 100; // 这是一个公共的、静态的、不可变的常量
void myMethod(); // 这是一个抽象方法
}
接口的成员变量就一定会被实现类所继承,并且这个成员变量无法更改 -
接口中的方法默认都是抽象方法,从Java 8开始,接口可以包含默认方法和静态方法,这些方法可以有具体的实现。
默认方法使用default关键字修饰,允许在接口中提供方法的默认实现。例如:
public interface MyInterface {
default void myMethod() {
System.out.println(“This is a default method.”);
}
}
在这个例子中,myMethod是一个默认方法,实现MyInterface接口的类可以选择是否重写这个方法。
静态方法 Java 8还引入了静态方法,这些方法可以直接通过接口名调用,而不需要实现类的实例。例如:
public interface MyInterface {
static void myStaticMethod() {
System.out.println(“This is a static method.”);
}
} -
在Java语言中,接口回调是指:可以把实现某一接口的类创建的对象的引用赋值给该接口声明的接口变量,那么该接口变量就可以调用被类实现的接口方法。实际上,当接口变量调用被类实现的接口方法时,就是通知相应的对象调用这个方法。
接口回调比较类似于上转型对象的逻辑 但是还是要区别理解
接口类创建的对象 即 接口对象(在使用机制上类似于上转型对象)
可以调用
实现类中 (在使用机制上类似于上转型对象对应的子类)
的
重写后的方法 和 从接口中得来的的默认方法(即接口中的default方法)
(// 当然如果接口中的默认方法 也在实现类中被重写了 那接口对象调用的就是重写后的默认方法了)
但是 接口对象 不能调用 实现类中 新写的方法
简单点说 就是 接口对象可以调用实现类中重写后的方法 和未重写的默认方法 ,但是不能调用实现类中新写的方法
注意: 接口对象 和 上转型对象 虽然都不能调用新写的方法,但他们的逻辑原因是完全不同的:
- 接口对象不能调用实现类新写的方法是基于接口的定义规范,它只定义了特定的方法签名集合,实现类额外的方法不在这个定义范围内;
//意思就是说 接口对象只能调用接口中的定义“行为规范”的方法 就是没有方法体,没写出具体实现的方法
在实现类中,已经对方法进行了重写即进行了实例化,所以实现类中的方法已经不再是一种“行为规范” 了,方法已经有具体实现了,所以接口对象不能调用
- 而上转型对象不能访问子类新定义的成员变量更多是基于类的继承和多态的设计机制。
//以父类是Animal 子类是Dog()为例 对于 Animal animal = new Dog();
上转型对象的引用已经指向了子类对象却还是不能调用子类中新填的成员的原因:
编译器只知道animal是一个Animal类型的引用,它无法确定这个引用实际上指向的是一个Dog类的实例,因此它只能允许访问Animal类中定义的成员去避免错误访问 所以无法调用能调用重写方法的原因:
在编译阶段,编译器只检查引用类型(即父类类型)中是否存在要调用的方法。由于子类重写的方法与父类中的方法具有相同的签名(方法名、参数列表、返回类型等都相同),所以在编译时是合法的
那么在编译时通过了之后就进入到了运行阶段 在这个运行阶段就有JVM虚拟机来识别具体引用了
JVM会根据对象引用指向的实际类型(这里是子类类型)来确定调用的方法。因为实际对象是子类对象,而子类重写了父类的方法,所以会调用子类中重写后的方法。
对于接口回调: 接口对象 和 实现类对象
与
上转型对象: 上转型对象 和 上转型对象的子类对象
这两对关系
调用机制上差不多 主要的差别就是接口回调只存在方法的重写调用 不存在属性变量之类的交互和访问了 只涉及方法
而上转型对象和它的子类对象除了存在方法的重写调用外
还涉及到成员变量的重写调用(即成员变量的隐藏)
深入理解接口的作用:
问题:
既然接口的接口调用 和 抽象类的上转型对象
这两种操作行为 都是让各自的 “子类” 去重写抽象方法,选择性重写 “实例方法”
那为什么还要有接口这个东西呢 ? 感觉就靠抽象类就可以完成了啊?
(这段问题描述中加引号的概念要注意 接口中并没有 “子类” 的概念 这里指的是实现类 同理 加引号的“实例方法”其实在接口中指的是 默认方法 之所以这么写 是为了让问题描述更加简洁 但是不要产生理解误区 )
回答:
因为接口可以多重继承 而抽象类不能
在Java中,一个类只能继承一个抽象类,但可以实现多个接口。
例如,假设有一个类既需要表现出“可打印”的行为(有打印相关的方法),又需要表现出“可序列化”的行为(有序列化相关的方法)。如果使用抽象类,由于不能多重继承,就很难同时从两个抽象类中获取这些特性。但如果定义“Printable”和“Serializable”两个接口,一个类就可以同时实现这两个接口来获得两种不同的行为。
对该例子的理解:
问题:
直接在抽象类中写两个抽象方法 一个是可打印 一个是可序列化 然后在子类中把这俩方法都重写,一个类不就可以同时实现两种行为了吗 为什么非要用到接口呢?
回答:
从功能实现的角度看:
- 单一抽象类的局限性
从功能实现上来说,这种利用抽象类的做法确实可以让子类继承一个抽象类并实现打印和序列化相关的功能。然而,这种做法存在一些局限性。如果将打印和序列化这两种不同性质的行为放在一个抽象类中,从设计的角度来看,这两种行为可能并没有内在的类层次关系。例如,打印功能可能与用户界面相关的类层次结构更相关,而序列化功能可能与数据存储相关的类层次结构更相关。把它们放在一个抽象类中会导致抽象类的职责不单一,违反了单一职责原则。
而接口就可以很好地解决这个问题。
这里再举一个例子以便更容易理解,
假设有一个出租车类需要实现 控制内部温度(实际生活中意思就是安装空调) 和 收费 这两种功能,那又有一个电影院类也需要实现 控制内部温度 和 收费 两种功能;那从功能角度上来讲,虽然说可以在一个抽象类里写两个抽象方法,分别是 控制内部温度抽象方法 和 收费抽象方法,然后让出租车类和电影院类都去继承这个类以解决 但是这样的设计是不合理的(违背了单一职责原则)
接下来说一下为什么这样设计是不合理的: 虽然刚才这种设计在功能方面可行了,可是 ,这符合现实生活中的逻辑吗,出租车和电影院怎么能是一类的呢?怎么会有相同的父类呢?这样的设计显然会导致误解或者出现不必要的耦合问题(职责耦合),比如说在让出租车类和电影院类都去继承完这个抽象类后,用户又要求写一个电瓶车类,只需实现收费功能,那这个时候让电瓶车类去继承刚才写的抽象类的话,那电动车类肯定也需要重写控制温度的抽象方法,但是我压根不需要控制温度啊,那这个时候我该怎么处理呢,问题就变得复杂难以解决了。想要解决就得再单独写一个只具有收费抽象方法的抽象类,然后让电动车类去继承,就比较麻烦,这只具有两个功能还好说,那如果需要实现的功能多了呢,用户要求的类也多了呢,难道每个都去新建一个抽象类再去继承吗,那代码就会出现大量的类似重复,也会增加不必要的工作量,这也是为什么抽象类的职责要单一,要遵循单一职责原则,因为如果不遵循单一职责原则去设计抽象类,就会导致职责耦合,代码的可维护性和复用性降低。
所以这样看来,以继承抽象类去解决这样的问题显然是不合理的,这个时候就需要用到接口了。
比如若是让 出租车类 和 电影院类 都去实现 控制内部温度的接口 和 收费的接口 那就符合逻辑了
那么让我们再回到最初举的打印和序列化的例子,来看一下接口在处理这种问题时的优点
- 接口的灵活性与解耦性:
使用接口时,每个接口都代表一种独立的行为规范。“Printable”接口专门定义打印相关的行为,“Serializable”接口专门定义序列化相关的行为。当一个类需要这些行为时,它实现这些接口。这样在设计上更加灵活,也更容易解耦。如果以后需要对打印或者序列化功能进行修改或者扩展,只需要在对应的接口及其实现类中进行操作,而不会影响到与其他不相关功能的类关系。例如,如果要改变打印的实现方式(如从普通打印变为彩色打印),只需要修改实现“Printable”接口的类,而不会影响到与序列化相关的类。
从复用性角度来看
-
抽象类复用的限制
在复用性方面,抽象类中的方法可能会包含一些与抽象类自身性质相关的默认实现或者状态。如果将打印和序列化放在一个抽象类中,当其他类只需要其中一种行为(比如只需要打印功能而不需要序列化功能)时,由于继承的是整个抽象类,可能会引入一些不必要的代码或者状态。 -
接口的高复用性
接口则具有更高的复用性。不同的类可以根据自己的需求选择性地实现不同的接口。例如,一个只用于打印报表的类可以只实现“Printable”接口,而一个需要在网络传输前进行序列化的数据类可以只实现“Serializable”接口。这种高复用性使得代码更加模块化,易于维护和扩展。
因此 接口的存在是十分必要的
但是,要注意的是,并不是说所有的情况都是使用接口最好。
要注意接口和抽象类的区别,因为抽象类中虽然有时候会增加不必要的耦合,但是它可以写非抽象方法啊,在继承的时候还可以传递成员变量啊,所以要根据具体情况具体分析。
有时,还将抽象类的继承和接口一起使用, 比如: 所有的汽车都需要具有刹车方法,而出租车还需要额外拥有 控制内部温度 和 收费 功能;那首先就需要定义一个所有汽车的抽象类abstract class car,在里面定义一个抽象的刹车方法; 然后定义两个接口A和B,A接口里有控制内部温度的方法,B接口里有收费方法
这个时候出租车类就需要这样写:
class 出租车 extends car implements A,B{ … //具体实现 }
这样就可以完美的实现我们所需要的所有功能了,也不会导致不必要的耦合
注意:
不要把接口中的default方法理解为 普通的实例方法
接口本身是一种抽象类型,不应该有状态(即实例属性),接口中的默认方法虽然看起来像是实例方法(可以被实现类的对象调用),但它是接口的一部分。接口主要是定义行为规范,默认方法更多的是为了方便接口的演化。例如,当在接口中添加一个新的方法时,如果没有默认方法的概念,所有实现该接口的类都需要立即实现这个新方法,这可能会导致大量的代码修改。默认方法提供了一种默认的实现,实现类可以选择重写或者直接使用这个默认实现。