Java Interface私有方法你真的会用吗?

【本文更新于2019年5月20日】

更新内容是将例子讲的更通俗易懂。

由于本文属于中高阶知识,其中涉及interface知识、类的继承、抽象类、方法重载等基础知识,同学可以先自行了解一下。

1. interface简介

interface是java中的一个关键字,用于定义接口类,它最主要的作用封装一定功能的集合,被定义为接口的类不能实例化。

它的另外一个常见的用法,是用于回调功能。

通俗点讲,接口类它本身不具备具体的功能,只定义一些模版或者规范,由子类去实现具体的功能。

Java 7 版本以前,接口类只能定义一些抽象方法与常量,子类必须重写接口类中定义的全部方法。

这样就有一个明显的弊端,不需要实现的方法,也要在子类重写。

在Java 8 的时候,JDK支持接口类定义静态方法和默认方法。

在Java 9 的时候,又进行了改进,支持私有方法和私有静态方法的定义。

注意,这里我说的是方法,而不是抽象方法。也就是说JDK8与JDK9可以在接口类中定义方法体了。

2. 三个JDK版本的接口设计功能对比

Java 7 Java 8 Java 9
常量 常量 常量
抽象方法 抽象方法 抽象方法
默认方法 默认方法
静态方法 静态方法
私有方法
私有静态方法

2.1 常量与抽象方法

常量与抽象方法是接口类的基础功能。

接口中定义的变量默认是public static final 型,也就是一个常量,且必须给其初值,实现类中不能重新定义,也不能改变其值

抽象方法使用abstract修饰,通常abstract省略不写。

如下代码,是演示定义一个接口类Test以及它的常量与抽象方法。

public interface Test {
    String str = "abc";
    int a = 10;
    abstract void test();
    abstract int sum();
    void test2();
}

2.2 默认方法

java 8开始支持,也称扩展方法。

默认方法使用default 关键字修饰、定义。

默认方法需要写方法体,实现具体的逻辑。

实现类可以不重写默认方法,在需要的时候进行重写。

以下是实现的示例代码:

//接口定义,并定一个默认方法
public interface Test {
    //默认方法
    default void testDefault(){
        System.out.println("default method");
    }
}

//第一个实现类,不重写默认方法
public class TestImpl implements Test {
}
//第二个实现类,重写默认方法
public class TestImpl2 implements Test {
   @Override
    public void testDefault() {
        System.out.println("TestImpl2 method");
    }
}

//测试运行
public class Run {
    public static void main(String[] args) {
        Test test = new TestImpl();
        test.testDefault();

        Test test2 = new TestImpl2();
        test2.testDefault();
    }
}

运行结果:

没有重写父类的方法:default method
重写了父类的方法:TestImpl2 method

【代码解释说明】

接口类Test中定义了一个默认的方法testDefault(),实现了一些具体的功能,示例代码中是打印一句话“default method”。

接着,TestImpl与TestImpl2都实现了接口类Test。

其中只有TestImpl2重写了默认方法并修改了它的功能,示例代码中是打印一句话“TestImpl2 method”。

运行后的结果,也正如所见,默认方法,可以不被子类重写,被子类重写后,会执行子类所实现的具体逻辑。

总结一下:

1.JDK后它允许我们在接口类里添加一个默认方法。
2.默认方法不会破坏实现这个接口的已有类的兼容性,也就是不会强迫接口的实现类去重写默认方法。
3.java.util.Collection包中添加的stream(), forEach()等方法就是最好的例子。

2.3 静态方法

JDK 8开始支持

静态方法使用static关键字修饰、定义。

静态方法需要写方法体,实现具体的逻辑。

静态方法不可以被子类实现或继承。


//定义接口、定义静态方法
public interface TestFactory {

      static Test createTest(int type){
        if(type == 1){
            return new TestImpl();
        }else{
            return new TestImpl2();
        }
    }
}

//测试运行
public class Run {
    public static void main(String[] args) {
       Test test = TestFactory.createTest(1);
       test.testDefault();
       Test test2 = TestFactory.createTest(2);
       test2.testDefault();
    }
}

