Java编程笔记6:接口

Java编程笔记6:接口

5c9c3b3b392ac581.jpg

图源:PHP中文网

在Java中,接口可能只是特指使用interface关键字声明的一种抽象类型,但实际上在UML或者OOP中,接口往往是作为一种底层抽象概念而言的,其具体实现可能是接口也可能是抽象类,甚至因为语言的差异会有很大区别。

抽象类

在Java中,可以使用abstract将一个类声明为抽象类,被声明为抽象类的类不能被实例化:

package ch6.abstract1;

abstract class MyClass{

}

public class Main {
    public static void main(String[] args) {
        // MyClass mc = new MyClass();    
        // Cannot instantiate the type MyClass
    }
}

被注释的代码无法通过编译。

之所以会这么设计,是因为某些类作为基类,只是充当一种“概念”,并不需要真的创建具体实例,比如:

abstract class Tank{
}

class LightTank extends Tank{}

class HeavyTank extends Tank{}

在上面这个继承层次中,Tank仅仅作为一种基础类型,而不应当真的在代码中初始化,所以这里将其定义为abstract

通常抽象类会包含“抽象方法”,这些抽象方法同样会用abstract进行声明。抽象方法与普通方法的区别在于:只包含返回值和方法签名,而没有方法体。

package ch6.abstract3;
abstract class Tank{
    abstract public void move();
    abstract public void fire(); 
}

抽象方法必须被子类重写,除非子类也是个抽象类(一般不会这样做):

package ch6.abstract3;

abstract class Tank {
    abstract public void move();

    abstract public void fire();

}

class LightTank extends Tank {

    @Override
    public void move() {
        System.out.println("Light Tank is moving.");
    }

    @Override
    public void fire() {
        System.out.println("Light Tank is firing.");
    }

}

class HeavyTank extends Tank {

    @Override
    public void move() {
        System.out.println("Heavy Tank is moving.");
    }

    @Override
    public void fire() {
        System.out.println("Heavy Tank is firing.");
    }
}

public class Main {
    public static void main(String[] args) {
        Tank t1 = new HeavyTank();
        Tank t2 = new LightTank();
        t1.move();
        t1.fire();
        t2.move();
        t2.fire();
        // Heavy Tank is moving.
        // Heavy Tank is firing.
        // Light Tank is moving.
        // Light Tank is firing.
    }
}

通常你不需要担心你在继承某个抽象类时忘记实现相应的抽象方法,因为IDE会提醒你,甚至帮你创建“骨架代码”。

我们可以利用抽象类和抽象方法的这种特性来应用“模版方法模式”。

假设每辆坦克出厂后都需要经过移动和开火两个步骤来进行试车,那么我们完全可以在抽象类Tank中添加一个test方法来完成试车工作,虽然此时需要调用的两个方法movefire都是抽象方法,没有任何实现,但是其抽象方法的特性决定了子类必然会重写这两个方法。换句话说,对于任意的一个继承自Tank的子类实例,在调用test时,都会因为多态机制而调用正确的movefire方法完整试车工作。

下面是修改后的代码:

package ch6.abstract3;

abstract class Tank {
    abstract public void move();

    abstract public void fire();

    final public void test() {
        this.move();
        this.fire();
    }
}

...

public class Main {
    public static void main(String[] args) {
        Tank t1 = new HeavyTank();
        Tank t2 = new LightTank();
        t1.test();
        t2.test();
        // Heavy Tank is moving.
        // Heavy Tank is firing.
        // Light Tank is moving.
        // Light Tank is firing.
    }
}

这里将test定义为final,是因为作为一个模版方法,往往不希望子类进行重写,所以可以考虑声明为final

要了解更多的模版方法模式,可以阅读设计模式 with Python 8:模版方法模式 - 魔芋红茶’s blog (icexmoon.xyz)

接口

就像前边说的,在OOP中接口和抽象类有着类似的定位,但因为具体语言实现的方式不同,使用起来有着一些差异。

在Java中,接口从定义方式而言,要比抽象类简单很多,由interface关键字定义的接口通常只会包含一组抽象方法:

interface CarTestable{
    void move();
    void fire();
}

需要注意的是,接口中的方法默认都是抽象方法,所以不需要显式使用abstract关键字,此外接口的方法都默认是public访问权限。这是因为接口的特性决定的,因为接口往往是作为一种定义类的“协议”,也就是说实现了某种接口就可以很自然地当做某种接口来调用相应的方法,从这个角度而言接口中定义的方法只能是public的。

