Java总结 - 抽象类与接口

  • 本文是自己对抽象类和接口的理解,如果不对请指正,谢谢

抽象类的简介

  • 抽象?抽象是什么意思?之前在我的 封装继承多态 一文中提到了一个杯子的概念,简单概括一下就是嘴说出来的是一个抽象的概念,因为并不知道这个杯子的具体参数,比如颜色之类的特点,所以抽象也就是将一个事物的大体结构提取出来,比如我的杯子有盖子,是保温的等,然而盖子是弹射开的还是拧开的以及保温材料的使用一概不知,所以对应到Java中的抽象类,那么这个 抽象类也就是对一个事物的概括,(只是嘴说出来的)
  • 之前提到的is-ahas-a在这看来,抽象类更符合is-a的关系,抽象类可以提供方法实现,也可以不提供,但是其被称为抽象类的话,那么必定在类描述上有abstract关键字,而其中的方法完全可以没有抽象方法的定义
  • 方法提供实现与否即是否是抽象方法,就像是你看中一款杯子,但是杯子的提供商拿不准每个人的手型,所以在你购买这个杯子的时候,需要自己选择杯子柄的形状,这是强制的,对应到Java抽象类中就是抽象方法,即必须由子类提供实现
  • 而杯子的其他特点已经是大众认可了,比如杯子口是圆的,所以提供商就在你不指定的情况下默认这个形状了,对应到Java抽象类中就是非抽象方法,当然你也可以定制杯口形状,对应到Java抽象类中就是子类重写父类方法了
  • 上面提到了杯柄的强制指定,所以在你不指定杯柄的情况下,杯子提供商是不知道你的意思的,因此就无法为你生成一个合适你自己的杯子,那么对应到抽象类中就是强制子类去实现这个抽象方法,所以在这就可以看到抽象类是不提供创建抽象对象的操作的,因为这是风险的,如果你不指定实现,那么它就不知道怎么做,做什么,换句话说就是抽象类就是为了被继承而存在的
  • 总结:抽象类是对一个事物的概括,属于is-a,并且由abstract关键字进行修饰,其中的内在方法可以有方法实现也可以没有,没有方法实现的,子类必须重写,有方法实现的,子类可以沿用父类的实现,或者再进行重写定制

抽象类的语法

  • 上面提到了重写,那么就必然涉及到继承,所以在抽象类中, 方法不可以是private abstract,因为这些限定符就使得子类获取不到父类的方法了,违背抽象类的使用原则,所以方法的修饰符就只能是publicprotected,默认为public
  • abstract修饰类,表示只能被继承,修饰方法,表明必须由子类实现重写.而final修饰的类不能被继承,final修饰的方法不能被重写,所以final和abstract不能同时修饰类和方法
  • static与abstract不能同时修饰某个方法,即没有所谓的类抽象方法.但是可以同时修饰内部类,
  • 如果有一个子类继承了抽象类,抽象类其中有抽象方法,如果子类也不实现父类中的抽象方法,那么这个子类也必须定义为抽象类,原因很简单:因为子类也拿不准主意,所以还需要其他类提供实现,因此一个子类如果继承了抽象类,必须实现抽象类中定义的所有抽象方法
  • 抽象类因为是类,也是class修饰,所以它的子类需要继承抽象类的时候,也是采用extends
  • 抽象方法不能有方法体,必须由abstract修饰
  • 抽象类可以包含成员变量,方法,构造器,初始化块,内部类.抽象类的构造器不能用于创建实例,主要是用于被其子类调用
  • 总结:上面都需要记住

