Effective Java 3rd (三)

8 篇文章 0 订阅
4 篇文章 0 订阅

21. 为后代设计接口

jdk8新增的default 默认方法会让类继承这些默认方法,存在一些风险,例如:
Collection接口的removeIf方法是jdk8新增的default 方法,org.apache.commons.collections4.collection.SynchronizedCollection 类承诺提供的方法都是同步的,SynchronizedCollection 默认会继承removeIf方法,却没重写removeIf方法,打破了承诺。

截至今天(2019年6月25日),还没有修复这个问题,看来apache的开发人员没看最新版的effective java。

总结:
除非必须这么做,否则应该避免用默认方法向接口里面添加新方法。
默认方法不是用来改变一些接口的签名的。

22. 接口仅用来定义类型

当类实现接口时,该接口作为一种类型(type),可以用来引用类的实例。因此,一个类实现了一个接口,因此表明客户端可以如何处理类的实例。为其他目的定义接口是不合适的。

反例:常量接口
将常量定义到接口中。
原因:
子类的常量会受接口污染;不需要这些常量,为了保证其他地方引用不会报错,仍然需要实现这个接口
正确的做法:
1、将其添加到类中。
2、如果常量被当作枚举,则应该用枚举来实现。
3、用不可实例化的类来导出。

// 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;
}

数字之间合理利用下划线"_"能提高阅读效率。
同一个类中,常量类多处使用可以考虑静态导入。

23. 类层次结构优于标签类

// 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);
    }
}
}

这么做的缺点:
1、可读性差。
2、违反开闭原则。

正确的做法:
把公共方法抽取出来,成为一个抽象类

// 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; }
}

修改过后:
1、更灵活。
2、更有层次。

24. 支持使用静态成员类而不是非静态类

嵌套类(nested class)是在一个类中定义的类。 嵌套类应该只存在于其宿主类(enclosing class)中。 如果一个嵌套类在其他一些情况下是有用的,那么它应该是一个顶级类。 有四种嵌套类:
静态成员类,非静态成员类,匿名类和局部类。
除了第一种以外,剩下的三种都被称为内部类(inner class)。

静态成员类
1、可以访问类成员,包括私有的。
2、常见用途是作为公共帮助类,定义在类中的枚举类也是一种静态成员类。

非静态成员类
1、能访问外部类的实例方法、实例变量、类成员类方法。
2、会自动持有外部类的实例,所以会占用额外内存(外部类+内部类)。
3、常见用法是在内部定义一个不需要暴露给外部的类

// Typical use of a nonstatic member class
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> {

	}
}

匿名类
除了在声明的时候之外,不能实例化它们。 你不能执行 instanceof 方法测试
或者做任何其他需要你命名的类。 不能声明一个匿名类来实现多个接口,或者继承一个类并同时实现一个接口。 匿名类的客户端不能调用除父类型继承的成员以外的任何成员。 因为匿名类在表达式中出现,所以它们必须保持简短—— 约十行或更少 —— 否则可读性将受到影响。
常见用途:
实现一次性使用的接口回调,静态工厂方法

// Concrete implementation built atop skeletal implementation
static List<Integer> intArrayAsList(int[] a) {
	Objects.requireNonNull(a);
// The diamond operator is only legal here in Java 9 and later
// If you're using an earlier release, specify <Integer>
	return new AbstractList<>() {
	@Override
		public Integer get(int i) {
		return a[i]; // Autoboxing ([Item 6]
		 @Override
		public Integer set(int i, Integer val) {
			int oldVal = a[I];
			a[i] = val; // Auto-unboxing
			return oldVal; // Autoboxing
		} 
		@Override
		public int size() {
			return a.length;
		}
	};
}

局部类
应该保持简短,以免损害可读性。
在作用范围内可以重复使用。

总结:
如果一个嵌套的类需要在一个方法之外可见,或者太长而不能很好地适应一个方法,使用一个成员类。 如果一个成员类的每个实例都需要一个对其宿主实例的引用,使其成为非静态的; 否则,使其静态。 假设这个类属于一个方法内部,如果你只需要从一个地方创建实例,并且存在一个预置类型来说明这个类的特征,那么把它作为一个匿名类;否则,把它变成局部类。

25. 将源文件限制为单个顶级类

一个类文件可以定义多个顶级类,但不建议这么做。
原文的例子:
创建Main.java

public class Main {
	public static void main(String[] args) {
	System.out.println(Utensil.NAME + [Dessert.NAME](http://Dessert.NAME));
	}
}

创建Utensil.java,定义两个顶级类

// 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";
}

假设不小心创建了另一个名为 Dessert.java 的源文件并且定义了两个类名跟Utensil.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";
}

注意,上面3个类都在同一个package下。
那么,用javac Main.java Dessert.java 命令会报错,说已经多次定义Utensil 和 Dessert

总结:
建议一个源文件不要定义多个顶级类,如果类之间不需要可见,可定义成静态内部类(条目24)
其实现在我们用的IDE,都会有提示 :已经定义过类。

26. 不要使用原始类型

个人观点:
现在不用泛型的人都outdate了吧,5就开始有了,都多少年了。
原因:
1、编译期检查,而不是到运行期检查,提高编码效率。
2、可读性更好,例如List<String>可以表达这个list中存的都是string
3、建议用List<Object>而不是 List 。
前者可以表示可以存放任何类型的对象且不能引用其他泛型类的List。
后者可以随便持有任何引用,非常不安全。

// Fails at runtime - unsafeAdd method uses a raw type (List)!
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);
}