JavaSE8给接口添加了新特性,可以定义静态方法和“默认方法”,这点将在之后进行说明。

在上边的示例中,我将接口命名为CarTestable而非Tank是有意为之,在Java中,接口和类的概念是明显不同的,类是一种基本类型,而接口仅仅是一种“能力”和“特性”。所以CartTestable这个接口代表某种军队中用来测试军车的"能力",任何军用车辆只要能“移动”和"开火",都可以说符合这种能力,可以用来试车,而不仅仅局限于Tank,所以两者是有很明显的区别的。

顺带一提,因为上面所说的原因,Java中习惯使用XXXable这样的命名方式来命名接口,比如Runable

使用接口同样简单,这里依然沿用之前坦克的例子:

package ch6.interface2;

interface CarTestable {
    void move();

    void fire();
}

abstract class Tank implements CarTestable {
}

class LightTank extends Tank {

    @Override
    public void move() {
        System.out.println("Light Tank is moving.");
    }

    @Override
    public void fire() {
        System.out.println("Light Tank is firing.");
    }
}

class HeavyTank extends Tank {

    @Override
    public void move() {
        System.out.println("Heavy Tank is moving.");
    }

    @Override
    public void fire() {
        System.out.println("Heavy Tank is firing.");
    }
}

//装甲车
class ArmouredCar implements CarTestable{

    @Override
    public void move() {
        System.out.println("Armoured Car is moving.");
    }

    @Override
    public void fire() {
        System.out.println("Armoured Car is firing.");
    }}

public class Main {
    private static void test(CarTestable ct){
        ct.move();
        ct.fire();
    }
    public static void main(String[] args) {
        CarTestable ct1 = new HeavyTank();
        CarTestable ct2 = new LightTank();
        CarTestable ct3 = new ArmouredCar();
        test(ct1);
        test(ct2);
        test(ct3);
    }
}

上面的例子中没有去除抽象类Tank,而是让其直接实现CarTestable接口。可以看出接口和抽象类并非一定是对立的,它们依然可以共存,就像我说的,在Java中,因为代码实现的方式不同,它们在概念上有着明显的差异。

此外,接口明显要比抽象类灵活的多,装甲车明显不是一个坦克,但这并不妨碍它实现CarTestable接口,并且可以被test(CarTestable ct)方法接受并进行车辆测试。

上面这个例子中Main中的test静态方法显然是为接口CarTestable专门设计的。如果仅仅会调用一次,这样做并没有什么问题,但如果要调用多次,就很难进行重用。当然我们可以将其放入一个命名为util包中的某个工具类中,但并不是很合适。理想的情况是可以将其直接与接口CartTestable进行关联,毕竟没有这个接口也就不会有test方法。

JavaSE8中添加的接口静态方法可以解决这个问题:

package ch6.interface3;

interface CarTestable {
    void move();

    void fire();

    static void test(CarTestable ct) {
        ct.move();
        ct.fire();
    }
}
...
public class Main {

    public static void main(String[] args) {
        CarTestable ct1 = new HeavyTank();
        CarTestable ct2 = new LightTank();
        CarTestable ct3 = new ArmouredCar();
        CarTestable.test(ct1);
        CarTestable.test(ct2);
        CarTestable.test(ct3);
    }
}

此外,JavaSE8还给接口添加了一种“默认方法”,使用default进行声明。“默认方法”与抽象方法不同,可以有方法体,而实现接口的类可以对其进行重写,也可以不重写:

package ch6.interface4;

interface CarTestable {
    void move();

    default void fire(){
        System.out.println("Skip fire test.");
    };

    static void test(CarTestable ct) {
        ct.move();
        ct.fire();
    }
}
...
class Jeep implements CarTestable{

    @Override
    public void move() {
        System.out.println("Jeep is moving.");
    }}

public class Main {

    public static void main(String[] args) {
		...
        CarTestable ct4 = new Jeep();
        CarTestable.test(ct4);
        // Jeep is moving.
        // Skip fire test.
    }
}

