为什么要有默认方法

在 Java8 之前,接口与其实现类之间的耦合度太高了,当需要为一个接口添加方法时,所有的实现类都必须随之修改。默认方法解决了这个问题,它可以为接口添加新的方法,而不会破坏已有的接口的实现。这在 lambda 表达式作为 Java8 语言的重要特性而出现之际,为升级旧接口且保持向后兼容提供了途径。

1
2
3
	String[] array = new String[] { "hello", ", ", "world", };
	List<String> list = Arrays.asList(array);
	list.forEach(System.out::println); // 这是 jdk 1.8 新增的接口默认方法

这个forEach方法是 jdk 1.8 新增的接口默认方法,正是因为有了默认方法的引入,才不会因为Iterable接口中添加了forEach方法就需要修改所有Iterable接口的实现类。

1
2
3
4
5
6
7
8
public interface Iterable<T> {
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
}

默认方法的继承

 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
26
27
28
29
30
31
32
33
34
35
36
37

interface InterfaceA {
    default void foo() {
        System.out.println("InterfaceA foo");
    }
}
 
interface InterfaceB extends InterfaceA {
}
 
interface InterfaceC extends InterfaceA {
    @Override
    default void foo() {
        System.out.println("InterfaceC foo");
    }
}
 
interface InterfaceD extends InterfaceA {
    @Override
    void foo();
}
 
public class Test {
    public static void main(String[] args) {
        new InterfaceB() {}.foo(); // 打印:“InterfaceA foo”
        new InterfaceC() {}.foo(); // 打印:“InterfaceC foo”
        new InterfaceD() {
            @Override
            public void foo() {
                System.out.println("InterfaceD foo");
            }
        }.foo(); // 打印:“InterfaceD foo”
        
        // 或者使用 lambda 表达式
        ((InterfaceD) () -> System.out.println("InterfaceD foo")).foo();
    }
}

接口默认方法的继承分三种情况(分别对应上面的 InterfaceB 接口、InterfaceC 接口和 InterfaceD 接口):

  • 不覆写默认方法,直接从父接口中获取方法的默认实现。
  • 覆写默认方法,这跟类与类之间的覆写规则相类似。
  • 覆写默认方法并将它重新声明为抽象方法,这样新接口的子类必须再次覆写并实现这个抽象方法。

默认方法的多继承

Java 使用的是单继承、多实现的机制,为的是避免多继承带来的调用歧义的问题。当接口的子类同时拥有具有相同签名的方法时,就需要考虑一种解决冲突的方案。

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

public class DefaultMethodMuiltExtendTest {

	interface InterfaceA {
		default void foo() {
			System.out.println("InterfaceA foo");
		}
	}

	interface InterfaceB {
		default void bar() {
			System.out.println("InterfaceB bar");
		}
	}

	interface InterfaceC {
		default void foo() {
			System.out.println("InterfaceC foo");
		}

		default void bar() {
			System.out.println("InterfaceC bar");
		}
	}

	class ClassA implements InterfaceA, InterfaceB {
	}

// 错误
//	class ClassB implements InterfaceB, InterfaceC {
//	}

	class ClassB implements InterfaceB, InterfaceC {
		@Override
		public void bar() {
			InterfaceB.super.bar(); // 调用 InterfaceB 的 bar 方法
			InterfaceC.super.bar(); // 调用 InterfaceC 的 bar 方法
			System.out.println("ClassB bar"); // 做其他的事
		}
	}

}

在 ClassA 类中,它实现的 InterfaceA 接口和 InterfaceB 接口中的方法不存在歧义,可以直接多实现。

在 ClassB 类中,它实现的 InterfaceB 接口和 InterfaceC 接口中都存在相同签名的 foo 方法,需要手动解决冲突。覆写存在歧义的方法,并可以使用InterfaceName.super.methodName();的方式手动调用需要的接口默认方法。

接口继承行为发生冲突时的解决规则

有如下情况

 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

interface InterfaceA {
    default void foo() {
        System.out.println("InterfaceA foo");
    }
}
 
interface InterfaceB extends InterfaceA {
    @Override
    default void foo() {
        System.out.println("InterfaceB foo");
    }
}
 
// 正确
class ClassA implements InterfaceA, InterfaceB {
}
 
class ClassB implements InterfaceA, InterfaceB {
    @Override
    public void foo() {
//        InterfaceA.super.foo(); // 错误
        InterfaceB.super.foo();
    }
}

当 ClassA 类多实现 InterfaceA 接口和 InterfaceB 接口时,不会出现方法名歧义的错误。当 ClassB 类覆写 foo 方法时,无法通过 InterfaceA.super.foo(); 调用 InterfaceA 接口的 foo方法。

因为 InterfaceB 接口继承了 InterfaceA 接口,那么 InterfaceB 接口一定包含了所有 InterfaceA 接口中的字段方法,因此一个同时实现了 InterfaceA 接口和 InterfaceB 接口的类与一个只实现了 InterfaceB 接口的类完全等价。

这很好理解,就相当于 class SimpleDateFormat extends DateFormat 与 class SimpleDateFormat extends DateFormat, Object 等价(如果允许多继承)。

而覆写意味着对父类方法的屏蔽,这也是 Override 的设计意图之一。因此在实现了 InterfaceB 接口的类中无法访问已被覆写的 InterfaceA 接口中的 foo 方法。

建议

通过这个例子,应该注意到在使用一个默认方法前,一定要考虑它是否真的需要。因为 默认方法会带给程序歧义,并且在复杂的继承体系中容易产生编译错误。滥用默认方法可能给代码带来意想不到、莫名其妙的错误。