探究一个问题:接口中的方法到底能有具体实现吗?
发现问题
这是我无意间发现的一个问题,有一天在写代码的时候突然发现,我实现了一个接口却没有将他的抽象方法重写完,这还不是问题,问题是他还不报错。这是怎么回事呢?按理说接口的定义不就是:一个类实现该接口就必须重写其所有方法吗,为啥我没有把他的抽象方法重写完,还不报错呢?难道是编译器出错了?
寻找答案
于是我查了很多资料,发现:原来接口中的方法也是能有具体的实现的,这种改动源自于java8(我们口中所说的java8,JDK8,JDK1.8其实都是同一个东西,这里我就这样叫了)。也就是说在java8之前是没有这种语法的。看来,要想在IT一行干的好,必须要与时俱进啊。
具体原因
自从 java8开始就允许了接口里的方法有具体的实现,这个方法包含:默认方法和静态方法。这两个种方法都有具体的应用场景
默认方法的应用场景
我们先回忆一下接口的定义:接口里的方法默认都是public abstract修饰的
因此接口里的方法都是抽象方法,但是看完我的讲解,你会有不一样的发现
场景一:
我们都知道一个软件在使用的过程中免不了要更新,这种更新有助于拓展新的功能、修复bug等。当然JDK也不例外,当用户在使用过程中遇到什么更加复杂的场景,那么JDK的开发团队就会想办法新增一些功能来应对这些场景。但是如果直接在我们原先定义好的接口上新增的话,那么实现这个接口的所有类必须要重写这个接口的所有方法。如果是这样,那些在公司中已经在运转的项目难道要停一段时间,先把方法给重写了吗?这显然是不现实的,如果要是这样搞,那谁还想继续升级JDK啊,升级一次就迎来一次经济损失。况且,即使重写了方法也是个空的实现,根本用不到(此时还用不到这新增的功能)。这就体现了默认方法的重要性。
总结来说就是:默认方法提供了一种在接口中添加新方法的方式,而不会破坏已有的实现类,这样做的好处就是:我们可以向接口中添加新的功能,但不会破坏已有的代码。
场景二:
每当我们实现一个接口的时候,总要重写这个接口的所有抽象方法,但是有一种可能:我们不需要将所有的方法重写完就能完成我们的业务。此时默认方法的作用也就体现出来了,新增默认方法没有重写的负担,也就是在后来实现这个接口的时候不用重写默认方法,但是可以直接调用,这也是接口里的方法具体化的好处,还能减少了代码的无用逻辑。
默认方法的使用
我们可以看到:Cat直接实现Animal但不重写bark方法时,如果接口里的方法是默认方法(带有default修饰),是不会报错的。
java
public interface Animal {
//这个方法就是默认方法
default void bark(){
System.out.println("动物会叫");
}
}
public class Cat implements Animal{
}
加上public不会报错,但是如果我们将abstract加上会发生报错,说明此时的方法已经不是abstract修饰了,而是public default修饰,说到这里不知道你会不会有点懵,为啥default和public一起用了,这两个不是同一级别的吗,不都是修饰限定符吗?其实这里是个特殊的用法。这里有一些百度的解释,可以了解一下。
因此我们就不用重写,直接能调用我们继承的属性了
其实说到这,继承(extend)和实现(implement)的区别也想一起说说,
说实话,他们有联系又有区别。但总结来说:
- 继承是:is a 的关系,就比如猫是动物,猫继承了动物的属性
- 实现是:has a 的关系,一个猫实现了 " 跑 " 接口,就表明它拥有了跑这个功能
那是概念上来说。如果放在代码层面,其实都能为我们原来的类新增一些功能或属性以便我们直接调用。
java
public interface Animal {
default void bark(){
System.out.println("动物会叫");
}
}
public class Cat implements Animal{
public static void main(String[] args) {
//这里我们没有重写bark方法,但是能打印出来结果,说明默认方法被调用了
Cat cat = new Cat();
cat.bark();
}
}
运行结果
静态方法的应用场景
想一想如果此时有一个类继承了两个接口,但是两个接口同时含有重名的方法时,此时如果你不进行重写,那么你实例化以后调用的到底是哪一个bark?可以看到IDEA都报错了,所以我们是不会知道到底会调用哪一个方法的,这就体现了静态方法的重要性。下面有图:
静态方法:默认使用public static修饰,用于提供与接口相关的实用方法或工具方法。
java
public interface Animal {
default void bark(){
System.out.println("动物会叫");
}
}
public interface Creature {
default void bark(){
System.out.println("生物会叫");
}
}
public class Cat implements Animal,Creature{
public static void main(String[] args) {
Cat cat =new Cat();
cat.bark();
}
}
静态方法的使用
此时我们将其中一个接口里的方法改成静态的,那么就不会发生报错,但是由于方法成了静态方法,就成了类的属性,而不是对象的属性了,所有我们使用new对象的方式是拿不到静态方法的,所以只能打印default方法的内容。
java
public interface Animal {
//我们将这里改成static
static void bark(){
System.out.println("动物会叫");
}
}
public interface Creature {
default void bark(){
System.out.println("生物会叫");
}
}
public class Cat implements Animal,Creature{
public static void main(String[] args) {
Cat cat =new Cat();
cat.bark();
}
}
虽然不能使用对象调用,但是我们可以通过 类名. 方法名的方式来调用
java
public interface Animal {
static void bark(){
System.out.println("动物会叫");
}
}
public interface Creature {
default void bark(){
System.out.println("生物会叫");
}
}
public class Cat implements Animal,Creature{
public static void main(String[] args) {
// Cat cat =new Cat();
// cat.bark();
Animal.bark();
}
}
运行结果:
这也大大方便了我们。
思维拓展(我们身边的实例)
不知道你有没有注意过一些接口,能用于集合排序的Collections接口和 Map接口
Collections接口的源码:
可以看到这个sort方法就是static修饰的,因此能直接使用类名.方法名调用,这也就是我们能直接调用sort方法的原因。
你说我之前直接用这个方法的时候怎么没有想这么多呢?知道他能排序,便感觉又掌握了一个新的方法,感觉自己又进步了,殊不知真正的进步是追根溯源。
Map接口源码:
可以看到forEach方法被default方法修饰的,所以我们在实现完这个接口以后不用重写forEach方法,只需要new对象调用就行了。
承接上文,我们知道了接口里的方法并不一定是public abstract修饰了
也能是 public default ,public static修饰。
说到最后,你应该对接口有了更加深入的了解了。
总的来说:
其实接口就是相当于一个框架一样,他把一些事物的统一属性给抽象出来了,主要功能就是为了让我们在写代码的时候不跑偏,如果按照这个框架写,那么你就不会跑偏,这也是前人给我们总结的经验啊,唉,为了我们操碎了心。