美文网首页java后端转载java爱好者
重新认识java(八) ---- 抽象类与接口

重新认识java(八) ---- 抽象类与接口

作者: Sharember | 来源:发表于2017-03-09 09:19 被阅读531次

    你很清楚的知道什么时候用抽象类,什么时候用接口么?
    p.s. 多文字预警!

    1 抽象类和接口简介

    1.1 抽象类

    1.1.1 一个小案例

    我们先来看这样一个案例:世界上有许许多多不同种类的动物,每一种动物都要吃东西,移动(走路?飞?)等等。现在让你用java语言描述一下这个案例。

    啊,你会觉得,so easy。我可是学过继承的人,一个小继承就能解决问题:

    // 父类
    public class Animal {
        public void move(){
            System.out.println("i an move");
        }
    }
    // 鸟 类
    public class Bird extends Animal {
        @Override
        public void move() {
            System.out.println("i can fly");
        }
    }
    // 狗 类
    public class Dog extends Animal {
        @Override
        public void move() {
            System.out.println("i can run");
        }
    }
    //......more
    

    看起来,你大概完成的不错。

    但是,我想问你一个问题: new Animal().move()这段代码描述了一个什么现实情景?

    ”创建了一个动物,然后让这个动物移动“,你可能会这么回答我。但是,你难道没有发现问题么?现实世界里,有叫做【动物】的生物么?你见过这个叫做【动物】的生物移动么?

    动物,是对生物的一种统称,狗是动物,鸟也是动物。但是【动物】本身是一个抽象的概念,你在现实世界中,并没有见过一种叫做【动物】生物吧?

    你应该明白了,我们可以new一个Bird,new一个Dog,因为它们是实实在在的对象,但是我们不应该new出一个Animal来,因为动物是一个抽象的概念,实际上它并不存在。

    事实上,Animal中的move()方法,也是有问题的不是么?既然Animal不存在,那它怎么会有真实存在的move()方法呢?

    问题来了。。。

    1.1.2 抽象类和抽象方法

    在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。

    就像我们上面中的例子一样。Dog和Bird可以用一个普通类来描绘,但是Animal不可以,Animal就应该是一个抽象类。

    在java中,被abstract修饰的类,叫做抽象类。抽象类中可以定义抽象方法,也可以定义普通方法。抽象类不可以被实例化,只有被实体类继承后,抽象类才会有作用。

    抽象方法:

    • 被abstract修饰的方法叫做抽象方法,抽象方法没有方法体,也就是说抽象方法没有具体的实现。
    • 抽象方法必须定义在抽象类中。
    • 举个例子: abstrac void move(); 。这就是一个抽象方法。

    回到刚才的问题,我们现在利用抽象类来重构一下我们的代码:

    // 父类
    public abstract class Animal {
        public abstract void move();//抽象方法
    }
    // 鸟 类
    public class Bird extends Animal {
        @Override
        public void move() {
            System.out.println("i can fly");
        }
    }
    // 狗 类
    public class Dog extends Animal {
        @Override
        public void move() {
            System.out.println("i can run");
        }
    }
    //......more
    

    因为抽象类不可以实例化,所以现在就不用担心new Animal()这样的情况出现了。并且我们将Animal类中的move方法也定义为抽象方法,所以上面的所有问题,都迎刃而解了。

    抽象类就是用来被继承的,脱离的继承,抽象类就失去了价值。继承了抽象类的子类,需要重写抽象类中所有的抽象方法。

    在使用抽象类时需要注意几点:

    • 抽象类不能被实例化,实例化的工作应该交由它的子类来完成,它只需要有一个引用即可。

    为什么抽象类不能实例化对象:

    • 抽象类的设计目的就是为了处理类似于Animal这种无法准确描述为一个对象的情况。所以不可以实例化。

    • 抽象类中可以定义抽象方法。抽象方法是没有方法体的,必须被子类重写后,该方法才能被正确调用。如果抽象类能实例化,那么抽象方法也就可以被调用,这显然是不行的。
    • 子类必须重写所有抽象方法。

      当然,不都重写也可以,但是这样的话,子类也必须是抽象类。

    • 一个类里只要有一个抽象方法,那么这个类必须定义为抽象类。

    • 抽象类中可以包含具体的方法,当然也可以不包含抽象方法。

    • abstract不能与final并列修饰同一个类。

      abstract类就是为了让子类继承,而final类不能被继承。

    • abstract 不能与private、static、final或native并列修饰同一个方法。

      抽象方法必须被子类重写才能使用。

    1.2 接口

    java中的接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为。

    接口是一种比抽象类更加抽象的【类】。这里给【类】加引号是我找不到更好的词来表示,但是我们要明确一点就是,接口本身就不是类。为什么说它更抽象呢?因为抽象类中还可以定义普通方法,但是接口中只能写抽象方法。

    接口是用来建立类与类之间的协议,它所提供的只是一种形式,而没有具体的实现。接口中的所有方法默认都是public abstract的。

    接口是抽象类的延伸,java了保证数据安全是不能多重继承的,也就是说继承只能存在一个父类,但是接口不同,一个类可以同时实现多个接口,不管这些接口之间有没有关系,所以接口弥补了抽象类不能多重继承的缺陷,但是推荐继承和接口共同使用,因为这样既可以保证数据安全性又可以实现多重继承。

    在使用接口过程中需要注意如下几个问题:

    • 接口中的所有方法访问权限自动被声明为public。确切的说只能为public,当然你可以显示的声明为protected、private,但是编译会出错。

    • 接口中可以定义变量,但是它会被强制变为不可变的常量,因为接口中的“成员变量”会自动变为为public static final。可以通过类命名直接访问:ImplementClass.name。

    • 实现接口的非抽象类必须要实现该接口的所有方法。抽象类可以不用实现。

    • 在实现多接口的时候一定要避免方法名的重复。

    因为一个类可能会实现多个接口,如果这两个接口有名字相同的方法,会产生意想不到的问题。

    • 不能使用new操作符实例化一个接口,但可以声明一个接口变量,该变量必须引用(refer to)一个实现该接口的类的对象。可以使用 instanceof 检查一个对象是否实现了某个特定的接口。例如:if(anObject instanceof Comparable){}。

    • 接口中不存在具体的方法。

    值得一提的是,在java8中,接口里也可以定义默认方法:

    public interface java8{
        //在接口里定义默认方法
        default void test(){
            System.out.println("java 新特性");
        }
    }
    

    2 抽象类和接口的区别

    基础知识看完了,我们来看抽象类和接口的区别。

    2.1 从概念上来看

    前面讲过了,这里不再赘述。

    2.2 语法定义层面看

    在语法层面,Java语言对于abstract class和interface给出了不同的定义方式。

    //抽象类
    public abstract class AbstractTest {  
        abstract void method1();      
        void method2(){  
            //实现  
        }
    }
    
    //接口
    interface InterfaceTest {  
        void method1();  
        void method2();  
    }    
    

    2.3 设计理念层面看

    前面已经提到过,abstarct class在Java语言中体现了一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在【is-a】关系,即父类和派生类在概念本质上应该是相同的。

    对于interface 来说则不然,并不要求interface的实现者和interface定义在概念本质上是一致的,仅仅是实现了interface定义的协议而已。

    我们来看一个例子:假设在我们的问题领域中有一个关于Door的抽象概念,该Door具有执行两个动作open和close,此时我们可以通过abstract class或者interface来定义一 个表示该抽象概念的类型,定义方式分别如下所示:

    //抽象类
    abstract class Door{  
        abstract void open();  
        abstract void close();  
    } 
    
    //接口
    interface Door{  
        void open();  
        void close();  
    } 
    

    其他具体的Door类型可以extends使用abstract class方式定义的Door或者implements使 用interface方式定义的Door。看起来好像使用abstract class和interface没有大的区别。

    如果现在要求Door还要具有报警的功能。我们该如何设计针对该例子的类结构呢(在本例中,主要是为了展示abstract class和interface反映在设计理念上的区别,其他方面无关的问题都做了简化或者忽略)下面将罗列出可能的解决方案,并从设计理念层面对 这些不同的方案进行分析。

    解决方案一:

    简单的在Door的定义中增加一个alarm方法,如下:

    abstract class Door{  
        abstract void open();  
        abstract void close();  
        abstract void alarm();  
    }  
    

    或者

    interface Door{  
        void open();  
        void close();  
        void alarm();  
    }  
    

    这种方法违反了面向对象设计中的一个核心原则 ISP,在Door的定义中把Door概念本身固有的行为方法和另外一个概念"报警器"的行为方法混在了一起。这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为"报警器"这个概念的改变而改变,反之依然。

    比如说,有一个普普通通的门,实现了Door接口,或者继承了Door抽象类,它只需要开门和关门的行为,但是当你像上面一样修改了接口或者抽象类以后,那么这个【普通门】也不得不具备了【报警】的功能,这显然是不合理的。

    ISP(Interface Segregation Principle):面向对象的一个核心原则。它表明使用多个专门的接口比使用单一的总接口要好。
    一个类对另外一个类的依赖性应当是建立在最小的接口上的。
    一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。

    解决方案二

    既然open()、close()和alarm()属于两个不同的概念,那么我们依据ISP原则将它们分开定义在两个代表两个不同概念的抽象类里面,定义的方式有三种:

    • 两个都使用抽象类来定义。
    • 两个都使用接口来定义。
    • 一个使用抽象类定义,一个是用接口定义。

    由于java不支持多继承所以第一种是不可行的。后面两种都是可行的,但是选择何种就反映了你对问题域本质的理解。

    如果选择第二种都是接口来定义,那么就反映了两个问题:

    • 我们可能没有理解清楚问题域,AlarmDoor在概念本质上到底是门还报警器。

    • 如果我们对问题域的理解没有问题,比如我们在分析时确定了AlarmDoor在本质上概念是一致的,那么我们在设计时就没有正确的反映出我们的设计意图。因为你使用了两个接口来进行定义,他们概念的定义并不能够反映上述含义。

    第三种,如果我们对问题域的理解是这样的:

    • AlarmDoor本质上Door,但同时它也拥有报警的行为功能,这个时候我们使用第三种方案恰好可以阐述我们的设计意图。

    • AlarmDoor本质是门,所以对于这个概念我们使用抽象类来定义,同时AlarmDoor具备报警功能,说明它能够完成报警概念中定义的行为功能,所以alarm可以使用接口来进行定义。如下:
    abstract class Door{  
        abstract void open();  
        abstract void close();  
    }  
      
    interface Alarm{  
        void alarm();  
    }  
      
    class AlarmDoor extends Door implements Alarm{  
        void open(){}  
        void close(){}  
        void alarm(){}  
    }  
    

    这种实现方式基本上能够明确的反映出我们对于问题领域的理解,正确的揭示我们的设计意图。

    其实abstract class表示的是【is-a】关系,interface表示的是【like- a】关系,大家在选择时可以作为一个依据,当然这是建立在对问题领域的理解上的,比如:如果我们认为AlarmDoor在概念本质上是报警器,同时又具有 Door的功能,那么上述的定义方式就要反过来了。

    3 抽象类和接口的使用

    看了那么多乱糟糟的分析,我们究竟如何选择呢?到底是使用抽象类,还是使用接口?

    首先,我们要明确一点:抽象类是为了把相同的东西提取出来, 是为了重用; 而接口的作用是提供程序里面固化的契约, 是为了降低偶合。抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什么。

    比如说,现在,我要用java描述一下学生和老师。学生和老师都有姓名,年龄,性别等,都会走路,吃饭;但是老师要授课,而学生要听课,不同的老师授课的科目不同,不同专业的学生听的课也不同。

    我们可以把老师和学生共有的属性和方法提取出来,用抽象类表示:

    public abstract class Person {
        String name;
        int age;
        String sex;
    
        abstract void eat();
        abstract void run();
    }
    

    老师会授课,不同的老师授课不同,我们可以定义一个接口:

    public interface Teach {
        void teach(String className);
    }
    

    学生要上课,不同专业的学生上的科目不同,我们也可以定义为接口:

    public interface TakeClass {
        void takeClass(String className);
    }
    

    定义老师:

    public class Teacher extends Person implements Teach {
    
        @Override
        public void teach(String className) {
            System.out.println("teach " + className);
        }
    
        @Override
        void eat() {
            System.out.println("teacher eat");
        }
    
        @Override
        void run() {
            System.out.println("teacher run");
        }
    }
    

    定义学生:

    public class Student extends Person implements TakeClass {
    
        @Override
        public void takeClass(String className) {
            System.out.println("take class: " + className);
        }
    
        @Override
        void eat() {
            System.out.println("student eat");
        }
    
        @Override
        void run() {
            System.out.println("student run");
        }
    }
    

    这样使用抽象类和接口,我觉得是一种很合理的方式。

    现在有很多讨论和建议提倡用interface代替abstract类,两者从理论上可以做一般性的混用,但是在实际应用中,他们还是有一定区别的。抽象类一般作为公共的父类为子类的扩展提供基础,这里的扩展包括了属性上和行为上的。而接口一般来说不考虑属性,只考虑方法,使得子类可以自由的填补或者扩展接口所定义的方法。

    就像这个老师和学生的例子,抽象类提取了他们共有的属性,他们各自有什么属性可以交给子类去完成。有人可能会说,为什么不把eat 和 run 方法定义为接口呢?这当然也是可以的。但是我觉得,吃和走,是人自身的一种行为,它不像授课和上课这种是因为某种身份而特有的行为,吃和走与人自身的属性(姓名,年龄)都是【人】本身就有的,所以我觉得一起放到抽象类里更合适一些。当-然,你单独定义一个【人行为】的接口从语法角度讲也没问题。

    4 再谈多态

    前面讲过,继承(实现)是多态的前提之一。现在学完了抽象类和接口,多态的使用场景就更多了。

    比如我们常用的List接口:

    List<String> l = new ArrayList<>();
    List<Integer> l1 = new LinkedList<>();
    

    这就是多态的体现。

    由于篇幅已经过长,我就不细说了~

    5 总结

    总结一下抽象类和接口:

    1、抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。

    2、抽象类要被子类继承,接口要被类实现。

    3、接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现(不讨论java8的情况下)

    4、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。

    5、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。

    6、抽象方法只能申明,不能实现。不能写成abstract void abc(){}。

    7、抽象类里可以没有抽象方法

    8、如果一个类里有抽象方法,那么这个类只能是抽象类

    9、抽象方法要被实现,所以不能是静态的,也不能是私有的。

    10、接口可继承接口,并可多继承接口,但类只能单根继承。

    11、从实践的角度来看,如果依赖于抽象类来定义行为,往往导致过于复杂的继承关系,而通过接口定义行为能够更有效地分离行为与实现,为代码的维护和修改带来方便。

    12、选择抽象类和接口的时候记得一句话:抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什么。

    13、使用抽象类,要保证和实现类之间是【is-a】关系。


    其实抽象类和接口的使用时有很多争议的,没有一个人敢说他的想法就是绝对正确的,而别人的想法就是错误的。在设计的时候,如何选择,不仅仅是根据一些理解,还需要一些经验。有的时候,抽象类是配合接口一起使用的,接口为几个【普通类】定义了一系列方法,然后抽象类实现该接口并实现了这几个【普通类】共同的方法,然后几个【普通类】再继承抽象类,分别实现各自不同的方法。

    知识是死的,人是活的,怎么使用不是听别人怎么说你就怎么用,更多的是自己的理解。因为,他说的也不一定对啊?


    本篇文章到这里就结束了。满满的文字,大概你认真看完也累的半死了。但是学习就是这样,不辛苦一点怎么能学的全,学得会呢?
    如果文章内容有什么问题或者错误,请及时与我联系。

    转载请注明出处:
    本文地址:http://blog.csdn.net/qq_31655965/article/details/54972723
    原创自csdn:http://blog.csdn.net/qq_31655965
    同步更新在:
    我的博客:wpblog.improvecfan.cn
    简书:http://www.jianshu.com/u/8dc5811b228f
    博主:cleverfan

    看完了,如果对你有帮助,随手点个赞呗~~~


    参考资料:
    http://blog.csdn.net/ttgjz/article/details/2960451
    http://blog.csdn.net/xw13106209/article/details/6923556
    http://blog.csdn.net/wenwen091100304/article/details/48381023

    相关文章

      网友评论

      • WangYeKun:能告诉我字体或者主题是什么吗?觉得很好看
      • 72d110bf7050:总结的很好,很透彻,赞
      • 丫丫亚历克斯:不能使用new操作符实例化一个接口,但可以声明一个接口变量,该变量必须引用(refer to)一个实现该接口的类的对象。怎么理解第二个,后面的那句话
      • 一个人的自诉:实现接口的非抽象类必须要实现该接口的所有方法。抽象类可以不用实现。
        博主我觉得这句话 我是这样理解的 : 实现接口的非抽象类必须重写该接口的所有方法 ,而并不是一定要去实现。 抽象类可以不用重写 。
        这样理解。。
        一个人的自诉:@cleverfan 也是
        Sharember:@从日落到日出 重写其实就是实现了,你重写的方法里一行代码也不写,也是实现:stuck_out_tongue:
      • ca71653439d3:讲的不错 java8的接口允许有方法了?
        Sharember:@MINo_ 是的哦 允许有默认的实现

      本文标题:重新认识java(八) ---- 抽象类与接口

      本文链接:https://www.haomeiwen.com/subject/yyqwgttx.html