Java 抽象类和接口

Java 抽象类和接口

内部类

定义及简单示例

在 Java 中,允许在一个方法语句块的内部定义另一个类,称为内部类(Inner Class)嵌套类(Nested Class)

内部类和外层封装它的类之间存在逻辑上的所属关系,一般只用在定义它的类或语句块之内,实现一些没有通用意义的功能逻辑,在外部引用它时必须给出完整的名称;

使用内部类的主要原因有:

  • 内部类可以访问外部类中的数据,包括私有的数据;
  • 内部类可以对同一个包中的其它类隐藏起来;
  • 当想要定义一个回调函数时,使用匿名内部类比较便捷;
  • 内部类的使用可以减少类的命名冲突(命名真的很头疼);

内部类只是一颗语法糖,只是编译器的一种行为,内部类在编译后一样会生成常规的 .class 类文件,内部类与 JVM 无关。

使用内部类的例子:

对于实例成员类,必须先有外部类的对象才能生成内部类的对象,因为内部类需要访问外部类中的成员变量,成员变量必须实例化才有意义。

内部类的分类

内部类分为:实例成员类静态成员类具名局部类匿名局部类

实例成员类静态成员类就像一个类中的普通成员(属性、方法),因此可以使用 public/protected/private/[default]、static、final、abstract 等修饰符;它们之间仅仅就是普通成员和静态成员的区别;
具名局部类匿名局部类就像一个普通的局部变量(方法、语句块中),因此它们和普通的局部变量一样,不能被 static、public/protected/private 修饰符修饰;但匿名局部类必须继承一个类或实现一个接口。

无论哪种内部类,实质都是一个独立 .class 字节码文件,本质就是一个拥有特殊类名($美元符)的普通类

实例成员类、静态成员类

在类内部(不在方法、语句块)直接定义的类就是成员类,和类的普通成员一样,有着静态成员与实例成员之分:

  • 实例成员类:可以在内部类中直接引用外部类的所有成员(静态、非静态),因此必须先有外部类对象才能构造内部类对象;
  • 静态成员类:可以在内部类中直接引用外部类的静态成员,静态内部类其实就是一个普通的全局类,只不过名字特殊点而已。

成员式内部类和普通成员一样,支持 public、protected、private、[default] 访问修饰符;支持 static、final、abstract 属性修饰符。

若有 static 修饰符,就为类级(即静态成员类),否则为对象级(即实例成员类);类级可以通过外部类直接访问,对象级需要先生成外部类对象后才能访问;

对于实例成员类,类内不能声明任何 static 成员;同一个外部类的不同的内部类之间可以互相调用,就如同类中的不同方法之间可以互相调用一样。

并且,外部类可以访问内部类的所有成员,包括被 private 修饰的成员,在上面的例子中已经进行演示。

创建实例成员类的对象
因为需要访问外部类的实例变量、实例方法,因此必须在外部类对象的基础上构造内部类对象。
语法:outObj.new Inner(param ...)(具名对象)、new Outer(param ...).new Inner(param ...)(匿名对象)。

创建静态成员类的对象
静态成员类就是特殊点的全局类,当然作为成员类还是有点特权的(可以访问外部类的所有静态成员,包括私有的)。
语法:new Inner(param ...)(类内引用)、new Outer.Inner(param ...)(类外引用,类的权限不能为 private)。

在内部类中访问外部类
具体的调用细节我已经在开头的例子中详细说明了,因为编译器搜索一个变量名时总是从近到远的搜索,因此内部类与外部类的成员存在同名时就需要显式的调用,否则默认使用内部类中的同名成员。

允许定义成员接口
因为接口是从抽象类(抽象类属于类)演变过来的,因此除非特别规定,接口和类享受着同样的待遇(接口的详细介绍在后面);
因此,Java 完全允许我们在一个外部类中定义内部接口(静态成员,不管写不写 static),不存在什么实例内部接口,没有意义;
也可以在外部接口中定义内部接口(公开静态成员,不管写不写 static),这个应该是应用最广泛的,Java 类库中有很多的例子。

具名局部类、匿名局部类

局部类就是定义在方法、语句块中的类,我先把局部类的相关特点说明一下:

  • 局部类只在定义了它们的方法、语句块中可见;
  • 外部作用域可以访问局部类的所有成员,包括私有的;
  • 局部类不可以是 static 的,也不存在 static 成员;
  • 局部类不可以被 public、protected、private 修饰;
  • 具名局部类可以被 abstract 修饰,但不存在所谓局部接口;
  • 匿名局部类必须继承一个类或实现一个接口,但只能二选一;
  • 局部类会捕获外部作用域中的变量(值传递),在类内部使用final修饰。

前几点没什么好讲的,主要是最后两个。我们先来分析最后一条,解释一下”类内部使用final修饰”。

先定义一个具名局部类 Student,暂时不涉及外部变量捕获:

和使用普通的 Student 类一样,都是通过函数传参来进行数据传递,没问题。