在上面这个示例中,添加了一个新的类型Jeep,虽然同样实现了CarTestable,但一般的用来通勤的吉普车是没有火力的,所以自然无法进行开火测试。所以接口中的fire方法被修改为“默认方法”,默认情况下会直接跳过开火测试。而Jeep类只实现了move方法,没有实现fire方法,所以最终的效果是进行了移动测试,跳过了开火测试。

这个例子并不一定恰当,仅作为参考。

完全解耦

通过抽象类和接口的使用,可以对系统中的类进行“解耦”,让设计更具扩展性和灵活性。

下面通过示例来进行说明:

package ch6.decouple;

abstract class Sequence {
    abstract public Object next();

    abstract public boolean hasNext();

    public void print() {
        while (this.hasNext()) {
            System.out.print(this.next() + " ");
        }
        System.out.println();
    }
}

class NumberSequence extends Sequence {
    private int[] numbers;
    private int cursor;

    public NumberSequence(int[] numbers) {
        this.numbers = numbers;
    }

    @Override
    public Integer next() {
        Integer item = numbers[cursor];
        cursor++;
        return item;
    }

    @Override
    public boolean hasNext() {
        if (cursor >= numbers.length) {
            return false;
        }
        return true;
    }

}

class CharSequence extends Sequence {
    private char[] chars;
    private int cursor;

    public CharSequence(char[] chars) {
        this.chars = chars;
    }

    @Override
    public Character next() {
        Character item = chars[cursor];
        cursor++;
        return item;
    }

    @Override
    public boolean hasNext() {
        if (cursor >= chars.length) {
            return false;
        }
        return true;
    }

}

public class Main {
    public static void main(String[] args) {
        Sequence s1 = new NumberSequence(new int[] { 1, 2, 3 });
        Sequence s2 = new CharSequence(new char[]{'a','b','c'});
        s1.print();
        s2.print();
    }
}

这个示例中有一个抽象基类Sequence代表一种序列类型,而子类NumberSequence代表数字序列,CharSequence代表字符序列,序列可以进行遍历,所以基类定义了两个遍历用的抽象方法nexthasNext。同时为了能方便地在屏幕上进行打印,提供了一个打印方法print

假设我们有一个新的类型NumberGenerator,这个类可以随机产生一个数字:

class NumberGenerator{
    private static Random random = new Random();
    public int getNumber(){
        return random.nextInt(100);
    }
}

我们现在同样想通过类似序列的print方法一样能简单地获取若干个NumberGenerator产生的数字并打印,要怎么做呢?

可能有人会试图让NumberGenerator去继承Sequence,但这样做是不合适的,因为前者从概念上并不是一个序列,其次前者产生的数字是无限多个,也不适合去实现一个hasNext这样的方法。更不适合通过类似的方法进行遍历,那样做只会陷入死循环。

换种方式思考,我们这里只是想调用一个“统一”的print方法,并不关心具体的对象是一个序列还是一个随机数产生器不是吗?所以完全可以将print方法抽象成一个Printable接口,让相应的类型实现这个接口即可:

package ch6.decouple3;

import java.util.Random;

interface Printable {
    void print();
}

abstract class Sequence implements Printable {
    abstract public Object next();

    abstract public boolean hasNext();

    public void print() {
        while (this.hasNext()) {
            System.out.print(this.next() + " ");
        }
        System.out.println();
    }
}

...

class NumberGenerator implements Printable {
    private static Random random = new Random();
    private int printTimes = random.nextInt(10);

    public int getNumber() {
        return random.nextInt(100);
    }

    @Override
    public void print() {
        for (int i = 0; i < printTimes; i++) {
            System.out.print(getNumber() + " ");
        }
        System.out.println();
    }

    public void setPrintTimes(int times) {
        if (times > 0) {
            printTimes = times;
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Printable p1 = new NumberSequence(new int[] { 1, 2, 3 });
        Printable p2 = new CharSequence(new char[] { 'a', 'b', 'c' });
        p1.print();
        p2.print();
        NumberGenerator ng = new NumberGenerator();
        ng.setPrintTimes(5);
        ng.print();
        // 1 2 3 
        // a b c
        // 58 90 17 3 65 
    }
}

这样做依然有一个不太合适的地方,即为了给NumberGenerator添加打印功能,我们不得不将其进行了一定程度的修改,为了控制打印次数,还添加上了printTimes这个属性以及相应的修改器。一般情况下这样做是没什么太大问题的,但有时候可能出于某种原因,你不希望对既有类型做出这样的修改,但同时又想要让既有类型能实现某个接口,这种情况下可以使用适配器模式。

适配器模式简单地讲,就是通过定义一个适配器类,将某个已有类型“适配”为目标类型,这个过程就像是插线板上的适配器插头那样,可以将原本不能直接使用的220V高压交流电变成能直接使用的低压直流电。

更多的适配器模式介绍可以阅读设计模式 with Python 7:适配器模式 - 魔芋红茶’s blog (icexmoon.xyz)

使用适配器模式修改后的代码:

...
class NumberGenerator {
    private static Random random = new Random();

