BlackFlame33

BlackFlame33

若无力驾驭,自由便是负担。个人博客 https://blackflame33.cn/

《On Java》阅读笔记-03

第 10 章 接口#

本章介绍了抽象类和接口的定义,使用,以及区别和意义。这章进一步阐释了 “使用与功能实现分离” 的优秀设计,还顺带介绍了一些设计模式。图灵导读目标如图所示:

第 10 章 接口

接口,到底代表了什么?#

接口描述了一个类应该是什么样的和可以做什么,但是不包括怎么做。它实际上是定义了一种类之间的 “协议”。我个人认为接口是一种更高阶的抽象:在代码中对一些相同行为的类进行了进一步的抽象,这带来了代码复用和很强的扩展性,但也带来了额外的复杂性。从这里可以看出,要保持合理设计,就要适当封装,将抽象控制在合理的层次,否则这是一种过度设计!

接口的默认方法#

接口本来是不能实现任何方法的,它仅仅代表 “协议”,需要交给子类实现(implements)。但JDK 8依然为借口提供了默认方法。当一个接口有默认方法时,子类可以不用重写这个方法,以作为默认实现。一种说法是默认方法增强了代码的兼容性和灵活性。《On Java》认为:这允许向现有接口中添加方法,而不会破坏已经在使用该接口的所有代码。默认方法应该是为JDK 8引入流的解决方案。

抽象类与接口的区别#

特性接口抽象类
组合可以在新类中组合多个接口只能继承一个抽象类
状态(字段)不能包含字段(静态字段除外,但它们不支持对象状态)可以包含字段,非抽象方法可以引用这些字段
默认方法与抽象方法默认方法不用被子类实现,它只能引用接口内的方法(字段不行)抽象方法必须在子类实现
构造器不能有构造器可以有构造器
访问权限限制隐式的 public可以为protected或包访问权限

使用准则:“在合理的范围内尽可能抽象”。所以一般是优先用接口,等需要使用抽象类的时候你自然会知道。但除非必要,否则两个都不要使用。在后面会讲到,这是一种过早优化。接口和抽象类可以作为我们重构代码的强力工具。

完全解耦#

比起抽象类,接口更受人喜欢些,毕竟如果你调用的方法传入抽象类及其子类,你将只能使用基类及其子类,不能跳出其继承体系。而使用接口不受继承体系限制,能编写更多可复用的代码。

浅谈策略设计模式#

首先,有关策略设计模式,这篇文章非常好,后续如果有设计模式相关问题我也会引用。

策略设计模式就是创建一个方法,它能根据传递的参数对象来表现出不同的行为。这使得上下文没有必要知道它这个策略是怎么实现的,我直接用就行了,后续如果有新策略,不用更改上下文,使得方法更加灵活,通用,可复用性也更高。也就是 “我这个方法可以操作任何对象,只要这个对象遵循我的接口”。这也是 “将变化的事物与不变的事物分开” 的设计体现。

这里写上Java的简单的实现,后续可以展开讲讲:

import java.util.Random;

enum concreteStrategy {
    // 加法
    addition,
    // 减法
    subtraction,
    // 乘法
    multiplication

}

/**
 * 策略接口声明了某个算法各个不同版本间所共有的操作。上下文会使用该接口来
 * 调用有具体策略定义的算法。
 */
interface Strategy {
    /**
     * 该方法可实现某种二元运算,并返回值。
     *
     * @param num1 运算数 1
     * @param num2 运算数 2
     * @return double 进行运算后的结果
     * @author BlackFlame33
     */
    double execute(int num1, int num2);
}

/**
 * 具体策略会在遵循策略基础接口的情况下实现算法。该接口实现了它们在上下文
 * 中的互换性。
 */
class ConcreteStrategyAdd implements Strategy {

    @Override
    public double execute(int num1, int num2) {
        return num1 + num2;
    }
}

class ConcreteStrategySubtract implements Strategy {
    @Override
    public double execute(int num1, int num2) {
        return num1 - num2;
    }
}

class ConcreteStrategyMultiply implements Strategy {

    @Override
    public double execute(int num1, int num2) {
        return num1 * num2;
    }
}

/**
 * 上下文定义了客户端关注的接口。
 */
class Context {
    /**
     * 上下文会维护指向某个策略对象的引用。上下文不知晓策略的具体类。上下
     * 文必须通过策略接口来与所有策略进行交互。
     */
    private Strategy strategy;

    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    /**
     * 上下文会将一些工作委派给策略对象,而不是自行实现不同版本的算法。
     *
     * @param num1 运算数 1
     * @param num2 运算数 2
     * @return double 运算结果
     */
    public double executeStrategy(int num1, int num2) {
        return strategy.execute(num1, num2);
    }
}

/**
 * 客户端代码会选择具体策略并将其传递给上下文。客户端必须知晓策略之间的差
 * 异,才能做出正确的选择。
 *
 * @author BlackFlame33
 */
public class ExampleApplication {