上面这个例子,如果用List<Object>则无法通过编译。
4、不关心泛型类型的话,可以用无限制通配符,如 Set<?>
这个做法除了null之外,无法添加任何元素,适合遍历比较集合A是否包含集合B的元素。

对于不应该使用原始类型的例外:
1、因为泛型最后都会有类型擦出,所以,class类应该用原始类型
List.class ,String[].class 和 int.class 都是合法的,但 List.class 和 List<?>.class 不是合法的。

2、instanceof 操作符也需要用原始类型

27. 消除非检查警告

1、如果你不能消除警告,但你可以证明引发警告的代码是类型安全的,那么(并且只能这样)用
@SuppressWarnings(“unchecked”) 注解来抑制警告。但是作用范围应该尽可能小,假如范围过大,可能会在新添加的代码中抑制了这些未处理的警告

2、每当使用 @SuppressWarnings(“unchecked”) 注解时,请添加注释,说明为什么是安全的。

28. 列表优于数组

数组与列表的区别:
1、数组是协变的
如Person是父类,Student是子类,则 Person[] p = new Student[1];是正确的。而List<Type1> 跟List<Type2>是不同的类型。
2、数组被具体化了
数组在运行期报错,而泛型列表在编译期报错。

// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException
// Won't compile!
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in");

数组不能和泛型混合使用
下面的代码不能通过编译,仅举例用:

//创建一个泛型列表数组
List<String>[] stringLists = new List<String>[1]; 
//创建一个数值列表
List<Integer> intList = List.of(42); 
 //将stringLists赋值给Object数组,数组协变特性
Object[] objects = stringLists;
//将Object数组下标一存放数值列表,此时的Object数组其实是个String的数组且stringLists的
//第一个元素存放的其实是一个数值列表
objects[0] = intList; 
 //取出元素,此时会报错,因为这个元素是Integer
String s = stringLists[0].get(0);

所以,违背了泛型设计,在第一句代码的时候就会编译报错,来阻止这种情况。

由于泛型是类型擦除实现的,所以无法使用元素类型,如果把强制类型转换T[]去掉将报错

// A first cut at making Chooser generic - won't compile
public class Chooser<T> {
	private final T[] choiceArray;
	public Chooser(Collection<T> choices) {
		choiceArray = (T[])choices.toArray();
	} 
// choose method unchanged
}

建议

// List-based Chooser - typesafe
public class Chooser<T> {
private final List<T> choiceList;
	public Chooser(Collection<T> choices) {
		choiceList = new ArrayList<>(choices);
	} 
	public T choose() {
		Random rnd = ThreadLocalRandom.current();
		return choiceList.get(rnd.nextInt(choiceList.size()));
	}
}

总结:
1、列表在编译期检查类型错误,而数组是在运行期。
2、数组和泛型无法混合使用,如果出现警告,则考虑重构成列表。

29. 优先考虑泛型

stack
左边的泛型有两种改法第一种是把Object数组改成E,第二种是保留Object
第一种可读性较好,但会有堆污染,条目32.
第二种每次pop都需要转换。

30. 优先使用泛型方法

语法:类型参数的类型参数列表位于方法的修饰符和返回类型之间

// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
	Set<E> result = new HashSet<>(s1);
	result.addAll(s2);
	return result;
}

这么做的好处:
安全、不需要做类型转换、无编译警告。

泛型单例工厂:
需要创建一个不可改变但适用于许多不同类型的对象。 因为泛型是通过擦除来实现的(条目 28),所以可以使用单个对象进行所有必需的类型参数化,但是需要编写一个静态工厂方法来重复地为每个请求的类型参数化分配对象。

// Generic singleton factory pattern
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;

@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
	return (UnaryOperator<T>) IDENTITY_FN;
}

递归类型限制

// Using a recursive type bound to express mutual comparability
public static <E extends Comparable<E>> E max(Collection<E> c);

可以理解为“任何可以与自己比较的类型 E"

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值