关于hashCode和equals的处理,遵循如下规则:1)只要覆写equals,就必须覆写hashCode。2)因为Set存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须覆写这两个方法。3)如果自定义对象作为Map的键,那么必须覆写hashCode和equals。【说明:String已覆写hashCode和equals方法,所以我们可以愉快地使用String对象作为key来使用】
ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException异常,即java.util.RandomAccessSubList cannot be cast to java.util.ArrayList【说明:subList返回的是ArrayList的内部类SubList,并不是ArrayList而是ArrayList的一个视图,对于SubList子列表的所有操作最终会反映到原列表上】
使用Map的方法keySet()/values()/entrySet()返回集合对象时,不可以对其进行添加元素操作,否则会抛出UnsupportedOperationException异常
Collections类返回的对象,如:emptyList()/singletonList()等都是immutable list,不可对其进行添加或者删除元素的操作【反例:如果查询无结果,返回 Collections.emptyList()空集合对象,调用方一旦进行了添加元素的操作,就会触发UnsupportedOperationException异常。】
在subList场景中,高度注意对原集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生ConcurrentModificationException异常
使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的是类型完全一致、长度为0的空数组【反例:直接使用toArray无参方法存在问题,此方法返回值只能是Object[]类,若强转其它类型数组将出现ClassCastException错误。】
// 正例 List<String> list = new ArrayList<>(2); list.add("行无际"); list.add("itwild"); String[] array = list.toArray(new String[0]); /* 说明: 使用toArray带参方法,数组空间大小的length: 1)等于0,动态创建与size相同的数组,性能最好 2)大于0但小于size,重新创建大小等于size的数组,增加GC负担 3)等于size,在高并发情况下,数组创建完成之后,size正在变大的情况下,负面影响与上相同 4)大于size,空间浪费,且在size处插入null值,存在NPE隐患 */在使用Collection接口任何实现类的addAll()方法时,都要对输入的集合参数进行NPE判断 【说明:在ArrayList#addAll方法的第一行代码即Object[] a = c.toArray();其中c为输入集合参数,如果为null,则直接抛出异常。】
使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常【说明:asList的返回对象是一个Arrays内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。】
String[] str = new String[] { "it", "wild" }; List list = Arrays.asList(str); // 第一种情况:list.add("itwild"); 运行时异常 // 第二种情况:str[0] = "changed1"; 也会随之修改 // 反之亦然 list.set(0, "changed2");
泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用add方法,而<? super T>不能使用get方法,作为接口调用赋值时易出错。【说明:扩展说一下PECS(Producer Extends Consumer Super)原则:第一、频繁往外读取内容的,适合用<? extends T>。第二、经常往里插入的,适合用<? super T>】
这个地方我觉得有必要简单解释一下(行无际本人的个人理解哈,有不对的地方欢迎指出),上面的说法可能有点官方或者难懂。其实我们一直也是这么干的,不过没注意而已。举个最简单的例子,用泛型的时候,如果你遍历(read)一个List,你是不是希望List里面装的越具体越好啊,你希望里面装的是Object吗,如果里面装的是Object那么你想想你会有多痛苦,每个对象都用instanceof判断一下再类型强转,所以这个方法的参数List主要用于遍历(read)的时候,大多数情况你可能会要求里面的元素最大是T类型,即用<? extends T>限制一下。再看你往List里面插入(write)数据又会怎么样,为了灵活性和可扩展性,你马上可能就要说我当然希望List里面装的是Object了,这样我什么类型的对象都能往List里面写啊,这样设计出来的接口的灵活性和可扩展性才强啊,如果里面装的类型太靠下(假定继承层次从上往下,父类在上,子孙类在下),那么位于上级的很多类型的数据你就无法写入了,这个时候用<? super T>来限制一下最小是T类型。下面我们来看Collections.copy()这个例子。
// 这里就要求dest的List里面的元素类型 不能在src的List元素类型 之下 // 如果dest的List元素类型位于src的List元素类型之下,就会出现写不进dest public static <T> void copy(List<? super T> dest, List<? extends T> src) { //....省略具体的copy代码 } // 下面再看我写的测试代码就更容易理解了 static class Animal {} static class Dog extends Animal {} static class BlackDog extends Dog {} @Test public void test() throws Exception { List<Dog> dogList = new ArrayList<>(2); dogList.add(new BlackDog()); dogList.add(new BlackDog()); List<Animal> animalList = new ArrayList<>(2); animalList.add(new Animal()); animalList.add(new Animal()); // 错误,无法编译通过 Collections.copy(dogList, animalList); // 正确 Collections.copy(animalList, dogList); // Collections.copy()的泛型参数就起作到了很好的限制作用 // 编译期就能发现类型不对 }