    public static void main(String[] args) {
        // 创建上下文对象。
        Context context = new Context();
        // 读取第一个数。
        int num1 = new Random().nextInt(10);
        // 读取最后一个数。
        int num2 = new Random().nextInt(10);
        // 应用会知道应该执行的具体策略
        concreteStrategy action = randomStrategy();
        if (action == concreteStrategy.addition) {
            context.setStrategy(new ConcreteStrategyAdd());
        }
        if (action == concreteStrategy.subtraction) {
            context.setStrategy(new ConcreteStrategySubtract());
        }
        if (action == concreteStrategy.multiplication) {
            context.setStrategy(new ConcreteStrategyMultiply());
        }
        double result = context.executeStrategy(num1, num2);
        // 打印结果
        System.out.println(num1 + " 与 " + num2 + " 的 " + action + " 等于 " + result);
    }

    private static concreteStrategy randomStrategy() {
        return concreteStrategy.values()[new Random().nextInt(concreteStrategy.values().length)];
    }
}

组合接口时的名称冲突#

将接口组合在一起时,在不同的接口中使用相同的方法名称通常会导致代码可读性变差。(不要这样命名。。。)

适配接口#

适配器模式#

一样,看这篇文章就行。

个人理解:适配器就好像在不兼容的对象之间的一层兼容层一样。有时候不兼容的代码我们没有权限修改,这时候我们通过适配器内的方法,将参数适配到合适的格式, 然后调用定向到其封装对象中的一个或多个方法。

如此以来,一个把接口作为参数的方法,几乎可以让任何类适应它,只要实现接口的方法就行。这让我深刻感受到接口的威力。

接口的字段#

抽象类与接口的区别我们了解到接口只有静态字段。其静态字段存储在接口的静态存储区中。

嵌套接口#

嵌套接口

浅谈工厂模式#

工厂模式将对创建对象的代码与使用对象的代码分离,成为其间接层,使得我们在添加新种类的对象时可以直接添加新种类的类,而不用修改客户端代码。通常发生在复杂的创建对象的情况时。

更多信息还是看这篇文章

接口,用起来?#

指导方针:“优先使用类而不是接口”。从类开始设计,如果明显感觉接口是必要,那么就重构吧!

第 11 章 内部类#

定义在另一个类中的类称为内部类。

除了内部类的相关语法外,匿名内部类在后续会非常实用,应该熟练掌握。

第 11 章

内部类到外部类的链接#

内部类会隐含一个指向外部类对象的引用。因此内部类拥有访问外部类所有元素的访问权。也是因此,创建内部类时,为了让内部类持有外部类对象的引用,必须先创建外部类对象,再通过外部类对象创建内部类。

匿名内部类#

public Contents contents() {
    return new Contents() {
        int i = 11;
    };
}

这种声明类的方式初看起来比较怪:我正写到return的时候,要返回一个Contents对象了,哦~等等,让我先定义一个Contents的子类。但当类仅需要使用一次或极少重复使用时,也没有必要专门为它写一个类。

内部类的价值#

内部类的价值体现为:每个内部类可以独立地继承类或者实现接口,这完善了多重继承问题的解决方案。

内部类还可以实现闭包。它能保留它被创建时所处作用域的信息。

第 12 章 集合#

如果对象的数量是固定的,而且这些对象的生命周期都是已知的,那么这样的程序是相当简单的。

数组非常实用,但是其大小长度是已经固定了的,这会带来很大的局限性,因为通常情况下我们并不能准确知道需要多少个对象。

集合

Iterator#

迭代器可以实现向任意类型的集合的正向遍历。它让程序员不用关心所处理的集合的类型(我们说不用关心,其实就是在说,我们不必考虑传入集合的类型而被迫做不同的处理),它统一了对集合的访问。

糟糕的设计 ——Stack#

尽管Java提供了Stack类,但这个类的设计非常糟糕(这很可能与其不合理的继承关系有关),所以JDK 6加入了ArrayDeque,提供了直接使用栈的方法。
警钟长鸣,谨慎考虑继承关系!

面向接口#

通过面向接口而不是面向实现来编写代码,可以让我们的代码可以应用于更多对象类型。

总结#

Java提供了很多持有对象的方式。最重要的四大类:ListMapSetQueue:

  1. Collection保存单个元素,Map保存键值对;集合的大小可以自动调节;通过泛型,我们可以约束可存放的类型,且取出时也不用强制类型转换;集合只能保存引用类型的数据,基本类型的数据可以通过包装类自动装箱存入。
  2. List线性表是一种有序集合。某种意义上,它将数字索引与对象关联起来。
    • List中,ArrayList在随机访问上效率很高;而LinkedList在插入删除上效率很高。
  3. Map对象与其他对象关联起来。
    • Map中,HashMap可以快速访问元素;TreeMap将它的键以有序方式保存;LinkedHashMap 按照元素的插入顺序保存,但也通过哈希实现快速访问。
  4. Set中,相同的元素只能保存一个,Set的底层实现其实就是Map,所以它的子实现的特性与Map的子实现的特性很相似。
    • HashSet可以快速访问元素;TreeSet 以有序方式保存元素;LinkedHashSet按照元素的插入顺序保存。
  5. 集合类中的部分不常使用,且设计不合理的类被称为 “遗留类”。
    • HashtableVectorStack,它们都是线程安全的,但不要在新代码中使用它们。