抽象类的使用

  • 拿得准的实现,通用的实现写到抽象类中,否则你就定义抽象方法,由于类是单继承了,所以只能实现一个抽象类,就不存在抽象方法冲突了
  • 下面是一个简单实现.

    public abstract class Animals {
        abstract void say();
    }
    class Cat extends Animals{
        @Override
        void say() {
            System.out.println("mm");
        }
    }
    class Dog extends Animals{
        @Override
        void say() {
            System.out.println("ww");
        }
    }
    class Tests {
        public static void main(String[] args) {
            Animals cat = new Cat();
            Animals dog = new Dog();
            cat.say();
            dog.say();
        }
    }
  • 上面仅仅是实现了父类中的方法,那么跟下面这种有啥区别呢?

    public abstract class Animals {
    }
    class Cat extends Animals{
        void say() {
            System.out.println("mm");
        }
    }
    class Dog extends Animals{
        void say() {
            System.out.println("ww");
        }
    }
  • 上面代码没有错误,但是当运行多态去编写代码的时候就会出错了,因为父类Animals中并没有say方法,虽然程序运行逻辑看子类但是父类总的先定义一下,所以抽象类的存在就使我们可以更加方便的运行多态,多态其好处是一旦需求有改动的时候,修改起来灵活,变化起来容易,不用修改过多的代码
  • 抽象类就是为继承而存在的,继承是复用代码的一个重要的机制,所以抽象类可以将一些事物的默认实现尽量的在类中进行实现,以减少子类代码的书写
  • 总结:存在继承关系在在抽象类,抽象类使多态运用的更加灵活,不足的就是单继承的限制

接口的简介

  • 接口是啥?我可能直接想到的就是网线接口,USB接口,所以这就给了我们很好的启发了,当需要用USB的时候,我们就插上,不需要就拔下来相当灵活,所以对应到Java中接口主要也是类似的作用,比抽象类更加的灵活,因为接口可以多实现,需要一个功能我们就可以实现一个接口
  • 使用USB你会发现插在那个主机上都可以使用,所以这里面存在一个USB协议,大家都遵守这个规定,所以USB可以到处插拔,接口的第二个作用就是在这了,即定义协议,一切按协议走,方便你我他
  • 在Java中的接口的定义是使用interface修饰符的

接口的演进和语法

  • 在JDK8之前,接口只能是有抽象方法,就是跟抽象类中的抽象方法一样的作用,必须由子类实现,而且接口没有实现,所以在之前接口比抽象类还抽象
  • 但是在JDK8的更新中,加入了Stream,Lmabda等一系列功能以及函数式编程的支持,所以新增了一个概念叫做:函数式接口,该接口依旧是interface定义,不同的是其中只允许有一个抽象方法的定义,并且标有@FunctionalInterface注解,这些功能有一部分是对当前的集合类进行操作,但是之前的集合类接口上都是抽象方法,怎么才能直接对接口进行操作呢,他们没实现啊,所以为了这个要求JDK8加入了default修饰符在接口中,比如

    public interface Eat {
        default void say(){
            System.out.println("say");
        }
    }
    class Animals implements Eat{}
    class Tests{
        public static void main(String[] args) {
            Eat e = new Animals();
            e.say();
        }
    }
  • 如上代码是正确的,所以你要知道JDK8的接口中可以有默认实现了,一些集合类中把他的子类的相同操作逻辑提取到了默认方法,所以才可以直接对接口中的方法进行操作,比如List接口中的替换方法

    default void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final ListIterator<E> li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }
  • 还加入了static修饰的方法

    public interface Eat {
        static void st(){
            System.out.println("st");
        }
    }
    class Animals implements Eat{}
    class Tests{
        public static void main(String[] args) {
            Eat.st();
            //error Static method may be invoked on containing interface class only
            Animals.st();
        }
    }
  • 上面说明在接口定义的静态方法,只可以interfaceName.method调用
  • 在JDK9中还加入了private修饰方法,JDK8中不支持,但是后来发现如果没有这个private修饰的方法,会造成接口中的实现会有重复代码,所以引入了private,如下

    public interface SSSS {
        private static void st(){
            System.out.println("st");
        }
        private void sts(){
            System.out.println("sts");
        }
        static void impl(){
            st();
            sts(); //error 静态不能引用实例
        }
    }
    class Testss{
        public static void main(String[] args) {
            SSSS.impl();
        }
    }
  • 如上如果将那个error去掉,代码就是正确的,但是接口中依旧是不可以对普通方法提供实现的,因为这是抽象方法
  • 下面是函数式接口的一个例子

    @FunctionalInterface
    public interface SSSS {
        void say();
        static void sta(){
        }
    }
  • 上面是正确的,有且仅有一个抽象方法的接口可以被标注为@FunctionalInterface,如果你还不是很了解函数式接口,可以去看一下我的 Java8学习专辑,这是非常有用的
  • 一些细枝末节:

    • 由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义,可以包含常量(静态常量),方法(只能是抽象实例方法,类方法,默认和私有方法),内部类定义
    • 接口中的静态常量是接口相关的.因此默认增加static和final修饰符.所以在接口中的成员变量总是使用public static final修饰,并且只能在定义时指定初始值
    • 接口中如果不是定义默认方法类方法或私有方法,系统自动为普通方法增加abstract修饰符.接口中的普通方法总是使用public abstract修饰
    • 接口中定义的内部类,内部接口,内部枚举默认都采用public static两个修饰符,不管定义时是否指定,系统会为其自动修饰
    • 类方法总是public修饰,可以直接使用接口名来调用
    • 默认方法都是采用default修饰,不能使用static修饰,并且总是public修饰.需要使用接口的实现类实例来调用这些方法
  • 对了,如果一个类实现的两个接口中有相同的默认方法,那么必须在子类中进行重新实现

    interface SSSS {
        default void say(){
            System.out.println("ss");
        }
    }
    interface AAAA {
        default void say(){
            System.out.println("aa");
        }
    }
    class Demo implements AAAA,SSSS{
        @Override
        public void say() {
    
        }
    }

