Java 之旅

初级10 - 接口与抽象类(上)

2019-08-06  本文已影响0人  晓风残月1994

Java 接口和抽象类的设计是为了 :

  1. 最大程度的灵活性
  2. 最大程度的复用

1. 抽象类

抽象类是使用abstract声明的类,其可能包含abstract 声明的方法,抽象类不能被实例化,因此需要继承后的非抽象子类补充方法的实现后,才能使用。除此之外,和普通类没有区别,也可以拥有静态成员和静态方法。

2. 接口

接口比抽象类的抽象程度更高,也不能直接被实例化,是一组能力定义的集合。可以大概理解为没有任何方法实现的抽象类(Java8之前),或者说当一个抽象类中全部都是抽象方法时,有时直接声明为接口会更好。

接口不是类,是抽象方法的集合,类描述的是对象的属性和方法,而接口包含类需要实现的方法,因此接口也是不能实例化的,只能被实现。一个实现接口的类必须实现接口中所描述的全部方法,如果这是个抽象类,那么其子类中要继续去实现。

在 Java 的类继承体系中只能是单根继承,而接口体系可以继承多个,就像一个东西可以同时拥有多个功能。

类和接口的关系(实线表示继承,虚线表示实现) 可以参考JDK内部实现——HashSet等

接口中的成员都是默认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. 抽象类和接口的区别:

共同点:

不同点:

例子:

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的第二个参数实现了文件遍历时的自定义过滤操作。

测试文件夹:


test-root
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);
    }
}

参考:

  1. Java中Comparable和Comparator区别小结
上一篇 下一篇

猜你喜欢

热点阅读