    public int getNumber() {
        return random.nextInt(100);
    }
}

class NGPrintAdapter implements Printable {
    private NumberGenerator ng;
    private int printTimes;

    public NGPrintAdapter(NumberGenerator ng, int printTimes) {
        this.ng = ng;
        if (printTimes < 0) {
            throw new Error();
        }
        this.printTimes = printTimes;
    }

    @Override
    public void print() {
        for (int i = 0; i < printTimes; i++) {
            System.out.print(ng.getNumber() + " ");
        }
        System.out.println();
    }

}

public class Main {
    public static void main(String[] args) {
        Printable p1 = new NumberSequence(new int[] { 1, 2, 3 });
        Printable p2 = new CharSequence(new char[] { 'a', 'b', 'c' });
        p1.print();
        p2.print();
        NumberGenerator ng = new NumberGenerator();
        Printable p3 = new NGPrintAdapter(ng, 6);
        p3.print();
        // 1 2 3 
        // a b c
        // 40 26 82 31 78 6
    }
}

可以看到,这样的代码比之前的实现要“干净”很多,并且在NGPrintAdapter中设置打印次数也更为合理。这样做也更符合设计模式的单一职责原则

多重继承

“多继承”是一个相当古老的问题,伴随着编程语言的发展史。

具体来说,“多继承”是指一个类可以同时继承多个不同的基类。显然在Java中并不支持这样的做法。

但是在OOP中,多继承显然是必须的,就像之前例子中展示的,一辆轻型坦克在是坦克的基础上,也具备车辆测试的能力,一个数字序列在是一个序列的基础上,也具备打印的能力。

对此不同的编程语言有不同的解决方式,在Python中,是直接对多继承提供支持,并通过“菱形策略”来解决随之而来的“方法冲突”问题。在Go中,因为没有类,只有struct,并且不同的struct全部是通过组合的方式来进行复用,所以压根不存在多继承的问题。而PHP的OOP部分是借鉴自Java,与Java几乎一模一样。

Java对此给出的解决方案是一个类只能继承一个基类,但可以实现一个或多个接口。

虽然绝大多数情况下,Java的这种做法可以避免出现多继承的“方法冲突”问题,但是某些极端情况下依然可能会产生类似的问题:

package ch6.multi_ext;

interface Printable1 {
    void print();
}

interface Printable2 {
    int print();
}

class MyClass implements Printable1, Printable2 {

    // Duplicate method print() in type MyClass
    @Override
    public int print() {
        return 0;
    }

    @Override
    public void print() {
    }

}

public class Main {

}

上边的代码无法通过编译,会提示“方法重复定义”。这里的原因在于,在Java编程笔记2:初始化和清理 - 魔芋红茶’s blog (icexmoon.xyz)中提到过,方法重载是通过方法签名来进行区分的,这里边并不包含返回值类型。道理在于在调用方法的时候,比如a.test(),并不需要一定接收返回值,所以是没法在调用时通过返回值类型来区分方法的,自然返回值类型就不会作为重载的依据。而重写方法时则要求方法签名与返回值都必须完全相同,至少也要是“协变返回值”。这就导致了上边出现的问题,即两个接口有着方法签名相同,但返回值类型不同的方法,而某个类同时实现这两个接口的时候就会发现,无法同时实现这两个接口的方法,因为只有返回值类型不同是不算做方法重载的,这种情况只会出现“方法重复定义”这样的错误。

但不需要过于担心这样的问题,因为很少会有接口定义类似的方法,且还要同时实现这些接口的情况出现。

扩展接口

在Go中,可以很容易地组合不同的接口。在Java中,同样可以通过类似的方式来扩展接口:

package ch6.ext_if;

import java.util.Arrays;
import java.util.Random;

interface Writer {
    int write(char[] content);
}

interface ReaderWriter extends Writer {
    int read(char[] content);
}

class CharSequence implements ReaderWriter {
    private char[] chars;

