https://github.com/clxering/Effective-Java-3rd-edition-Chinese-English-bilingual/blob/dev/Chapter-5/Chapter-5-Introduction.md
准则一 不要使用原始类型
首先来看一下什么是原始类型呢?
List 对应的原始类型是 List,那其实就是说不带参数化类型。
直接使用原始类型会有什么危害呢?上例子
List list = new ArrayList();
// 添加一个点对象
list.add(new Point(1, 2));
// 添加一个字符串
list.add("add String");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Point next = (Point) iterator.next();
System.out.println(next.x + " : " + next.y);
}
上面这个代码可以编译通过,但是运行的时候会报错,当然平时我们可能不会这样去写。之所以能够编译通过,是因为Java是伪泛型,通过擦写为Object来实现的,所以相当于往List《Object》里放数据。
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
unsafeAdd1(strings, Integer.valueOf(42));
unsafeAdd2(strings, Integer.valueOf(42));
String s = strings.get(0);
}
private static void unsafeAdd1(List list, Object o) {
list.add(o);
}
private static void unsafeAdd2(List<Object> list, Object o) {
list.add(o);
}
为什么unsafeAdd2通不过编译呢?那是因为如果参数化类型写成Objet,List《String》不是List《Object》的子类,这一点千万要记住,译器会抛出错误,因为它不允许你将一个List《String》当作List《Object》使用。这是因为List《String》是一个更具体的类型,而《Object》是一个更通用的类型。
所以最佳实践是我们不使原始类型,也不是用List《Object》而是使用采用下面的方式:
// 这里为什么要Collection 呢?因为这样的话,我们的方法会更通用
public static <E> void unsafeAdd4(Collection<E> target, E element) {
target.add(element);
}
或者使用通配符:
这样做确实可以达到目的,但是我们一般不这么使用,我们一般来说要通用一点,但是这样写就是检查了一个类型,并不是一个通用的方法。
private static void unsafeAdd3(List<? super String> list, String o) {
list.add(o);
}
也许你会这样写?
private static void safeAdd3(List<? super Object> list, Object o) {
list.add(o);
}
这样是不是更通用了,答案是如果你还是像上面那样使用,编译通不过,问题就在于你传入的是List《String》,但是往里面添加的元素却是Object类型,类型是不匹配的。
补充知识,关于参数中使用通配符 ? extends 和 ? super 是有区别的:
如果说你是消费集合里面的元素要用extends,如果你要修改集合往里面添加或者删除则要用 super,什么意思呢?还是举个例子:
public static void lower(List<? extends Number> producer, List<? super Number> consumer) {
// 消费数据用 extend 比如我们遍历数据就是消费,也就是读取数据 这里不在写代码进行演示
// 报错 不允许添加因为不确定与实际类型是否兼容
producer.add(Integer.valueOf(1));
// 操作数据用super
// 可以正常添加
consumer.add(Integer.valueOf(1));
}
List<? extends Number> producer:表示这是一个列表,其中的元素类型是Number或其子类。我们不能确定具体是什么类型,但可以确定它一定是Number的一个子类。这就可能出现 实际类型为 javaList<Integer>、List<Double> 或 List<Byte>
会导致类型不兼容,比如我们的类型是Integer,如果允许操作的话,这个时候来一个Double怎么办呢?所以extends不允许我们进行操作。
List<? super Number> consumer:表示这是一个列表,其中的元素类型是Number或其父类。这可以包括Number类型或其超类(比如Object)。这就意味着Integer,Double都可以添加,因为Number是一个更通用的类型,类型是兼容的。
友情连接:
https://blog.csdn.net/qq_43259860/article/details/137127886
https://blog.csdn.net/qq_43259860/article/details/137032842
准则二 消除 unchecked 警告
如果不能消除警告,但是可以证明引发警告的代码是类型安全的,那么(并且只有在那时)使用 SuppressWarnings(“unchecked”) 注解来抑制警告。
准则三 list 优于数组
- 理由一
如果 Sub 是 Super 的一个子类型,那么数组类型 Sub[] 就是数组类型 Super[] 的一个子类型。
但是List<sub> 和List<super>不存在这样的关系
为什么呢?
里氏替换原则(Liskov Substitution Principle,LSP)面向对象设计的基本原则之一。里氏替换原则指出:任何父类可以出现的地方,子类一定可以出现。
你想想看比如java List<String> 如果是List<Object>的子类
根据里氏替换原则是不是说不通啊。Sting的List只能添加Sting的类型也很明显不符合里氏替换原则。
// 数组类型 Sub[] 就是数组类型 Super[] 的一个子类型 所以下面这样写编译时可以通过的
Object object[] = new Long[1];
// 这样就导致了运行时异常
object[0] = "java";
// 编译无法通过
List<Object> list = new ArrayList<String>();
数组是具体化的 [JLS, 4.7]。这意味着数组在运行时知道并强制执行他们的元素类型。相比之下,泛型是通过擦除来实现的 [JLS, 4.6]。这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)元素类型信息。
由于这些基本差异创建泛型、参数化类型或类型参数的数组是非法的。因此,这些数组创建表达式都不是合法的:new List[]、new List[]、new E[]。
// (1) 不会通过编译
List<String>[] stringLists = new List<String>[1];
// (2)
List<Integer> intList = Arrays.asList(1);
// (3)
Object[] objects = stringLists;
// (4)
objects[0] = intList;
// (5)
String s = stringLists[0].get(0);
为什么呢?因为这样存在类型安全,假如第一步中的创建是合法的,那么由于前面提到的sub[] 和 super[]存在父子关系,所以第三步是合法的,那么在第四步我们这个操作完全没毛病吧,反正是一个对象类型的数组都可以存,但是使用的时候这就报错了ClassCastException。
- 理由二
// 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)];
}
}
// 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()));
}
}
我们依次来分析这三段代码,首先第一段我们可以实现随机选取一个对象,但是我们必须知道对象是什么类型,每次使用的时候都必须强转。所以我们有了第二段代码,进行了优化,更加的通用,但是第二段代码在(T[]) 这个步骤的时候对得到一个unchecked,但是其实这个警告我们可以忽略,因为我们明确的知道此处不会存在类型转换安全。但是如果要进一步消除这个警告,我们就可以按照第三段代码的方式来写。
准则四 优先使用泛型
但是我们在泛型的使用过程中存在一些问题,让我来看一个例子。
public class Stack<E> {
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
}
问题在哪里?
elements = new E[DEFAULT_INITIAL_CAPACITY]; 泛型是不允许实例化的,为了解决这个问题我们有两种方案。
- 方案一
尽管这里会得到unchecked异常,但是我们自己清楚这里这样做是安全的,所以我们可以忽略这个警告。
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
- 方案二:
尽然是栈,那我们参考一下Java源码的实现,我们可以看到源码中的Stack继承了Vector
我们可以清晰的看到,它定义的是对象类型的数组。那么类型转换是在那里完成的呢?我们可以看到添加的时候是直接进行了添加:
我们可以看看pop方法,可以看到最终取元素的逻辑如下:
那就是在每一个元素使用的时候进行转换。这里依然可能存在类型安全的问题,同样的我们清晰的知道是允许的,所以方法上申明了一个@SuppressWarnings(“unchecked”),这也是符合准则二。
准则五 优先使用泛型方法
使用泛型方法是为了更通用,这里可以参考源码,比如UnaryOperator:
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
/**
* Returns a unary operator that always returns its input argument.
*
* @param <T> the type of the input and output of the operator
* @return a unary operator that always returns its input argument
*/
static <T> UnaryOperator<T> identity() {
return t -> t;
}
}
又比如经典的Comparable:
public interface Comparable<T> {
public int compareTo(T o);
}
准则六 使用有界通配符增加 API 的灵活性
那为什么通配符能够增加灵活性呢?
public static void main(String[] args) {
DemoStack<Number> stack = new DemoStack<>();
stack.push(new Integer(1));
Iterable<Integer> integers = Arrays.asList(1, 2, 3);
stack.pushAll(integers);
}
public static class DemoStack<E> {
private Object[] data;
private int idx = 0;
public DemoStack() {
data = new Object[10];
}
public void push(E e) {
this.data[idx++] = e;
}
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
}
我们可以看到这样的代码有个缺陷:
那如何解决呢?
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
那如果是进行,我们把元素取出来添加到另一个集合,因为这里是要进行操作,所以只有当元素是E类或者是E的父类才允许操作,关于为什么可以看前面准则一,举的例子:
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
生产者使用 extends,消费者使用 super(PECS)。
在我们的 Stack 示例中,pushAll 的 src 参数生成 E 的实例供 Stack 使用,因此 src 的适当类型是 Iterable<? extends E>;popAll 的 dst 参数使用 Stack 中的 E 实例,因此适合 dst 的类型是 Collection<? super E>。
还要记住,所有的 comparable 和 comparator 都是消费者。Comparables 始终是消费者,所以一般应优先使用 Comparable<? super T> 而不是 Comparable,比较器也是如此;因此,通常应该优先使用 Comparator<? super T> 而不是 Comparator。
如果它是一个无界类型参数,用一个无界通配符替换它;无解参数会存在问题,下面的代码不会通过编译:
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
问题是 list 的类型是 List<?>,你不能在 List<?> 中放入除 null 以外的任何值。但是你可以liyong下面这种写法来实现这个功能。
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
// Private helper method for wildcard capture
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
准则七 考虑类型安全的异构容器
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));
}
}
如果一个容器能够同时存储Integer、String和其他类型的对象,那么它就是一个异构容器。
比如我们来看Favorites 这个类,与普通 Map 不同,所有键都是不同类型的,也就是可以存储不同的类型。
上面这样做还是有一定的缺陷,因为类型的一致是通过键和值类确定的,那么如果值的类型可能会和键不一样,如果使用人员想要这样做的话。所以我们为了保障类型安全:
// 这样就能保证类型的安全 所以推荐实践过程中可以选用异构安全的容器
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(type, type.cast(instance));
}
同时这里还有一个缺陷:
你可以存储的 Favorites 实例类型为 String 类型或 String[],但不能存储 List。原因是你不能为 List 获取 Class 对象,List.class 是一个语法错误,这也是一件好事。List 和 List 共享一个 Class 对象,即 List.class。