但是这样的话就发挥不了局部类的最大作用了啊,我们来让它自动捕获外部的变量:

是不是发现很好用,这就是局部类的便利之处,可以自动捕获外部作用域的变量,但是如果我要修改捕获的变量呢?

这里有几点要说明:如果被捕获的变量(意思就是在局部类中被使用的,未使用的不需要)是函数、代码块中的,那么这些变量必须被 final 修饰;如果被捕获的变量是在类中定义(静态、非静态)的,则无此要求。并且,在 JDK1.8 之后,函数、代码块中的被捕获变量不需要显式使用 final 修饰,会自动变为 final 变量。

报错了,意思是说”从内部类引用的局部变量必须是 final 的”。这就是局部类的猥琐之处;因为局部类捕获外部变量时采取的是值传递(C++ 的 lambda 表达式支持引用传递),因此无法修改变量值(对于引用类型就是无法改变指针的指向)。

Java 为什么要这么做呢,因为 Java 中只存在值传递。这就是抛弃指针运算的恶果(来自 C/C++ 程序猿的嘲笑)。换个角度理解就是前面一开始说的”在类内部使用final修饰”。

编译器在编译之前做了手脚,上面的程序大概会变成这样(当然我不敢保证是这样,猜测而已):

这就很容易理解了,final 成员自然是不可以被改变了,因此在编译时会报错(虽然那个报错信息很诡异)。
但是被 final 修饰的引用类型变量,只是意味着不可以改变指针的指向,并没有说不让我们更改指向的内容。

我说这话是什么意思呢?别着急,我们看例子就知道了:

好了,具名局部类就说到这里,我们来简单的看一下匿名局部类,上面讨论的东西一样适合匿名局部类,后面不再重复演示。

匿名局部类,即没有名字的局部类,但实际上是有名字的,这个后面会进行讨论。匿名局部类不可单独存在,它必须是一个类的子类或者是实现了一个接口的类。

匿名局部类的特点
1、匿名局部类必须继承一个类或者实现一个接口,但两者不可兼得,同时也只能继承一个类或者实现一个接口;
2、匿名局部类没有构造函数,因为没有类名因此无法实现构造函数;可以使用初始化代码块替代构造函数;
3、匿名局部类是具名局部类的一种特殊形式,因此具名局部类的所有限制同样对匿名局部类有效;
4、匿名局部类不能是 abstract 的,它必须要实现继承的类或者实现的接口的所有抽象方法。

匿名内部类实际还是一个独立的类,它有着自己的唯一类名,如果将一个使用 new 创建的匿名类对象赋值给其基类/实现的接口的引用变量,那么此过程中将发生向上转型,因为匿名类就是 new 后面的类/接口的一个子类,它们之间的关系就如同普通的基类、派生类。

我们先来看一下第一种,一个类的子类:

再来看一下第二种,实现一个接口:

最后我们来说一下,如何利用反射创建匿名局部类的对象,反射的具体知识请参考 - Java 反射

抽象类

在自上而下的继承层次结构中,位于上层的类更具有通用性,甚至可能更加抽象;
从某种角度看,祖先类更加通用,它只包含一些最基本的成员,人们只将它作为派生其他类的基类,而不会用来创建对象;
甚至,你可以只给出方法的定义而不实现,由子类根据具体需求来具体实现;

这种只给出方法定义而不具体实现的方法被称为抽象方法,抽象方法是没有方法体的,在代码的表达上就是没有“{}”;
如果一个类包含一个或多个抽象方法就必须被声明为抽象类;抽象类不能被实例化,抽象方法必须在子类中被实现;

使用abstract修饰符来表示抽象方法抽象类;抽象类除了包含抽象方法外,还可以包含具体的变量和具体的方法;

一个类即使没有任何抽象方法,也可以被声明为抽象类,防止被实例化;

抽象类不能直接使用,必须用子类去实现抽象类,然后使用其子类的实例;
可以创建一个变量,其类型是一个抽象类,并让它指向具体子类的一个实例,即多态的使用;
特别注意,abstract不能和static一起使用,即不能有抽象静态方法,也不能有抽象构造函数;

如果一个抽象基类拥有多个抽象方法,那么继承他的派生类也必须实现所有抽象方法,否则不能创建对象;
当然如果没有实现所有的抽象方法,也可以将派生类声明为抽象类,让它的派生类去实现剩下的抽象方法;

典型的错误:抽象类一定包含抽象方法;但是反过来说“包含抽象方法的类一定是抽象类”就是正确的;
事实上,抽象类可以是一个完全正常实现的类;

例子:

接口

在抽象类中,可以包含一个或多个抽象方法;但在接口(interface)中,所有的方法必须都是抽象的,不能有方法体,它比抽象类更加“抽象”;

接口使用interface关键字来声明,接口可以看做是一种特殊的抽象类,它可以指定一个类必须做什么,而不是规定它如何去做;