运行结果:

default method
TestImpl2 method

【代码解释说明】

接口类TestFactory 定义了一个静态方法 createTest(),功能是创建Test的对象实例并返回给调用者。Test类的代码定义见2.2节。

测试运行代码中,通过得到的Test类的对象实例,成功调用其内部的方法。

2.4 默认方法与静态方法的关系

  • 他们都是一个完整的方法体,各自都有具体的功能实现。
  • 静态方法使用static修饰,默认方法使用default修饰。
  • 默认方法内部可以调用静态方法,静态方法内部不可以调用默认方法,因为静态方法只能调用静态方法,这是语法上的限制,很容易理解,下面是默认方法调用静态方法的示例。
public interface Test {
    
//调用静态方法
    default void testDefault(){
        testStatic();
        System.out.println("default method");
    }
    static void testStatic(){
        System.out.println("static method");
    }
}

//测试运行
public class Run {
    public static void main(String[] args) {
       Test test = TestFactory.createTest(1);
       test.testDefault();
    }
}

运行结果:

static method
default method

2.5 私有方法与私有静态方法

  • java 9开始支持
  • 私有方法使用private关键字修饰、定义。
  • 私有静态方法使用private static关键字修饰、定义。
  • private与private static方法,只能接口自身内部调用,实现类或子类不可重写重载。
  • 都需要写方法体,实现具体的逻辑。

他们的定义示例如下:

public interface Test {
   private void test(){
        System.out.println(" private method");
    }
   private static void test2(){
        System.out.println(" private static method");
    }
}

如果你问我,私有方法与私有静态方法只能自身内部调用,是不是没什么意义?

答案肯定是,不是的!

其实,它同class类型对象的私有方法功能是一样的。

  • 可以提高代码的重用性
  • 不让子类或实现类调用,也有很好的安全性。

就拿一个数字签名来说,有PKCS#1与PKCS#7签名。他们有共性就是签名与验签,这两个功能的实现都需要数字证书,其中数字证书的解析功能等,不需要子类去实现,也不希望他们调用,避免遭破坏。那么就可以定义为私有方法。

大致过程如下:

  • 1.需要实现类实现抽象方法,设置签名的类型、证书等
  • 2.接口将签名的具体逻辑使用私有方法达到保护以及可重用
    下面拿代码说明,首先定义签名接口类
public interface Signature {

    int SIGN_PKCS1 = 0;
    int SIGN_PKCS7 = 1;

    //设置签名类型 p1或者P7
    int setType();
    //设置签名的数字证书
    String setCert();

    //具体的签名方法,签名逻辑用私有方法
    default String sign(){
        if(!checkCert()) return null;
        int type = setType();
        if(type == SIGN_PKCS1){
            return pkcs1();
        }else {
            return pkcs7();
        }
    }

    private Object parseCert(){
        //解析证书
        return obj;
    }
    private boolean checkCert(){
        //检验证书是否过期、是否吊销
        parseCert();
        return true;
    }
    private String pkcs1(){
        return "P1 签名结果";
    }
    private String pkcs7(){
        return "P7 签名结果";
    }
}

接口类定义了签名类型、签名方法、检验证书等具体的私有方法, sign()方法为默认方法,实现类可以重写,也可不写。这样子类只需要设置类型和证书即可,没有额外的代码。看代码:

public class SignatureP1 implements Signature {
    @Override
    public int setType() {
        return SIGN_PKCS1;
    }
    @Override
    public String setCert() {
        return null;
    }
}

public class SignatureP7 implements Signature {
    @Override
    public int setType() {
        return SIGN_PKCS7;
    }
    @Override
    public String setCert() {
        return null;
    }
}

有了默认方法和私有方法,实现类不需要强制实现各自公共的逻辑。交由接口类来实现。最后使用的方式为:

public class Run {
    public static void main(String[] args) {
        Signature signature1 = new SignatureP1();
        signature1.sign();
        Signature signature7 = new SignatureP7();
        signature7.sign();
    }
}

