diff --git a/README.md b/README.md index 7121808..c6e71f5 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ **目的**:本着开源的精神,为了让广大 Javaer 不浪费搜索的时间和金钱,可以快速的浏览/学习面试最常见的知识点,我创建了这个 Repository。 **适合阅读人群:** - -* 面临校招的相关专业大学生。 + +* 面临校招的相关专业大学生。 * 准备面试的 Java 初/中级工程师。 * 希望掌握 Java 最流行知识点的编程爱好者。 @@ -22,7 +22,7 @@ * **微信:naerjiajia207** # Java 知识点及面试题 -## Java 基础 +## Java 基础 - [Java 基础](https://github.com/lvminghui/Java-Notes/blob/master/docs/Java%E5%9F%BA%E7%A1%80.md) - [计算机网络面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E9%9D%A2%E8%AF%95%E9%A2%98.md) ## Java 容器 @@ -44,6 +44,11 @@ - [MySQL 面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/MySQL%E9%9D%A2%E8%AF%95%E9%A2%98.md) - [MySQL 常见知识点总结](https://github.com/lvminghui/Java-Notes/blob/master/docs/MySQL%E5%B8%B8%E8%A7%81%E7%9F%A5%E8%AF%86%E7%82%B9%E6%80%BB%E7%BB%93.md) +### 深入理解 MySQL +- [基础篇](https://github.com/lvminghui/Java-Notes/blob/master/docs/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3MySQL%E5%9F%BA%E7%A1%80%E7%AF%87.md) + +- [高性能实战篇](https://github.com/lvminghui/Java-Notes/blob/master/docs/%E9%AB%98%E6%80%A7%E8%83%BD%E5%AE%9E%E8%B7%B5%E7%AF%87.md) + ## Redis - [Redis 面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/Redis%20%E9%9D%A2%E8%AF%95%E9%A2%98.md) @@ -51,12 +56,13 @@ - [SpringBean 的生命周期](https://github.com/lvminghui/Java-Notes/blob/master/docs/SpringBean%20%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.md) - [Spring 面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/Spring%20%E9%9D%A2%E8%AF%95%E9%A2%98.md) - [Spring 的设计理念和设计模式分析](https://github.com/lvminghui/Java-Notes/blob/master/docs/Spring%E7%9A%84%E8%AE%BE%E8%AE%A1%E7%90%86%E5%BF%B5%E5%92%8C%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%88%86%E6%9E%90.md) -## Mybatis +### Mybatis - [Mybatis](https://github.com/lvminghui/Java-Notes/blob/master/docs/Mybatis.md) -## Spring Boot +### Spring Boot - [Spring Boot 面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/SpringBoot.md) ## Spring Cloud - [Spring Cloud](https://github.com/lvminghui/Java-Notes/blob/master/docs/Spring%20Cloud.md) +- [项目常用工具](https://github.com/lvminghui/Java-Notes/blob/master/docs/%E9%A1%B9%E7%9B%AE%E5%B8%B8%E7%94%A8%E5%B7%A5%E5%85%B7.md) diff --git a/docs/Effective Java.md b/docs/Effective Java.md new file mode 100644 index 0000000..3e5e3f9 --- /dev/null +++ b/docs/Effective Java.md @@ -0,0 +1,1088 @@ +20 为后代设计接口  + + 在 Java 8 之前,不可能在不破坏现有实现的情况下为接口添加方法。 如果向接口添加了一个新方法,现有的实现通常会缺少该方法,从而导致编译时错误。 在 Java 8 中,添加了默认方法(default method)构造[JLS 9.4],目的是允许将方法添加到现有的接口。 但是增加新的方法到现有的接口是充满风险的。 + +  默认方法的声明包含一个默认实现,该方法允许实现接口的类直接使用,而不必实现默认方法。 虽然在 Java 中添加默认方法可以将方法添加到现有接口,但不能保证这些方法可以在所有已有的实现中使用。 默认的方法被「注入(injected)」到现有的实现中,没有经过实现类的知道或同意。 在 Java 8 之前,这些实现是用默认的接口编写的,它们的接口永远不会获得任何新的方法。 + +  许多新的默认方法被添加到 Java 8 的核心集合接口中,主要是为了方便使用 lambda 表达式(第 6 章)。 Java 类库的默认方法是高质量的通用实现,在大多数情况下,它们工作正常。 但是,编写一个默认方法并不总是可能的,它保留了每个可能的实现的所有不变量。 + +  例如,考虑在 Java 8 中添加到 Collection 接口的 removeIf 方法。此方法删除给定布尔方法(或 Predicate 函数式接口)返回 true 的所有元素。默认实现被指定为使用迭代器遍历集合,调用每个元素的谓词,并使用迭代器的 remove 方法删除谓词返回 true 的元素。 据推测,这个声明看起来像这样:默认实现被指定为使用迭代器遍历集合,调用每个元素的 Predicate 函数式接口,并使用迭代器的 remove 方法删除 Predicate 函数式接口返回 true 的元素。 根据推测,这个声明看起来像这样: + +// Default method added to the Collection interface in Java 8 +default boolean removeIf(Predicate filter) { + Objects.requireNonNull(filter); + boolean result = false; + for (Iterator it = iterator(); it.hasNext(); ) { + if (filter.test(it.next())) { + it.remove(); + result = true; + } + } + return result; +} +  这是可能为 removeIf 方法编写的最好的通用实现,但遗憾的是,它在一些实际的 Collection 实现中失败了。 例如,考虑 org.apache.commons.collections4.collection.SynchronizedCollection 方法。 这个类出自 Apache Commons 类库中,与 java.util 包中的静态工厂 Collections.synchronizedCollection 方法返回的类相似。 Apache 版本还提供了使用客户端提供的对象进行锁定的能力,以代替集合。 换句话说,它是一个包装类(条目 18),它们的所有方法在委托给包装集合类之前在一个锁定对象上进行同步。 + +  Apache 的 SynchronizedCollection 类仍然在积极维护,但在撰写本文时,并未重写 removeIf 方法。 如果这个类与 Java 8 一起使用,它将继承 removeIf 的默认实现,但实际上不能保持类的基本承诺:自动同步每个方法调用。 默认实现对同步一无所知,并且不能访问包含锁定对象的属性。 如果客户端在另一个线程同时修改集合的情况下调用 SynchronizedCollection 实例上的 removeIf 方法,则可能会导致 ConcurrentModificationException 异常或其他未指定的行为。 + +  为了防止在类似的 Java 平台类库实现中发生这种情况,比如 Collections.synchronizedCollection 返回的包级私有的类,JDK 维护者必须重写默认的 removeIf 实现和其他类似的方法来在调用默认实现之前执行必要的同步。 原来不属于 Java 平台的集合实现没有机会与接口更改进行类似的改变,有些还没有这样做。 + +  在默认方法的情况下,接口的现有实现类可以在没有错误或警告的情况下编译,但在运行时会失败。 虽然不是非常普遍,但这个问题也不是一个孤立的事件。 在 Java 8 中添加到集合接口的一些方法已知是易受影响的,并且已知一些现有的实现会受到影响。 + +  应该避免使用默认方法向现有的接口添加新的方法,除非这个需要是关键的,在这种情况下,你应该仔细考虑,以确定现有的接口实现是否会被默认的方法实现所破坏。然而,默认方法对于在创建接口时提供标准的方法实现非常有用,以减轻实现接口的任务(详见第 20 条)。 + +  还值得注意的是,默认方法不是被用来设计,来支持从接口中移除方法或者改变现有方法的签名的目的。在不破坏现有客户端的情况下,这些接口都不可能发生更改。 + +  准则是清楚的。 尽管默认方法现在是 Java 平台的一部分,但是非常悉心地设计接口仍然是非常重要的。 虽然默认方法可以将方法添加到现有的接口,但这样做有很大的风险。 如果一个接口包含一个小缺陷,可能会永远惹怒用户。 如果一个接口严重缺陷,可能会破坏包含它的 API。 + +  因此,在发布之前测试每个新接口是非常重要的。 多个程序员应该以不同的方式实现每个接口。 至少,你应该准备三种不同的实现。 编写多个使用每个新接口的实例来执行各种任务的客户端程序同样重要。 这将大大确保每个接口都能满足其所有的预期用途。 这些步骤将允许你在发布之前发现接口中的缺陷,但仍然可以轻松地修正它们。 虽然在接口被发布后可能会修正一些存在的缺陷,但不要太指望这一点。 + +22. 接口仅用来定义类型 +  当类实现接口时,该接口作为一种类型(type),可以用来引用类的实例。因此,一个类实现了一个接口,因此表明客户端可以如何处理类的实例。为其他目的定义接口是不合适的。 + +  一种失败的接口就是所谓的常量接口(constant interface)。 这样的接口不包含任何方法; 它只包含静态 final 属性,每个输出一个常量。 使用这些常量的类实现接口,以避免需要用类名限定常量名。 这里是一个例子: + +// Constant interface antipattern - do not use! +public interface PhysicalConstants { + // Avogadro's number (1/mol) + static final double AVOGADROS_NUMBER = 6.022_140_857e23; + + // Boltzmann constant (J/K) + static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23; + + // Mass of the electron (kg) + static final double ELECTRON_MASS = 9.109_383_56e-31; +} +  常量接口模式是对接口的糟糕使用。 类在内部使用一些常量,完全属于实现细节。实现一个常量接口会导致这个实现细节泄漏到类的导出 API 中。对类的用户来说,类实现一个常量接口是没有意义的。事实上,它甚至可能使他们感到困惑。更糟糕的是,它代表了一个承诺:如果在将来的版本中修改了类,不再需要使用常量,那么它仍然必须实现接口,以确保二进制兼容性。如果一个非 final 类实现了常量接口,那么它的所有子类的命名空间都会被接口中的常量所污染。 + +  Java 平台类库中有多个常量接口,如 java.io.ObjectStreamConstants。 这些接口应该被视为不规范的,不应该被效仿。 + +  如果你想导出常量,有几个合理的选择方案。 如果常量与现有的类或接口紧密相关,则应将其添加到该类或接口中。 例如,所有数字基本类型的包装类,如 Integer 和 Double,都会导出 MIN_VALUE 和 MAX_VALUE 常量。 如果常量最好被看作枚举类型的成员,则应该使用枚举类型(详见第 34 条)导出它们。 否则,你应该用一个不可实例化的工具类来导出常量(详见第 4 条)。 下是前面所示的 PhysicalConstants 示例的工具类的版本: + +// Constant utility class +package com.effectivejava.science; + +public class PhysicalConstants { + private PhysicalConstants() { } // Prevents instantiation + + public static final double AVOGADROS_NUMBER = 6.022_140_857e23; + public static final double BOLTZMANN_CONST = 1.380_648_52e-23; + public static final double ELECTRON_MASS = 9.109_383_56e-31; +} +  顺便提一下,请注意在数字文字中使用下划线字符(_)。 从 Java 7 开始,合法的下划线对数字字面量的值没有影响,但是如果使用得当的话可以使它们更容易阅读。 无论是固定的浮点数,如果他们包含五个或更多的连续数字,考虑将下划线添加到数字字面量中。 对于底数为 10 的数字,无论是整型还是浮点型的,都应该用下划线将数字分成三个数字组,表示一千的正负幂。 + +  通常,实用工具类要求客户端使用类名来限定常量名,例如 PhysicalConstants.AVOGADROS_NUMBER。 如果大量使用实用工具类导出的常量,则通过使用静态导入来限定具有类名的常量: + +// Use of static import to avoid qualifying constants +import static com.effectivejava.science.PhysicalConstants.*; + +public class Test { + double atoms(double mols) { + return AVOGADROS_NUMBER * mols; + } + ... + // Many more uses of PhysicalConstants justify static import +} +  总之,接口只能用于定义类型。 它们不应该仅用于导出常量。 + + 23. 类层次结构优于标签类 +  有时你可能会碰到一个类,它的实例有两个或更多的风格,并且包含一个标签字段(tag field),表示实例的风格。 例如,考虑这个类,它可以表示一个圆形或矩形: + +// Tagged class - vastly inferior to a class hierarchy! +class Figure { + enum Shape { RECTANGLE, CIRCLE }; + + // Tag field - the shape of this figure + final Shape shape; + + // These fields are used only if shape is RECTANGLE + double length; + double width; + + // This field is used only if shape is CIRCLE + double radius; + + // Constructor for circle + Figure(double radius) { + shape = Shape.CIRCLE; + this.radius = radius; + } + + // Constructor for rectangle + Figure(double length, double width) { + shape = Shape.RECTANGLE; + this.length = length; + this.width = width; + } + + double area() { + switch(shape) { + case RECTANGLE: + return length * width; + case CIRCLE: + return Math.PI * (radius * radius); + default: + throw new AssertionError(shape); + } + } +} +  这样的标签类具有许多缺点。 它们充斥着杂乱无章的样板代码,包括枚举声明,标签字段和 switch 语句。 可读性更差,因为多个实现在一个类中混杂在一起。 内存使用增加,因为实例负担属于其他风格不相关的领域。 字段不能成为 final,除非构造方法初始化不相关的字段,导致更多的样板代码。 构造方法在编译器的帮助下,必须设置标签字段并初始化正确的数据字段:如果初始化错误的字段,程序将在运行时失败。 除非可以修改其源文件,否则不能将其添加到标记的类中。 如果你添加一个风格,你必须记得给每个 switch 语句添加一个 case,否则这个类将在运行时失败。 最后,一个实例的数据类型没有提供任何关于风格的线索。 总之,标签类是冗长的,容易出错的,而且效率低下。 + +  幸运的是,像 Java 这样的面向对象的语言为定义一个能够表示多种风格对象的单一数据类型提供了更好的选择:子类型化(subtyping)。标签类仅仅是一个类层次的简单的模仿。 + +  要将标签类转换为类层次,首先定义一个包含抽象方法的抽象类,该标签类的行为取决于标签值。 在 Figure 类中,只有一个这样的方法,就是 area 方法。 这个抽象类是类层次的根。 如果有任何方法的行为不依赖于标签的值,把它们放在这个类中。 同样,如果有所有的方法使用的数据字段,把它们放在这个类。Figure 类中不存在这种与类型无关的方法或字段。 + +  接下来,为原始标签类的每种类型定义一个根类的具体子类。 在我们的例子中,有两个类型:圆形和矩形。 在每个子类中包含特定于改类型的数据字段。 在我们的例子中,半径字段是属于圆的,长度和宽度字段都是矩形的。 还要在每个子类中包含根类中每个抽象方法的适当实现。 这里是对应于 Figure 类的类层次: + +// Class hierarchy replacement for a tagged class +abstract class Figure { + abstract double area(); +} + +class Circle extends Figure { + final double radius; + + Circle(double radius) { this.radius = radius; } + + @Override double area() { return Math.PI * (radius * radius); } +} +class Rectangle extends Figure { + final double length; + final double width; + + Rectangle(double length, double width) { + this.length = length; + this.width = width; + } + @Override double area() { return length * width; } +} +  这个类层次纠正了之前提到的标签类的每个缺点。 代码简单明了,不包含原文中的样板文件。 每种类型的实现都是由自己的类来分配的,而这些类都没有被无关的数据字段所占用。 所有的字段是 final 的。 编译器确保每个类的构造方法初始化其数据字段,并且每个类都有一个针对在根类中声明的每个抽象方法的实现。 这消除了由于缺少 switch-case 语句而导致的运行时失败的可能性。 多个程序员可以独立地继承类层次,并且可以相互操作,而无需访问根类的源代码。 每种类型都有一个独立的数据类型与之相关联,允许程序员指出变量的类型,并将变量和输入参数限制为特定的类型。 + +  类层次的另一个优点是可以使它们反映类型之间的自然层次关系,从而提高了灵活性,并提高了编译时类型检查的效率。 假设原始示例中的标签类也允许使用正方形。 类层次可以用来反映一个正方形是一种特殊的矩形(假设它们是不可变的): + +class Square extends Rectangle { + Square(double side) { + super(side, side); + } +} +  请注意,上述层次结构中的字段是直接访问的,而不是通过访问器方法访问的。 这里是为了简洁起见,如果类层次是公开的(详见第 16 条),这将是一个糟糕的设计。 + +  总之,标签类很少有适用的情况。 如果你想写一个带有显式标签字段的类,请考虑标签字段是否可以被删除,并是否能被类层次结构替换。 当遇到一个带有标签字段的现有类时,可以考虑将其重构为一个类层次结构。 + +24. 支持使用静态成员类而不是非静态类 +  嵌套类(nested class)是在另一个类中定义的类。 嵌套类应该只存在于其宿主类(enclosing class)中。 如果一个嵌套类在其他一些情况下是有用的,那么它应该是一个顶级类。 有四种嵌套类:静态成员类,非静态成员类,匿名类和局部类。 除了第一种以外,剩下的三种都被称为内部类(inner class)。 这个条目告诉你什么时候使用哪种类型的嵌套类以及为什么使用。 + +  静态成员类是最简单的嵌套类。 最好把它看作是一个普通的类,恰好在另一个类中声明,并且可以访问所有宿主类的成员,甚至是那些被声明为私有类的成员。 静态成员类是其宿主类的静态成员,并遵循与其他静态成员相同的可访问性规则。 如果它被声明为 private,则只能在宿主类中访问,等等。 + +  静态成员类的一个常见用途是作为公共帮助类,仅在与其外部类一起使用时才有用。 例如,考虑一个描述计算器支持的操作的枚举类型(详见第 34 条)。 Operation 枚举应该是 Calculator 类的公共静态成员类。 Calculator 客户端可以使用 Calculator.Operation.PLUS 和 Calculator.Operation.MINUS 等名称来引用操作。 + +  在语法上,静态成员类和非静态成员类之间的唯一区别是静态成员类在其声明中具有 static 修饰符。 尽管句法相似,但这两种嵌套类是非常不同的。 非静态成员类的每个实例都隐含地与其包含的类的宿主实例相关联。 在非静态成员类的实例方法中,可以调用宿主实例上的方法,或者使用限定的构造[JLS,15.8.4] 获得对宿主实例的引用。 如果嵌套类的实例可以与其宿主类的实例隔离存在,那么嵌套类必须是静态成员类:不可能在没有宿主实例的情况下创建非静态成员类的实例。 + +  非静态成员类实例和其宿主实例之间的关联是在创建成员类实例时建立的,并且之后不能被修改。 通常情况下,通过在宿主类的实例方法中调用非静态成员类构造方法来自动建立关联。 尽管很少有可能使用表达式 enclosingInstance.new MemberClass(args) 手动建立关联。 正如你所预料的那样,该关联在非静态成员类实例中占用了空间,并为其构建添加了时间开销。 + +  非静态成员类的一个常见用法是定义一个 Adapter [Gamma95],它允许将外部类的实例视为某个不相关类的实例。 例如,Map 接口的实现通常使用非静态成员类来实现它们的集合视图,这些视图由 Map 的 keySet,entrySet 和 values 方法返回。 同样,集合接口(如 Set 和 List)的实现通常使用非静态成员类来实现它们的迭代器: + +// Typical use of a nonstatic member class +public class MySet extends AbstractSet { + ... // Bulk of the class omitted + + @Override + public Iterator iterator() { + return new MyIterator(); + } + + private class MyIterator implements Iterator { + ... + } +} +  如果你声明了一个不需要访问宿主实例的成员类,总是把 static 修饰符放在它的声明中,使它成为一个静态成员类,而不是非静态的成员类。 如果你忽略了这个修饰符,每个实例都会有一个隐藏的外部引用给它的宿主实例。 如前所述,存储这个引用需要占用时间和空间。 更严重的是,并且会导致即使宿主类在满足垃圾回收的条件时却仍然驻留在内存中(详见第 7 条)。 由此产生的内存泄漏可能是灾难性的。 由于引用是不可见的,所以通常难以检测到。 + +  私有静态成员类的常见用法是表示由它们的宿主类表示的对象的组件。 例如,考虑将键与值相关联的 Map 实例。 许多 Map 实现对于映射中的每个键值对都有一个内部的 Entry 对象。 当每个 entry 都与 Map 关联时,entry 上的方法 (getKey,getValue 和 setValue) 不需要访问 Map。 因此,使用非静态成员类来表示 entry 将是浪费的:私有静态成员类是最好的。 如果意外地忽略了 entry 声明中的 static 修饰符,Map 仍然可以工作,但是每个 entry 都会包含对 Map 的引用,浪费空间和时间。 + +  如果所讨论的类是导出类的公共或受保护成员,则在静态和非静态成员类之间正确选择是非常重要的。 在这种情况下,成员类是导出的 API 元素,如果不违反向后兼容性,就不能在后续版本中从非静态变为静态成员类。 + +  正如你所期望的,一个匿名类没有名字。 它不是其宿主类的成员。 它不是与其他成员一起声明,而是在使用时同时声明和实例化。 在表达式合法的代码中,匿名类是允许的。 当且仅当它们出现在非静态上下文中时,匿名类才会封装实例。 但是,即使它们出现在静态上下文中,它们也不能有除常量型变量之外的任何静态成员,这些常量型变量包括 final 的基本类型,或者初始化常量表达式的字符串属性[JLS,4.12.4]。 + +  匿名类的适用性有很多限制。 除了在声明的时候之外,不能实例化它们。 你不能执行 instanceof 方法测试或者做任何其他需要你命名的类。 不能声明一个匿名类来实现多个接口,或者继承一个类并同时实现一个接口。 匿名类的客户端不能调用除父类型继承的成员以外的任何成员。 因为匿名类在表达式中出现,所以它们必须保持简短 —— 约十行或更少 —— 否则可读性将受到影响。 + +  在将 lambda 表达式添加到 Java(第 6 章)之前,匿名类是创建小函数对象和处理对象的首选方法,但 lambda 表达式现在是首选(详见第 42 条)。 匿名类的另一个常见用途是实现静态工厂方法(请参阅条目 20 中的 intArrayAsList)。 + +  局部类是四种嵌套类中使用最少的。 一个局部类可以在任何可以声明局部变量的地方声明,并遵守相同的作用域规则。 局部类与其他类型的嵌套类具有共同的属性。 像成员类一样,他们有名字,可以重复使用。 就像匿名类一样,只有在非静态上下文中定义它们时,它们才会包含实例,并且它们不能包含静态成员。 像匿名类一样,应该保持简短,以免损害可读性。 + +  回顾一下,有四种不同的嵌套类,每个都有它的用途。 如果一个嵌套的类需要在一个方法之外可见,或者太长而不能很好地适应一个方法,使用一个成员类。 如果一个成员类的每个实例都需要一个对其宿主实例的引用,使其成为非静态的; 否则,使其静态。 假设这个类属于一个方法内部,如果你只需要从一个地方创建实例,并且存在一个预置类型来说明这个类的特征,那么把它作为一个匿名类;否则,把它变成局部类。 + + 25. 将源文件限制为单个顶级类 +  虽然 Java 编译器允许在单个源文件中定义多个顶级类,但这样做没有任何好处,并且存在重大风险。 风险源于在源文件中定义多个顶级类使得为类提供多个定义成为可能。 使用哪个定义会受到源文件传递给编译器的顺序的影响。 + +  为了具体说明,请考虑下面源文件,其中只包含一个引用其他两个顶级类(Utensil 和 Dessert 类)的成员的 Main 类: + +public class Main { + public static void main(String[] args) { + System.out.println(Utensil.NAME + [Dessert.NAME](http://Dessert.NAME)); + } +} +复制ErrorOK! +  现在假设在 Utensil.java 的源文件中同时定义了 Utensil 和 Dessert: + +// Two classes defined in one file. Don't ever do this! +class Utensil { + static final String NAME = "pan"; +} + +class Dessert { + static final String NAME = "cake"; +} +复制ErrorOK! +  当然,main 方法会打印 pancake。 + +  现在假设你不小心创建了另一个名为 Dessert.java 的源文件,它定义了相同的两个类: + +// Two classes defined in one file. Don't ever do this! +class Utensil { + static final String NAME = "pot"; +} + +class Dessert { + static final String NAME = "pie"; +} +复制ErrorOK! +  如果你足够幸运,使用命令 javac Main.java Dessert.java 编译程序,编译将失败,编译器会告诉你,你已经多次定义了类 Utensil 和 Dessert。 这是因为编译器首先编译 Main.java,当它看到对 Utensil 的引用(它在 Dessert 的引用之前)时,它将在 Utensil.java 中查找这个类并找到 Utensil 和 Dessert。 当编译器在命令行上遇到 Dessert.java 时,它也将拉入该文件,导致它遇到 Utensil 和 Dessert 的定义。 + +  如果使用命令 javac Main.java 或 javac Main.java Utensil.java 编译程序,它的行为与在编写 Dessert.java 文件(即打印 pancake)之前的行为相同。 但是,如果使用命令 javac Dessert.java Main.java 编译程序,它将打印 potpie。 程序的行为因此受到源文件传递给编译器的顺序的影响,这显然是不可接受的。 + +  解决这个问题很简单,将顶层类(如我们的例子中的 Utensil 和 Dessert)分割成单独的源文件。 如果试图将多个顶级类放入单个源文件中,请考虑使用静态成员类(详见第 24 条)作为将类拆分为单独的源文件的替代方法。 如果这些类从属于另一个类,那么将它们变成静态成员类通常是更好的选择,因为它提高了可读性,并且可以通过声明它们为私有(详见第 15 条)来减少类的可访问性。下面是我们的例子看起来如何使用静态成员类: + +// Static member classes instead of multiple top-level classes +public class Test { + public static void main(String[] args) { + System.out.println(Utensil.NAME + [Dessert.NAME](http://Dessert.NAME)); + } + + private static class Utensil { + static final String NAME = "pan"; + } + + private static class Dessert { + static final String NAME = "cake"; + } +} +复制ErrorOK! +  这个教训很清楚:永远不要将多个顶级类或接口放在一个源文件中。 遵循这个规则保证在编译时不能有多个定义。 这又保证了编译生成的类文件以及生成的程序的行为与源文件传递给编译器的顺序无关。 + +  自 Java 5 以来,泛型已经成为该语言的一部分。 在泛型之前,你必须转换从集合中读取的每个对象。 如果有人不小心插入了错误类型的对象,则在运行时可能会失败。 使用泛型,你告诉编译器在每个集合中允许哪些类型的对象。 编译器会自动插入强制转换,并在编译时告诉你是否尝试插入错误类型的对象。 这样做的结果是既安全又清晰的程序,但这些益处,不限于集合,是有代价的。 本章告诉你如何最大限度地提高益处,并将并发症降至最低。 + +26. 不要使用原始类型 +  首先,有几个术语。一个类或接口,它的声明有一个或多个类型参数(type parameters ),被称之为泛型类或泛型接口[JLS,8.1.2,9.1.2]。 例如,List 接口具有单个类型参数 E,表示其元素类型。 接口的全名是 List(读作「E」的列表),但是人们经常称它为 List。 泛型类和接口统称为泛型类型(generic types)。 + +  每个泛型定义了一组参数化类型(parameterized types),它们由类或接口名称组成,后跟一个与泛型类型的形式类型参数[JLS,4.4,4.5] 相对应的实际类型参数的尖括号「<>」列表。 例如,List(读作「字符串列表」)是一个参数化类型,表示其元素类型为 String 的列表。 (String 是与形式类型参数 E 相对应的实际类型参数)。 + +  最后,每个泛型定义了一个原始类型(raw type),它是没有任何类型参数的泛型类型的名称[JLS,4.8]。 例如,对应于 List 的原始类型是 List。 原始类型的行为就像所有的泛型类型信息都从类型声明中被清除一样。 它们的存在主要是为了与没有泛型之前的代码相兼容。 + +  在泛型被添加到 Java 之前,这是一个典型的集合声明。 从 Java 9 开始,它仍然是合法的,但并不是典型的声明方式了: + +// Raw collection type - don't do this! + +// My stamp collection. Contains only Stamp instances. +private final Collection stamps = ... ; +复制ErrorOK! +  如果你今天使用这个声明,然后不小心把 coin 实例放入你的 stamp 集合中,错误的插入编译和运行没有错误(尽管编译器发出一个模糊的警告): + +// Erroneous insertion of coin into stamp collection +stamps.add(new Coin( ... )); // Emits "unchecked call" warning +复制ErrorOK! +  直到您尝试从 stamp 集合中检索 coin 实例时才会发生错误: + +// Raw iterator type - don't do this! +for (Iterator i = stamps.iterator(); i.hasNext(); ) + Stamp stamp = (Stamp) i.next(); // Throws ClassCastException + stamp.cancel(); +复制ErrorOK! +  正如本书所提到的,在编译完成之后尽快发现错误是值得的,理想情况是在编译时。 在这种情况下,直到运行时才发现错误,在错误发生后的很长一段时间,以及可能远离包含错误的代码的代码中。 一旦看到 ClassCastException,就必须搜索代码类库,查找将 coin 实例放入 stamp 集合的方法调用。 编译器不能帮助你,因为它不能理解那个说「仅包含 stamp 实例」的注释。 + +  对于泛型,类型声明包含的信息,而不是注释: + +// Parameterized collection type - typesafe +private final Collection stamps = ... ; +复制ErrorOK! +  从这个声明中,编译器知道 stamps 集合应该只包含 Stamp 实例,并保证它是 true,假设你的整个代码类库编译时不发出(或者抑制;参见条目 27)任何警告。 当使用参数化类型声明声明 stamps 时,错误的插入会生成一个编译时错误消息,告诉你到底发生了什么错误: + +Test.java:9: error: incompatible types: Coin cannot be converted +to Stamp + c.add(new Coin()); + ^ +复制ErrorOK! +  当从集合中检索元素时,编译器会为你插入不可见的强制转换,并保证它们不会失败(再假设你的所有代码都不会生成或禁止任何编译器警告)。 虽然意外地将 coin 实例插入 stamp 集合的预期可能看起来很牵强,但这个问题是真实的。 例如,很容易想象将 BigInteger 放入一个只包含 BigDecimal 实例的集合中。 + +  如前所述,使用原始类型(没有类型参数的泛型)是合法的,但是你不应该这样做。 如果你使用原始类型,则会丧失泛型的所有安全性和表达上的优势。 鉴于你不应该使用它们,为什么语言设计者首先允许原始类型呢? 答案是为了兼容性。 泛型被添加时,Java 即将进入第二个十年,并且有大量的代码没有使用泛型。 所有这些代码都是合法的,并且与使用泛型的新代码进行交互操作被认为是至关重要的。 将参数化类型的实例传递给为原始类型设计的方法必须是合法的,反之亦然。 这个需求,被称为迁移兼容性,驱使决策支持原始类型,并使用擦除来实现泛型(详见第 28 条)。 + +  虽然不应使用诸如 List 之类的原始类型,但可以使用参数化类型来允许插入任意对象(如 List)。 原始类型 List 和参数化类型 List 之间有什么区别? 松散地说,前者已经选择了泛型类型系统,而后者明确地告诉编译器,它能够保存任何类型的对象。 虽然可以将 List 传递给 List 类型的参数,但不能将其传递给 List 类型的参数。 泛型有子类型的规则,List 是原始类型 List 的子类型,但不是参数化类型 List 的子类型(条目 28)。 因此,如果使用诸如 List 之类的原始类型,则会丢失类型安全性,但是如果使用参数化类型(例如 List)则不会。 + +  为了具体说明,请考虑以下程序: + +// Fails at runtime - unsafeAdd method uses a raw type (List)! +public static void main(String[] args) { + List strings = new ArrayList<>(); + unsafeAdd(strings, Integer.valueOf(42)); + String s = strings.get(0); // Has compiler-generated cast +} + +private static void unsafeAdd(List list, Object o) { + list.add(o); +} +复制ErrorOK! +  此程序可以编译,它使用原始类型列表,但会收到警告: + +Test.java:10: warning: [unchecked] unchecked call to add(E) as a +member of the raw type List + list.add(o); + ^ +复制ErrorOK! +  实际上,如果运行该程序,则当程序尝试调用 strings.get(0) 的结果(一个 Integer)转换为一个 String 时,会得到 ClassCastException 异常。 这是一个编译器生成的强制转换,因此通常会保证成功,但在这种情况下,我们忽略了编译器警告并付出了代价。 + +  如果用 unsafeAdd 声明中的参数化类型 List 替换原始类型 List,并尝试重新编译该程序,则会发现它不再编译,而是发出错误消息: + +Test.java:5: error: incompatible types: List cannot be +converted to List + unsafeAdd(strings, Integer.valueOf(42)); +复制ErrorOK! +  你可能会试图使用原始类型来处理元素类型未知且无关紧要的集合。 例如,假设你想编写一个方法,它需要两个集合并返回它们共同拥有的元素的数量。 如果是泛型新手,那么您可以这样写: + +// Use of raw type for unknown element type - don't do this! +static int numElementsInCommon(Set s1, Set s2) { + int result = 0; + for (Object o1 : s1) + if (s2.contains(o1)) + result++; + return result; +} +复制ErrorOK! +  这种方法可以工作,但它使用原始类型,这是危险的。 安全替代方式是使用无限制通配符类型(unbounded wildcard types)。 如果要使用泛型类型,但不知道或关心实际类型参数是什么,则可以使用问号来代替。 例如,泛型类型 Set 的无限制通配符类型是 Set(读取「某种类型的集合」)。 它是最通用的参数化的 Set 类型,能够保持任何集合。 下面是 numElementsInCommon 方法使用无限制通配符类型声明的情况: + +// Uses unbounded wildcard type - typesafe and flexible +static int numElementsInCommon(Set s1, Set s2) { ... } +复制ErrorOK! +  无限制通配符 Set 与原始类型 Set 之间有什么区别? 问号真的给你放任何东西吗? 这不是要点,但通配符类型是安全的,原始类型不是。 你可以将任何元素放入具有原始类型的集合中,轻易破坏集合的类型不变性(如第 119 页上的 unsafeAdd 方法所示); 你不能把任何元素(除 null 之外)放入一个 Collection 中。 试图这样做会产生一个像这样的编译时错误消息: + +WildCard.java:13: error: incompatible types: String cannot be +converted to CAP#1 + c.add("verboten"); + ^ + where CAP#1 is a fresh type-variable: + CAP#1 extends Object from capture of ? +复制ErrorOK! +  不可否认的是,这个错误信息留下了一些需要的东西,但是编译器已经完成了它的工作,不管它的元素类型是什么,都不会破坏集合的类型不变性。 你不仅不能将任何元素(除 null 以外)放入一个 Collection 中,并且根本无法猜测你会得到那种类型的对象。 如果这些限制是不可接受的,可以使用泛型方法(详见第 30 条)或有限制的通配符类型(详见第 31 条)。 + +  对于不应该使用原始类型的规则,有一些小例外。 你必须在类字面值(class literals)中使用原始类型。 规范中不允许使用参数化类型(尽管它允许数组类型和基本类型)[JLS,15.8.2]。 换句话说,List.class,String[].class 和 int.class 都是合法的,但 List.class 和 List.class 都是不合法的。 + +  规则的第二个例外与 instanceof 操作符有关。 因为泛型类型信息在运行时被擦除,所以在无限制通配符类型以外的参数化类型上使用 instanceof 运算符是非法的。 使用无限制通配符类型代替原始类型,不会对 instanceof 运算符的行为产生任何影响。 在这种情况下,尖括号(<>)和问号(?)就显得多余。 以下是使用泛型类型的 instanceof 运算符的首选方法: + +// Legitimate use of raw type - instanceof operator +if (o instanceof Set) { // Raw type + Set s = (Set) o; // Wildcard type + ... +} +复制ErrorOK! +  请注意,一旦确定 o 对象是一个 Set,则必须将其转换为通配符 Set,而不是原始类型 Set。 这是一个受检查的(checked)转换,所以不会导致编译器警告。 + +  总之,使用原始类型可能导致运行时异常,所以不要使用它们。 原始类型只是为了与引入泛型机制之前的遗留代码进行兼容和互用而提供的。 作为一个快速回顾,Set 是一个参数化类型,表示一个可以包含任何类型对象的集合,Set 是一个通配符类型,表示一个只能包含某些未知类型对象的集合,Set 是一个原始类型,它不在泛型类型系统之列。 前两个类型是安全的,最后一个不是。 + +  为了快速参考,下表中总结了本条目(以及本章稍后介绍的一些)中介绍的术语: + +术语 中文含义 举例 所在条目 +Parameterized type 参数化类型 List 条目 26 +Actual type parameter 实际类型参数 String 条目 26 +Generic type 泛型类型 List 条目 26 和 条目 29 +Formal type parameter 形式类型参数 E 条目 26 +Unbounded wildcard type 无限制通配符类型 List 条目 26 +Raw type 原始类型 List 条目 26 +Bounded type parameter 限制类型参数 条目 29 +Recursive type bound 递归类型限制 > 条目 30 +Bounded wildcard type 限制通配符类型 List 条目 31 +Generic method 泛型方法 static List asList(E[] a) 条目 30 +Type token 类型令牌 String.class 条目 33 + + + +// +27. 消除非检查警告 +  使用泛型编程时,会看到许多编译器警告:未经检查的强制转换警告,未经检查的方法调用警告,未经检查的参数化可变长度类型警告以及未经检查的转换警告。 你使用泛型获得的经验越多,获得的警告越少,但不要期望新编写的代码能够干净地编译。 + +  许多未经检查的警告很容易消除。 例如,假设你不小心写了以下声明: + +Set exaltation = new HashSet(); +复制ErrorOK! +  编译器会提醒你你做错了什么: + +Venery.java:4: warning: [unchecked] unchecked conversion + Set exaltation = new HashSet(); + ^ + required: Set + found: HashSet。 +复制ErrorOK! +  然后可以进行指示修正,让警告消失。 请注意,实际上并不需要指定类型参数,只是为了表明它与 Java 7 中引入的钻石运算符(「<>」)一同出现。然后编译器会推断出正确的实际类型参数(在本例中为 Lark): + +Set exaltation = new HashSet<>(); +复制ErrorOK! +  但一些警告更难以消除。 本章充满了这种警告的例子。 当你收到需要进一步思考的警告时,坚持不懈! 尽可能地消除每一个未经检查的警告。 如果你消除所有的警告,你可以放心,你的代码是类型安全的,这是一件非常好的事情。 这意味着在运行时你将不会得到一个 ClassCastException 异常,并且增加了你的程序将按照你的意图行事的信心。 + +  如果你不能消除警告,但你可以证明引发警告的代码是类型安全的,那么(并且只能这样)用 @SuppressWarnings("unchecked") 注解来抑制警告。 如果你在没有首先证明代码是类型安全的情况下压制警告,那么你给自己一个错误的安全感。 代码可能会在不发出任何警告的情况下进行编译,但是它仍然可以在运行时抛出 ClassCastException 异常。 但是,如果你忽略了你认为是安全的未经检查的警告(而不是抑制它们),那么当一个新的警告出现时,你将不会注意到这是一个真正的问题。 新出现的警告就会淹没在所有的错误警告当中。 + +  SuppressWarnings 注解可用于任何声明,从单个局部变量声明到整个类。 始终在尽可能最小的范围内使用 SuppressWarnings 注解。 通常这是一个变量声明或一个非常短的方法或构造方法。 切勿在整个类上使用 SuppressWarnings 注解。 这样做可能会掩盖重要的警告。 + +  如果你发现自己在长度超过一行的方法或构造方法上使用 SuppressWarnings 注解,则可以将其移到局部变量声明上。 你可能需要声明一个新的局部变量,但这是值得的。 例如,考虑这个来自 ArrayList 的 toArray 方法: + +public T[] toArray(T[] a) { + if (a.length < size) + return (T[]) Arrays.copyOf(elements, size, a.getClass()); + System.arraycopy(elements, 0, a, 0, size); + if (a.length > size) + a[size] = null; + return a; +} + +如果编译 ArrayList 类,则该方法会生成此警告: +ArrayList.java:305: warning: [unchecked] unchecked cast + return (T[]) Arrays.copyOf(elements, size, a.getClass()); + ^ + required: T[] + found: Object[] +复制ErrorOK! +  在返回语句中设置 SuppressWarnings 注解是非法的,因为它不是一个声明[JLS,9.7]。 你可能会试图把注释放在整个方法上,但是不要这要做。 相反,声明一个局部变量来保存返回值并标注它的声明,如下所示: + +// Adding local variable to reduce scope of @SuppressWarnings +public T[] toArray(T[] a) { + if (a.length < size) { + // This cast is correct because the array we're creating + // is of the same type as the one passed in, which is T[]. + @SuppressWarnings("unchecked") T[] result = + (T[]) Arrays.copyOf(elements, size, a.getClass()); + return result; + } + System.arraycopy(elements, 0, a, 0, size); + if (a.length > size) + a[size] = null; + return a; +} +复制ErrorOK! +  所产生的方法干净地编译,并最小化未经检查的警告被抑制的范围。 + +  每当使用 @SuppressWarnings(“unchecked”) 注解时,请添加注释,说明为什么是安全的。 这将有助于他人理解代码,更重要的是,这将减少有人修改代码的可能性,从而使计算不安全。 如果你觉得很难写这样的注释,请继续思考。 毕竟,你最终可能会发现未经检查的操作是不安全的。 + +  总之,未经检查的警告是重要的。 不要忽视他们。 每个未经检查的警告代表在运行时出现 ClassCastException 异常的可能性。 尽你所能消除这些警告。 如果无法消除未经检查的警告,并且可以证明引发该警告的代码是安全类型的,则可以在尽可能小的范围内使用 @SuppressWarnings(“unchecked”) 注解来禁止警告。 记录你决定在注释中抑制此警告的理由。 + + 28. 列表优于数组 +  数组在两个重要方面与泛型不同。 首先,数组是协变的(covariant)。 这个吓人的单词意味着如果 Sub 是 Super 的子类型,则数组类型 Sub[] 是数组类型 Super[] 的子类型。 相比之下,泛型是不变的(invariant):对于任何两种不同的类型 Type1 和 Type2,List 既不是 List 的子类型也不是父类型。[JLS,4.10; Naftalin07, 2.5]。 你可能认为这意味着泛型是不足的,但可以说是数组缺陷。 这段代码是合法的: + +// Fails at runtime! +Object[] objectArray = new Long[1]; +objectArray[0] = "I don't fit in"; // Throws ArrayStoreException +  但这个不是: + +// Won't compile! +List ol = new ArrayList(); // Incompatible types +ol.add("I don't fit in"); +  无论哪种方式,你不能把一个 String 类型放到一个 Long 类型容器中,但是用一个数组,你会发现在运行时产生了一个错误;对于列表,可以在编译时就能发现错误。 当然,你宁愿在编译时找出错误。 + +  数组和泛型之间的第二个主要区别是数组被具体化了(reified)[JLS,4.7]。 这意味着数组在运行时知道并强制执行它们的元素类型。 如前所述,如果尝试将一个 String 放入 Long 数组中,得到一个 ArrayStoreException 异常。 相反,泛型通过擦除(erasure)来实现[JLS,4.6]。 这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)它们的元素类型信息。 擦除是允许泛型类型与不使用泛型的遗留代码自由互操作(详见第 26 条),从而确保在 Java 5 中平滑过渡到泛型。 + +  由于这些基本差异,数组和泛型不能很好地在一起混合使用。 例如,创建泛型类型的数组,参数化类型的数组,以及类型参数的数组都是非法的。 因此,这些数组创建表达式都不合法:new List[],new List[],new E[]。 所有将在编译时导致泛型数组创建错误。 + +  为什么创建一个泛型数组是非法的? 因为它不是类型安全的。 如果这是合法的,编译器生成的强制转换程序在运行时可能会因为 ClassCastException 异常而失败。 这将违反泛型类型系统提供的基本保证。 + +  为了具体说明,请考虑下面的代码片段: + +// Why generic array creation is illegal - won't compile! +List[] stringLists = new List[1]; // (1) +List intList = List.of(42); // (2) +Object[] objects = stringLists; // (3) +objects[0] = intList; // (4) +String s = stringLists[0].get(0); // (5) +  让我们假设第 1 行创建一个泛型数组是合法的。第 2 行创建并初始化包含单个元素的 List。第 3 行将 List 数组存储到 Object 数组变量中,这是合法的,因为数组是协变的。第 4 行将 List 存储在 Object 数组的唯一元素中,这是因为泛型是通过擦除来实现的:List 实例的运行时类型仅仅是 List,而 List[] 实例是 List[],所以这个赋值不会产生 ArrayStoreException 异常。现在我们遇到了麻烦。将一个 List 实例存储到一个声明为仅保存 List 实例的数组中。在第 5 行中,我们从这个数组的唯一列表中检索唯一的元素。编译器自动将检索到的元素转换为 String,但它是一个 Integer,所以我们在运行时得到一个 ClassCastException 异常。为了防止发生这种情况,第 1 行(创建一个泛型数组)必须产生一个编译时错误。 + +  类型 E,List 和 List 等在技术上被称为不可具体化的类型(nonreifiable types)[JLS,4.7]。 直观地说,不可具体化的类型是其运行时表示包含的信息少于其编译时表示的类型。 由于擦除,可唯一确定的参数化类型是无限定通配符类型,如 List 和 Map(详见第 26 条)。 尽管很少有用,创建无限定通配符类型的数组是合法的。 + +  禁止泛型数组的创建可能会很恼人的。 这意味着,例如,泛型集合通常不可能返回其元素类型的数组(但是参见条目 33 中的部分解决方案)。 这也意味着,当使用可变参数方法(详见第 53 条)和泛型时,会产生令人困惑的警告。 这是因为每次调用可变参数方法时,都会创建一个数组来保存可变参数。 如果此数组的元素类型不可确定,则会收到警告。 SafeVarargs 注解可以用来解决这个问题(详见第 32 条)。 + +  当你在强制转换为数组类型时,得到泛型数组创建错误,或是未经检查的强制转换警告时,最佳解决方案通常是使用集合类型 List 而不是数组类型 E[]。 这样可能会牺牲一些简洁性或性能,但作为交换,你会获得更好的类型安全性和互操作性。 + +  例如,假设你想用带有集合的构造方法来编写一个 Chooser 类,并且有个方法返回随机选择的集合的一个元素。 根据传递给构造方法的集合,可以使用该类的实例对象作为游戏骰子,魔力 8 号球或蒙特卡罗模拟的数据源。 这是一个没有泛型的简单实现: + +// Chooser - a class badly in need of generics! +public class Chooser { + private final Object[] choiceArray; + + + public Chooser(Collection choices) { + choiceArray = choices.toArray(); + } + + + public Object choose() { + Random rnd = ThreadLocalRandom.current(); + return choiceArray[rnd.nextInt(choiceArray.length)]; + } +} +  要使用这个类,每次调用方法时,都必须将 choose 方法的返回值从 Object 转换为所需的类型,如果类型错误,则转换在运行时失败。 我们先根据条目 29 的建议,试图修改 Chooser 类,使其成为泛型的。 + +// A first cut at making Chooser generic - won't compile +public class Chooser { + private final T[] choiceArray; + + public Chooser(Collection choices) { + choiceArray = choices.toArray(); + } + + // choose method unchanged +} +  如果你尝试编译这个类,会得到这个错误信息: + +Chooser.java:9: error: incompatible types: Object[] cannot be +converted to T[] + choiceArray = choices.toArray(); + ^ + where T is a type-variable: + T extends Object declared in class Chooser +  没什么大不了的,将 Object 数组转换为 T 数组: + +choiceArray = (T[]) choices.toArray(); +  这没有了错误,而是得到一个警告: + +Chooser.java:9: warning: [unchecked] unchecked cast + choiceArray = (T[]) choices.toArray(); + ^ + required: T[], found: Object[] + where T is a type-variable: +T extends Object declared in class Chooser +  编译器告诉你在运行时不能保证强制转换的安全性,因为程序不会知道 T 代表什么类型——记住,元素类型信息在运行时会被泛型删除。 该程序可以正常工作吗? 是的,但编译器不能证明这一点。 你可以证明这一点,在注释中提出证据,并用注解来抑制警告,但最好是消除警告的原因(详见第 27 条)。 + +  要消除未经检查的强制转换警告,请使用列表而不是数组。 下面是另一个版本的 Chooser 类,编译时没有错误或警告: + +// List-based Chooser - typesafe +public class Chooser { + private final List choiceList; + + + public Chooser(Collection choices) { + choiceList = new ArrayList<>(choices); + } + + + public T choose() { + Random rnd = ThreadLocalRandom.current(); + return choiceList.get(rnd.nextInt(choiceList.size())); + } +} +  这个版本有些冗长,也许运行比较慢,但是值得一提的是,在运行时不会得到 ClassCastException 异常。 + +  总之,数组和泛型具有非常不同的类型规则。 数组是协变和具体化的; 泛型是不变的,类型擦除的。 因此,数组提供运行时类型的安全性,但不提供编译时类型的安全性,对于泛型则是相反。 一般来说,数组和泛型不能很好地混合工作。 如果你发现把它们混合在一起,得到编译时错误或者警告,你的第一个冲动应该是用列表来替换数组。 + + 29. 优先考虑泛型 +  参数化声明并使用 JDK 提供的泛型类型和方法通常不会太困难。 但编写自己的泛型类型有点困难,但值得努力学习。 + +  考虑条目 7 中的简单堆栈实现: + +// Object-based collection - a prime candidate for generics +public class Stack { + private Object[] elements; + private int size = 0; + private static final int DEFAULT_INITIAL_CAPACITY = 16; + + public Stack() { + elements = new Object[DEFAULT_INITIAL_CAPACITY]; + } + + public void push(Object e) { + ensureCapacity(); + elements[size++] = e; + } + + public Object pop() { + if (size == 0) + throw new EmptyStackException(); + Object result = elements[--size]; + elements[size] = null; // Eliminate obsolete reference + return result; + } + + public boolean isEmpty() { + return size == 0; + } + + private void ensureCapacity() { + if (elements.length == size) + elements = Arrays.copyOf(elements, 2 * size + 1); + } +} +  这个类应该已经被参数化了,但是由于事实并非如此,我们可以对它进行泛型化。 换句话说,我们可以参数化它,而不会损害原始非参数化版本的客户端。 就目前而言,客户端必须强制转换从堆栈中弹出的对象,而这些强制转换可能会在运行时失败。 泛型化类的第一步是在其声明中添加一个或多个类型参数。 在这种情况下,有一个类型参数,表示堆栈的元素类型,这个类型参数的常规名称是 E(详见第 68 条)。 + +  下一步是用相应的类型参数替换所有使用的 Object 类型,然后尝试编译生成的程序: + +// Initial attempt to generify Stack - won't compile! +public class Stack { + private E[] elements; + private int size = 0; + private static final int DEFAULT_INITIAL_CAPACITY = 16; + + public Stack() { + elements = new E[DEFAULT_INITIAL_CAPACITY]; + } + + public void push(E e) { + ensureCapacity(); + elements[size++] = e; + } + + public E pop() { + if (size == 0) + throw new EmptyStackException(); + E result = elements[--size]; + elements[size] = null; // Eliminate obsolete reference + return result; + } + ... // no changes in isEmpty or ensureCapacity +} +  你通常会得到至少一个错误或警告,这个类也不例外。 幸运的是,这个类只产生一个错误: + +Stack.java:8: generic array creation + elements = new E[DEFAULT_INITIAL_CAPACITY]; + ^ +  如条目 28 所述,你不能创建一个不可具体化类型的数组,例如类型 E。每当编写一个由数组支持的泛型时,就会出现此问题。 有两种合理的方法来解决它。 第一种解决方案直接规避了对泛型数组创建的禁用:创建一个 Object 数组并将其转换为泛型数组类型。 现在没有了错误,编译器会发出警告。 这种用法是合法的,但不是(一般)类型安全的: + +Stack.java:8: warning: [unchecked] unchecked cast +found: Object[], required: E[] + elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; + ^ +  编译器可能无法证明你的程序是类型安全的,但你可以。 你必须说服自己,不加限制的类型强制转换不会损害程序的类型安全。 有问题的数组(元素)保存在一个私有属性中,永远不会返回给客户端或传递给任何其他方法。 保存在数组中的唯一元素是那些传递给 push 方法的元素,它们是 E 类型的,所以未经检查的强制转换不会造成任何伤害。 + +  一旦证明未经检查的强制转换是安全的,请尽可能缩小范围(条目 27)。 在这种情况下,构造方法只包含未经检查的数组创建,所以在整个构造方法中抑制警告是合适的。 通过添加一个注解来执行此操作,Stack 可以干净地编译,并且可以在没有显式强制转换或担心 ClassCastException 异常的情况下使用它: + +// The elements array will contain only E instances from push(E). +// This is sufficient to ensure type safety, but the runtime +// type of the array won't be E[]; it will always be Object[]! +@SuppressWarnings("unchecked") +public Stack() { + elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; +} +  消除 Stack 中的泛型数组创建错误的第二种方法是将属性元素的类型从 E[] 更改为 Object[]。 如果这样做,会得到一个不同的错误: + +Stack.java:19: incompatible types +found: Object, required: E + E result = elements[--size]; + ^ +  可以通过将从数组中检索到的元素转换为 E 来将此错误更改为警告: + +Stack.java:19: warning: [unchecked] unchecked cast +found: Object, required: E + E result = (E) elements[--size]; + ^ +  因为 E 是不可具体化的类型,编译器无法在运行时检查强制转换。 再一次,你可以很容易地向自己证明,不加限制的转换是安全的,所以可以适当地抑制警告。 根据条目 27 的建议,我们只在包含未经检查的强制转换的分配上抑制警告,而不是在整个 pop 方法上: + +// Appropriate suppression of unchecked warning +public E pop() { + if (size == 0) + throw new EmptyStackException(); + + // push requires elements to be of type E, so cast is correct + @SuppressWarnings("unchecked") E result = + (E) elements[--size]; + + elements[size] = null; // Eliminate obsolete reference + return result; +} +  两种消除泛型数组创建的技术都有其追随者。 第一个更可读:数组被声明为 E[] 类型,清楚地表明它只包含 E 实例。 它也更简洁:在一个典型的泛型类中,你从代码中的许多点读取数组;第一种技术只需要一次转换(创建数组的地方),而第二种技术每次读取数组元素都需要单独转换。 因此,第一种技术是优选的并且在实践中更常用。 但是,它确实会造成堆污染(heap pollution)(详见第 32 条):数组的运行时类型与编译时类型不匹配(除非 E 碰巧是 Object)。 这使得一些程序员非常不安,他们选择了第二种技术,尽管在这种情况下堆的污染是无害的。 + +  下面的程序演示了泛型 Stack 类的使用。 该程序以相反的顺序打印其命令行参数,并将其转换为大写。 对从堆栈弹出的元素调用 String 的 toUpperCase 方法不需要显式强制转换,而自动生成的强制转换将保证成功: + +// Little program to exercise our generic Stack +public static void main(String[] args) { + Stack stack = new Stack<>(); + for (String arg : args) + stack.push(arg); + while (!stack.isEmpty()) + System.out.println(stack.pop().toUpperCase()); +} +  上面的例子似乎与条目 28 相矛盾,条目 28 中鼓励使用列表优先于数组。 在泛型类型中使用列表并不总是可行或可取的。 Java 本身生来并不支持列表,所以一些泛型类型(如 ArrayList)必须在数组上实现。 其他的泛型类型,比如 HashMap,是为了提高性能而实现的。 + +  绝大多数泛型类型就像我们的 Stack 示例一样,它们的类型参数没有限制:可以创建一个 Stack, Stack,Stack> 或者其他任何对象的 Stack 引用类型。 请注意,不能创建基本类型的堆栈:尝试创建 Stack 或 Stack 将导致编译时错误。 这是 Java 泛型类型系统的一个基本限制。 可以使用基本类型的包装类(详见第 61 条)来解决这个限制。 + +  有一些泛型类型限制了它们类型参数的允许值。 例如,考虑 java.util.concurrent.DelayQueue,它的声明如下所示: + +class DelayQueue implements BlockingQueue +  类型参数列表()要求实际的类型参数 E 是 java.util.concurrent.Delayed 的子类型。 这使得 DelayQueue 实现及其客户端可以利用 DelayQueue 元素上的 Delayed 方法,而不需要显式的转换或 ClassCastException 异常的风险。 类型参数 E 被称为限定类型参数。 请注意,子类型关系被定义为每个类型都是自己的子类型[JLS,4.10],因此创建 DelayQueue 是合法的。 + +  总之,泛型类型比需要在客户端代码中强制转换的类型更安全,更易于使用。 当你设计新的类型时,确保它们可以在没有这种强制转换的情况下使用。 这通常意味着使类型泛型化。 如果你有任何现有的类型,应该是泛型的但实际上却不是,那么把它们泛型化。 这使这些类型的新用户的使用更容易,而不会破坏现有的客户端(条目 26)。 + + 31. 使用限定通配符来增加 API 的灵活性 +  如条目 28 所述,参数化类型是不变的。换句话说,对于任何两个不同类型的 Type1 和 Type2,List 既不是 List 的子类型也不是其父类型。尽管 List 不是 List 的子类型是违反直觉的,但它确实是有道理的。 可以将任何对象放入 List 中,但是只能将字符串放入 List 中。 由于 List 不能做 List 所能做的所有事情,所以它不是一个子类型(条目 10 中的里氏替代原则)。 + +  相对于提供的不可变的类型,有时你需要比此更多的灵活性。 考虑条目 29 中的 Stack 类。下面是它的公共 API: + +public class Stack { + + public Stack(); + + public void push(E e); + + public E pop(); + + public boolean isEmpty(); + +} +  假设我们想要添加一个方法来获取一系列元素,并将它们全部推送到栈上。 以下是第一种尝试: + +// pushAll method without wildcard type - deficient! +public void pushAll(Iterable src) { + for (E e : src) + push(e); +} +  这种方法可以干净地编译,但不完全令人满意。 如果可遍历的 src 元素类型与栈的元素类型完全匹配,那么它工作正常。 但是,假设有一个 Stack,并调用 push(intVal),其中 intVal 的类型是 Integer。 这是因为 Integer 是 Number 的子类型。 从逻辑上看,这似乎也应该起作用: + +Stack numberStack = new Stack<>(); +Iterable integers = ... ; +numberStack.pushAll(integers); +  但是,如果你尝试了,会得到这个错误消息,因为参数化类型是不变的: + +StackTest.java:7: error: incompatible types: Iterable +cannot be converted to Iterable + numberStack.pushAll(integers); + ^ +  幸运的是,有对应的解决方法。 该语言提供了一种特殊的参数化类型来调用一个限定通配符类型来处理这种情况。 pushAll 的输入参数的类型不应该是「E 的 Iterable 接口」,而应该是「E 的某个子类型的 Iterable 接口」,并且有一个通配符类型,这意味着:Iterable。 (关键字 extends 的使用有点误导:回忆条目 29 中,子类型被定义为每个类型都是它自己的子类型,即使它本身没有继承。)让我们修改 pushAll 来使用这个类型: + +// Wildcard type for a parameter that serves as an E producer +public void pushAll(Iterable src) { + for (E e : src) + push(e); +} +  有了这个改变,Stack 类不仅可以干净地编译,而且客户端代码也不会用原始的 pushAll 声明编译。 因为 Stack 和它的客户端干净地编译,你知道一切都是类型安全的。 + +  现在假设你想写一个 popAll 方法,与 pushAll 方法相对应。 popAll 方法从栈中弹出每个元素并将元素添加到给定的集合中。 以下是第一次尝试编写 popAll 方法的过程: + +// popAll method without wildcard type - deficient! +public void popAll(Collection dst) { + while (!isEmpty()) + dst.add(pop()); +} +  同样,如果目标集合的元素类型与栈的元素类型完全匹配,则干净编译并且工作正常。 但是,这又不完全令人满意。 假设你有一个 Stac 和 Object 类型的变量。 如果从栈中弹出一个元素并将其存储在该变量中,它将编译并运行而不会出错。 你不应该也这样做吗? + +Stack numberStack = new Stack(); + +Collection objects = ... ; + +numberStack.popAll(objects); +  如果尝试将此客户端代码与之前显示的 popAll 版本进行编译,则会得到与我们的第一版 pushAll 非常类似的错误:Collection 不是 Collection 的子类型。 通配符类型再一次提供了一条出路。 popAll 的输入参数的类型不应该是「E 的集合」,而应该是「E 的某个父类型的集合」(其中父类型被定义为 E 是它自己的父类型[JLS,4.10])。 再次,有一个通配符类型,正是这个意思:Collection。 让我们修改 popAll 来使用它: + +// Wildcard type for parameter that serves as an E consumer +public void popAll(Collection dst) { + while (!isEmpty()) + dst.add(pop()); +} +  通过这个改动,Stack 类和客户端代码都可以干净地编译。 + +  这个结论很清楚。 为了获得最大的灵活性,对代表生产者或消费者的输入参数使用通配符类型。 如果一个输入参数既是一个生产者又是一个消费者,那么通配符类型对你没有好处:你需要一个精确的类型匹配,这就是没有任何通配符的情况。 + +  这里有一个助记符来帮助你记住使用哪种通配符类型: PECS 代表: producer-extends,consumer-super。 + +  换句话说,如果一个参数化类型代表一个 T 生产者,使用 ;如果它代表 T 消费者,则使用 。 在我们的 Stack 示例中,pushAll 方法的 src 参数生成栈使用的 E 实例,因此 src 的合适类型为 Iterable;popAll 方法的 dst 参数消费 Stack 中的 E 实例,因此 dst 的合适类型是 Collection 。 PECS 助记符抓住了使用通配符类型的基本原则。 Naftalin 和 Wadler 称之为获取和放置原则(Get and Put Principle)[Naftalin07,2.4]。 + +  记住这个助记符之后,让我们来看看本章中以前项目的一些方法和构造方法声明。 条目 28 中的 Chooser 类构造方法有这样的声明: + +public Chooser(Collection choices) +  这个构造方法只使用集合选择来生产类型 T 的值(并将它们存储起来以备后用),所以它的声明应该使用一个 extends T 的通配符类型。下面是得到的构造方法声明: + +// Wildcard type for parameter that serves as an T producer + +public Chooser(Collection choices) +  这种改变在实践中会有什么不同吗? 是的,会有不同。 假你有一个 List,并且想把它传递给 Chooser 的构造方法。 这不会与原始声明一起编译,但是它只会将限定通配符类型添加到声明中。 + +  现在看看条目 30 中的 union 方法。下是声明: + +public static Set union(Set s1, Set s2) +  两个参数 s1 和 s2 都是 E 的生产者,所以 PECS 助记符告诉我们该声明应该如下: + +public static Set union(Set s1, Set s2) +  请注意,返回类型仍然是 Set。 不要使用限定通配符类型作为返回类型。除了会为用户提供额外的灵活性,还强制他们在客户端代码中使用通配符类型。 通过修改后的声明,此代码将清晰地编译: + +Set integers = Set.of(1, 3, 5); + +Set doubles = Set.of(2.0, 4.0, 6.0); + +Set numbers = union(integers, doubles); +  如果使用得当,类的用户几乎不会看到通配符类型。 他们使方法接受他们应该接受的参数,拒绝他们应该拒绝的参数。 如果一个类的用户必须考虑通配符类型,那么它的 API 可能有问题。 + +  在 Java 8 之前,类型推断规则不够聪明,无法处理先前的代码片段,这要求编译器使用上下文指定的返回类型(或目标类型)来推断 E 的类型。union 方法调用的目标类型如前所示是 Set。 如果尝试在早期版本的 Java 中编译片段(以及适合的 Set.of 工厂替代版本),将会看到如此长的错综复杂的错误消息: + +Union.java:14: error: incompatible types + Set numbers = union(integers, doubles); + ^ + required: Set + found: Set + where INT#1,INT#2 are intersection types: + INT#1 extends Number,Comparable + INT#2 extends Number,Comparable +  幸运的是有办法来处理这种错误。 如果编译器不能推断出正确的类型,你可以随时告诉它使用什么类型的显式类型参数[JLS,15.12]。 甚至在 Java 8 中引入目标类型之前,这不是你必须经常做的事情,这很好,因为显式类型参数不是很漂亮。 通过添加显式类型参数,如下所示,代码片段在 Java 8 之前的版本中进行了干净编译: + +// Explicit type parameter - required prior to Java 8 +Set numbers = Union.union(integers, doubles); +  接下来让我们把注意力转向条目 30 中的 max 方法。这里是原始声明: + +public static > T max(List list) +  为了从原来到修改后的声明,我们两次应用了 PECS。首先直接的应用是参数列表。 它生成 T 实例,所以将类型从 List 更改为 List。 棘手的应用是类型参数 T。这是我们第一次看到通配符应用于类型参数。 最初,T 被指定为继承 Comparable,但 Comparable 的 T 消费 T 实例(并生成指示顺序关系的整数)。 因此,参数化类型 Comparable 被替换为限定通配符类型 Comparable。 Comparable 实例总是消费者,所以通常应该使用 Comparable 优于 Comparable。 Comparator 也是如此。因此,通常应该使用 Comparator 优于 Comparator。 + +  修改后的 max 声明可能是本书中最复杂的方法声明。 增加的复杂性是否真的起作用了吗? 同样,它的确如此。 这是一个列表的简单例子,它被原始声明排除,但在被修改后的版本里是允许的: + +List> scheduledFutures = ... ; +  无法将原始方法声明应用于此列表的原因是 ScheduledFuture 不实现 Comparable。 相反,它是 Delayed 的子接口,它继承了 Comparable。 换句话说,一个 ScheduledFuture 实例不仅仅和其他的 ScheduledFuture 实例相比较: 它可以与任何 Delayed 实例比较,并且足以导致原始的声明拒绝它。 更普遍地说,通配符要求来支持没有直接实现 Comparable(或 Comparator)的类型,但继承了一个类型。 + +  还有一个关于通配符相关的话题。 类型参数和通配符之间具有双重性,许多方法可以用一个或另一个声明。 例如,下面是两个可能的声明,用于交换列表中两个索引项目的静态方法。 第一个使用无限制类型参数(条目 30),第二个使用无限制通配符: + +// Two possible declarations for the swap method +public static void swap(List list, int i, int j); +public static void swap(List list, int i, int j); +  这两个声明中的哪一个更可取,为什么? 在公共 API 中,第二个更好,因为它更简单。 你传入一个列表(任何列表),该方法交换索引的元素。 没有类型参数需要担心。 通常, 如果类型参数在方法声明中只出现一次,请将其替换为通配符。 如果它是一个无限制的类型参数,请将其替换为无限制的通配符; 如果它是一个限定类型参数,则用限定通配符替换它。 + +  第二个 swap 方法声明有一个问题。 这个简单的实现不会编译: + +public static void swap(List list, int i, int j) { + list.set(i, list.set(j, list.get(i))); +} +  试图编译它会产生这个不太有用的错误信息: + +Swap.java:5: error: incompatible types: Object cannot be +converted to CAP#1 + list.set(i, list.set(j, list.get(i))); + ^ + where CAP#1 is a fresh type-variable: + CAP#1 extends Object from capture of ? +  看起来我们不能把一个元素放回到我们刚刚拿出来的列表中。 问题是列表的类型是 List,并且不能将除 null 外的任何值放入 List 中。 幸运的是,有一种方法可以在不使用不安全的转换或原始类型的情况下实现此方法。 这个想法是写一个私有辅助方法来捕捉通配符类型。 辅助方法必须是泛型方法才能捕获类型。 以下是它的定义: + +public static void swap(List list, int i, int j) { + swapHelper(list, i, j); +} + +// Private helper method for wildcard capture +private static void swapHelper(List list, int i, int j) { + list.set(i, list.set(j, list.get(i))); +} +  swapHelper 方法知道该列表是一个 List。 因此,它知道从这个列表中获得的任何值都是 E 类型,并且可以安全地将任何类型的 E 值放入列表中。 这个稍微复杂的 swap 的实现可以干净地编译。 它允许我们导出基于通配符的漂亮声明,同时利用内部更复杂的泛型方法。 swap 方法的客户端不需要面对更复杂的 swapHelper 声明,但他们从中受益。 辅助方法具有我们认为对公共方法来说过于复杂的签名。 + +  总之,在你的 API 中使用通配符类型,虽然棘手,但使得 API 更加灵活。 如果编写一个将被广泛使用的类库,正确使用通配符类型应该被认为是强制性的。 记住基本规则: producer-extends, consumer-super(PECS)。 还要记住,所有 Comparable 和 Comparator 都是消费者。 + + 32. 合理地结合泛型和可变参数 +  在 Java 5 中,可变参数方法(详见第 53 条)和泛型都被添加到平台中,所以你可能希望它们能够正常交互; 可悲的是,他们并没有。 可变参数的目的是允许客户端将一个可变数量的参数传递给一个方法,但这是一个脆弱的抽象(leaky abstraction):当你调用一个可变参数方法时,会创建一个数组来保存可变参数;那个应该是实现细节的数组是可见的。 因此,当可变参数具有泛型或参数化类型时,会导致编译器警告混淆。 + +  回顾条目 28,非具体化(non-reifiable)的类型是其运行时表示比其编译时表示具有更少信息的类型,并且几乎所有泛型和参数化类型都是不可具体化的。 如果某个方法声明其可变参数为非具体化的类型,则编译器将在该声明上生成警告。 如果在推断类型不可确定的可变参数参数上调用该方法,那么编译器也会在调用中生成警告。 警告看起来像这样: + +warning: [unchecked] Possible heap pollution from + parameterized vararg type List +复制ErrorOK!复制ErrorOK! +  当参数化类型的变量引用不属于该类型的对象时会发生堆污染(Heap pollution)[JLS,4.12.2]。 它会导致编译器的自动生成的强制转换失败,违反了泛型类型系统的基本保证。 + +  例如,请考虑以下方法,该方法是第 127 页上的代码片段的一个不太明显的变体: + +// Mixing generics and varargs can violate type safety! +static void dangerous(List... stringLists) { + List intList = List.of(42); + Object[] objects = stringLists; + objects[0] = intList; // Heap pollution + String s = stringLists[0].get(0); // ClassCastException +} +复制ErrorOK!复制ErrorOK! +  此方法没有可见的强制转换,但在调用一个或多个参数时抛出 ClassCastException 异常。 它的最后一行有一个由编译器生成的隐形转换。 这种转换失败,表明类型安全性已经被破坏,并且将值保存在泛型可变参数数组参数中是不安全的。 + +  这个例子引发了一个有趣的问题:为什么声明一个带有泛型可变参数的方法是合法的,当明确创建一个泛型数组是非法的时候呢? 换句话说,为什么前面显示的方法只生成一个警告,而 127 页上的代码片段会生成一个错误? 答案是,具有泛型或参数化类型的可变参数参数的方法在实践中可能非常有用,因此语言设计人员选择忍受这种不一致。 事实上,Java 类库导出了几个这样的方法,包括 Arrays.asList(T... a),Collections.addAll(Collection c, T... elements),EnumSet.of(E first, E... rest)。 与前面显示的危险方法不同,这些类库方法是类型安全的。 + +  在 Java 7 中,@SafeVarargs 注解已添加到平台,以允许具有泛型可变参数的方法的作者自动禁止客户端警告。 实质上,@SafeVarargs 注解构成了作者对类型安全的方法的承诺。 为了交换这个承诺,编译器同意不要警告用户调用可能不安全的方法。 + +  除非它实际上是安全的,否则注意不要使用 @SafeVarargs 注解标注一个方法。 那么需要做些什么来确保这一点呢? 回想一下,调用方法时会创建一个泛型数组,以容纳可变参数。 如果方法没有在数组中存储任何东西(它会覆盖参数)并且不允许对数组的引用进行转义(这会使不受信任的代码访问数组),那么它是安全的。 换句话说,如果可变参数数组仅用于从调用者向方法传递可变数量的参数——毕竟这是可变参数的目的——那么该方法是安全的。 + +  值得注意的是,你可以违反类型安全性,即使不会在可变参数数组中存储任何内容。 考虑下面的泛型可变参数方法,它返回一个包含参数的数组。 乍一看,它可能看起来像一个方便的小工具: + +// UNSAFE - Exposes a reference to its generic parameter array! +static T[] toArray(T... args) { + return args; +} +复制ErrorOK!复制ErrorOK! +  这个方法只是返回它的可变参数数组。 该方法可能看起来并不危险,但它是! 该数组的类型由传递给方法的参数的编译时类型决定,编译器可能没有足够的信息来做出正确的判断。 由于此方法返回其可变参数数组,它可以将堆污染传播到调用栈上。 + +  为了具体说明,请考虑下面的泛型方法,它接受三个类型 T 的参数,并返回一个包含两个参数的数组,随机选择: + +static T[] pickTwo(T a, T b, T c) { + switch(ThreadLocalRandom.current().nextInt(3)) { + case 0: return toArray(a, b); + case 1: return toArray(a, c); + case 2: return toArray(b, c); + } + throw new AssertionError(); // Can't get here +} +复制ErrorOK!复制ErrorOK! +  这个方法本身不是危险的,除了调用具有泛型可变参数的 toArray 方法之外,不会产生警告。 + +  编译此方法时,编译器会生成代码以创建一个将两个 T 实例传递给 toArray 的可变参数数组。 这段代码分配了一个 Object[] 类型的数组,它是保证保存这些实例的最具体的类型,而不管在调用位置传递给 pickTwo 的对象是什么类型。 toArray 方法只是简单地将这个数组返回给 pickTwo,然后 pickTwo 将它返回给调用者,所以 pickTwo 总是返回一个 Object[] 类型的数组。 + +public static void main(String[] args) { + String[] attributes = pickTwo("Good", "Fast", "Cheap"); +} +复制ErrorOK!复制ErrorOK! +  这种方法没有任何问题,因此它编译时不会产生任何警告。 但是当运行它时,抛出一个 ClassCastException 异常,尽管不包含可见的转换。 你没有看到的是,编译器已经生成了一个隐藏的强制转换为由 pickTwo 返回的值的 String[] 类型,以便它可以存储在属性中。 转换失败,因为 Object[] 不是 String[] 的子类型。 这种故障相当令人不安,因为它从实际导致堆污染(toArray)的方法中移除了两个级别,并且在实际参数存储在其中之后,可变参数数组未被修改。 + +  这个例子是为了让人们认识到给另一个方法访问一个泛型的可变参数数组是不安全的,除了两个例外:将数组传递给另一个可变参数方法是安全的,这个方法是用 @SafeVarargs 正确标注的, 将数组传递给一个非可变参数的方法是安全的,该方法仅计算数组内容的一些方法。 + +  这里是安全使用泛型可变参数的典型示例。 此方法将任意数量的列表作为参数,并按顺序返回包含所有输入列表元素的单个列表。 由于该方法使用 @SafeVarargs 进行标注,因此在声明或其调用站位置上不会生成任何警告: + +// Safe method with a generic varargs parameter +@SafeVarargs +static List flatten(List... lists) { + List result = new ArrayList<>(); + for (List list : lists) + result.addAll(list); + return result; +} +复制ErrorOK!复制ErrorOK! +  决定何时使用 @SafeVarargs 注解的规则很简单:在每种方法上使用 @SafeVarargs,并使用泛型或参数化类型的可变参数,这样用户就不会因不必要的和令人困惑的编译器警告而担忧。 这意味着你不应该写危险或者 toArray 等不安全的可变参数方法。 每次编译器警告你可能会受到来自你控制的方法中泛型可变参数的堆污染时,请检查该方法是否安全。 提醒一下,在下列情况下,泛型可变参数方法是安全的: + +它不会在可变参数数组中存储任何东西 +它不会使数组(或克隆)对不可信代码可见。 如果违反这些禁令中的任何一项,请修复。 +  请注意,SafeVarargs 注解只对不能被重写的方法是合法的,因为不可能保证每个可能的重写方法都是安全的。 在 Java 8 中,注解仅在静态方法和 final 实例方法上合法; 在 Java 9 中,它在私有实例方法中也变为合法。 + +  使用 SafeVarargs 注解的替代方法是采用条目 28 的建议,并用 List 参数替换可变参数(这是一个变相的数组)。 下面是应用于我们的 flatten 方法时,这种方法的样子。 请注意,只有参数声明被更改了: + +// List as a typesafe alternative to a generic varargs parameter +static List flatten(List> lists) { + List result = new ArrayList<>(); + for (List list : lists) + result.addAll(list); + return result; +} +复制ErrorOK!复制ErrorOK! +  然后可以将此方法与静态工厂方法 List.of 结合使用,以允许可变数量的参数。 请注意,这种方法依赖于 List.of 声明使用 @SafeVarargs 注解: + +audience = flatten(List.of(friends, romans, countrymen)); +复制ErrorOK!复制ErrorOK! +  这种方法的优点是编译器可以证明这种方法是类型安全的。 不必使用 @SafeVarargs 注解来证明其安全性,也不用担心在确定安全性时可能会犯错。 主要缺点是客户端代码有点冗长,运行可能会慢一些。 + +  这个技巧也可以用在不可能写一个安全的可变参数方法的情况下,就像第 147 页的 toArray 方法那样。它的列表模拟是 List.of 方法,所以我们甚至不必编写它;Java 类库作者已经为我们完成了这项工作。 pickTwo 方法然后变成这样: + +static List pickTwo(T a, T b, T c) { + switch(rnd.nextInt(3)) { + case 0: return List.of(a, b); + case 1: return List.of(a, c); + case 2: return List.of(b, c); + } + throw new AssertionError(); +} +复制ErrorOK!复制ErrorOK! +  main 方变成这样: + +public static void main(String[] args) { + List attributes = pickTwo("Good", "Fast", "Cheap"); +} +复制ErrorOK!复制ErrorOK! +  生成的代码是类型安全的,因为它只使用泛型,不是数组。 + +  总而言之,可变参数和泛型不能很好地交互,因为可变参数机制是在数组上面构建的脆弱的抽象,并且数组具有与泛型不同的类型规则。 虽然泛型可变参数不是类型安全的,但它们是合法的。 如果选择使用泛型(或参数化)可变参数编写方法,请首先确保该方法是类型安全的,然后使用 @SafeVarargs 注解对其进行标注,以免造成使用不愉快。 + + 33. 优先考虑类型安全的异构容器 +  泛型的常见用法包括集合,如 Set 和 Map 和单个元素容器,如 ThreadLocal 和 AtomicReference。 在所有这些用途中,它都是参数化的容器。 这限制了每个容器只能有固定数量的类型参数。 通常这正是你想要的。 一个 Set 有单一的类型参数,表示它的元素类型; 一个 Map 有两个,代表它的键和值的类型;等等。 + +  然而有时候,你需要更多的灵活性。 例如,数据库一行记录可以具有任意多列,并且能够以类型安全的方式访问它们是很好的。 幸运的是,有一个简单的方法可以达到这个效果。 这个想法是参数化键(key)而不是容器。 然后将参数化的键提交给容器以插入或检索值。 泛型类型系统用于保证值的类型与其键一致。 + +  作为这种方法的一个简单示例,请考虑一个 Favorites 类,它允许其客户端保存和检索任意多种类型的 favorite 实例。 该类型的 Class 对象将扮演参数化键的一部分。其原因是这 Class 类是泛型的。 类的类型从字面上来说不是简单的 Class,而是 Class。 例如,String.class 的类型为 Class,Integer.class 的类型为 Class。 当在方法中传递字面类传递编译时和运行时类型信息时,它被称为类型令牌(type token)[Bracha04]。 + +  Favorites 类的 API 很简单。 它看起来就像一个简单 Map 类,除了该键是参数化的以外。 客户端在设置和获取 favorites 实例时呈现一个 Class 对象。 如下是 API: + +// Typesafe heterogeneous container pattern - API +public class Favorites { + public void putFavorite(Class type, T instance); + public T getFavorite(Class type); +} +复制ErrorOK! +  下面是一个演示 Favorites 类,保存,检索和打印喜欢的 String,Integer 和 Class 实例: + +// Typesafe heterogeneous container pattern - client +public static void main(String[] args) { + Favorites f = new Favorites(); + f.putFavorite(String.class, "Java"); + f.putFavorite(Integer.class, 0xcafebabe); + f.putFavorite(Class.class, Favorites.class); + + String favoriteString = f.getFavorite(String.class); + int favoriteInteger = f.getFavorite(Integer.class); + Class favoriteClass = f.getFavorite(Class.class); + System.out.printf("%s %x %s%n", favoriteString, + favoriteInteger, favoriteClass.getName()); +} +复制ErrorOK! +  正如你所期望的,这个程序打印 Java cafebabe Favorites。 请注意,顺便说一下,Java 的 printf 方法与 C 语言的不同之处在于,你应该在 C 中使用 \n 的地方改用 %n。%n 用于生成适用于特定平台的行分隔符,在大多数平台上面的值为 \n,但并不是所有平台的分隔符都为 \n。 + +  Favorites 实例是类型安全的:当你请求一个字符串时它永远不会返回一个整数。 它也是异构的:与普通 Map 不同,所有的键都是不同的类型。 因此,我们将 Favorites 称为类型安全异构容器(typesafe heterogeneous container)。 + +  Favorites 的实现非常小巧。 这是完整的代码: + +// Typesafe heterogeneous container pattern - implementation +public class Favorites { + private Map, Object> favorites = new HashMap<>(); + + public void putFavorite(Class type, T instance) { + favorites.put(Objects.requireNonNull(type), instance); + } + + public T getFavorite(Class type) { + return type.cast(favorites.get(type)); + } +} +复制ErrorOK! +  这里有一些微妙的事情发生。 每个 Favorites 实例都由一个名为 favorites 私有的 Map, Object> 来支持。 你可能认为无法将任何内容放入此 Map 中,因为这是无限定的通配符类型,但事实恰恰相反。 需要注意的是通配符类型是嵌套的:它不是通配符类型的 Map 类型,而是键的类型。 这意味着每个键都可以有不同的参数化类型:一个可以是 Class,下一个 Class 等等。 这就是异构的由来。 + +  接下来要注意的是,favorites 的 Map 的值类型只是 Object。 换句话说,Map 不保证键和值之间的类型关系,即每个值都是由其键表示的类型。 事实上,Java 的类型系统并不足以表达这一点。 但是我们知道这是真的,并在检索一个 favorite 时利用了这点。 + +  putFavorite 实现很简单:只需将给定的 Class 对象映射到给定的 favorites 的实例即可。 如上所述,这丢弃了键和值之间的“类型联系(type linkage)”;无法知道这个值是不是键的一个实例。 但没关系,因为 getFavorites 方法可以并且确实重新建立这种关联。 + +  getFavorite 的实现比 putFavorite 更复杂。 首先,它从 favorites Map 中获取与给定 Class 对象相对应的值。 这是返回的正确对象引用,但它具有错误的编译时类型:它是 Object(favorites map 的值类型),我们需要返回类型 T。因此,getFavorite 实现动态地将对象引用转换为 Class 对象表示的类型,使用 Class 的 cast 方法。 + +  cast 方法是 Java 的 cast 操作符的动态模拟。它只是检查它的参数是否由 Class 对象表示的类型的实例。如果是,它返回参数;否则会抛出 ClassCastException 异常。我们知道,假设客户端代码能够干净地编译,getFavorite 中的强制转换不会抛出 ClassCastException 异常。 也就是说,favorites map 中的值始终与其键的类型相匹配。 + +  那么这个 cast 方法为我们做了什么,因为它只是返回它的参数? cast 的签名充分利用了 Class 类是泛型的事实。 它的返回类型是 Class 对象的类型参数: + +public class Class { + T cast(Object obj); +} +复制ErrorOK! +  这正是 getFavorite 方法所需要的。 这正是确保 Favorites 类型安全,而不用求助一个未经检查的强制转换的 T 类型。 + +  Favorites 类有两个限制值得注意。 首先,恶意客户可以通过使用原始形式的 Class 对象,轻松破坏 Favorites 实例的类型安全。 但生成的客户端代码在编译时会生成未经检查的警告。 这与正常的集合实现(如 HashSet 和 HashMap)没有什么不同。 通过使用原始类型 HashSet(条目 26),可以轻松地将字符串放入 HashSet 中。 也就是说,如果你愿意为此付出一点代价,就可以拥有运行时类型安全性。 确保 Favorites 永远不违反类型不变的方法是,使 putFavorite 方法检查该实例是否由 type 表示类型的实例,并且我们已经知道如何执行此操作。只需使用动态转换: + +// Achieving runtime type safety with a dynamic cast +public void putFavorite(Class type, T instance) { + favorites.put(type, type.cast(instance)); +} +复制ErrorOK! +  java.util.Collections 中有一些集合包装类,可以发挥相同的诀窍。 它们被称为 checkedSet,checkedList,checkedMap 等等。 他们的静态工厂除了一个集合(或 Map)之外还有一个 Class 对象(或两个)。 静态工厂是泛型方法,确保 Class 对象和集合的编译时类型匹配。 包装类为它们包装的集合添加了具体化。 例如,如果有人试图将 Coin 放入你的 Collection 中,则包装类在运行时会抛出 ClassCastException。 这些包装类对于追踪在混合了泛型和原始类型的应用程序中添加不正确类型的元素到集合的客户端代码很有用。 + +  Favorites 类的第二个限制是它不能用于不可具体化的(non-reifiable)类型(详见第 28 条)。 换句话说,你可以保存你最喜欢的 String 或 String[],但不能保存 List。 如果你尝试保存你最喜欢的 List,程序将不能编译。 原因是无法获取 List 的 Class 对象。 List.class 是语法错误,也是一件好事。 List 和 List 共享一个 Class 对象,即 List.class。 如果“字面类型(type literals)”List .class 和 List.class 合法并返回相同的对象引用,那么它会对 Favorites 对象的内部造成严重破坏。 对于这种限制,没有完全令人满意的解决方法。 + +  Favorites 使用的类型令牌 type tokens) 是无限制的:getFavorite 和 putFavorite 接受任何 Class 对象。 有时你可能需要限制可传递给方法的类型。 这可以通过一个有限定的类型令牌来实现,该令牌只是一个类型令牌,它使用限定的类型参数(详见第 30 条)或限定的通配符(详见第 31 条)来放置可以表示的类型的边界。 + +  注解 API(详见第 39 条)广泛使用限定类型的令牌。 例如,以下是在运行时读取注解的方法。 此方法来自 AnnotatedElement 接口,该接口由表示类,方法,属性和其他程序元素的反射类型实现: + +public + T getAnnotation(Class annotationType); +复制ErrorOK! +  参数 annotationType 是表示注解类型的限定类型令牌。 该方法返回该类型的元素的注解(如果它有一个);如果没有,则返回 null。 本质上,注解元素是一个类型安全的异构容器,其键是注解类型。 + +  假设有一个 Class 类型的对象,并且想要将它传递给需要限定类型令牌(如 getAnnotation)的方法。 可以将对象转换为 Class,但是这个转换没有被检查,所以它会产生一个编译时警告(详见第 52 条)。 幸运的是,Class 类提供了一种安全(动态)执行这种类型转换的实例方法。 该方法被称为 asSubclass,并且它转换所调用的 Class 对象来表示由其参数表示的类的子类。 如果转换成功,该方法返回它的参数;如果失败,则抛出 ClassCastException 异常。 + +  以下是如何使用 asSubclass 方法在编译时读取类型未知的注解。 此方法编译时没有错误或警告: + +// Use of asSubclass to safely cast to a bounded type token +static Annotation getAnnotation(AnnotatedElement element, + String annotationTypeName) { + Class annotationType = null; // Unbounded type token + try { + annotationType = Class.forName(annotationTypeName); + } catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + return element.getAnnotation( + annotationType.asSubclass(Annotation.class)); +} +复制ErrorOK! +  总之,泛型 API 的通常用法(以集合 API 为例)限制了每个容器的固定数量的类型参数。 你可以通过将类型参数放在键上而不是容器上来解决此限制。 可以使用 Class 对象作为此类型安全异构容器的键。 以这种方式使用的 Class 对象称为类型令牌。 也可以使用自定义键类型。 例如,可以有一个表示数据库行(容器)的 DatabaseRow 类型和一个泛型类型 Column 作为其键。 diff --git "a/docs/typora-user-images/B+\346\240\221.png" "b/docs/typora-user-images/B+\346\240\221.png" new file mode 100644 index 0000000..4823b10 Binary files /dev/null and "b/docs/typora-user-images/B+\346\240\221.png" differ diff --git "a/docs/typora-user-images/InnoDB \351\224\201\345\205\263\347\263\273\347\237\251\351\230\265.png" "b/docs/typora-user-images/InnoDB \351\224\201\345\205\263\347\263\273\347\237\251\351\230\265.png" new file mode 100644 index 0000000..7c4b17d Binary files /dev/null and "b/docs/typora-user-images/InnoDB \351\224\201\345\205\263\347\263\273\347\237\251\351\230\265.png" differ diff --git "a/docs/typora-user-images/InnoDB\346\236\266\346\236\204.png" "b/docs/typora-user-images/InnoDB\346\236\266\346\236\204.png" new file mode 100644 index 0000000..5a9e407 Binary files /dev/null and "b/docs/typora-user-images/InnoDB\346\236\266\346\236\204.png" differ diff --git "a/docs/typora-user-images/InnoDB\347\237\245\350\257\206\344\275\223\347\263\273.png" "b/docs/typora-user-images/InnoDB\347\237\245\350\257\206\344\275\223\347\263\273.png" new file mode 100644 index 0000000..1664ddc Binary files /dev/null and "b/docs/typora-user-images/InnoDB\347\237\245\350\257\206\344\275\223\347\263\273.png" differ diff --git a/docs/typora-user-images/MVCC3.png b/docs/typora-user-images/MVCC3.png new file mode 100644 index 0000000..8e7eb5e Binary files /dev/null and b/docs/typora-user-images/MVCC3.png differ diff --git "a/docs/typora-user-images/MVCC\345\256\236\347\216\260.png" "b/docs/typora-user-images/MVCC\345\256\236\347\216\260.png" new file mode 100644 index 0000000..b62bf71 Binary files /dev/null and "b/docs/typora-user-images/MVCC\345\256\236\347\216\260.png" differ diff --git "a/docs/typora-user-images/MVCC\345\256\236\347\216\2602.png" "b/docs/typora-user-images/MVCC\345\256\236\347\216\2602.png" new file mode 100644 index 0000000..b64a96a Binary files /dev/null and "b/docs/typora-user-images/MVCC\345\256\236\347\216\2602.png" differ diff --git "a/docs/typora-user-images/MySQL\346\236\266\346\236\204.png" "b/docs/typora-user-images/MySQL\346\236\266\346\236\204.png" new file mode 100644 index 0000000..e1f9661 Binary files /dev/null and "b/docs/typora-user-images/MySQL\346\236\266\346\236\204.png" differ diff --git "a/docs/typora-user-images/SQL\350\257\255\345\217\245\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213.png" "b/docs/typora-user-images/SQL\350\257\255\345\217\245\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213.png" new file mode 100644 index 0000000..9a070ee Binary files /dev/null and "b/docs/typora-user-images/SQL\350\257\255\345\217\245\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213.png" differ diff --git "a/docs/typora-user-images/\345\206\205\345\255\230\345\222\214\347\211\251\347\220\206\347\273\223\346\236\204.png" "b/docs/typora-user-images/\345\206\205\345\255\230\345\222\214\347\211\251\347\220\206\347\273\223\346\236\204.png" new file mode 100644 index 0000000..5632da0 Binary files /dev/null and "b/docs/typora-user-images/\345\206\205\345\255\230\345\222\214\347\211\251\347\220\206\347\273\223\346\236\204.png" differ diff --git "a/docs/typora-user-images/\345\220\204\345\255\227\350\212\202\347\261\273\345\236\213\345\215\240\347\224\250\347\232\204\347\251\272\351\227\264.png" "b/docs/typora-user-images/\345\220\204\345\255\227\350\212\202\347\261\273\345\236\213\345\215\240\347\224\250\347\232\204\347\251\272\351\227\264.png" new file mode 100644 index 0000000..4ab8906 Binary files /dev/null and "b/docs/typora-user-images/\345\220\204\345\255\227\350\212\202\347\261\273\345\236\213\345\215\240\347\224\250\347\232\204\347\251\272\351\227\264.png" differ diff --git "a/docs/typora-user-images/\346\205\242\346\237\245\350\257\242\344\276\213\345\255\220.png" "b/docs/typora-user-images/\346\205\242\346\237\245\350\257\242\344\276\213\345\255\220.png" new file mode 100644 index 0000000..6393995 Binary files /dev/null and "b/docs/typora-user-images/\346\205\242\346\237\245\350\257\242\344\276\213\345\255\220.png" differ diff --git "a/docs/typora-user-images/\346\237\245\350\257\242\344\274\230\345\214\226.png" "b/docs/typora-user-images/\346\237\245\350\257\242\344\274\230\345\214\226.png" new file mode 100644 index 0000000..e46c780 Binary files /dev/null and "b/docs/typora-user-images/\346\237\245\350\257\242\344\274\230\345\214\226.png" differ diff --git "a/docs/\346\267\261\345\205\245\347\220\206\350\247\243MySQL\345\237\272\347\241\200\347\257\207.md" "b/docs/\346\267\261\345\205\245\347\220\206\350\247\243MySQL\345\237\272\347\241\200\347\257\207.md" new file mode 100644 index 0000000..0d97774 --- /dev/null +++ "b/docs/\346\267\261\345\205\245\347\220\206\350\247\243MySQL\345\237\272\347\241\200\347\257\207.md" @@ -0,0 +1,343 @@ +# MySQL体系结构与存储引擎 + +## MySQL体系结构 + +先看 MySQL 数据库的体系结构,如下图所示。 + + + + + + + +MySQL体系结构由ClientConnectors层、MySQLServer层及存储引擎层组成。 + +**ClientConnectors层** + +负责处理客户端的连接请求,与客户端创建连接。目前 MySQL 几乎支持所有的连接类型,例如常见的 JDBC、Python、Go 等。 + +**MySQL Server 层** + +MySQLServer层主要包括ConnectionPool、Service&utilities、SQLinterface、Parser解析器、Optimizer查询优化器、Caches缓存等模块。 + +1. ConnectionPool,负责处理和存储数据库与客户端创建的连接,线程池资源管理,一个线程负责管理一个连接。还包括了用户认证模块,就是用户登录身份的认证和鉴权及安全管理,也就是用户执行操作权限校验。 + +2. Service & utilities 是管理服务&工具集,包括备份恢复、安全管理、集群管理服务和工具。 +3. SQL interface,负责接收客户端发送的各种 SQL 语句,比如 DML、DDL 和存储过程等。 +4. Parser 解析器会对 SQL 语句进行语法解析生成解析树。 +5. Optimizer 查询优化器会根据解析树生成执行计划,并选择合适的索引,然后按照执行计划执行 SQL 语言并与各个存储引擎交互。 +6. Caches缓存包括各个存储引擎的缓存部分,比如:InnoDB存储的BufferPool,Caches中也会缓存一些权限,也包括一些 Session 级别的缓存。 + +**存储引擎层** + +存储引擎包括MyISAM、InnoDB,以及支持归档的Archive和内存的Memory等。MySQL是插件式的存储引擎,只要正确定义与MySQLServer交互的接口,任何引擎都可以访问MySQL。 + +**物理存储层** + +存储引擎底部是物理存储层,是文件的物理存储层,包括二进制日志、数据文件、错误日志、慢查询日志、全日志、redo/undo 日志等。 + + + +下面是一条SQL SELECT语句的执行过程: + + + +## 存储引擎 + +存储引擎是 MySQL 中具体与文件打交道的子系统,它是根据 MySQL AB 公司提供的文件访问层的抽象接口,定制的一种文件访问机制,这种机制就叫作存储引擎。 + +InnoDB 存储引擎的具体架构如下图所示。上半部分是实例层(计算层),位于内存中,下半部分是物理层,位于文件系统中。 + + + + + +### 实例层 + +实例层分为线程和内存。 + +InnoDB 重要的线程有 Master Thread,Master Thread 是 InnoDB 的主线程,负责调度其他各线程。 + +* MasterThread的优先级最高,其内部包含几个循环:主循环(loop)、后台循环(backgroundloop)、刷新循环(flushloop)、暂停循环(suspendloop)。Master Thread 会根据其内部运行的相关状态在各循环间进行切换。 + + 大部分操作在主循环(loop)中完成,其包含 1s 和 10s 两种操作。 + +* buf_dump_thread 负责将 buffer pool 中的内容 dump 到物理文件中,以便再次启动 MySQL 时,可以快速加热数据。 + +* page_cleaner_thread负责将bufferpool中的脏页刷新到磁盘,在5.6版本之前没有这个线程,刷新操作都是由主线程完成的,所以在刷新脏页时会非常影响MySQL的处理能力,在5.7 版本之后可以通过参数设置开启多个 page_cleaner_thread。 + +* purge_thread 负责将不再使用的 Undo 日志进行回收。 + +* read_thread 处理用户的读请求,并负责将数据页从磁盘上读取出来,可以通过参数设置线程数量。 + +* write_thread 负责将数据页从缓冲区写入磁盘,也可以通过参数设置线程数量,page_cleaner 线程发起刷脏页操作后 write_thread 就开始工作了。 + +* redo_log_thread 负责把日志缓冲中的内容刷新到 Redo log 文件中。 + +* insert_buffer_thread 负责把 Insert Buffer 中的内容刷新到磁盘。 + +实例层的内存部分主要包含InnoDBBufferPool,这里包含InnoDB最重要的缓存内容。数据和索引页、undo页、insertbuffer页、自适应Hash索引页、数据字典页和锁信息等。additionalmemorypool后续已不再使用。Redobuffer里存储数据修改所产生的Redolog。doublewritebuffer是 double write 所需的 buffer,主要解决由于宕机引起的物理写入操作中断,数据页不完整的问题。 + +### 物理层 + +物理层在逻辑上分为系统表空间、用户表空间和 Redo日志。 + +系统表空间里有 ibdata 文件和一些 Undo,ibdata 文件里有 insert buffer 段、double write段、回滚段、索引段、数据字典段和 Undo 信息段。 + +用户表空间是指以 .ibd 为后缀的文件,文件中包含 insert buffer 的 bitmap 页、叶子页(这里存储真正的用户数据)、非叶子页。 + +Redo日志中包括多个Redo文件,这些文件循环使用,当达到一定存储阈值时会触发checkpoint刷脏页操作,同时也会在MySQL实例异常宕机后重启,InnoDB表数据自动还原恢复过程中使用。 + +### 内存和物理结构 + +内存和物理结构,如下图所示。 + + + +**BufferPool** + +用户读取或者写入的最新数据都存储在BufferPool中,如果BufferPool中没有找到则会读取物理文件进行查找,之后存储到BufferPool中并返回给MySQLServe。Buffer Pool 采用LRU 机制。 + +BufferPool决定了一个SQL执行的速度快慢,如果查询结果页都在内存中则返回结果速度很快,否则会产生物理读(磁盘读),返回结果时间变长。但我们又不能将所有数据页都存储到BufferPool中。在单机单实例情况下,我们可以配置BufferPool为物理内存的60%~80%,剩余内存用于session产生的sort和join等,以及运维管理使用。如果是单机多实例,所有实例的bufferpool总量也不要超过物理内存的80%。开始时我们可以根据经验设置一个BufferPool的经验值,比如16GB,之后业务在MySQL运行一段时间后可以根据show global status like'%buffer_pool_wait%' 的值来看是否需要调整 Buffer Pool 的大小。 + +**Redolog** + +Redolog是一个循环复用的文件集,**负责记录InnoDB中所有对BufferPool的物理修改日志**,当Redolog文件空间中,检查点位置的LSN和最新写入的LSN差值(checkpoint_age)达到Redolog文件总空间的75%后,InnoDB会进行异步刷新操作,直到降至75%以下,并释放Redolog的空间;当checkpoint_age达到文件总量大小的 90% 后,会触发同步刷新,此时 InnoDB 处于挂起状态无法操作。 + +补充: + +* 日志序号 (LSN:Log sequence number) 标识特定日志文件记录在日志文件中的位置。 +* checkpoint_age:检查点,将缓冲池中的脏页刷回到磁盘。当缓冲池不够用时,根据LRU算法溢出的页,若此页为脏页,那么需要强制执行Checkpoint,将脏页也就是页的新版本刷回磁盘。 + +每个页有LSN,重做日志中也有LSN,Checkpoint也有LSN。可以通过命令SHOW ENGINE INNODB STATUS来观察 。 + +这样我们就看到**Redolog的大小直接影响了数据库的处理能力**,如果设置太小会导致强行checkpoint操作频繁刷新脏页,那我们就需要将Redolog设置的大一些,5.6版本之前Redo log 设置的大一些,5.6 版本之前 Redo log 总大小不能超过 3.8GB,5.7 版本之后放开了这个限制。 + +事务提交时 log buffer 会刷新到 Redo log 文件中,具体刷新机制由参数控制。 + +### Myisam和InnoDB的区别 + +* **是否支持行级锁** : MyISAM 只有表级锁,而InnoDB 支持行级锁和表级锁,默认为行级锁,适合高并发操作。 +* **是否支持外键**: MyISAM不支持,而InnoDB支持 +* **是否支持事务**:MyISAM不支持,而InnoDB支持 +* **缓存**:MyISAM只缓存索引,InnoDB缓存索引和真实数据,所以对内存要求高 +* **崩溃恢复**:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 +* InnoDB 支持 MVCC,MyISAM 不支持; + + +InnoDB 表最大还可以支持 64TB,支持聚簇索引、支持压缩数据存储,支持数据加密,支持查询/索引/数据高速缓存,支持自适应hash索引、空间索引,支持热备份和恢复等 + +## InnoDB 核心要点 + + + + + + +ARIES 三原则 + +WriteAheadLogging(WAL)。 + +* 先写日志后写磁盘,日志成功写入后事务就不会丢失,后续由checkpoint机制来保证磁盘物理文件与Redo日志达到一致性; +* 利用Redo 记录变更后的数据,即 Redo 记录事务数据变更后的值; +* 利用 Undo 记录变更前的数据,即 Undo 记录事务数据变更前的值,用于回滚和其他事务多版本读。 + +show engine innodb status\G 的结果里面有详细的 InnoDB 运行态信息,分段记录的,包括内存、线程、信号、锁、事务。 + +# 深入理解事务与锁机制 + +## 事务及其特性 + +一个逻辑工作单元要成为事务,在关系型数据库管理系统中,必须满足 4 个特性,即所谓的 ACID:原子性、一致性、隔离性和持久性。 + +Atomicity(原子性):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行 + +Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。 + +Isolation(隔离性):同一时间,只允许一个事务操作同一数据,不同的事务之间彼此没有任何干扰。 + +Durability(持久性):事务处理结束后,对数据的修改是永久的。 + +### 一致性 + +一致性其实包括两部分内容,分别是约束一致性和数据一致性。 + +* 约束一致性:数据库创建表结构时所制定的外键,唯一索引等约束。 +* 数据一致性:是一个综合性的规定,或者说是一个把握全局的规定。因为它是由原子性、持久性、隔离性共同保证的结果,而不是单单依赖于某一种技术。 + +### 原子性 + +原子性就是前面提到的两个“要么”,即要么改了,要么没改。也就是说用户感受不到一个正在改的状态。MySQL 是通过 WAL(Write Ahead Log)技术来实现这种效果的。 + +举例来讲,如果事务提交了,那改了的数据就生效了,如果此时BufferPool的脏页没有刷盘,如何来保证改了的数据生效呢?就需要使用Redo日志恢复出来的数据就需要使用Redo日志恢复出来的数据。而如果事务没有提交,且BufferPool的脏页被刷盘了,那这个本不应该存在的数据如何消失呢?就需要通过 Undo 来实现了,Undo 又是通过 Redo 来保证的,所以最终原子性的保证还是靠 Redo 的 WAL 机制实现的。 + +### 持久性 + +所谓持久性,就是指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,接下来的操作或故障不应该对其有任何影响。持久性是如何保证的呢?一旦事务提交,通过原子性,即便是遇到宕机,也可以从逻辑上将数据找回来后再次写入物理存储空间,这样就从逻辑和物理两个方面保证了数据不会丢失,即保证了数据不会丢失,即保证了数据库的持久性。 + +### 隔离性 + +所谓隔离性,指的是一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他的并发事务是隔离的。锁和多版本控制就符合隔离性。 + +## 并发事务控制 + +### 单版本控制-锁 + +锁用独占的方式来保证在只有一个版本的情况下事务之间相互隔离,所以锁可以理解为单版本控制。在MySQL事务中,锁的实现与隔离级别有关系,在RR(RepeatableRead)隔离级别下,MySQL为了解决幻读的问题,以牺牲并行度为代价,通过Gap锁来防止数据的写入,而这种锁,因为其并行度不够,冲突很多,经常会引起死锁。现在流行的Row模式可以避免很多冲突甚至死锁问题,所以推荐默认使用 Row + RC(Read Committed)模式的隔离级别,可以很大程度上提高数据库的读写并行度。 + +补充: 在row level模式下,bin-log中可以不记录执行的sql语句的上下文相关的信息,仅仅只需要记录那一条被修改。 + +### 多版本控制-MVCC + + MVCC,是指在数据库中,为了实现高并发的数据访问,对数据进行多版本处理,并通过事务的可见性来保证事务能看到自己应该看到的数据版本。 + +那个多版本是如何生成的呢?每一次对数据库的修改,都会在Undo日志中记录当前修改记录的事务号及修改前数据状态的存储地址(即ROLL_PTR),以便在必要的时候可以回滚到老的数据版本。例如,一个读事务查询到当前记录,而最新的事务还未提交,根据原子性,读事务看不到最新数据,但可以去回滚段中找到老版本的数据,这样就生成了多个版本。多版本控制很巧妙地将稀缺资源的独占互斥转换为并发,大大提高了数据库的吞吐量及读写性能。 + +## 技术原理 + +### 原子性技术原理 + +每一个写事务,都会修改BufferPool,从而产生相应的Redo日志,这些日志信息会被记录到ib_logfiles文件中。因为Redo日志是遵循WriteAheadLog的方式写的,所以事务是顺序被记录的。在MySQL中,任何BufferPool中的页被刷到磁盘之前,都会先写入到日志文件中,这样做有两方面的保证。如果BufferPool中的这个页没有刷成功,此时数据库挂了,那在数据库再次启动之后,可以通过 Redo 日志将其恢复出来,以保证脏页写下去的数据不会丢失,所以必须要保证 Redo 先写。 + +为 Buffer Pool 的空间是有限的,要载入新页时,需要从 LRU 链表中淘汰一些页,而这些页必须要刷盘之后,才可以重新使用,那这时的刷盘,就需要保证对应的 LSN 的日志也要提前写到 ib_logfiles 中,如果没有写的话,恰巧这个事务又没有提交,数据库挂了,在数据库启动之后,这个事务就没法回滚了。所以如果不写日志的话,这些数据对应的回滚日志可能就不存在,导致未提交的事务回滚不了,从而不能保证原子性,所以原子性就是通过 WAL 来保证的。 + +### 持久性技术原理 + +通过原子性可以保证逻辑上的持久性,通过存储引擎的数据刷盘可以保证物理上的持久性。这个过程与前面提到的Redo日志、事务状态、数据库恢复、参数innodb_flush_log_at_trx_commit 有关,还与 binlog 有关。 + +### 隔离性技术原理 + +InnoDB 支持的隔离性有 4 种,隔离性从低到高分别为:读未提交、读提交、可重复读、可串行化。 + +具体说到隔离性的实现方式,我们通常用ReadView表示一个事务的可见性。RC级别的事务可见性比较高,它可以看到已提交的事务的所有修改。而RR级别的事务,则没有这个功能,一个读事务中,不管其他事务对这些数据做了什么修改,以及是否提交,只要自己不提交,查询的数据结果就不会变。 + +随着时间的推移,读提交每一条读操作语句都会获取一次 Read View,每次更新之后,都会获取数据库中最新的事务提交状态,也就可以看到最新提交的事务了,即每条语句执行都会更新其可见性视图。 + +而反观可重复读,这个可见性视图,只有在自己当前事务提交之后,才去更新,所以与其他事务是没有关系的。 + +**在 RR 级别下,长时间未提交的事务会影响数据库的 PURGE 操作,从而影响数据库的性能,所以可以对这样的事务添加一个监控**。 + +### 一致性技术原理 + +数据的完整性是通过其他三个特性来保证的,为了保证数据的完整性,提出来三个特性,这三个特性又是由同一个技术来实现的,所以理解 Redo/Undo 才能理解数据库的本质。 + +### MVCC 实现原理 + +MVCC最大的好处是读不加锁,读写不冲突。在读多写少的OLTP(On-LineTransactionProcessing)应用中,读写不冲突是非常重要的,极大的提高了系统的并发性能。 + +在 MVCC 并发控制中,读操作可以分为两类: 快照读(Snapshot Read)与当前读 (Current Read)。 + +* 快照读:读取的是记录的可见版本(有可能是历史版本),不用加锁。 +* 当前读:读取的是记录的最新版本,并且当前读返回的记录,都会加锁,保证其他事务不会再并发修改这条记录。 + +如何区分快照读和当前读呢? 可以简单的理解为: + +* 快照读:简单的 select 操作,属于快照读,不需要加锁。 +* 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。 + +下面用一个事务对某行记录更新的过程来说明MVCC中多版本的实现。 + +假设 F1~F6 是表中字段的名字,1~6 是其对应的数据。后面三个隐含字段分别对应该行的隐含ID、事务号和回滚指针,如下图所示。 + +image-20200412113401177 + +* 隐含ID(DB_ROW_ID),6个字节,当由InnoDB自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值。 +* 事务号(DB_TRX_ID),6个字节,标记了最新更新这条行记录的TransactionID,每处理一个事务,其值自动+1。 +* 回滚指针(DB_ROLL_PT),7个字节,指向当前记录项的RollbackSegment的Undolog记录,通过这个指针才能查找之前版本的数据。 + +首先,假如这条数据是刚 INSERT 的,可以认为 ID 为 1,其他两个字段为空。 + +然后,当事务 1 更改该行的数据值时,会进行如下操作,如下图所示。 + + + +* 用排他锁锁定该行;记录 Redo log +* 把该行修改前的值复制到 Undo log,即图中下面的行 +* 修改当前行的值,填写事务编号,使回滚指针指向 Undo log 中修改前的行 + +如果数据继续执行,此时Undolog中有两行记录,并且通过回滚指针连在一起。因此,如果Undolog一直不删除,则会通过当前记录的回滚指针回溯到该行创建时的初始内容,所幸的是在InnoDB中存在purge 线程,它会查询那些比现在最老的活动事务还早的 Undo log,并删除它们,从而保证 Undo log 文件不会无限增长,如下图所示。 + + + +## 并发事务问题及解决方案 + +脏读 :表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录 A,此时该事务还未提交,然后另一个事务尝试读取到了记录 A。 + +不可重复读 :是指在一个事务内,多次读同一数据数据发生了变化。 + +幻读 :指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。 + +产生的这些问题,MySQL 数据库是通过事务隔离级别来解决的。值得一提的是,InnoDB通过Gap锁解决了幻读的问题。 + +不可重复读重点在于 UPDATA 和 DELETE,而幻读的重点在于 INSERT。它们之间最大的区别是如何通过锁机制来解决它们产生的问题。 + + + +## InnoDB 的锁 + +InnoDB 的锁分为行锁和表锁。 + +其中行锁包括两种: + +* 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。 +* 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。 + +为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。表锁又分为三种。 + +* 意向共享锁(IS):事务计划给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁 +* 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的的 IX 锁。 +* 自增锁(AUTO-INC Locks):特殊表锁,自增长计数器通过该“锁”来获得子增长计数器最大的计数值。 + +在加行锁之前必须先获得表级意向锁,否则等待 innodb_lock_wait_timeout 超时后根据innodb_rollback_on_timeout 决定是否回滚事务。 + +### InnoDB自增锁 + +在MySQLInnoDB存储引擎中,我们在设计表结构的时候,通常会建议添加一列作为自增主键。这里就会涉及一个特殊的锁:自增锁(即:AUTO-INCLocks),它属于表锁的一种,在 INSERT 结束后立即释放。我们可以执行 **show engine innodb status\G** 来查看自增锁的状态信息。 + +InnoDB 锁关系矩阵如下图,其中:+ 表示兼容,- 表示不兼容。 + + + + + +### InnoDB 行锁 + +InnoDB 行锁是通过对索引数据页上的记录(record)加锁实现的。主要实现算法有 3 种:Record Lock、Gap Lock 和 Next-key Lock。 + +* RecordLock锁:单个行记录的锁(锁数据,不锁Gap)。 +* GapLock锁:间隙锁,锁定一个范围,不包括记录本身(不锁数据,仅仅锁数据前面的Gap)。 +* Next-keyLock 锁:同时锁住数据,并且锁住数据前面的 Gap。 + +**排查 InnoDB 锁问题** + +排查InnoDB锁问题通常有2种方法。打开innodb_lock_monitor表,注意使用后记得关闭,否则会影响性能。在MySQL5.5版本之后,可以通过查看information_schema 库下面的 innodb_locks、innodb_lock_waits、innodb_trx 三个视图排查 InnoDB 的锁问题。 + +### InnoDB死锁 + +在MySQL中死锁不会发生在MyISAM存储引擎中,但会发生在InnoDB存储引擎中,因为InnoDB是逐行加锁的,极容易产生死锁。那么死锁产生的四个条件是什么呢? + +* 互斥条件:一个资源每次只能被一个进程使用; +* 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放; +* 不剥夺条件:进程已获得的资源,在没使用完之前,不能强行剥夺; +* 循环等待条件:多个进程之间形成的一种互相循环等待资源的关系。 + +在发生死锁时,InnoDB存储引擎会自动检测,并且会自动回滚代价较小的事务来解决死锁问题。但很多时候一旦发生死锁,InnoDB存储引擎的处理的效率是很低下的或者有时候根本解决不了问题,需要人为手动去解决。 + +既然死锁问题会导致严重的后果,那么在开发或者使用数据库的过程中,如何避免死锁的产生呢?这里给出一些建议: + +* 加锁顺序一致; +* 尽量基于primary或uniquekey更新数据。 +* 单次操作数据量不宜过多,涉及表尽量少。 +* 减少表上索引,减少锁定资源。 +* 相关工具:pt-deadlock-logger + +查看MySQL数据库中死锁的相关信息,可以执行showengineinnodbstatus\G来进行查看,重点关注“LATESTDETECTEDDEADLOCK”部分。 + +一些开发建议来避免线上业务因死锁造成的不必要的影响。 + +* 更新SQL的where条件时尽量用索引; +* 加锁索引准确,缩小锁定范围; +* 减少范围更新,尤其非主键/非唯一索引上的范围更新。 +* 控制事务大小,减少锁定数据量和锁定时间长度 (innodb_row_lock_time_avg)。 +* 加锁顺序一致,尽可能一次性锁定所有所需的数据行。 + diff --git "a/docs/\351\241\271\347\233\256\345\270\270\347\224\250\345\267\245\345\205\267.md" "b/docs/\351\241\271\347\233\256\345\270\270\347\224\250\345\267\245\345\205\267.md" new file mode 100644 index 0000000..e65cfa0 --- /dev/null +++ "b/docs/\351\241\271\347\233\256\345\270\270\347\224\250\345\267\245\345\205\267.md" @@ -0,0 +1,113 @@ +## 整体介绍 + +**团队协作** + +- Ant:较少使用 +- Maven +- Gradle +- Git +- SVN:较少使用 + +**质量保证** + +- Checkstyle +- FindBugs:代码检测工具 +- SonarQube:平台,集成了上面两种工具 + +**压测** + +- JMeter +- JMH +- AB +- LoadRunner + +**容器与代理(随着微服务的盛行,Envoy、OpenResty、Kong等API网关的使用也越来越普遍)** + +- Tomcat +- Jetty +- Nginx +- Envoy +- OpenResty +- Kong + +**CI/CD** + +- Gitlab-CI +- Jenkins +- Travis + +**JVM相关** + +- JMC(JFR) +- jstack、jmap、jstat + +**系统分析** + +- vmstat +- iostat & iotop +- ifstat & iftop +- netstat +- dstat +- strace +- GDB +- lsof +- tcpdump +- traceroute + +**文档管理** + +- JavaDoc +- Swagger + +**网络工具** + +- PostMan +- WireShark(网络包分析工具) +- Fiddler(只针对HTTP进行抓捕) +- Charies + +## JVM相关工具 + +### JMC(Java Mission Control) + +1.7之后提供的 JVM 图形化监控工具,包括 JVM 浏览器,飞行控制器 JFR(Java Filght Recorder),JMX(Java Management Extensions)控制台。 + +jps:查看java进程信息 +jmap:查看JVM中对象的统计信息,**可以在内存溢出时查看堆中最大的对象类型** +jstat:对JVM的资源和性能进行实时监控 +jstack:查看JVM线程栈信息,**可以检查线程死锁问题** +jinfo:动态查看、调整jvm参数 +jcmd:1.7提供的综合工具,使用飞行计数器分析性能。 + + + +## Git + +## ![git命令](C:\Users\吕明辉\Desktop\github笔记\git命令.png) + +分布式管理,有四个保存数据区域,如图中的工作区,暂存区,本地仓库,远程仓库。 + +开发时线程远程仓库拉取代码到工作区,可以使用clone,pull,fetch,checkout。pull=fetch+merge + +提交代码时先使用add,再commit,再push。 + +### Git 工作流 + +![git工作流](C:\Users\吕明辉\Desktop\github笔记\git工作流.png) + +github 工作流:master 分支的代码一直是可发布状态,需要新开发就直接开一个feature分支,完成后pullrequest(PR),当Review通过后,合并到mater分支。 + + + +## Linux 分析工具 + +* **vmstat**:进程、虚拟内存、页面交换、IO读写、CPU活动等 +* iostat & iotop:系统IO状态信息 +* ifstat & iftop:实时网络流量监控 +* **netstat**:查看网络相关信息,各种网络协议套接字状态 +* dstat:全能型实时系统信息统计 +* strace:诊断、调试程序的系统调用 +* GDB:程序调试、coredump分析 +* Isof:查看系统当前打开的文件信息 +* tcpdump:网络抓包工具 +* traceroute:网络路由分析工具 diff --git "a/docs/\351\253\230\346\200\247\350\203\275\345\256\236\350\267\265\347\257\207.md" "b/docs/\351\253\230\346\200\247\350\203\275\345\256\236\350\267\265\347\257\207.md" new file mode 100644 index 0000000..7909d36 --- /dev/null +++ "b/docs/\351\253\230\346\200\247\350\203\275\345\256\236\350\267\265\347\257\207.md" @@ -0,0 +1,470 @@ +# 高性能库表设计 + +### 范式和反范式 + +#### 范式 + +范式是关系数据库理论的基础,也是我们在设计数据库结构过程中所要遵循的规则和指导方法。数据库的设计范式是数据库设计所需要满足的规范。只有理解数据库的设计范式,才能设计出高效率、优雅的数据库,否则可能会设计出低效的库表结构。目前关系数据库有六种范式:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)、第四范式(4NF)和第五范式(5NF,还又称完美范式。 + +满足最低要求的叫第一范式,简称1NF。在第一范式基础上进一步满足一些要求的为第二范式,简称2NF。其余依此类推。各种范式呈递次规范,越高的范式数据库冗余越小。通常所用到的只是前三个范式,即:第一范式(1NF),第二范式(2NF),第三范式(3NF)。 + +**第一范式** + +第一范式无重复的列,表中的每一列都是拆分的基本数据项,即列不能够再拆分成其他几列,强调的是列的原子性。 + +如果在实际场景中,一个联系人有家庭电话和公司电话,那么以“姓名、性别、电话”为表头的表结构就没有达到1NF。要符合1NF我们只需把电话列拆分,让表头变为姓名、性别、家庭电话、公司电话即可。 + +**第二范式** + +属性完全依赖于主键,首先要满足它符合1NF,另外还需要包含两部分内容: + +* 表必须有一个主键; +* 没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分。即要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性。 + +**第三范式** + +第三范式属性不传递依赖于其他非主属性,首先需要满足 2NF,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况。 + +**第二范式和第三范式的区别第二范式:** + +* 非主键列是否依赖主键(包括一列通过某一列间接依赖主键),要是有依赖关系就是第二范式; +* 第三范式:非主键列是否直接依赖主键,不能是那种通过传递关系的依赖。要是符合这种依赖关系就是第三范式。通过对前三个范式的了解,我们知道 3NF 是 2NF 的子集,2NF 是 1NF 的子集。 + +**范式优缺点** + +优点: + +* 避免数据冗余,减少维护数据完整性的麻烦; +* 减少数据库的空间; +* 数据变更速度快。 + +缺点: + +* 按照范式的规范设计的表,等级越高的范式设计出来的表数量越多。 +* 获取数据时,表关联过多,性能较差。 + +表的数量越多,查询所需要的时间越多。也就是说所用的范式越高,对数据操作的性能越低。 + +#### 反范式 + +范式是普适的规则,满足大多数的业务场景的需求。对于一些特殊的业务场景,范式设计的表,无法满足性能的需求。此时,就需要根据业务场景,在范式的基础之上进行灵活设计,也就是反范式设计。 + +反范式设计主要从三方面考虑: + +**业务场景,相应时间,字段冗余。** + +反范式设计就是用空间来换取时间,提高业务场景的响应时间,减少多表关联。主要的优点如下: + +允许适当的数据冗余,业务场景中需要的数据几乎都可以在一张表上显示,避免关联; + +可以设计有效的索引。 + +#### 范式与反范式异同 + +**范式化模型**:数据没有冗余,更新容易;当表的数量比较多,查询数据需要多表关联时,会导致查询性能低下。 + +**反范式化模型**:冗余将带来很好的读取性能,因为不需要join很多很多表;虽然需要维护冗余数据,但是对磁盘空间的消耗是可以接受的。 + +## MySQL 使用原则和设计规范 + +### 概述 + +MySQL虽然具有很多特性并提供了很多功能,但是有些特性会严重影响它的性能,比如,在数据库里进行计算,写大事务、大SQL、存储大字段等。想要发挥MySQL的最佳性能,需要遵循3个基本使用原则。 + +* 首先是需要让MySQL回归存储的基本职能:MySQL数据库只用于数据的存储,不进行数据的复杂计算,不承载业务逻辑,确保存储和计算分离; +* 其次是查询数据时,尽量单表查询,减少跨库查询和多表关联; +* 还有就是要杜绝大事务、大 SQL、大批量、大字段等一系列性能杀手。 + +补充: + +大事务,运行步骤较多,涉及的表和字段较多,容易造成资源的争抢,甚至形成死锁。一旦事务回滚,会导致资源占用时间过长。 + +大SQL,复杂的SQL意味着过多的表的关联,MySQL数据库处理关联超过3张表以上的SQL时,占用资源多,性能低下。 + +大批量,意味着多条SQL一次性执行完成,必须确保进行充分的测试,并且在业务低峰时段或者非业务时段执行。大字段,blob、text等大字段,尽量少用。必须要用时,尽量与主业务表分离,减少对这类字段的检索和更新。 + +下面具体讲解数据库的基本设置规则: + +1. 必须指定默认存储引擎为InnoDB,并且禁用MyISAM存储引擎,随着MySQL8.0版本的发布,所有的数据字典表都已经转换成了InnoDB,MyISAM存储引擎已成为了历史。 +2. 默认字符集UTF8mb4,以前版本的UTF8是UTF8mb3,未包含个别特殊字符,新版本的UTF8mb4包含所有字符,官方强烈建议使用此字符集。 +3. 关闭区分大小写功能。设置lower_case_tables_name=1,即可关闭区分大小写功能,即大写字母 T 和小写字母 t 一样。 + +MySQL数据库提供的功能很全面,但并不是所有的功能性能都高效。 + +1. 存储过程、触发器、视图、event。为了存储计算分离,这类功能尽量在程序中实现。这些功能非常不完整,调试、排错、监控都非常困难,相关数据字典也不完善,存在潜在的风险。一般在生产数据库中,禁止使用。 +2. lob、text、enum、set。这些字段类型,在MySQL数据库的检索性能不高,很难使用索引进行优化。如果必须使用这些功能,一般采取特殊的结构设计,或者与程序结合使用其他的字段类型替代。比如:set 可以使用整型(0,1,2,3)、注释功能和程序的检查功能集合替代。 + +### 规范命名 + +统一的规范命名,可以增加可读性,减少隐式转换。 + +命名时的字符取值范围为:a~z,0~9 和 _(下画线)。 + +1. 所有表名小写,不允许驼峰式命名; + +2. 允许使用 -(横线)和 (空格);如下图所示,当使用 -(横线),后台默认会转化成 @002d; + +3. 不允许使用其他特殊字符作为名称,减少潜在风险。 + +数据库库名的命名规则必须遵循“见名知意”的原则,即库名规则为“数据库类型代码 + 项目简称 + 识别代码 + 序号”。 + +**表名的命名规则分为:** + +单表仅使用 a~z、_; + +分表名称为“表名_编号”; + +业务表名代表用途、内容:子系统简称\_业务含义_后缀。 + +常见业务表类型有:临时表,tmp;备份表,bak;字典表,dic;日志表,log。 + +**字段名精确**,遵循“见名知意”的原则,格式:名称_后缀。 + +* 避免普遍简单、有歧义的名称。用户表中,用户名的字段为 UserName 比 Name 更好。 +* 布尔型的字段,以助动词(has/is)开头。用户是否有留言 hasmessage,用户是否通过检查 ischecked 等。 + +**索引命名格式**,主要为了区分哪些对象是索引: + +前缀\_表名(或缩写)_字段名(或缩写);主键必须使用前缀“pk\_”;UNIQUE约束必须使用前缀“uk\_”;普通索引必须使用前缀“idx\_”。 + +### 表创建的注意事项 + +1. 主键列,UNSIGNED整数,使用auto_increment;禁止手动更新auto_increment,可以删除。 +2. 必须添加comment注释。 +3. 必须显示指定的engine。 +4. 表必备三字段:id、xxx_create、xxx_modified。 + * id为主键,类型为unsignedbigint等数字类型; + * xxx_create、xxx_modified的类型均为 datetime 类型,分别记录该条数据的创建时间、修改时间。 + +#### 备份表/临时表等常见表的设计规范 + +1. 备份表,表名必须添加bak和日期,主要用于系统版本上线时,存储原始数据,上线完成后,必须及时删除。 +2. 临时表,用于存储中间业务数据,定期优化,及时降低表碎片。 +3. 日志类表,首先考虑不入库,保存成文件,其次如果入库,明确其生命周期,保留业务需求的数据,定期清理。 +4. 大字段表,把主键字段和大字段,单独拆分成表,并且保持与主表主键同步,尽量减少大字段的检索和更新。 +5. 大表,根据业务需求,从垂直和水平两个维度进行拆分。 + +### 字段设计要求 + +1. 根据业务场景需求,选择合适的类型,最短的长度;确保字段的宽度足够用,但也不要过宽。所有字段必须为NOTNULL,空值则指定default值,空值难以优化,查询效率低。比如:人的年龄用unsignedtinyint(范围0~255,人的寿命不会超过255岁);海龟就必须是smallint,但如果是太阳的年龄,就必须是int;如果是所有恒星的年龄都加起来,那么就必须使用bigint。 +2. 表字段数少而精,尽量不加冗余列。 +3. 单实例表个数必须控制在 2000 个以内。 +4. 单表分表个数必须控制在 1024 个以内。 +5. 单表字段数上限控制在 20~50 个。 + +**禁用ENUM、SET类型。**兼容性不好,性能差。 + +解决方案:使用TINYINT,在COMMENT信息中标明被枚举的含义。\`is_disable` TINYINT UNSIGNED DEFAULT'0'COMMENT '0:启用1:禁用2:异常’。 + +**禁用列为NULL。**MySQL难以优化NULL列;NULL列加索引,需要额外空间;含NULL复合索引无效。 + +解决方案:在列上添加 NOT NULL DEFAULT 缺省值。 + +**禁止 VARBINARY、BLOB 存储图片、文件等。**禁止在数据库中存储大文件,例如照片,可以将大文件存储在对象存储系统中,数据库中存储路径。 + +不建议使用TEXT/BLOB:处理性能差;行长度变长;全表扫描代价大。 + +解决方案:拆分成单独的表。 + +存储字节越小,占用空间越小。尽量选择合适的整型,如下图所示。 + + +建议: + +* 主键列,无负数,建议使用INTUNSIGNED或者BIGINTUNSIGNED;预估字段数字取值会超过42亿,使用BIGINT类型。 +* 短数据使用TINYINT或SMALLINT,比如:人类年龄,城市代码。 +* 使用 UNSIGNED 存储非负数值,扩大正数的范围。 + +推荐字符集都为 UTF8mb4,中文存储占三个字节,而数据或字母,则只占一个字节。 + +CHAR(N) 和 VARCHAR(N) 的长度 N,不是字节数,是**字符数**。 + +例:username VARCHAR(40):username 最多能存储 40 个字符,占用 120 个字节。 + +**Char 与 Varchar 类型** + +存储字符串长度相同的全部使用 Char 类型;字符长度不相同的使用 Varchar 类型,不预先分配存储空间,长度不要超过 255。 + +## 案例处理 + +### IP处理 + +一般使用Char(15)进行存储,但是当进行查找和统计时,字符类型不是很高效。MySQL数据库内置了两个IP相关的函数INET_ATON()、INET_NTOA(),可以以实现 IP 地址和整数的项目转换。 + +因此,我们使用 INT UNSIGNED(占用 4 个字节)存储 IP。 + +将 IP 的存储从字符型转换成整形,转化后数字是连续的,提高了查询性能,使查询更快,占用空间更小。 + +### TIMESTAMP处理 + +同样的方法,我们使用MySQL内置的函数(FROM_UNIXTIME(),UNIX_TIMESTAMP()),可以将日期转化为数字,用INTUNSIGNED存储日期和时间。 + +# 高性能索引设计 + +## 索引概述 + +数据库索引是一种数据结构,它以额外的写入和存储空间为代价来提高数据库表上数据检索操作的速度。通俗来说,索引类似于书的目录,根据其中记录的页码可以快速找到所需的内容。 + +MySQL官方对索引(Index)的定义是存储引擎用于快速查找记录的一种数据结构。 + +* 索引是物理数据页,数据库页大小(PageSize)决定了一个页可以存储多少个索引行,以及需要多少页来存储指定大小的索引。 +* 索引可以加快检索速度,但同时也降低索引列插入、删除、更新的速度,索引维护需要代价。 + +## 索引原理 + +二分查找是索引实现的理论基础。在数据库中大部分索引都是通过 B+Tree 来实现的。当然也涉及其他数据结构,在 MySQL 中除了 B+Tree 索引外我们还需要关注下 Hash 索引。 + +### Hash 索引 + +哈希表是数据库中哈希索引的基础,是根据键值存储数据的结构。简单说,哈希表是使用哈希函数将索引列计算到桶或槽的数组,实际存储是根据哈希函数将key换算成确定的存储位置,并将 value 存放到该数组位置上。访问时,只需要输入待查找的 key,即可通过哈希函数计算得出确定的存储位置并读取数据。 + +#### Hash 索引的实现 + +数据库中哈希索引是基于哈希表实现的,对于哈希索引列的数据通过Hash算法计算,得到对应索引列的哈希码形成哈希表,由哈希码及哈希码指向的真实数据行的指针组成了哈希索引。哈希索引的应用场景是只在对哈希索引列的等值查询才有效。 + +因为哈希索引只存储哈希值和行指针,不存储实际字段值,所以其结构紧凑,查询速度也非常快,在无哈希冲突的场景下访问哈希索引一次即可命中。但是哈希索引只适用于等值查询,包括=、IN()、<=>(安全等于,selectnull<=>null和selectnull=null是不一样的结果),不支持范围查询。 + +#### Hash碰撞的处理 + +Hash碰撞是指不同索引列值计算出相同的哈希码,表中字段为A和B两个不同值根据Hash算法计算出来的哈希码都一样就是出现了哈希碰撞。对于 Hash 碰撞通用的处理方法是使用链表,将 Hash 冲突碰撞的元素形成一个链表,发生冲突时在链表上进行二次遍历找到数据。 + +#### MySQL 中如何使用 Hash 索引? + +在 MySQL 中主要是分为 Memory 存储引擎原生支持的 Hash 索引 、InnoDB 自适应哈希索引及 NDB 集群的哈希索引3类。 + +InnoDB自适应哈希索引是为了提升查询效率,InnoDB存储引擎会监控表上各个索引页的查询,当InnoDB注意到某些索引值访问非常频繁时,会在内存中基于B+Tree索引再创建一个哈希索引,使得内存中的 B+Tree 索引具备哈希索引的功能,即能够快速定值访问频繁访问的索引页。 + +为什么要为B+Tree索引页二次创建自适应哈希索引呢?这是因为B+Tree索引的查询效率取决于B+Tree的高度,在数据库系统中通常B+Tree的高度为3~4层,所以访问数据需要做 3~4 次的查询。而 Hash 索引访问通常一次查找就能定位数据(无 Hash 碰撞的情况),其等值查询场景 Hash 索引的查询效率要优于 B+Tree。 + +自适应哈希索引的建立使得InnoDB存储引擎能自动根据索引页访问的频率和模式自动地为某些热点页建立哈希索引来加速访问。另外InnoDB自适应哈希索引的功能,用户只能选择开启或关闭功能,无法进行人工干涉。功能开启后可以通过 Show Engine Innodb Status 看到当前自适应哈希索引的使用情况。 + +### B+Tree 索引 + +如下图所示为一个简单的、标准的 B+tree,每个节点有 K 个键值和 K+1 个指针。 + + +B+Tree索引能够快速访问数据,就是因为存储引擎可以不再需要通过全表扫描来获取数据,而是从索引的根结点(通常在内存中)开始进行二分查找,根节点的槽中都存放了指向子节点的指针,存储引擎根据这些指针能快速遍历数据。 + +叶子节点存放的 ,对于真正要存放哪些数据还得取决于该 B+Tree 是聚簇索引(Clustered Index)还是辅助索引(Secondary Index)。 + +#### 聚簇索引和辅助索引 + +聚簇索引是一种数据存储方式,它表示表中的数据按照主键顺序存储,是索引组织表。InnoDB的聚簇索引就是按照主键顺序构建B+Tree,B+Tree的叶子节点就是行记录,数据行和主键值紧凑地存储在一起。这也意味着InnoDB的主键索引就是数据表本身,它按主键顺序存放了整张表的数据。而InnoDB辅助索引(也叫作二级索引)只是根据索引列构建B+Tree,但在B+Tree的每一行都存了主键信息,加速回表操作。 + +聚簇索引占用的空间就是整个表数据量的大小,而二级索引会比聚簇索引小很多, 通常创建辅助索引就是为了提升查询效率。 + +在MySQLInnoDB中索引通常可以分为两大类:主键索引(即聚簇索引)和辅助索引(非聚簇索引)。对于没有指定主键的表,InnoDB会自己选择合适字段为主键,其选择顺序如下: + +* 显式主键; +* 第一个唯一索引(要求唯一索引所有列都非 NULL); +* 内置的 6 字节 ROWID。 + +建议使⽤ UNSIGNED 自增列显示创建主键。 + +#### 联合索引和覆盖索引 + +根据索引列个数和功能描述不同索引也可以分为:联合索引和覆盖索引。 + +* 联合索引是指在多个字段联合组建索引的。 +* 当通过索引即可查询到所有记录,不需要回表到聚簇索引时,这类索引也叫作覆盖索引。 +* 主键查询是天然的覆盖索引,联合索引可以是覆盖索引。 + +那么如何看SQL语句是否使用到覆盖索引了呢?通常在查看执行计划时,Extra列为Usingindex则表示优化器使用了覆盖索引。 + +我们通常建议优先考虑使用覆盖索引,这是因为如果SQL需要查询辅助索引中不包含的数据列时,就需要先通过辅助索引查找到主键值,然后再回表通过主键查询到其他数据列(即回表查询),需要查询两次。而覆盖索引能从索引中直接获取查询需要的所有数据,从⽽避免回表进行二次查找,节省IO,效率较⾼。 + +## 索引使用技巧 + +接下来聊一聊索引使用技巧的基础知识,这些知识可以帮助你建立高效索引,主要有谓词、过滤因子、基数(Cardinality)、选择率和回表。 + +#### 谓词和过滤因子 + +谓词本身就是条件表达式,通俗讲就是过滤字段。如下图中这句SQL语句,可以拆解为下面所示: + +`select * from city where city ='BeiJing' and last_updata = '2019-09-01'` + +简单谓词:city和last_updata。组合谓词:cityandlast_updata。 + +知道谓词后就可以计算谓词的过滤因子了,过滤因子直接描述了谓词的选择性,表示满足谓词条件的记录行数所占比例,过滤因子越小意味着能过滤越多数据,你需要在这类谓词字段上创建索引。 + +过滤因子的计算算法,就是满足谓词条件的记录行数除以表总行数。 + +* 简单谓词的过滤因子 = 谓词结果集的数量 / 表总行数 +* 组合谓词的过滤因子 = 谓词 1 的过滤因子 × 谓词 2 的过滤因子 + +下面用一个例子来看下,如何快速根据SQL语句计算谓词、过滤因子。 + +1. 根据SQL语句可以快速得到谓词信息:简单谓词 city 和 last_update,组合谓词 city and last_update。 +2. 计算每个谓词信息的过滤因子,过滤因子越小表示选择性越强,字段越适合创建索引。例如: + * city 的过滤因子=谓词 city 结果集的数量/表总行数=select count(\*) from city where city = ‘BeiJing’ / select countt(\*) from city = 20%; + * \*last_update 的过滤因子 = 谓词 last_update 结果集的数量 / 表总行数 = select count(\*) from city where last_update = ‘2019-08-01’ / select count(\*) from city = 10%; + * 组合谓词 = city 过滤因子 * last_update 过滤因子 = 20% × 10% = 2%,组合谓词的过滤因子为 2%,即只有表总行数的 2% 匹配过滤条件,可以考虑创建组合索引 (city,last_update)。 + +#### 基数和选择率 + +基数(Cardinality )是某个键值去重后的行数,索引列不重复记录数量的预估值,MySQL优化器会依赖于它。选择率是count (distinct city) / count(*),选择率越接近1则越适合创建索引,例如主键和唯一键的选择率都是 1。回表是指无法通过索引扫描访问所有数据,需要回到主表进行数据扫描并返回。 + +Cardinality 能快速告知字段的选择性,高选择性字段有利于创建索引。优化器在选择执行计划时会依赖该信息,通常这类信息也叫作统计信息,数据库中对于统计信息的采集是在存储引擎层进行的。 + +执行 show index from table_name会看到 Cardinality,同时也会触发 MySQL 数据库对 Cardinaltiy 值的统计。除此之外,还有三种更新策略。 + +* 触发统计:Cardinality 统计信息更新发生在 INSERT 和 UPDATE 时,InnoDB 存储引擎内部更新的 Cardinality 信息的策略为:1.表中超过1/16的数据发生变化;2.stat_modified_counter > 2000 000 000 (20亿)。 +* 采样统计(sample):为了减少统计信息更新造成的资源消耗,数据库对 Cardinality 通过采样来完成统计信息更新,每次随机获取 innodb_stats_persistent_sample_pages 页的数量进行 Cardinality 统计。 +* 手动统计:alter table table_name engine=innodb或 analyze table table_name,当发现优化器选择错误的执行计划或没有走理想的索引时,执行 SQL 语句来手动统计信息有时是一种有效的方法。 + +由于采样统计的信息是随机获取8个(8是由innodb_stats_transient_sample_pages参数指定)页面数进行分析,这就意味着下一次随机的8个页面可能是其他页面,其采集页面的 Carinality 也不同。因此当表数据无变化时也会出现 Cardinality 发生变化的情况。 + +## 索引使用细节 + +创建索引后如何确认SQL语句是否走索引了呢?创建索引后通过查看执行SQL语句的执行计划即可知道SQL语句是否走索引。执行计划重点关注跟索引相关的关键项,有type、possible_keys、key、key_len、ref、Extra 等。 + +其中,**possible_keys** 表示查询可能使用的索引,**key**表示真正实际使用的索引,**key_len** 表示使用索引字段的长度。另外执行计划中Extra选项也值得关注,例如**Extra**显示use index时就表示该索引是覆盖索引,通常性能排序的结果是use index>use where>use filsort + +当索引选择组合索引时,通过计算key_len来了解有效索引长度对索引优化也是非常重要的,接下来重点讲解key_len计算规则。key_len表示得到结果集所使用的选择索引的长度[字节数],不包括orderby,也就是说如果orderby也使用了索引则key_len不计算在内。key_len计算规则从两个方面考虑,一方面是索引字段的数据类型,另一方面是表、字段所使用的字符集。 + +索引字段的数据类型,根据索引字段的定义可以分为变长和定长两种数据类型: + +1. 索引字段为定长数据类型,比如char、int、datetime,需要有是否为空的标记,这个标记需要占用1个字节; +2. 对于变长数据类型,比如Varchar,除了是否为空的标记外,还需要有长度信息,需要占用两个字节。 + +表所使用的字符集,不同的字符集计算的 key_len 不一样,例如,GBK 编码的是一个占用 2 个字节大小的字符,UTF8 编码的是一个占用 3 个字节大小的字符。 + +通过key_len计算也帮助我们了解索引的**最左前缀匹配原则**。最左前缀匹配原则是指在使用B+Tree联合索引进行数据检索时,MySQL优化器会读取谓词(过滤条件)并按照联合索引字段创建顺序一直向右匹配直到遇到范围查询或非等值查询后停止匹配,此字段之后的索引列不会被使用,这时计算 key_len 可以分析出联合索引实际使用了哪些索引列。 + +## 设计高性能索引 + +1. **定位由于索引不合适或缺少索引而导致的慢查询。**通常在业务建库建表时就需要提交业务运行相关的SQL给DBA审核,也可以借助ArkcontrolArkit来自动化审核。比如,慢查询日志分析,抓出运行慢的SQL进行分析,也可以借助第三方工具例如Arkcontrol慢查询分析系统进行慢查询采集和分析。在分析慢查询时进行参数最差输入,同时,对SQL语句的谓词进行过滤因子、基数、选择率和SQL查询回表情况的分析。 +2. **设计索引**的目标是让查询语句运行得足够快,同时让表、索引维护也足够快,例如,使用业务不相关自增字段为主键,减缓页分裂、页合并等索引维护成本,加速性能。也可以使用第三方工具进行索引设计,例如Arkcontrol SQL 优化助手,会给出设计索引的建议。 +3. **创建索引策略:**优先为搜索列、排序列、分组列创建索引,必要时加入查询列创建覆盖索引;计算字段列基数和选择率,选择率越接近于1越适合创建索引;索引选用较小的数据类型(整型优于字符型),字符串可以考虑前缀索引;不要建立过多索引,优先基于现有索引调整顺序;参与比较的字段类型保持匹配并创建索引。 +4. **调优索引:**分析执行计划;更新统计信息(AnalyzeTable);Hint优化,方便调优(FORCEINDEX、USEINDEX、IGNOREINDEX、STRAIGHT_JOIN);检查连接字段数据类型、字符集;避免使用类型转换;关注 optimizer_switch,重点关注索引优化特性 MRR(Multi-Range Read)和 ICP(Index Condition Pushdown)。 + * MRR优化是为了减少磁盘随机访问,将随机IO转化为顺序IO的数据访问,其方式是将查询得到辅助索引的键值放到内存中进行排序,通常是按照主键或RowID进行排序,当需要回表时直接根据主键或 RowID 排序顺序访问实际的数据文件,加速 SQL 查询。 + * ICP优化同样也是对索引查询的优化特性,MySQL根据索引查询到数据后会优先应用where条件进行数据过滤,即无法使用索引过滤的where子句,其过滤由之前Server层的数据过滤下推到了存储引擎层,可以减少上层对记录的检索,提高数据库的整体性能。 + +## 创建索引规范 + +* 命名规范,各个公司内部统一。 +* 考虑到索引维护的成本,单张表的索引数量不超过5个,单个索引中的字段数不超过5个。 +* 表必需有主键,推荐使⽤UNSIGNED自增列作为主键。表不设置主键时InnoDB会默认设置隐藏的主键列,不便于表定位数据同时也会增大MySQL运维成本(例如主从复制效率严重受损、pt工具无法使用或正确使用)。 +* 唯一键由3个以下字段组成,并且在字段都是整形时,可使用唯一键作为主键。其他情况下,建议使用自增列或发号器作主键。 +* 禁止冗余索引、禁止重复索引,索引维护需要成本,新增索引时优先考虑基于现有索引进行rebuild,例如(a,b,c)和(a,b),后者为冗余索引可以考虑删除。重复索引也是如此,例如索引(a)和索引(a,主键ID) 两者重复,增加运维成本并占用磁盘空间,按需删除冗余索引。 +* 联表查询时,JOIN 列的数据类型必须相同,并且要建⽴索引。 +* 不在低基数列上建⽴索引,例如“性别”。 在低基数列上创建的索引查询相比全表扫描不一定有性能优势,特别是当存在回表成本时。 +* 选择区分度(选择率)大的列建立索引。组合索引中,区分度(选择率)大的字段放在最前面。 +* 对过长的Varchar段建立索引。建议优先考虑前缀索引,或添加CRC32或MD5伪列并建⽴索引。 +* 合理创建联合索引,(a,b,c) 相当于 (a) 、(a,b) 、(a,b,c)。 +* 合理使用覆盖索引减少IO,避免排序。 + +# 查询优化 + +MySQL采用基于开销的优化器,以确定处理查询的最佳方式,也就是说执行查询之前,都会先选择一条自以为最优的方案。在很多情况下,MySQL能够计算最佳的可能查询计划,但在某些情况下,MySQL没有关于数据的足够信息,或者是提供太多的相关数据信息,它所采用的可能并非就是事实上的最优方案。这里举了两个例子来说明。 + +### 案例一 + +**Range Query Optimizer的流程** + +1. 根据查询条件计算所有的 possible keys。 +2. 计算全表扫描代价(cost_all)。 +3. 计算最小的索引范围访问代价(这一步很关键,直接决定了 Range 的查询效率),它有三步: + 1. 对于每一个possible keys(可选索引),调用records_in_ranges函数计算范围中的rows; + 2. 根据rows,计算二级索引访问代价; + 3. 获取cost最小的二级索引访问(cost_range)。 +4. 选择执行最小化访问代价的执行计划。如果 cost_all <= cost_range,则全表扫描,否则索引范围扫描。 + +Range使用了records_in_range函数估算每个值范围的rows,结果依赖于possible_keys;possible_keys越多,随机IO代价越大,Range查询效率。所以,索引不是越多越好,相反,我们应该尽量减少possible_keys,减少records_in_range从而减少IO的消耗。这里给大家推荐两个工具,用pt-index-usage工具来删除冗余索引,用 pt-duplicate-key-checker 工具来删除重复索引。 + +### 案例二 + +优化前有一个索引idx_global_id。图中的这条SQL语句的where条件包括一个sub_id的等值查询和一个global_id的范围查询。执行一次需要2.37秒。从下一页的执行计划中,我们可以看到虽然查询优化器使用了唯一索引uniq_subid_globalid,但是由于idx_global_id的干扰,实际只使用了前面的4个长度就access,剩余8个长度都被filter了。 + +从优化后的执行计划中可以看到,使用了forceindex来强制使用唯一索引。正如上文列举的,相似的命令还有ignoreindex忽略索引,straght_join强制优化器按特定的顺序使强制优化器按特定的顺序使用数据表,high_priority 或 low_priority 两个命令来控制 SQL 的执行优先权。 + +### ICP,MRR,BKA + +1.ICPICP是IndexConditionPushdown的简称,是MySQL使用索引从表中检索行数据的一种优化方式。目的是减少从基表中全记录读取操作的数量,从而降低IO操作。 + +在没有开启ICP之前,存储引擎会通过遍历索引查找基表中的行,然后返回给MySQLServer层,再去为这些数据行进行where后的条件过滤。开启ICP之后,如果部分where条件能使用索引中的字段,MySQLServer会把这部分下推到存储引擎层,存储引擎通过索引过滤,把满足的行从表中读取出来。ICP能减少引擎层访问基表的次数和MySQLServer访问存储引擎的次数。对于 InnoDB 表来说,ICP 只适用于辅助索引. + +2.MRR + +MRR 是 Multi-Range Read 的简称,是 MySQL 优化器将随机 IO转化为顺序IO以降低查询过程中IO开销的一种手段。MRR的适用场景是辅助索引,如INDEX(key1),查询key1在n到m范围内的数据。使用限制就是MRR,MR适用于range、ref、eq_ref的查询。 + +3.BKA和BNLBKA是BatchedKeyAccess的简称,是MySQL优化器提高表join性能的一种手段,它是一种算法。而BNL是BlockNestedLoop的简称,它是默认的处理表join的方式和算法。那么二者有什么区别呢? + +•BNL比BKA出现的早,BKA直到MySQL 5.6 版本才出现,而 BNL 至少在 MySQL 5.1 版本中就存在了; + +• BNL 主要用于被 join 的表上无索引时; + +• BKA 只在被 join 表上有索引时可以使用,那么就在行提交给被 join 的表之前,对这些行按照索引字段进行排序,因此减少了随机 IO,排序才是两者最大的区别,但如果被 join 的表没有索引呢?那么就只能使用 BNL 了。 + +使用BKA需要注意一些问题,比如:BKA的适用场景支持inner join、outer join、semi-join operations、including nested outer joins等; + +BKA 有两个使用限制,一个是使用 BKA 特性,必须启用 MRR 特性;二是 BKA 主要适用于 join 的表上有索引可使用的情况,否则只能使用 BNL。 + +## MySQL执行计划分析三部曲 + +当有慢查询或者执行 SQL 遇到瓶颈时,我们分析这类问题时可以参考 MySQL 执行计划分析“三步曲”。 + +1. 查看 SQL 执行计划: + * explain SQL; + * desc 表名; + * show create table 表名。 +2. 通过 Profile 定位 QUERY 代价消耗: + * setprofiling=1; + * 执行SQL; + * show profiles;获取Query_ID。 + * show profile for query Query_ID;查看详细的profile信息 +3. 通过Optimizer Trace表查看SQL执行计划树: + * set session optimizer_trace = 'enabled = on'; + * 执行SQL; + * 查询information_schema.optimizer_trace 表,获取 SQL 查询计划树; + * set session optimizer_trace=‘enabled=off';开启此项影响性能,记得用后关闭。 + +## 查询相关参数和分析工具 + +MySQL 可以通过设置一些参数,将运行时间长或者非索引查找的 SQL 记录到慢查询文件中。可以分析慢查询文件中的 SQL,有针对性的进行优化。 + +* 参数slow_query_log,表示是否开启慢查询日志,ON或者1表示开启,OFF或者0表示关闭。 +* 参数long_query_time,设置慢查询的阈值,MySQL5.7版本支持微秒级。 +* 参数slow_query_log_file,慢查询文件的存放路径。 +* 参数log_queries_not_using_indexes,表示是否将非索引查找的SQL也记录到慢查询文件中未使用索引的 SQL 语句上限,0 表示没限制。 +* 参数 log_throttle_queries_not_using_indexes,表示每分钟记录到慢查询文件中未使用索引的 SQL 语句上限,0 表示没限制。 +* 参数 max_execution_time,用来控制 SELECT 语句的最大执行时间,单位毫秒,超过此值MySQL 自动 kill 掉该查询。 + + + + +如上图所示是一个慢查询的例子,通过这个例子你可以看到慢查询文件中记录了哪些信息。包括了慢SQL产生的时间,SQL源自的IP和对应的数据库用户名,以及访问的数据库名称;查询的总耗时,被lock 的时间,结果集行数,扫描的行数,以及字节数等。当然还有具体的 SQL 语句。 + +分析慢查询常用的工具有: + +explain; + +Mysql dump slow,官方慢查询分析工具; + +pt-query-digest,Percona公司开源的慢查询分析工具; + +vc-mysql-sniffer,第三方的慢查询抓取工具; + +pt-kill,Percona公司开源的慢查询kill工具,常用于生产环境的过载保护。 + +这里重点介绍pt-query-digest,它是用于分析MySQL慢查询的一个常用工具,先对查询语句的条件进行参数化,然后对参数化以后的查询进行分组统计,统计出各查询的执行时间、次数、占比等,同时把分析结果输出到文件中。也可以结合 Anemometer 工具将慢查询平台化展示。 +## 如何优化SQL + +1. 全表扫描还是索引扫描。对于小表来说,二者IO调用次数和返回时间相差不大;但对于大表,如果全表扫描,那么查询返回的时间就会很长,就需要使用索引扫描加快查询速度。但并不是要求DBA根据每一种查询条件组合都要创建索引,索引过多也会降低写入和修改的速度,而且如果导致表数据和索引数据比例失调,也不利于后期的正常维护。 + + + +2. 如何创建索引,在哪些列上建立索引适合业务需求?一般情况下,你可以在选择度高的列上创建索引,也可以在status列上创建索引。创建索引时,要注意避免冗余索引,除非一些特殊情况外。如index(a,b,c)和index(a),其中a的单列索引就是冗余索引。 +3. 创建索引以后,尽量不要过频修改。业务可以根据现有的索引情况合理使用索引,而不是每次都去修改索引。能在索引中完成的查找,就不要回表查询。比如SELECT某个具体字段,就有助于实现覆盖索引从而降低IO次数,达到优化SQL的目的。 +4. 多表关联的SQL,在关联列上要有索引且字段类型一致,这样MySQL在进行嵌套循环连接查找时可以使用索引,且不会因为字段类型不一致或者传入的参数类型与字段类型不匹配的情况,这样就会导致无法使用索引,在优化SQL时需要重点排查这种情况。另外索引列上使用函数也不会涉及索引。多表关联时,尽量让结果集小的表作为驱动表,注意是结果集小的表,不是小表。 +5. 在日常中你会发现全模糊匹配的查询,由于MySQL的索引是B+树结构,所以当查询条件为全模糊时,例如‘%**%’,索引无法使用,这时需要通过添加其他选择度高的列或者条件作为一种补充,从而加快查询速度。当然也可以通过强制SQL进行全索引扫描,但这种方式不好,尽量不要在SQL中添加hints。对于这种全模糊匹配的场景,可以放到ES或者solr中解决。尽量不要使用子查询,对子查询产生的临时表再扫描时将无索引可查询,只能进行全表扫描,并且MySQL对于出现在from中的表无所谓顺序,对于where中也无所谓顺序,这也是可以优化SQL的地方。 +6. 另外orderby/groupby的SQL涉及排序,尽量在索引中包含排序字段,并让排序字段的排序顺序与索引列中的顺序相同,这样可以避免排序或减少排序次数。 +7. 除此之外,复杂查询还是简单查询?貌似总会面临这样的疑问和选择。不要总想着用一个SQL解决所有事情,可以分步骤来进行,MySQL也十分擅长处理短而简单的SQL,总体耗时会更短,而且也不会产生臃肿的 SQL,让人难以理解和优化。 + +常用的SQL编写规范如下所示。 + +* SELECT只获取必要的字段,禁止使用SELECT*。这样能减少网络带宽消耗,有效利用覆盖索引,表结构变更对程序基本无影响。 +* 用IN代替OR。SQL语句中IN包含的值不宜过多,应少于1000个。过多会使随机IO增大,影响性能。 +* 禁止使用orderbyrand()。orderbyrand()会为表增加几个伪列,然后用rand() 函数为每一行数据计算 rand() 值,最后基于该行排序,这通常都会生成磁盘上的临时表,因此效率非常低。建议先使用 rand() 函数获得随机的主键值,然后通过主键获取数据。 +* SQL中避免出现now()、rand()、sysdate()、current_user()等不确定结果的函数。在语句级复制场景下,引起主从数据不一致;不确定值的函数,产生的SQL语句无法使用QUERY CACHE。 +