前言
隔一段时间读代码,有一股恶心,读了一遍《Effective Java 3》希望在代码质量和编码规范上有所提升,顺便总结归纳部分建议和技巧;
因为自己是Android开发,平常用不上的建议点就没有读取,后续有机会在补充;
正文
-
考虑使用静态工厂方法替代构造方法
-
public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }
-
优点:
-
不像构造方法,它们是有名字的
-
/** * 返回一个可能为素数 的 的构造方法 可以更好地表示为名为 * 的静态工厂方法。 */ public static BigInteger probablePrime(int bitLength, Random rnd) { if (bitLength < 2) throw new ArithmeticException("bitLength < 2"); return (bitLength < SMALL_PRIME_THRESHOLD ? smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) : largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd)); }
-
-
与构造方法不同,它们不需要每次调用时都创建一个新对象
-
/** * The {@code Boolean} object corresponding to the primitive * value {@code true}. */ public static final Boolean TRUE = new Boolean(true); /** * The {@code Boolean} object corresponding to the primitive * value {@code false}. */ public static final Boolean FALSE = new Boolean(false); public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }
-
-
与构造方法不同,它们可以返回其返回类型的任何子类型的对象
Shape shape = Shape.newInstance("circular")
根据参数可以返回Shape 的子类对象 -
是返回对象的类可以根据输入参数的不同而不同
我理解是23种设计模式中的工厂模式 同3
-
在编写包含该方法的类时,返回的对象的类不需要存在
-
-
缺点:
-
只提供静态工厂方法的主要限制是,没有公共或受保护构造方法的类不能被子类化
-
静态工厂方法的第二个缺点是,程序员很难找到它们
-
-
-
当构造方法参数过多时使用 builder 模式
-
可伸缩的构造方法模式
有很多参数时,很难编写客户端代码,而且很难读懂它。不知道这些值是什么意思,并且必须仔细地计算参数才能找到答案
-
public class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; public NutritionFacts(int servingSize, int servings) { this(servingSize, servings, 0); } public NutritionFacts(int servingSize, int servings,int calories) { this(servingSize, servings, calories, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat) { this(servingSize, servings, calories, fat, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) { this(servingSize, servings, calories, fat, sodium, 0); } ··· }
-
-
当在构造方法中遇到许多可选参数时,另一种选择是 JavaBeans 模式
构造方法在多次调用中被分割,所以在构造过程中 JavaBean 可能处于不一致的状态
-
public class NutritionFacts { // Parameters initialized to default values (if any) private int servingSize = -1; // Required; no default value private int servings private int calories private int fat private int sodium private int carbohydrate = 0; public NutritionFacts() { } public void setServingSize(int val) { servingSize = val; } ··· public void setCarbohydrate(int val) { carbohydrate = val; } }
-
-
Builder 模式(建造者模式)
-
Builder 模式也有缺点。为了创建对象,首先必须创建它的 builder
-
当设计类的构造方法或静态工厂的参数超过几个时,Builder 模式是一个不错的选择,特别是如果许 多参数是可选的或相同类型的。客户端代码比使用伸缩构造方法(telescoping constructors)更容易读写,并且 builder 比 JavaBeans 更安全。
-
public class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; public static class Builder { // Required parameters private final int servingSize; private final int servings; // Optional parameters - initialized to default values private int calories private int fat private int sodium private int carbohydrate = 0; /** * 必选参数 */ public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } /** * 可选参数 */ public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public Builder carbohydrate(int val) { carbohydrate = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } }
-
-
Builder 模式非常适合类层次结构。 使用平行层次的 builder,每个嵌套在相应的类中。 抽象类有抽象的 builder; 具体的类有具体的 builder
-
public abstract class Pizza { public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE} final Set<Topping> toppings; abstract static class Builder<T extends Builder<T>> { EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class); public T addTopping(Topping topping) { toppings.add(Objects.requireNonNull(topping)); return self(); } abstract Pizza build(); // Subclasses must override this method to return "this" protected abstract T self(); } Pizza(Builder<?> builder) { toppings = builder.toppings.clone(); // See Item 50 } } public class NyPizza extends Pizza { public enum Size { SMALL, MEDIUM, LARGE } private final Size size; public static class Builder extends Pizza.Builder<Builder> { private final Size size; public Builder(Size size) { this.size = Objects.requireNonNull(size); } @Override public NyPizza build() { return new NyPizza(this); } @Override protected Builder self() { return this; } } private NyPizza(Builder builder) { super(builder); size = builder.size; } } public class Calzone extends Pizza { private final boolean sauceInside; public static class Builder extends Pizza.Builder<Builder> { private boolean sauceInside = false; // Default public Builder sauceInside() { sauceInside = true; return this; } @Override public Calzone build() { return new Calzone(this); } @Override protected Builder self() { return this; } } private Calzone(Builder builder) { super(builder); sauceInside = builder.sauceInside; } }
-
-
-
-
使用私有构造方法或枚类实现 Singleton 属性
-
缺点: 使用enum 不能扩展父类
-
优点:防止反序列化创建新对象:枚举类在序列化和反序列化时会处理好实例化的问题,因此枚举单例能够防止通过反序列化创建新的对象。
-
1、单例模式(Singleton Pattern)
-
-
使用私有构造方法执行非实例
-
实用类(utility classes)不是设计用来被实例化的:一个实例是没有意义的。然而,在没有显式构造方法 的情况下,编译器提供了一个公共的、无参的默认构造方法。对于用户来说,该构造方法与其他构造方法没有什么区 别。在已发布的 API 中经常看到无意识的被实例的类
-
试图通过创建抽象类来强制执行非实例化是行不通的。 该类可以被子类化,子类可以被实例化。此外,它误导用户认为该类是为继承而设计的
-
因此可以通过包含一个私有构造方法来实现类的非实例化
-
public class UtilityClass { // Suppress default constructor for noninstantiability private UtilityClass() { throw new AssertionError(); } ... // Remainder omitted }
-
-
-
使用依赖注入取代硬连接资源(hardwiring resources)
-
许多类依赖于一个或多个底层资源。例如,拼写检查器依赖于字典。将此类类实现为静态实用工具类并不少见
-
public class SpellChecker { private static final Lexicon dictionary = ...; private SpellChecker() {} // Noninstantiable public static boolean isValid(String word) { ... } public static List<String> suggestions(String typo) { ... } }
-
将它们实现为单例也并不少见
-
public class SpellChecker { private final Lexicon dictionary = ...; private SpellChecker(...) {} public static INSTANCE = new SpellChecker(...); public boolean isValid(String word) { ... } public List<String> suggestions(String typo) { ... } }
-
-
该模式的一个有用的变体是将资源工厂传递给构造方法。 工厂是可以重复调用以创建类型实例的对象。 这种工 厂体现了工厂方法模式。 Supplier 接口非常适合代表 工厂。 在输入上采用 Supplier 的方法通常应该使用有界的通配符类型 约 束工厂的类型参数,以允许客户端传入工厂,创建指定类型的任何子类型。
-
例如,下面是一个使用客户端提供的工 厂生成 tile 的方法:
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
-
-
依赖注入
-
public class SpellChecker { private final Lexicon dictionary; public SpellChecker(Lexicon dictionary) { this.dictionary = Objects.requireNonNull(dictionary); } public boolean isValid(String word) { ... } public List<String> suggestions(String typo) { ... } }
-
-
不要使用单例或静态的实用类来实现一个类,该类依赖于一个或多个底层资源,这些资源的行为会影响类 的行为,并且不让类直接创建这些资源。相反,将资源或工厂传递给构造方法 (或静态工厂或 builder 模式)。这种称 为依赖注入的实践将极大地增强类的灵活性、可重用性和可测试性
-
-
-
避免创建不必要的对象 > 享元模式
-
案例1
-
将正则表达式显式编译为一个 Pattern 实例(不可变),缓存它
-
static boolean isRomanNumeral(String s) { return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"); } public class RomanNumerals { private static final Pattern ROMAN = Pattern.compile( "^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"); static boolean isRomanNumeral(String s) { return ROMAN.matcher(s).matches(); } }
-
-
-
案例2
-
自动装箱 错误示例
-
private static long sum() { Long sum = 0L; for (long i = 0; i <= Integer.MAX_VALUE; i++) sum += i; return sum; }
-
-
-
-
消除过期的对象引用
-
错误示例
-
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(); return elements[--size]; } /** * Ensure space for at least one more element, roughly * doubling the capacity each time the array needs to grow. */ private void ensureCapacity() { if (elements.length == size) } } 修改后 public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // Eliminate obsolete reference return result; }
-
-
-
避免使用 Finalizer 和 Cleaner 机制
-
执行时机不确定和性能影响
- Finalizer 和 Cleaner 机制的一个缺点是不能保证他们能够及时执行。 在一个对象变得无法访问时,到 Finalizer 和 Cleaner 机制开始运行时,这期间的时间是任意长的。 这意味着你永远不应该 Finalizer 和 Cleaner 机制做 任何时间敏感(time-critical)的事情。 例如,依赖于 Finalizer 和 Cleaner 机制来关闭文件是严重的错误,因为打开的 文件描述符是有限的资源。 如果由于系统迟迟没有运行 Finalizer 和 Cleaner 机制而导致许多文件被打开,程序可能会 失败,因为它不能再打开文件了。
-
Cleaner 使用demo
-
public class Room implements AutoCloseable { private static final Cleaner cleaner = Cleaner.create(); // Resource that requires cleaning. Must not refer to Room! private static class State implements Runnable { int numJunkPiles; // Number of junk piles in this room State(int numJunkPiles) { this.numJunkPiles = numJunkPiles; } // Invoked by close method or cleaner @Override public void run() { System.out.println("Cleaning room"); numJunkPiles = 0; } } // The state of this room, shared with our cleanable private final State state; // Our cleanable. Cleans the room when it’s eligible for gc private final Cleaner.Cleanable cleanable; public Room(int numJunkPiles) { state = new State(numJunkPiles); cleanable = cleaner.register(this, state); } @Override public void close() { cleanable.clean(); } }
-
-
-
使用 try-with-resources 语句替代 try-finally 语句
-
问题代码 异常可能会被冲掉
-
static void copy(String src, String dst) throws IOException { InputStream in = new FileInputStream(src); try { OutputStream out = new FileOutputStream(dst); try { byte[] buf = new byte[BUFFER_SIZE]; int n; while ((n = in.read(buf)) >= 0) out.write(buf, 0, n); } finally { out.close(); } } finally { in.close(); } }
-
-
try-with-resources
-
实现了AutoCloseable接口
-
static void copy(String src, String dst) throws IOException { try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dst)) { byte[] buf = new byte[BUFFER_SIZE]; int n; while ((n = in.read(buf)) >= 0) out.write(buf, 0, n); } }
-
-
-
重写 equals 方法时遵守通用约定
-
自反性:对于任何非空引用 x, x.equals(x) 必须返回 true
-
对称性: 对于任何非空引用 x 和 y,如果且仅当 y.equals(x) 返回 true 时 x.equals(y) 必须返回 true
-
传递性: 对于任何非空引用 x、y、z,如果 x.equals(y) 返回 true, y.equals(z) 返回 true,则 x.equals(z) 必须返回 true
-
一致性: 对于任何非空引用 x 和 y,如果在 equals 比较中使用的信息没有修改,则 x.equals(y) 的多次调用 必须始终返回 true 或始终返回 false
-
对于任何非空引用 x, x.equals(null) 必须返回 false
-
高质量的编写
-
使用 == 运算符检查参数是否为该对象的引用。如果是,返回 true。这只是一种性能优化,但是如果这种比较可 能很昂贵的话,那就值得去做。
-
使用 instanceof 运算符来检查参数是否具有正确的类型。 如果不是,则返回 false。 通常,正确的类型是 equals 方法所在的那个类。 有时候,改类实现了一些接口。 如果类实现了一个接口,该接口可以改进 equals 约 定以允许实现接口的类进行比较,那么使用接口。 集合接口(如 Set,List,Map 和 Map.Entry)具有此特性。 参数转换为正确的类型。因为转换操作在 instanceof 中已经处理过,所以它肯定会成功。
-
对于类中的每个“重要”的属性,请检查该参数属性是否与该对象对应的属性相匹配。如果所有这些测试成功, 返回 true,否则返回 false。如果步骤 2 中的类型是一个接口,那么必须通过接口方法访问参数的属性;如果类型 是类,则可以直接访问属性,这取决于属性的访问权限。
-
-
-
重写 equals 方法时同时也要重写 hashcode 方法
- 重写Java类的equals方法,必须要重写hashCode方法?
-
始终重写 toString 方法
-
使得类更加舒适 地使用和协助调试
-
以一种美观的格式返回对象的简明有用的描述 kotlin data :
Person(name=John, age=30)
-
-
谨慎地重写 clone 方法
-
必须实现Cloneable 接口,否则CloneNotSupportedException
-
绕过构造方法(调用native 方法)
-
clone 只是值copy; 对象只是引用传递
-
-
考虑实现 Comparable 接口
- 负数表示小于;正数表示大于;0表示等于
-
使类和成员的可访问性最小化
-
公共类的实例属性很少公开
- 属性直接public set没有办法进行安全检查
-
如果一个类有这样的属性或访问方法,客户端将能够修改数组的内容
-
public static final Thing[] VALUES = { ... }; -> private static final Thing[] PRIVATE_VALUES = { ... }; public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES)); -> private static final Thing[] PRIVATE_VALUES = { ... }; public static final Thing[] values() { return PRIVATE_VALUES.clone(); }
-
-
-
在公共类中使用访问方法而不是公共属性
-
最小化可变性
-
组合优于继承
-
继承打破了封装
-
组合
-
InstrumentedSet 类被称为包装类,因为每个 实例都包含(“包装”)另一个 Set 实例。 这也被称为装饰器模式
-
public class ForwardingSet<E> implements Set<E> { private final Set<E> s; public ForwardingSet(Set<E> s) { this.s = s; } public void clear() { s.clear(); } public boolean contains(Object o) { return s.contains(o); } public boolean isEmpty() { return s.isEmpty(); } public int size() { return s.size(); } public Iterator<E> iterator() { return s.iterator(); } public boolean add(E e) { return s.add(e); } ··· } public class InstrumentedSet<E> extends ForwardingSet<E> { private int addCount = 0; public InstrumentedSet(Set<E> s) { super(s); } @Override public boolean add(E e) { addCount++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); } public int getAddCount() { return addCount; } }
-
-
-
-
如使用继承则设计,应当文档说明,否则不该使用
-
Javadoc 标签 @implSpec
-
构造方法绝不能直接或间接调用可重写的方法
-
类构造方法在子类构造方法之前运行,所以在子类构造方法运行之前,子类中的重写方法被调 用。 如果重写方法依赖于子类构造方法执行的任何初始化,则此方法将不会按预期运行
-
public class Super { // Broken - constructor invokes an overridable method public Super() { overrideMe(); } public void overrideMe() { } } public final class Sub extends Super { // Blank final, set by constructor private final Instant instant; Sub() { super() instant = Instant.now(); } // Overriding method invoked by superclass constructor @Override public void overrideMe() { System.out.println(instant); } public static void main(String[] args) { Sub sub = new Sub(); sub.overrideMe(); } }
-
-
-
-
接口优于抽象类
-
骨架实现类被称为 AbstractInterface ,其中 Interface 是它们实现的接口的名称。 例如, 集合框架( Collections Framework)提供了一个框架实现以配合每个主要集合接口: AbstractSet、AbstractMap 等等
-
一个接口通常是定义允许多个实现的类型的最佳方式。 如果你导出一个重要的接口,应该强烈考虑提供一个骨架的实现类。 在可能的情况下,应该通过接口上的默认方法提供骨架实现,以便接口的所有实现者都可 以使用它。 也就是说,对接口的限制通常要求骨架实现类采用抽象类的形式;
-
-
为后代设计接口
-
接口仅用来定义类型
-
不要在接口类中定义常量
-
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; }
-
-
在实现类中定义常量
-
常量与多个类有关,在工具类或者枚举类中定义;
-
-
优先使用类层次而不是标签类
-
标签类
-
杂乱无章的样板代码,包括枚举声明,标签属性和 switch 语句。 可读性 更差,因为多个实现在一个类中混杂在一起。 内存使用增加,因为实例负担属于其他风格不相关的领域。 属性不能 成为 final,除非构造方法初始化不相关的属性,导致更多的样板代码。 构造方法在编译器的帮助下,必须设置标签 属性并初始化正确的数据属性:如果初始化错误的属性,程序将在运行时失败。 除非可以修改其源文件,否则不能 将其添加到标记的类中。 如果你添加一个风格,你必须记得给每个 switch 语句添加一个 case ,否则这个类将 在运行时失败。 最后,一个实例的数据类型没有提供任何关于风格的线索。 总之,标签类是冗长的,容易出错的, 而且效率低下。
-
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; 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); } }
-
-
类层次
-
类层次纠正了之前提到的标签类的每个缺点。 代码简单明了,不包含原文中的样板文件
-
类层次的另一个优点是可以使它们反映类型之间的自然层次关系,从而提高了灵活性,并提高了编译时类型检查 的效率
-
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; } }
-
-
-
优先考虑静态成员类
-
如果你声明了一个不需要访问宿主实例的成员类,总是把 static 修饰符放在它的声明中,使它成为一个静态成员 类,而不是非静态的成员类
-
静态成员类
-
静态成员类是最简单的嵌套类。 最好把它看作是一个普通的类,恰好在另一个类中声明,并且可以访问所有宿主类的成员,甚至是那些被声明为私有类的成员。 静态成员类是其宿主类的静态成员,并遵循与其他静态成员相同 的可访问性规则。 如果它被声明为 private ,则只能在宿主类中访问,等等。
-
静态成员类的一个常见用途是作为公共帮助类,仅在与其外部类一起使用时才有用。 例如,考虑一个描述计算 器支持的操作的枚举类型(条目 34)。 枚举应该是类的公共静态成员类。Operation.Calculator 客户端可以使用 用操作。
-
-
非静态成员类(内部类)
-
非静态成员类的每个实例都隐含地与其包含的类的宿主实例相关联。 在非 静态成员类的实例方法中,可以调用宿主实例上的方法,或者使用限定的构造[JLS,15.8.4] 获得对宿主实例的引用。 如果嵌套类的实例可以与其宿主类的实例隔离存在,那么嵌套类必须是静态成员类:不可能在没有宿主实例的情况下 创建非静态成员类的实例。
-
非静态成员类的一个常见用法是定义一个 Adapter [Gamma95],它允许将外部类的实例视为某个不相关类的 实例。 例如, Map 接口的实现通常使用非静态成员类来实现它们的集合视图,这些视图由 Map 的 keySet ,entrySet 和 values 方法返回。 同样,集合接口(如 Set 和 List )的实现通常使用非静态成员类来实现它 们的迭代器:
-
public class MySet<E> extends AbstractSet<E> { ... // Bulk of the class omitted @Override public Iterator<E> iterator() { return new MyIterator(); } private class MyIterator implements Iterator<E> { ... } }
-
-
-
匿名类(内部类)
-
匿名类没有名字。 它不是其宿主类的成员。 它不是与其他成员一起声明,而是在使用时 同时声明和实例化。 在表达式合法的代码中,匿名类是允许的。 当且仅当它们出现在非静态上下文中时,匿名类才 会封装实例。
-
public class MySet<E> extends AbstractSet<E> { ... // Bulk of the class omitted @Override public Iterator<E> iterator() { return new MyIterator(); } private class MyIterator implements Iterator<E> { ... } }
-
-
局部类(内部类)
- 局部类是四种嵌套类中使用最少的。 一个局部类可以在任何可以声明局部变量的地方声明,并遵守相同的作用 域规则。 局部类与其他类型的嵌套类具有共同的属性。 像成员类一样,他们有名字,可以重复使用。 就像匿名类一 样,只有在非静态上下文中定义它们时,它们才会包含实例,并且它们不能包含静态成员。 像匿名类一样,应该保 持简短,以免损害可读性。
-
-
将源文件限制为单个顶级类
- 永远不要将多个顶级类或接口放在一个源文件中。 遵循这个规则保证在编译时不能有多个定 义。 这又保证了编译生成的类文件以及生成的程序的行为与源文件传递给编译器的顺序无关
-
不要使用原始类型
-
正确 List<String> strings = new ArrayList<>(); 错误 List strings = new ArrayList<>();
-
错误代码
-
public static void main(String[] args) { List<String> 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); }
-
-
-
消除非检查警告
-
unchecked conversion
-
Set<Lark> exaltation = new HashSet();
-
然后可以进行指示修正,让警告消失。 请注意,实际上并不需要指定类型参数,只是为了表明它与 Java 7 中引 入的钻石运算符(“<>”)一同出现。然后编译器会推断出正确的实际类型参数;
-
Set<Lark> exaltation = new HashSet<>();
-
-
每当使用 @SuppressWarnings(“unchecked”) 注解时,请添加注释,说明为什么是安全的
- 未经检查的警告是重要的。 不要忽视他们。 每个未经检查的警告代表在运行时出现 ClassCastException 异常的可能性。 尽你所能消除这些警告。 如果无法消除未经检查的警告,并且可以证明引发该警告的代码是安全类型的,则可以在尽可能小的范围内使用 @SuppressWarnings(“unchecked”) 注解来禁止 警告。 记录你决定在注释中抑制此警告的理由。
-
-
列表优于数组
-
数组是协变
-
Object[] 是 String[] 的父类型
-
List 不是 List 的父类型
-
Object[] objs = new String[3]; objs[0] = 1L; 能编译,但运行出错 编译时报错 List<Object> ol = new ArrayList<Long>(); // Incompatible types ol.add("I don't fit in");
-
-
数组是真实类型,列表是类型擦除
-
-
优先考虑泛型
-
泛型类型比需要在客户端代码中强制转换的类型更安全,更易于使用
-
对外交互时,实际类型不限制,则用Object或宽泛型
-
对内交互时,实际类型不限制,则用泛型
-
-
优先使用泛型方法
-
使用限定通配符来增加 API 的灵活性
-
? super Number
表示接受Number及其父类的类型 -
? extends Number
表示接受Number及其子类的类型 -
List<Integer> list1 = new ArrayList<>(); List<Double> list2 = new ArrayList<>(); double sum(List<? extends Number> list){ 不允许add 可以get } double add(List<? super Number> list){ 允许add 不可以get }
-
-
合理地结合泛型和可变参数
-
优先考虑类型安全的异构容器
-
异构容器指的是可以存储不同类型对象的容器
-
不要这样存
Map<Object, Object> favorites
-
public class Favorites { private Map<Class<?>, Object> favorites = new HashMap<>(); public<T> void putFavorite(Class<T> type, T instance) { favorites.put(Objects.requireNonNull(type), instance); } public<T> T getFavorite(Class<T> type) { return type.cast(favorites.get(type)); } }
-
-
使用枚举类型替代整型常量
-
Java 枚举类型背后的基本思想很简单:它们是通过公共静态 final 属性为每个枚举常量导出一个实例的类。 由于没有可访问的构造方法,枚举类型实际上是 final 的。 由于客户既不能创建枚举类型的实例也不能继承它, 除了声明的枚举常量外,不能有任何实例。 换句话说,枚举类型是实例控制的。 它们是单例的泛型化,基本上是单元素的枚举。
-
枚举类型还允许添加任意方法和属性并实现任意接口
-
在枚举类型中声明一个抽象的 apply 方法,并用常量特定的类主体中的每个常量的具体方法重写它。 这种方法被称为特定于常量(constant-specific)的 方法实现:
-
添加新的常量,则不太可能会忘记提供 apply 方法,因为该方法紧跟在每个常量声 明之后。 万一忘记了,编译器会提醒你,因为枚举类型中的抽象方法必须被所有常量中的具体方法重写 public enum Operation { PLUS {public double apply(double x, double y){return x + y;}}, MINUS {public double apply(double x, double y){return x - y;}}, TIMES {public double apply(double x, double y){return x * y;}}, DIVIDE{public double apply(double x, double y){return x / y;}}; public abstract double apply(double x, double y); }
-
-
public enum Operation { PLUS("+") { public double apply(double x, double y) { return x + y; } }, MINUS("-") { public double apply(double x, double y) { return x - y; } }, TIMES("*") { public double apply(double x, double y) { return x * y; } }, DIVIDE("/") { public double apply(double x, double y) { return x / y; } }; private final String symbol; Operation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } public abstract double apply(double x, double y); }
-
-
使用实例属性替代序数
-
序号指ordinal
-
永远不要从枚举的序号中得出与它相关的值; 请将其保存在实例属性中:
-
public enum Ensemble { SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8), NONET(9), DECTET(10), TRIPLE_QUARTET(12); private final int numberOfMusicians; Ensemble(int size) { this.numberOfMusicians = size; } public int numberOfMusicians() { return numberOfMusicians; } }
-
-
-
使用 EnumSet 替代位属性
-
位属性
-
// Bit field enumeration constants - OBSOLETE! public class Text { public static final int STYLE_BOLD = 1 << 0; // 1 public static final int STYLE_ITALIC = 1 << 1; // 2 public static final int STYLE_UNDERLINE = 1 << 2; // 4 public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8 // Parameter is bitwise OR of zero or more STYLE_ constants public void applyStyles(int styles) { ... } } 这种表示方式允许你使用按位或(or)运算将几个常量合并到一个称为位属性(bit field)的集合中: text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
-
利用位运算,效率高,节省内存;缺点:调试麻烦,输出7 ,需要计算
-
-
EnumSet 位运算性能、输出清晰
-
public class Text { public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH } // Any Set could be passed in, but EnumSet is clearly best public void applyStyles(Set<Style> styles) { ... } } text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
-
applyStyles方法采用 而不是 参数。 尽管所有客户端都可能会将 EnumSet 传递给该方法,但接受接口类型而不是实现类型通常是很好的做法。 这允许一个不寻常的客户端通过其他 Set 实现的可能性
-
-
-
使用 EnumMap 替代序数索引
-
使用序数来索引数组很不合适:改用 EnumMap (很少使用 Enum.ordinal)
-
unChecked警告,数据越界异常
-
class Plant { enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL } final String name; final LifeCycle lifeCycle; Plant(String name, LifeCycle lifeCycle) { this.name = name; this.lifeCycle = lifeCycle; } @Override public String toString() { return name; } } // Using ordinal() to index into an array - DON'T DO THIS! Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length]; for (int i = 0; i < plantsByLifeCycle.length; i++) plantsByLifeCycle[i] = new HashSet<>(); for (Plant p : garden) plantsByLifeCycle[p.lifeCycle.ordinal()].add(p); // Print the results for (int i = 0; i < plantsByLifeCycle.length; i++) { System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]); }
-
-
EnumMap
-
没有不安全的转换; 无需手动标记输出,因为 map 键是知道如何将自己转换为可打印字符串的枚举; 并且不可能在计算数组索引时出错
-
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class); for (Plant.LifeCycle lc : Plant.LifeCycle.values()) plantsByLifeCycle.put(lc, new HashSet<>()); for (Plant p : garden) plantsByLifeCycle.get(p.lifeCycle).add(p); System.out.println(plantsByLifeCycle);
-
-
-
使用接口模拟可扩展的枚举
-
自定义Enum类直接继承Enum类
-
枚举可以实现扩展接口
-
public interface Operation { double apply(double x, double y); } public enum BasicOperation implements Operation { PLUS("+") { public double apply(double x, double y) { return x + y; } }, MINUS("-") { public double apply(double x, double y) { return x - y; } }, TIMES("*") { public double apply(double x, double y) { return x * y; } }, DIVIDE("/") { public double apply(double x, double y) { return x / y; } }; private final String symbol; BasicOperation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } }
-
枚举类型( BasicOperation )不可扩展,但接口类型( Operation )是可以扩展的,并且它是用于表 示 API 中的操作的接口类型。 你可以定义另一个实现此接口的枚举类型,并使用此新类型的实例来代替基本类型。 例如,假设想要定义前面所示的操作类型的扩展,包括指数运算和余数运算。 你所要做的就是编写一个实现Operation 接口的枚举类型:
-
public enum ExtendedOperation implements Operation { EXP("^") { public double apply(double x, double y) { return Math.pow(x, y); } }, REMAINDER("%") { public double apply(double x, double y) { return x % y; } }; private final String symbol; ExtendedOperation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } }
-
-
-
虽然不能编写可扩展的枚举类型,但是你可以编写一个接口来配合实现接口的基本的枚举类型,来对它进行模拟
-
-
注解优于命名模式
-
注解定义方法
-
测试框架
-
拼写错误 tsetFunc
-
多测试框架
-
不同包中的同名方法
-
-
-
始终使用 Override 注解
-
重写方法:检查入参和方法名称是否正确;
-
接口变换:子类编译报错;
-
-
使用标记接口定义类型
-
标记接口:Serializable,Cloneable
-
实现接口可以定义实例
-
方法可以使用接口参数
-
标记接口和标记注释都有其用处。 如果你想定义一个没有任何关联的新方法的类型,一个标记接口是一 种可行的方法。 如果要标记除类和接口以外的程序元素,或者将标记符合到已经大量使用注解类型的框架中,那么标记注解是正确的选择。 如果发现自己正在编写目标为 ElementType.TYPE 的标记注解类型,请花点时间确定它是否应该是注释类型,是不是标记接口是否更合适
-
-
lambda 表达式优于匿名类
-
在 Java 8 中,添加了函数式接口, lambda 表达式和方法引用,以便更容易地创建函数对象
-
lambda 更简洁
-
public enum Operation { PLUS("+") { public double apply(double x, double y) { return x + y; } }, MINUS("-") { public double apply(double x, double y) { return x - y; } }, TIMES("*") { public double apply(double x, double y) { return x * y; } }, DIVIDE("/") { public double apply(double x, double y) { return x / y; } }; private final String symbol; Operation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } public abstract double apply(double x, double y); } public enum Operation { PLUS ("+",(x,y)->x+y), MINUS ("-", (x, y) -> x - y), TIMES ("*", (x, y) -> x * y), DIVIDE("/", (x, y) -> x / y); private final String symbol; private final DoubleBinaryOperator op; Operation(String symbol, DoubleBinaryOperator op) { this.symbol = symbol; this.op = op; } @Override public String toString() { return symbol; } public double apply(double x, double y) { return op.applyAsDouble(x, y); } }
-
-
与方法和类不同,lambda 没有名称和文档; 如果计算不是自解释的,或者超过几行,则不要将其放入 lambda 表达式中。 一行代码对于 lambda 说是理想的,三行代码是合理的最大值
-
lambda 不能获得对自身的引用。 在 lambda 中, this 关键字引用封闭实例,这通常是你想要的。 在匿名类中, this 关键字引用匿名类实例。 如果你需要从其内部访问函数对象,则必须使用匿名类
-
-
方法引用优于 lambda 表达式
-
map.merge(key, 1, (count, incr) -> count + incr); Map<String, Integer> inventory = new HashMap<>(); inventory.put("apple", 5); inventory.put("orange", 3); String key = "apple"; inventory.merge(key, 1, (count, incr) -> count + incr); System.out.println("Updated inventory:"); inventory.forEach((fruit, count) -> System.out.println(fruit + ": " + count)); }
-
如果方法引用看起来更简短更清晰,请使用它们;否则,还是坚持 lambda
-
-
优先使用标准的函数式接口
-
只包含一个抽象方法:函数式接口中只能有一个抽象方法,这个方法定义了接口的主要行为或功能。
-
可以有默认方法和静态方法:函数式接口可以包含默认方法(default methods)和静态方法(static methods),用于提供接口的默认实现或者工具方法。
-
可以用Lambda表达式或方法引用实现:由于函数式接口只包含一个抽象方法,因此可以使用Lambda表达式或方法引用来实现接口的功能,使得代码更加简洁和灵活。
-
始终使用 @FunctionalInterface 注解标注你 的函数式接口。
-
-
在处理基本类型 int,long 和 double 的操作上,六个基本接口中还有三个变体。 它们的名字是通过在基本接口 前加一个基本类型而得到的。 因此,例如,一个接受 int 的Predicate 是一个IntPredicate ,而一个接受两个long 值并返回一个 long 的二元运算符是一个LongBinaryOperator 。除Function接口变体通过返回类型进行了参数化,其他变体类型都没有参数化。 例如, LongFunction<int[]> 使用long类型作为参数并返回了int[]类型;
-
Function 接口还有九个额外的变体,当结果类型为基本类型时使用。 源和结果类型总是不同,因为从类型到 它自身的函数是 UnaryOperator。 如果源类型和结果类型都是基本类型,则使用带有 SrcToResult 的前缀Function,例如 LongToIntFunction(六个变体)。如果源是一个基本类型,返回结果是一个对象引用,那么带有ToObj的前缀Function,例如DoubleToObjFunction;
-
不要试图使用基本的函数式接口来装箱基本类型的包装 类而不是基本类型的函数式接口
-
考虑我们的老朋友 Comparator ,它的结构与 ToIntBiFunction<T,T>接口相同。 即使将前者添加 到类库时后者的接口已经存在,使用它也是错误的。 值得拥有自己的接口有以下几个原因。 首先, 它的名称每次在 API 中使用时都会提供优秀的文档,并且使用了很多。 其次, Comparator 接口对构成有效实例的 构成有强大的要求,这些要求构成了它的普遍契约。 通过实现接口,就要承诺遵守契约。 第三,接口配备很多了有 用的默认方法来转换和组合多个比较器
-
-
明智审慎地使用 Stream