运行结果:

P1 签名结果
P7 签名结果

有人说,这他喵的不就是抽象类吗!别说,还真像。

他们有什么区别呢?请看下面我总结的几点:

  • 他们都是抽象类型,都可以定义抽象方法,都有默认方法,不强制实现类实现。

  • 类是单继承,接口可以多实现。

  • 设计理念的不同,抽象类所表现的关系是"is"关系,接口所表现的是"like"关系,有点像python中的鸭子类型,当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。而抽象类不行,必须是鸭子。

  • 前文中提到过,接口中定义的变量公开的静态常量,且必须给其初值,实现类中不能重新定义,也不能改变其值;抽象类中的变量其值可以在子类中重新定义,也可以重新赋值。

说到这里,另外强调接口的两个问题,一个是自身方法调用以及多实现的规则。

3. 方法互相调用问题

默认方法(default)可以调用abstract/private/static/private static方法。
而static方法只能调用static/private static方法。
调用关系不对,编译会不过。

4. 多实现的冲突问题

因为一个类可以实现多个接口,若是这些接口定义的方法存在一样的,便会有冲突。因为这样,子类在调用的时候,不知道调用哪一个接口定义的方法。

像这样,有两种情况下会发生:

  • 两个接口没有任何关系,有相同的方法,一个类同时实现了这两个接口
  • 一个接口继承了另一个接口,一个类同时实现这两个接口

4.1 一个类实现多个没有任何关系的接口

如下代码,TestC实现了TestA,TestB,而TestA,TestB中存在相同的方法

public interface TestA {
    default void test(){
        System.out.println("TestA");
    }
}
public interface TestB{
    default void test(){
        System.out.println("TestB");
    }
}

public class TestC implements TestA,TestB {
}

此时,会出现编译错误:

inherits unrelated defaults for test() from TestA and TestB

也就是说TestA 和 TestB 都有这个 test()方法,不知道要实现哪一个。

解决方法是重写这个相同的方法,方法内部指定一个具体的接口作为实现方法,下面代码指定TestA 作为实现方法。

当然也可以不指定父类接口的实现,重写自己的逻辑。

public class TestC implements TestA,TestB {
    @Override
    public void test() {
        TestA.super.test();
    }
}

进行测试:

public class Run {
    public static void main(String[] args) {
        TestC testC = new TestC();
        testC.test();
    }
}

运行结果

TestA

4.2 同时实现继承关系的两个接口

这种冲突,不能指定一个父接口来解决了,要么重写实现逻辑,要么按照默认的规则来调用,默认规则如下:

  • 声明在类里面的方法优先于任何默认的方法,也就是,在实现类中重写这个相同的方法,调用的时候,优先级最高。
  • 如果默认方法没有在类中实现,优先选取最具体的实现

重写的优先级最高,我们来看优先选取最具体的实现,接口定义如下:

public interface TestA {
    default void test(){
        System.out.println("TestA");
    }
}
public interface TestB extends TestA{
    default void test(){
        System.out.println("TestB");
    }
}
public class TestC implements TestA,TestB {
}

TestC 实现了TestB,TestB并且没用重写test()方法,TestB继承了TestA。测试运行:

public class Run {
    public static void main(String[] args) {
        TestC testC = new TestC();
        testC.test();
    }
}

运行结果

TestB

鉴于此,接口不能提供对Object类的任何方法的默认实现,如接口里不能提供对equals,hashCode以及toString的默认实现。

因为,一个类实现了这个接口并且是Object的子类,已经有了equals/hashCode/toString等方法的实现,那么接口定义的就没有意义了。

在类里实现的方法,调用优先级最高。

5. 总结

  • 掌握JDK7\8\9三个版本中接口类的变化
  • JDK 9之后,接口中支持定义方法可以包括私有的、抽象的、默认的、静态的。
  • 一个类实现了多接口时,若存在相同方法,注意他们的调用方式和冲突。

你可能感兴趣的:(Java Interface私有方法你真的会用吗?)