重识java8

你很清楚的知道什么时候用抽象类,什么时候用接口么? 
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
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

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

但是,我想问你一个问题: 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
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

因为抽象类不可以实例化,所以现在就不用担心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 新特性");
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2 抽象类和接口的区别

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

2.1 从概念上来看

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

2.2 语法定义层面看

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

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

//接口
interface InterfaceTest {  
    void method1();  
    void method2();  
}    
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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();  
} 
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
//接口
interface Door{  
    void open();  
    void close();  
} 
 
 
  • 1
  • 2
  • 3
  • 4
  • 5

其他具体的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();  
}  
 
 
  • 1
  • 2
  • 3
  • 4
  • 5

或者

interface Door{  
    void open();  
    void close();  
    void alarm();  
}  
 
 
  • 1
  • 2
  • 3
  • 4
  • 5

这种方法违反了面向对象设计中的一个核心原则 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(){}  
}  
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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

其实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();
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

public interface Teach {
    void teach(String className);
}
 
 
  • 1
  • 2
  • 3

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

public interface TakeClass {
    void takeClass(String className);
}
 
 
  • 1
  • 2
  • 3

定义老师:

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");
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

定义学生:

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");
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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

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

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

4 再谈多态

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

比如我们常用的List接口:

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

这就是多态的体现。

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

5 总结

总结一下抽象类和接口:

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值