现实中也有很多接口的实例,比如说串口电脑硬盘,Serial ATA 委员会指定了 Serial ATA 2.0 规范,这种规范就是接口;
Serial ATA 委员会不负责生产硬盘,只是指定通用的规范;
希捷、日立、三星等生产厂家会按照规范生产符合接口的硬盘,这些硬盘就可以实现通用化;
如果正在用一块 160G 日立的串口硬盘,现在要升级了,可以购买一块 320G 的希捷串口硬盘,安装上去就可以继续使用了;

接口是若干常量抽象方法的集合
接口中声明的成员变量默认都是public static final的,必须显示的初始化;因而在常量声明时可以省略这些修饰符;
接口中声明的成员函数默认都是public abstract的,不能提供任何方法体,需要让实现接口的子类去具体定义;

为什么使用接口
接口是可插入性的保证;在一个继承链中的任何一个类都可以实现一个接口,这个接口会影响到此类的所有子类,但不会影响到此类的任何父类;此类将不得不实现这个接口所规定的方法,而子类可以从此类自动继承这些方法,这时候,这些子类具有了可插入性;

我们关心的不是哪一个具体的类,而是这个类是否实现了我们需要的接口;

接口提供了关联以及方法调用上的可插入性,软件系统的规模越大,生命周期越长,接口使得软件系统的灵活性和可扩展性,可插入性方面得到保证;

接口在面向对象的 Java 程序设计中占有举足轻重的地位;事实上在设计阶段最重要的任务之一就是设计出各部分的接口,然后通过接口的组合,形成程序的基本框架结构;

接口的使用
接口的使用与类的使用有些不同;在需要使用类的地方,会直接使用 new 关键字来构建一个类的实例,但接口不可以这样使用,因为接口不能直接使用 new 关键字来构建实例;

接口必须通过类来实现(implements)它的抽象方法,然后再实例化类;类实现接口的关键字为implements
如果一个类不能实现该接口的所有抽象方法,那么这个类必须被定义为抽象类

不允许创建接口的实例,但允许定义接口类型的引用变量,该变量指向了实现接口的类的实例;

接口和抽象类、普通类的共同点
目前看来和抽象类差不多;确实如此,接口本就是从抽象类中演化而来的,因而除特别规定,接口享有和类同样的“待遇”;
比如一个文件中可以定义多个类或接口,但最多只能有一个 public 的类或接口,如果有则源文件必须取和 public 的类或接口相同的名字;

但是接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念;类描述对象的属性和方法;接口则包含类要实现的方法;

除非实现接口的类是抽象类,否则该类要定义接口中的所有方法;
接口无法被实例化,但是可以被实现;一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类;

接口的特性
但是接口有自己的一些特性,归纳如下:
1) 接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为public abstract,并且只能是public abstract
2) 接口中可以含有变量,但是接口中的变量会被隐式的指定为public static final静态常量,并且只能是public static final
3) 接口中没有构造方法,不能被实例化;
4) 一个接口不实现另一个接口,但可以继承多个其他接口;接口多继承特点弥补了单继承
5) 一个类只能继承一个父类,但却可以实现多个接口;实现接口使用关键字implements

接口和抽象类的区别
1) 抽象类本质还是一个类,但是接口不是类;
2) 抽象类可以有构造函数,但是接口没有构造函数;
3) 抽象类可以有具体的方法,但是接口中只能有抽象方法;
4) 抽象类中的成员变量可以是各种修饰符的,但是接口中的成员变量只能是 public static final 的;
5) 抽象类中可以有 static 代码块和 static 方法,但是接口中不能有 static 代码块和 static 方法;
6) 一个类只能继承一个抽象类,而一个类却可以实现多个接口;

接口的声明格式
public/[default] interface 接口名称 [extends 接口1[, 接口2, ...]] { 声明变量、抽象方法 }

类实现接口的格式
[修饰符] class 类名 [extends 父类] implements 接口1[, 接口2, ...] { 实现抽象方法 }

重写接口中声明的方法时,需要注意以下规则:
1) 类在实现接口的方法时,不能抛出强制性异常,只能在接口中,或者继承接口的抽象类中抛出该强制性异常;
2) 类在重写方法时要保持一致的方法名,并且应该保持相同或者相兼容的返回值类型;即应遵循方法重写的规则;

标记接口
标识接口是没有任何方法和属性的接口,它仅仅表明它的类属于一个特定的类型,供其他代码来测试允许做一些事情;
标识接口作用:简单形象的说就是给某个对象打个标(盖个戳),使对象拥有某个或某些特权;

没有任何方法的接口被称为标记接口;标记接口主要用于以下两种目的:
1) 建立一个公共的父接口;
2) 向一个类添加数据类型;
这种情况是标记接口最初的目的,实现标记接口的类不需要定义任何接口方法(因为标记接口根本就没有方法),但是该类通过多态性变成一个接口类型;

如何选择抽象类和接口
接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:
1) 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类;
2) 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能;

例子: