天天看点

怒肝俩月,新鲜出炉史上最有趣的Java小白手册,第一版,每个 Java 初学者都应该收藏(10)

03、具体示例

为了进一步展示抽象类的特性,我们再来看一个具体的示例。假设现在有一个文件,里面的内容非常简单——“Hello World”,现在需要有一个读取器将内容读取出来,最好能按照大写的方式,或者小写的方式。

这时候,最好定义一个抽象类,比如说 BaseFileReader:

public abstract class BaseFileReader {
    protected Path filePath;
    protected BaseFileReader(Path filePath) {
        this.filePath = filePath;
    }
    public List<String> readFile() throws IOException {
        return Files.lines(filePath)
                .map(this::mapFileLine).collect(Collectors.toList());
    }
    protected abstract String mapFileLine(String line);
}      

filePath 为文件路径,使用 protected 修饰,表明该成员变量可以在需要时被子类访问。

readFile() 方法用来读取文件,方法体里面调用了抽象方法 mapFileLine()——需要子类扩展实现大小写的方式。

你看,BaseFileReader 设计的就非常合理,并且易于扩展,子类只需要专注于具体的大小写实现方式就可以了。

小写的方式:

public class LowercaseFileReader extends BaseFileReader {
    protected LowercaseFileReader(Path filePath) {
        super(filePath);
    }
    @Override
    protected String mapFileLine(String line) {
        return line.toLowerCase();
    }
}      

大写的方式:

public class UppercaseFileReader extends BaseFileReader {

   protected UppercaseFileReader(Path filePath) {

       super(filePath);

   }

   @Override

   protected String mapFileLine(String line) {

       return line.toUpperCase();

}

你看,从文件里面一行一行读取内容的代码被子类复用了——抽象类 BaseFileReader 类中定义的普通方法 readFile()。与此同时,子类只需要专注于自己该做的工作,LowercaseFileReader 以小写的方式读取文件内容,UppercaseFileReader 以大写的方式读取文件内容。

接下来,我们来新建一个测试类 FileReaderTest:

public class FileReaderTest {

   public static void main(String[] args) throws URISyntaxException, IOException {

       URL location = FileReaderTest.class.getClassLoader().getResource("helloworld.txt");

       Path path = Paths.get(location.toURI());

       BaseFileReader lowercaseFileReader = new LowercaseFileReader(path);

       BaseFileReader uppercaseFileReader = new UppercaseFileReader(path);

       System.out.println(lowercaseFileReader.readFile());

       System.out.println(uppercaseFileReader.readFile());

项目的 resource 目录下有一个文本文件,名字叫 helloworld.txt。

可以通过 ClassLoader.getResource() 的方式获取到该文件的 URI 路径,然后就可以使用 LowercaseFileReader 和 UppercaseFileReader 两种方式读取到文本内容了。

输出结果如下所示:

[hello world]

[HELLO WORLD]

十、Java 接口

对于面向对象编程来说,抽象是一个极具魅力的特征。如果一个程序员的抽象思维很差,那他在编程中就会遇到很多困难,无法把业务变成具体的代码。在 Java 中,可以通过两种形式来达到抽象的目的,一种是抽象类,另外一种就是接口。

如果你现在就想知道抽象类与接口之间的区别,我可以提前给你说一个:

一个类只能继承一个抽象类,但却可以实现多个接口。

当然了,在没有搞清楚接口到底是什么,它可以做什么之前,这个区别理解起来会有点难度。

01、接口是什么

接口是通过 interface 关键字定义的,它可以包含一些常量和方法,来看下面这个示例。

public interface Electronic {

   // 常量

   String LED = "LED";

   // 抽象方法

   int getElectricityUse();

   // 静态方法

   static boolean isEnergyEfficient(String electtronicType) {

       return electtronicType.equals(LED);

   // 默认方法

   default void printDescription() {

       System.out.println("电子");

1)接口中定义的变量会在编译的时候自动加上 public static final 修饰符,也就是说 LED 变量其实是一个常量。

Java 官方文档上有这样的声明:

Every field declaration in the body of an interface is implicitly public, static, and final.

换句话说,接口可以用来作为常量类使用,还能省略掉 public static final,看似不错的一种选择,对吧?

不过,这种选择并不可取。因为接口的本意是对方法进行抽象,而常量接口会对子类中的变量造成命名空间上的“污染”。

2)没有使用 private、default 或者 static 关键字修饰的方法是隐式抽象的,在编译的时候会自动加上 public abstract 修饰符。也就是说 getElectricityUse() 其实是一个抽象方法,没有方法体——这是定义接口的本意。

3)从 Java 8 开始,接口中允许有静态方法,比如说 isEnergyEfficient() 方法。

静态方法无法由(实现了该接口的)类的对象调用,它只能通过接口的名字来调用,比如说 Electronic.isEnergyEfficient("LED")。

接口中定义静态方法的目的是为了提供一种简单的机制,使我们不必创建对象就能调用方法,从而提高接口的竞争力。

4)接口中允许定义 default 方法也是从 Java 8 开始的,比如说 printDescription(),它始终由一个代码块组成,为实现该接口而不覆盖该方法的类提供默认实现,也就是说,无法直接使用一个“;”号来结束默认方法——编译器会报错的。

怒肝俩月,新鲜出炉史上最有趣的Java小白手册,第一版,每个 Java 初学者都应该收藏(10)

允许在接口中定义默认方法的理由是很充分的,因为一个接口可能有多个实现类,这些类就必须实现接口中定义的抽象类,否则编译器就会报错。假如我们需要在所有的实现类中追加某个具体的方法,在没有 default 方法的帮助下,我们就必须挨个对实现类进行修改。

来看一下 Electronic 接口反编译后的字节码吧,你会发现,接口中定义的所有变量或者方法,都会自动添加上 public 关键字——假如你想知道编译器在背后都默默做了哪些辅助,记住反编译字节码就对了。

public interface Electronic
{
    public abstract int getElectricityUse();
    public static boolean isEnergyEfficient(String electtronicType)
    {
        return electtronicType.equals("LED");
    }
    public void printDescription()
    {
        System.out.println("\u7535\u5B50");
    }
    public static final String LED = "LED";
}      

有些读者可能会问,“二哥,为什么我反编译后的字节码和你的不一样,你用了什么反编译工具?”其实没有什么秘密,微信搜「沉默王二」回复关键字「JAD」就可以免费获取了,超级好用。