1、接口和抽象类
正如面向对象四大特征:抽象、继承、封装、多态所述,定义类的过程就是抽象和封装的过程。而接口和抽象类则是对实体类进行更高层次的抽象,仅定义公共行为和特征。两者的共同点是都不能被实例化,但可以通过定义引用变量指向实例对象。
下表示接口和抽象类的语法区别:
语法维度 | 抽象类 | 接口 |
---|---|---|
定义关键字 | abstract | interface |
子类继承或实现关键字 | extends | implements |
方法实现 | 可以有 | 不能有,JDK8以后,允许有default实现 |
方法访问控制符 | 无限制 | 有限制,默认是public abstract类型 |
属性访问控制符 | 无限制 | 有限制,默认是public static final类型 |
静态方法 | 可以有 | 不能有,JDK8以后,允许有 |
static{}静态代码块 | 可以有 | 不能有 |
本类型之间扩展 | 单继承 | 多继承 |
本类型之间扩展关键字 | extends | extends |
抽象类在被继承时体现的是is-a关系,而接口再被实现时体现的是can-do关系。
与接口相比,抽象类通常是对同类事物相对具体的抽象,通常包含抽象方法、实体方法、属性变量。如果一个抽象类只有一个抽象方法,那么它等同于一个接口。
is-a关系需要符合里式代换原则(即父类适用的地方,用子类来替代同样适用。举个例子:警匪片中,警察经常说,放下武器!而对面的匪徒有的使用匕首,有的使用手枪,这些都是武器的子类。父类出现的地方,即“放下武器”,那么,“放下匕首”、“放下手枪”都是对的)。
can-do关系要符合接口隔离原则。实现类要有能力去实现并执行接口中定义的行为,例如Plan can fly.Brid can fly.中应该把fly这个动作/功能定义成一个接口,而不是把fly()放在某个抽象类中,再由Plane和Brid利用is-a关系去继承此抽象类。因为严格意义上来说,除了fly这个行为外,在Plane和Brid两者中很难再找到相同或者相似的特征。
抽象类是模板设计,而接口是契约式设计。
抽象类包含一组相对具体的特征,性格偏内向,比如某品牌特定型号的汽车,底盘结构、控制电路、刹车系统等是抽象出来的共同特征,但根据动感型、舒适性、豪华型的区分,内饰、车头灯、显示屏等可以存在不同版本的实现。
接口是开放的,性格偏外箱,它就像是一份合同,定义了方法名、参数、返回值,甚至是抛出的异常类型。谁都可以来实现它,但如果向实现它的类就必须遵守这份接口约定合同,比如,任何类型的车辆都必须实现如下的接口:
public interface VehicleSafe{
/**
* @param initSpeed 刹车时的初始速度
* @param brakeTime 从initSpeed开始刹车到停止行驶的时间,单位是毫秒
* @return 从initSpeed开始刹车到停止行驶的距离
*/
double brake(int initSpeed,int brakeTime);
刹车是一个开放式的强制行为规范,任何车辆都必须具有刹车的能力,要明确在特定初速度的情况下,刹车时间多长,刹车距离多长。此规范对任何车辆都是强约束的,这就是契约。
接口是顶级的“类”,对应关键字是interface,但是编译后的字节码扩展名还是.class。抽象类是二当家,接口位于顶层,而抽象类对各个接口进行了组合,然后实现部分接口行为,其中AbstaractCollection是最典型的抽象类:
public abstract class AbstractCollection<E> implements Collection<E>{
//Collection定义的抽象方法,但本类没有实现
//Collection接口定义的方法,size()这个方法对于链表和顺序表有不同的实现方式
public abstract int size();
//实现Collection接口的这个方法,因为对AbstractCollection的子类它们的判空方式是一致的,这就是
//模板式设计,对于所有它的子类,实现共同的方法体,通过多态调用到子类的具体size()实现
public boolean isEmpty(){
//实现Collection的方法
return size() == 0;
}
//其他属性和部分方法实现.....
}
Java语言中类的继承采用单继承形式,避免继承泛滥、菱形继承、循环继承,甚至“四不像”实现类的出现。在JVM中,一个类如果有多个直接父类,那么绑定机制会变得非常复杂。
接口继承接口,关键字是extends,而不是implements,允许多重继承是因为接口有契约式的行为约定,没有任何具体实现和属性,某个实体类在实现多重继承后的借口时,只是说明“can do many things”。
当纠结定义接口还是抽象类时,优先推荐定义为接口,遵循接口隔离原则,按某个维度划分为多个接口,然后再用抽象类去implements某些接口,这样做可方便后续的扩展和重构。
2、内部类
在一个.java的源文件中,只能定义一个类名与文件名完全一致的公开类,并使用public class关键字来修饰。但是在面向对象语言中,任何一个类都可以在内部定义另外一个类,前者为外部类,后者为内部类。内部类本身就是类的一个属性,与其他属性定义方式一致。
比如:属性字段private static String str,由访问控制符、是否静态类型、属性类型、变量名组成。而内部类private static class Inner{},也是按照这样的顺序来定义的。类型可以为class、enum,甚至是interface,当然在内部类中定义接口是不推荐的。
内部类可以是静态和非静态的,它可以出现在属性定义、方法体和表达式中,甚至可以匿名出现(匿名内部类),具体分为如下四种:
- 静态内部类,如:static class StaticInnerClass{};
- 成员内部类,如:private class InstanceInnerClass{};
- 局部内部类,定义在方法或者表达式内部;
- 匿名内部类,如:(new Thread(){}).start()。
如下是最精简的4中内部类定义方式:
public class OuterClass{
//成员内部类
private class InstanceInnerClass{}
//静态内部类
static class StaicInnerClass{}
public static void main(String[] args){
//两个匿名内部类
(new Thread() {}).start();
(new Thread() {}).start();
//两个方法内部类
class MethodClass1{}
class MethodClass2{}
}
}
无论是什么类型的内部类,都会编译成一个独立的.class文件,上面这个类编译以后会产生如下的.class文件:
- OuterClass.class
- OuterClass$1.class
- OuterClass$1MethodClass1.class
- OuterClass$1MethodClass2.class
- OuterClass$2.class
- OuterClass$InstanceInnerClass.class
- OuterClass$StaticInnerClass.class
外部类与内部类之间使用$符号分隔,匿名内部类使用数字进行编号,而方法内部类,在类名前还有一个编号来标识是哪个方法。
匿名内部类和静态内部类是比较常用的方式