    public CharSequence(char[] chars) {
        this.chars = chars;
    }

    @Override
    public int write(char[] content) {
        if (content.length == 0) {
            return 0;
        }
        int counter = 0;
        for (int i = 0; i < chars.length; i++) {
            if (i >= content.length) {
                break;
            }
            chars[i] = content[i];
            counter++;
        }
        return counter;
    }

    @Override
    public int read(char[] content) {
        int counter = 0;
        for (int i = 0; i < content.length; i++) {
            if (i >= chars.length) {
                break;
            }
            content[i] = chars[i];
            counter++;
        }
        return counter;
    }

    @Override
    public String toString() {
        return Arrays.toString(chars);
    }
}

public class Main {
    private static Random random = new Random();

    private static char[] getRandomChars() {
        char[] chars = new char[random.nextInt(10) + 1];
        for (int i = 0; i < chars.length; i++) {
            chars[i] = (char) (random.nextInt(32) + 97);
        }
        return chars;
    }

    public static void main(String[] args) {
        CharSequence cs = new CharSequence(new char[10]);
        char[] chars1 = getRandomChars();
        cs.write(chars1);
        System.out.println(chars1);
        System.out.println(cs);
        char[] chars2 = new char[5];
        cs.read(chars2);
        System.out.println(chars2);
        // pyvgtqalyy
        // [p, y, v, g, t, q, a, l, y, y]
        // pyvgt
    }
}

这里采用了Go的风格命名接口,实际上应当命名为Writeable这样的名称。

一个接口还可以扩展自多个接口,比如上面的ReaderWriter接口可以拆分成两个接口:

interface Writer {
    int write(char[] content);
}

interface Reader {
    int read(char[] content);
}

interface ReaderWriter extends Reader, Writer {
}
...

这样更具灵活性,可以按需要让类实现Reader接口或Writer接口,甚至是ReaderWriter接口。

在同时扩展多个几口时,同样可能出现前面所说的“方法冲突”的问题。

适配接口

在前边【完全解耦】小节中,有说明如何使用适配器模式来更灵活地实现接口,这里再补充一个例子:

package ch6.adapter;

import java.io.IOException;
import java.nio.CharBuffer;
import java.util.Random;
import java.util.Scanner;

class NumberGenerator {
    private static Random random = new Random();

    public int getNumber() {
        return random.nextInt(100);
    }
}

class NGReadableAdapter implements Readable {
    private int times;
    private NumberGenerator ng;

    public NGReadableAdapter(NumberGenerator ng, int readTimes) {
        this.ng = ng;
        if (readTimes < 0) {
            throw new Error();
        }
        this.times = readTimes;
    }