关于集合还有很多可总结的,二次笔记时可以以集合为主题展开。
简单集合分类

第 13 章 函数式编程#

函数式编程语言处理代码片段就像处理数据一样简单。

就是有点费头发

函数式编程基于这样一种想法:使用代码以某种方式操纵其他代码。我们并非从零开始构建代码,而是从现有的、可靠的、经过测试的小片代码开始组合在一起,创建新的代码。

可以这样理解函数式编程:面对对象编程抽象数据,函数式编程抽象行为。

由于函数式编程规定所有的数据必须是不可变的:设置一次,永不改变。这非常适合在并行编程场景中使用。

函数式编程

lambda表达式#

建议lambda表达式的行数控制还 3 行内,如果超过 3 行,考虑使用方法引用。

函数式接口#

当一个接口只包含一个抽象方法时,这种接口也叫 “功能接口”。

从抽象的层面,可以理解为这就是将方法作为参数或者返回值。但从JVM实现的角度,类和对象才是Java的一等公民,方法是依附于对象或类的,无法独立存在,所以Java选择将其与功能接口绑定在一起。

使用函数式接口时,名字并不重要,重要的只有参数类型和返回类型。当然对功能接口而言,其命名模式能帮助我们快速理解其作用,例如:

  1. 只处理对象的接口,命名通常为Function, ConsumerPredicate
  2. 只接受一个基本类型参数的接口,命名通常使用第一部分表示其基本类型,如LongConsumber, DoubleFunction, IntPredicate
  3. 返回类型为基本类型的接口,命名通常使用 'To' 来表示,如ToLongFunction<T>IntToLongFunction
  4. 参数类型与返回值类型相同的接口,命名使用OperatorUnaryOperator表示一个参数,BinaryOperator表示两个参数;
  5. 接受一个参数并返回boolean类型的接口,命名使用Predicate
  6. 接受两个参数的接口,命名通常使用BiXxx,如BiPredicate 表示接受两个参数,返回类型为boolean

闭包#

内部类章节我们也讲到闭包。闭包可以作为函数对象或者匿名函数,持有上下文数据,可以传递或保存。Java要求其变量是 == 最终 == 不可变的。

第 14 章 流#

集合优化了对象的存储。而流(stream)与对象的成批处理有关。

我们平常说流,一般都是指I/O流,而用stream流指代本章所讲的流。大多数时候,我们将对象存储在一个集合中是为了处理它们,所以你会发现,自己编程的重点会从集合转向流。流使我们的程序更小,可读性更高,配合lambda表达式和方法引用,流大大提升了JDK 8的吸引力。

第 14 章 流

声明式编程#

声明式编程是一种有别于命令式编程的编程风格,我们声明 “What to do”(做什么),而不是像命令式编程那样指明 “How to do”(怎么做)。

内部迭代#

我们平常显式使用for循环遍历的迭代被称为 “外部迭代”,而流使用 “内部迭代”:你不知道它具体是如何执行迭代的。然而,通过放宽对迭代方式的掌控,我们可以将其交给某种并行机制 —— 流的内部迭代效率更高。

惰性求值#

惰性求值意味着流只有在必要时才会被求值。你可以认为在 “被求值” 之前,流就像没有被调用的方法一样,只是声明了逻辑;又或者流被声明塑造成了能如此求值的模型,当真正需要 “被求值” 时,模型开始运行。

JDK 8对流的支持#

接口的默认方法也是在JDK 8新增的,它与流的添加或许有着千丝万缕的联系。

在创造了流这个概念后,Java的设计者们遇到了一个难题:如何将流整合进库中,而又不影响已经使用了库的代码?因为,如果向接口加入新方法,会破坏每一个实现了该接口,但还没有实现这个新方法的类。。。无所谓,默认方法会出手默认方法使得我们可以向接口添加默认实现,实现子类而不必重写这些方法。

Optional类型#

设计Optional类型的初衷是为了避免流中发生异常而中断。如果流中没有元素,在执行某些流操作时会返回Optional对象。

reduce(BinaryOperator)#

形参命名(acc, i)#

由于reduce(BinaryOperator)传入的lambda表达式中的第一个参数是上次调用时的结果,第二个参数是来自流中的新值,最好将第一个参数命名为acc、第二个参数命名为i用以见名之意。

第 15 章 异常#

Java的基本哲学是‘写得不好的代码无法运行’。

第 15 章 异常

缺陷:异常缺失#

异常使用不当可能会导致异常被 “吞掉” 的情况。

异常说明#

“异常说明” 可以缩小,但不可以扩大,这与类在继承过程中的规则相反

Java的缺点#

除了内存的清理,其他清理都不会自动发生,必须告知客户程序员,让他们自己处理。

使用异常的准则#

  1. 尽可能使用try-with-resources
  2. 要在恰当的层次处理问题。除非你知道该如何处理,否则不要捕捉异常。
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。