抽象类与接口语法对比

  • 接口里只能包含抽象方法,静态方法,默认方法和私有方法.不能为普通方法提供实现,抽象类完全可以
  • 接口里只能定义静态常量,不能定义普通成员变量,抽象类都可以
  • 接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象.而是让其子类调用这些构造器来完成属于抽象类的初始化操作
  • 接口里不能包含初始化块,但抽象类则完全可以包含
  • 一个类最多只能有一个直接父类,包括抽象类.但一个类可以实现多个接口,通过实现多个接口可以弥补java单继承的不足

抽象类与接口设计对比

  • 并没有代码,现在有一个门的对象Door,熟知Door有一个开门以及关门的功能,这是一个门的最基本的功能,那么我们如果在写完代码后再次修改门对象的定义,需要添加一个报警功能,那么我们该怎么办,如果在抽象类中直接添加报警功能,如果是抽象方法,就必须重写,如果是父类已经实现的方法,子类如果在细化实现的话,那么也要重写,并且你父类中的实现可能会影响到子类的其他方法,这是一种方法,但是如果一直有改动或者方法很多的话,那么这个抽象类将变得相当麻烦,第二种方法就是:在不更改抽象类的情况下,可以编写一个报警的接口,用子类来实现他,那么子类就必须去实现此方法,这样就可以达到不做抽象类的更改并添加了报警功能.
  • 抽象类的编写就是基于子类的共同特性的,它是描述一个类的大致情况的,外貌轮廓,接口则是行为形式,描述是具体干什么的,如果一个工厂有什么部门,那么如果按照第一种方法,再去每个部门添加部门具体是做什么的,那么不仅仅影响到了继承他的子类,而且使代码变的不太容易维护,杂乱,采用第二种,可以避免这种情况,子类需要什么功能就实现什么接口,更加的灵活

总结

抽象类的实现就是基于子类的共同特性的,它是描述一个类的大致情况的,外貌轮廓,接口则是行为形式,描述是具体干什么的,在使用的时候,我们可以将相同子类的共同特性抽检出一个抽象类来作为其子类的大致轮廓,具体实现细节,可以编写接口并实现即可、一个类继承一个抽象类.抽象类规定了子类的大概轮廓,其具体实现的方法,可以使用抽象类中的,也可以通过实现接口,重写接口中的方法来实现子类的细化、可以利用抽象类和借口配合使类更具体,即抽象类和接口的配合可以生成一个独一无二的定制类

上一篇:DNN使用非80端口和总是跳转到http://localhost问题的解决(翻译)


下一篇:深入浅出https协议(ssl/tls)