    @Override
    public int read(CharBuffer cb) throws IOException {
        if (times > 0) {
            String strNum = Integer.toString(ng.getNumber());
            cb.append(strNum + " ");
            times--;
            return strNum.length() + 1;
        }
        return -1;
    }

}

public class Main {
    public static void main(String[] args) {
        NumberGenerator ng = new NumberGenerator();
        Scanner scanner = new Scanner(new NGReadableAdapter(ng, 10));
        while (scanner.hasNext()) {
            System.out.println(scanner.next());
        }
    }
}

这里同样使用NumberGenerator这个类,不过是使用标准库的Scanner类来对其进行遍历。Scanner类的构造函数支持多种类型,比如StreamFile等,这里使用一个简单的Readable接口。换言之,所有实现了Readable接口的类型都可以被Scanner进行遍历。

Readable需要实现一个read方法,其接收一个CharBuffer类型的参数,CharBuffer可以看做一个字符缓冲,使用append方法可以简单地向里边填充字符序列。并需要返回填充的字符个数,如果没有东西可以返回,就返回-1。需要注意的是,Scanner类遍历时是按照字符分隔符来遍历的,也就是说会以空白符或者换行来切分字符串,所以为了能让Scanner正确遍历,需要在填充完一个数字后填充一个空格,作为token

最后的遍历代码很简单,使用ScannerhasNextnext方法即可,Java中很多用于遍历的方法都采用类似的设计。

接口中的字段

《Java编程思想》将这里翻译为“接口中的域”,我认为是不合适的,即使没有看原版,也能猜测原文应当使用的是field这个单词,通常会用fieldattribute来称呼类属性,所以这里应当翻译为“字段”。

虽然不太常见,但接口中的确是可以定义属性(字段)的,接口中的属性默认是public static final的,所以在JavaSE5引入enum之前,通常会利用接口来定义枚举值:

package ch6.field;

interface WeekDay {
    int MONDAY = 1,
            TUESDAY = 2,
            WEDNESDAY = 3,
            THURSDAY = 4,
            FRIADAY = 5,
            SATURDAY = 6,
            SUNDAY = 7;
}

public class Main {
    public static void main(String[] args) {
        System.out.println(WeekDay.MONDAY);
    }
}

与类不同的是,接口因为不能被实例化,也无法被继承,所以只能拥有静态属性,不能定义非静态属性。此外,接口的静态属性作为默认的final常量,必须被在定义的同时初始化,也就是说不能定义“空final”形式的属性。这也不难理解,因为接口是没有构造函数的,“空final”形式的属性对接口没有意义。

但是接口的字段并非只能使用常量在编译期初始化,同样可以用某些表达式实现运行时的初始化:

package ch6.field2;

import java.util.Random;

interface WeekDay {
    int MONDAY = 1,
            TUESDAY = 2,
            WEDNESDAY = 3,
            THURSDAY = 4,
            FRIADAY = 5,
            SATURDAY = 6,
            SUNDAY = 7;
    Random RANDOM = new Random();
    int RANDOM_WEEK_DAY = RANDOM.nextInt(7) + 1;
}

public class Main {
    public static void main(String[] args) {
        System.out.println(WeekDay.MONDAY);
        System.out.println(WeekDay.RANDOM_WEEK_DAY);
    }
}

嵌套接口

Java编程笔记3:访问权限控制 - 魔芋红茶’s blog (icexmoon.xyz)中提到过,类只有两种访问权限:public和包访问权限。接口与之类似,同样只能有public和包访问权限。

其实除了常见的类定义外,还可以在类中定义类,这种方式可以看做是类嵌套,在Java中有个专有名词——“内部类”。与之类似的是,接口也可以进行嵌套,或许我们可以叫它“内部接口”?

好像并没有“内部接口”这样的官方称呼,或许这和接口嵌套并不常见有关。

用一个接口来嵌套另一个接口,仅能构建某种接口的从属关系,就像是给接口又套了一层包一样,和内部类相比似乎没有太大用处:

package ch6.inner;

import java.util.Random;

interface IOInterface {
    interface Reader {
        int read(char[] content);
    }

    interface Writer {
        int write(char[] content);
    }

    interface ReaderWriter extends Reader, Writer {
    }
}
...

当然这里可以给IOInterface添加一些额外方法,但似乎没有一定需要这么做的必要。

使用这样嵌套在内部的接口需要使用相应的外部接口名称:

package ch6.inner;

import java.util.Arrays;

import ch6.inner.IOInterface.ReaderWriter;

public class CharSequence implements ReaderWriter {
    private char[] chars;
    ...
}

除了可以在接口中定义接口,还可以在类中定义接口:

...
class IOInterface {
    public interface Reader {
        int read(char[] content);
    }

    public interface Writer {
        int write(char[] content);
    }

    public interface ReaderWriter extends Reader, Writer {
    }
}
...

和上边接口嵌套接口的例子没有太大差别。需要注意的是类中定义的接口默认是包访问权限,所以需要指定为public,否则就是包访问权限,这和在接口中定义明显不同(默认是public的)。

此外,在接口中定义的接口,其访问权限只能是public的,事实上接口中的所有元素(字段、方法、嵌套接口)都只能是public的,无论你有没有明确指定

显然类定义中并没有那么多限制,所以可能出现一些比较奇怪的现象,比如一个private的内部接口:

package ch6.inner3;

class MyClass {
    private interface Printable {
        void print();
    }

    public static void passPrintable(Printable p) {
        p.print();
    }

    public static Printable getPrintable() {
        return new Printable() {

            @Override
            public void print() {
                System.out.println("This is a printable test.");
            }

        };
    }
}

public class Main {
    public static void main(String[] args) {
        // MyClass.Printable p = MyClass.getPrintable();
        // The type MyClass.Printable is not visible

        // Object o = MyClass.getPrintable();
        // MyClass.passPrintable(o);
        // The method passPrintable(MyClass.Printable) in the type MyClass is not applicable for the arguments (Object)
        
        MyClass.passPrintable(MyClass.getPrintable());
        // This is a printable test.
    }
}

Printable是定义在MyClass内部的一个接口,它的访问权限是private的,也就是说只有MyClass内可以访问。静态方法getPrintable会返回一个Printable类型的实例。在测试用的main函数中,会发现无法通过类似MyClass.Printable p = MyClass.getPrintable();的语句获取到getPrintable的返回值,原因是MyClass.Printable是私有的,无法在main函数中访问,自然就无法通过编译。虽然可以用Object句柄来替代MyClass.Printable承接返回值,毕竟所有类都是Object的子类。但是如果尝试将获取到的实例o传递给passPrintable方法,就会发现并不可行,因为目标方法只能接收一个Printable接口类型。

这样我们就会陷入类似鸡生蛋蛋生鸡的困境中,但其实可以将getPrintable的返回值直接传递给passPrintable方法,比如MyClass.passPrintable(MyClass.getPrintable()),这样做就可以正常执行。或许这样看上去有点怪异,也想不出有这么做的必要性,但是这的确产生了一种“类A产生的值只能由类A自己处理,外部代码最多只能进行中途传递,而无法正常持有”的奇特效果。

《Thinking in Java》对此的看法是只要语言中存在一种特性,总会有用武之地,我对此持保留态度。

接口与工厂

在OOP设计的系统中,往往需要进行一些类构件工作,比如生产一辆坦克:

package ch6.factory;

class Tank{
    public void buildSites(){
        System.out.println("Tank sites is build.");
    }

    public void buildBarbette(){
        System.out.println("Tank barbette is build.");
    }

    public void buildWeaponSystem(){
        System.out.println("Tank weapon system is build.");
    }

    public void ready(){
        System.out.println("Tank build work is all over.");
    }
}

public class Main {
    public static void main(String[] args) {
        Tank t = new Tank();
        t.buildSites();
        t.buildBarbette();
        t.buildWeaponSystem();
        t.ready();
    }
}

Tank对象创建后,必须按顺序调用一系列方法完成相关的创建工作,只有所有工作执行完毕后,Tank对象才能交付给客户端代码进行使用。

这种创建对象的方式在OOP设计中相当常见,如果这些“准备”新对象的代码需要重复使用,那么对其进行封装是必然的选择:

package ch6.factory2;

public class Factory {
    public static Tank buildTank(){
        Tank t = new Tank();
        t.buildSites();
        t.buildBarbette();
        t.buildWeaponSystem();
        t.ready();
        return t;
    }
}

现在我们可以用更简洁的方式创建Tank实例:

package ch6.factory2;

public class Main {
    public static void main(String[] args) {
        Tank t = Factory.buildTank();
    }
}

这种解决方案在设计模式中被称作“简单工厂”。

在解决实际问题中,往往Tank是一簇产品,而非简单的单一产品,所以我们需要利用接口或抽象类来创建一个产品簇,相应的,工厂类同样也需要一簇工厂来进行“生产”:

package ch6.factory3;

public class Main {
    public static void main(String[] args) {
        Factory factory = new HTFactory();
        Tank tank1 = factory.buildTank();
        factory = new LTFactory();
        Tank tank2 = factory.buildTank();
        // Heavy Tank sites is build.
        // Heavy Tank barbette is build.
        // Heavy Tank weapon system is build.
        // Heavy Tank build work is all over.
        // Light Tank sites is build.
        // Light Tank barbette is build.
        // Light Tank weapon system is build.
        // Light Tank build work is all over.
    }
}

篇幅关系,这里只展示测试代码,完整代码见java-notebook/xyz/icexmoon/java_notes/ch6/factory3 at main · icexmoon/java-notebook (github.com)

需要说明的是,TankFactory作为底层抽象,使用接口或抽象类实现都是可行的,一般来说,Tank作为产品来说,使用抽象类更合适,Factory因为仅包含一个工厂方法,可以定义为接口,也可以定义为抽象类。

更多的工厂模式可以阅读设计模式 with Python 4:工厂模式 - 魔芋红茶’s blog (icexmoon.xyz)

谢谢阅读。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值