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之后,接口中支持定义方法可以包括私有的、抽象的、默认的、静态的。
  • 一个类实现了多接口时,若存在相同方法,注意他们的调用方式和冲突。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容

  • 一、基础知识:1、JVM、JRE和JDK的区别:JVM(Java Virtual Machine):java虚拟机...
    杀小贼阅读 2,360评论 0 4
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,076评论 1 32
  • 今天是个阴沉湿冷的天气,中午去超市遛了一圈,为了买漏斗倒腾一坛子黄酒。回家又炒了一碗尖椒鸡蛋酱。酒和辣味似属于阳,...
    苏梅LI阅读 189评论 0 6
  • @property与@synthesize : http://www.jianshu.com/p/bcf734db...
    DingMaster阅读 368评论 0 0
  • 咪蒙的文章又一次刷爆朋友圈。随后就看到和菜头、傅踢踢等人围绕《有趣,才是一辈子的春药》这篇文章发声。 为什么这么多...
    青珥阅读 466评论 4 7