初级10 - 接口与抽象类(上)
Java 接口和抽象类的设计是为了 :
- 最大程度的灵活性
- 最大程度的复用
1. 抽象类
抽象类是使用abstract
声明的类,其可能包含abstract
声明的方法,抽象类不能被实例化,因此需要继承后的非抽象子类补充方法的实现后,才能使用。除此之外,和普通类没有区别,也可以拥有静态成员和静态方法。
2. 接口
接口比抽象类的抽象程度更高,也不能直接被实例化,是一组能力定义的集合。可以大概理解为没有任何方法实现的抽象类(Java8之前),或者说当一个抽象类中全部都是抽象方法时,有时直接声明为接口会更好。
接口不是类,是抽象方法的集合,类描述的是对象的属性和方法,而接口包含类需要实现的方法,因此接口也是不能实例化的,只能被实现。一个实现接口的类必须实现接口中所描述的全部方法,如果这是个抽象类,那么其子类中要继续去实现。
在 Java 的类继承体系中只能是单根继承,而接口体系可以继承多个,就像一个东西可以同时拥有多个功能。
![](https://img.haomeiwen.com/i7038854/9896eb380b2076ce.png)
![](https://img.haomeiwen.com/i7038854/fa9924987d37b563.png)
接口中的成员都是默认public static
(可以省略不写,但不能改),因此常量要大写+下划线分割;
接口中的抽象方法默认都是public abstract
(可以省略不写,但不能改)。
这很好理解,你的主机上的 USB 接口如果不暴露出来,而是封装在主机箱里面,有什么意义?
public abstract class A {
String NAME = "wangpeng";
void fun();
}
public abstract class B {
}
接口可以多根继承:
public interface C extends A, B {
}
这也意味着,当类实现接口的时候,类要实现继续接口继承体系中的所有方法。否则,类必须声明为抽象类。类使用implements
关键字实现接口,并能同时实现多个接口:
public class User implements A, B {
}
这样很美好,可是一旦一个第三方接口增加了新的方法声明,那么全世界所有实现这个接口的类都要继续实现全部的接口,这很可怕,破坏了向后兼容性。
所以,Java8进行了妥协,引入了default
修饰符(modifier),可以为接口中的方法提供默认实现(默认的权限修饰符也只能是public
,不用写,也不能改),可以用来实现 mixin(混合),
public interface A{
void a();
default void b() {
// do something
}
}
但这样又引入了二义性(菱形继承),就像C++的继承体系那样,不过还好二义性是无法通过编译的:
interface A {
default void f() {
System.out.println("A");
}
}
interface B {
default void f() {
System.out.println("B");
}
}
class C implements A, B {
{
f(); // 此时产生了二义性,A 和 B 接口中都实现了默认的 f()
}
}
3. 抽象类和接口的区别:
共同点:
- 抽象、不可实例化
- 可包含抽象方法(没有方法体)
不同点:
- 抽象类可包含类的一切,而接口只能包含:
- 默认就是
public static
的静态成员 - 默认就是
public static
的静态方法 - 默认是
pulic abstract
的抽象方法,或 Java8 的default
默认方法;
- 默认就是
- 抽象类只能单一继承,而接口可以多继承,甚至可以继承多次,毕竟接口只是个方法声明而已,但最终使用时需要有一个实现。
例子:
import java.util.Arrays;
import java.util.List;
public class World {
// 现在有若干种对象,请尝试使用接口和抽象类将它们建造成类型体系
// 以最大限度的复用、简化代码
public static List<Object> objects =
Arrays.asList(new 麻雀(), new 喜鹊(), new 蝴蝶(), new 飞机(), new 救护车(), new 猫(), new 狗());
public static void main(String[] args) {
会飞的东西飞();
会叫的东西叫();
动物都能新陈代谢();
}
// 在建造成类型体系后,体会多态带来的好处
public static void 会飞的东西飞() {
for (Object obj : objects) {
if (obj instanceof 会飞的东西) {
((会飞的东西) obj).飞();
}
}
}
// 在建造成类型体系后,体会多态带来的好处
public static void 会叫的东西叫() {
for (Object obj : objects) {
if (obj instanceof 会叫的东西) {
((会叫的东西) obj).叫();
}
}
}
// 在建造成类型体系后,体会多态带来的好处
public static void 动物都能新陈代谢() {
for (Object obj : objects) {
if (obj instanceof 动物) {
((动物) obj).新陈代谢();
}
}
}
interface 动物 {
void 新陈代谢();
}
interface 会飞的东西 {
void 飞();
}
interface 会叫的东西 {
void 叫();
}
// 这里 活的动物 也可以用抽象类,不过由于没什么抽象方法需要子类进一步实现,所以简单起见,就用最基础的类
static class 活的动物 implements 动物 {
@Override
public void 新陈代谢() {
System.out.println("新陈代谢");
}
}
// 简单起见,不用声明为 抽象类
static class 鸟 extends 活的动物 implements 会飞的东西, 会叫的东西 {
@Override
public void 飞() {
System.out.println("鸟儿飞");
}
@Override
public void 叫() {
System.out.println("叽叽喳喳");
}
}
static class 麻雀 extends 鸟 {
}
static class 喜鹊 extends 鸟 {
}
static class 蝴蝶 extends 活的动物 implements 会飞的东西 {
@Override
public void 飞() {
System.out.println("蝴蝶飞");
}
}
static class 飞机 implements 会飞的东西 {
@Override
public void 飞() {
System.out.println("飞机飞");
}
}
static class 救护车 implements 会叫的东西 {
@Override
public void 叫() {
System.out.println("哇呜哇呜");
}
}
static class 猫 extends 活的动物 implements 会叫的东西 {
@Override
public void 叫() {
System.out.println("喵喵喵");
}
}
static class 狗 extends 活的动物 implements 会叫的东西 {
@Override
public void 叫() {
System.out.println("汪汪汪");
}
}
}
4. 接口实战:实现文件过滤器,过滤指定扩展名的文件
首先 JDK 内部类 Files.walkFileTree 方法可用来遍历文件,第一个参数指定根节点,第二个参数是实现了 FileVisitor 接口的实例,用来提供给 Files.walkFileTree 遍历文件,实现过滤器钩子功能。
FileVisitor 文档:
A visitor of files. An implementation of this interface is provided to the {@link Files#walkFileTree Files.walkFileTree} methods to visit each file in a file tree.
在实现一个接口之前,先检查是否有默认实现,以便继承它。比如 SimpleFileVisitor
。
再比如内置的List
接口 和AbstractList
抽象类,后者作为骨架,提供了一些默认实现。
本例通过继承SimpleFileVisitor
并覆盖了visitFile
来快速实现了FileVisitor
接口,再通过匿名内部类简化代码,防止注意力分散,将其实例作为Files.walkFileTree
的第二个参数实现了文件遍历时的自定义过滤操作。
测试文件夹:
![](https://img.haomeiwen.com/i7038854/fb58fca154571c01.png)
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
public class FileFilter {
public static void main(String[] args) throws IOException {
Path projectDir = Paths.get(System.getProperty("user.dir"));
Path testRootDir = projectDir.resolve("test-root");
if (!testRootDir.toFile().isDirectory()) {
throw new IllegalStateException(testRootDir.toAbsolutePath().toString() + "不存在!");
}
List<String> filteredFileNames = filter(testRootDir, ".csv");
System.out.println(filteredFileNames);
}
/**
* 实现一个按照扩展名过滤文件的功能
*
* @param rootDirectory 要过滤的文件夹
* @param extension 要过滤的文件扩展名,例如 .txt
* @return 所有该文件夹(及其后代子文件夹中)匹配指定扩展名的文件的名字
*/
public static List<String> filter(Path rootDirectory, String extension) throws IOException {
List<String> names = new ArrayList<>();
Files.walkFileTree(rootDirectory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (file.getFileName().toString().endsWith(extension)) {
names.add(file.getFileName().toString());
}
return FileVisitResult.CONTINUE;
}
});
return names;
}
}
以上的例子是多态的运用,通过覆盖方法或直接实现接口,来做出更加灵活的功能。
5. 接口实战:Comparable 接口
Comparable 约定:
比较给定的两个对象顺序,小于返回负数;大于返回整数;等于返回零。
类似的接口还有Comparator
(具体区别见底部参考文章)。
Comparable是排序接口,若一个类实现了Comparable接口,就意味着“该类支持排序”。而Comparator是比较器,我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。
Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
两种方法各有优劣, 用Comparable 简单, 只要实现Comparable 接口的对象直接就成为一个可以比较的对象,但是需要修改源代码。 用Comparator 的好处是不需要修改源代码, 而是另外实现一个比较器, 当某个自定义的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了, 并且在Comparator 里面用户可以自己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了。
JDK 内部类的Collections.sort
方法的签名中指明要参数实现Comparable
接口:
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
这可以看出一种策略模式,传入的List<T> 集合中的元素实现了Comparable
接口,提供了某个排序策略,而在Collections.sort
方法体中不需要改变,运行时会动态调用list
对象上相应的实现(多态)。
具体的例子看下面,先看注释的地方:
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Point implements Comparable<Point> {
private final int x;
private final int y;
// 代表笛卡尔坐标系中的一个点
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Point point = (Point) o;
if (x != point.x) {
return false;
}
return y == point.y;
}
@Override
public int hashCode() {
int result = x;
result = 31 * result + y;
return result;
}
@Override
public String toString() {
return String.format("(%d,%d)", x, y);
}
// 重点是这里实现了 Comparable 接口
@Override
public int compareTo(Point that) {
if (this.x < that.x) {
return -1;
} else if (this.x > that.x) {
return 1;
} else if (this.x == this.x) {
if (this.y < that.y) {
return -1;
} else if (this.y > that.y) {
return 1;
}
}
return 0;
}
// 按照先x再y,从小到大的顺序排序
// 例如排序后的结果应该是 (-1, 1) (1, -1) (2, -1) (2, 0) (2, 1)
public static List<Point> sort(List<Point> points) {
Collections.sort(points); // 这里将集合传入方法中
return points;
}
public static void main(String[] args) throws IOException {
List<Point> points =
Arrays.asList(
new Point(2, 0),
new Point(-1, 1),
new Point(1, -1),
new Point(2, 1),
new Point(2, -1));
System.out.println(Point.sort(points));
}
}
再看下一个例子前,先来看点 JDK 文档中提到的知识:
Set 是不能重复的元素集合;
TreeSet 的元素会根据它们的Comparable
(自然排序)或Comparator
方法进行排序和去重。
同样,重点看注释:
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.TreeSet;
public class User implements Comparable<User> {
/** 用户ID,数据库主键,全局唯一 */
private final Integer id;
/** 用户名 */
private final String name;
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
User person = (User) o;
return Objects.equals(id, person.id);
}
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
// 方便查看集合中的对象打印结果
@Override
public String toString() {
return "{ id: " + this.id + ", name: " + this.name + " }";
}
/** 老板说让我按照用户名排序 */
@Override
public int compareTo(User o) {
// 下面被注释了的就是有 bug 的实现
// return name.compareTo(o.name);
// 下面尝试修复bug,核心思想:全等时才可以放心被集合去重
if (name.equals(o.name)) {
return id.compareTo(o.id);
} else {
return name.compareTo(o.name);
}
}
public static void main(String[] args) {
List<User> users =
Arrays.asList(
new User(100, "b"),
new User(10, "z"),
new User(1, "a"),
new User(2000, "a"));
TreeSet<User> treeSet = new TreeSet<>(users);
// 为什么这里的输出是3?试着修复其中的bug
System.out.println(treeSet.size());
System.out.println(treeSet);
}
}
如上所示,被注释的那种粗暴实现,会造成当两个name
判定为相等时被集合去重,结果就是丢失了一个元素。
防坑经验:
对于两个不等的元素,compareTo
不能返回 0,否则早晚踩坑。
6. 接口实战:自定义实现一个过滤器
可以使用策略模式,每个策略都实现了 JDK 内置的 Predicate 接口,然后把策略实例传入通用过滤器中,进一步优化代码,直接使用匿名内部类,强行声明接口并实现test
方法(还可以再进一步把匿名内部类改为 Java8 的 lambda 表达式)。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class User {
/** 用户ID,数据库主键,全局唯一 */
private final Integer id;
/** 用户名 */
private final String name;
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
// 尝试通过Predicate接口抽取成一个公用的过滤器函数来简化函数
public static List<User> filter(List<User> users, Predicate<User> predicate) {
List<User> results = new ArrayList<>();
for (User user : users) {
if (predicate.test(user)) {
results.add(user);
}
}
return results;
}
// 过滤ID为偶数的用户
public static List<User> filterUsersWithEvenId(List<User> users) {
return filter(users, new Predicate<User>() {
@Override
public boolean test(User user) {
return user.id % 2 == 0;
}
});
}
// 过滤姓张的用户
public static List<User> filterZhangUsers(List<User> users) {
return filter(users, new Predicate<User>() {
@Override
public boolean test(User user) {
return user.name.startsWith("张");
}
});
}
// 过滤姓王的用户
public static List<User> filterWangUsers(List<User> users) {
return filter(users, new Predicate<User>() {
@Override
public boolean test(User user) {
return user.name.startsWith("王");
}
});
}
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User(0, "王尼玛"),
new User(1, "张笑龙"),
new User(2, "刘强冬"));
filterUsersWithEvenId(users);
filterZhangUsers(users);
filterWangUsers(users);
}
}
参考: