云栖大讲堂Java基础入门(二)—— 阿里专家与你分享:你必须注意的Java编程细节...
摘要:本文主要关注如何在Java中操作一系列对象,介绍了Java的内建类型——数组,并介绍了一些操作数组的方法;随后,介绍了JDK中的集合类,一元对象的存储使用了Collection,详细介绍了Collection的分类;同时,本文展示了Map的多种实现策略;本文的重点内容是处理细节注意事项,来源于Java开发手册。
数十款阿里云产品限时折扣中,赶紧点击这里,领劵开始云上实践吧!
演讲嘉宾简介:
邢凯航(花名:弗止),Java高级开发工程师,香港大学计算机科学硕士,16年加入阿里巴巴,目前就职于研发效能事业部用户声音及代码智能化团队,负责代码中心后端开发。
PPT地址:https://yq.aliyun.com/download/2656
以下内容根据演讲嘉宾视频分享以及PPT整理而成。
本次的分享主要围绕以下四个方面:
一、数组
二、Collection
三、Map
四、处理细节注意事项
一、数组
数组作为java的内建类型,它的大小和类型是固定的,访问性能高效。数组的大小和类型一旦被指定,在运行期间就不能再修改;同时,Java中的数组支持边界值检查,访问或设置越界的数组都会抛出异常;另外,数组对外提供了length变量,但它只能反映最大容量,但不能反映使用的大小,可以通过将数组的使用量存储在变量中或者遍历数组,确定数组的使用量;Java JDK中Arrays工具类提供了多种操作方式,比如填充数组(fill),二分查找(binarySearch,前提是整个数组是有序的),比较数组相等(equals),数组排序(sort)等方法。因为JDK提供的容器工具类提供了更丰富的能力,因此,在日常开发中,我们使用更多的其实是容器类,只有当我们确定这些容器工具类的效率成为问题时,我们才会切换到使用数组去实现。
二、Collection
下面我们看一下这些容器工具类,首先是一元对象存储Collection。
List:
List表示一系列有序的对象,List接口有多种实现,如下图所示。首先按照线程是否安全可以分为两类,线程安全指的是多个线程存取数据时都是安全的。非线程安全中最常用的是ArrayList,ArrayList基于数组实现,它跟数组类似,随机访问的操作时间复杂度是常数级别;与数组不同的是,arraylist的容量是可以自动增长的,最大容量可达到21亿。LinkedList是基于链表实现的,非线程安全,与ArrayList相比,LinkedList优势体现在,当我们经常在链表中增加删除元素时,LinkedList只需要修改前后对象的指针,而ArrayList需要拷贝剩下一部分的数组内容,因此LinkedList在这种情况下具有更高的效率。在下图中,线程安全的实现类包括Vector和CopyOnWriteArrayList,CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的,修改数据时,先对数据加锁,从原有的数组中拷贝一份出来,在新的数组做写操作,写完之后,替换掉原来的数组,CopyOnWriteArrayList通过这种方式达到线程安全的目的。同时,数组定义时使用了transient关键字,达到了修改后所有线程都是可见的目的。CopyOnWriteArrayList在读取数据时,通过直接访问数组生成迭代器的快照,以此实现读取操作不需要加锁,并且可以并行访问的目的。Vector类已经不推荐使用了,Vector 的所有方法加上了 synchronized 关键字,达到线程安全的目的,与CopyOnWriteArrayList相比效率较低,在用户不考虑线程安全时,采用ArrayList或LinkedList实现。
Set:
当我们需要找到链表中一个确定的对象时,对于List类,我们需要遍历整个链表,效率比较低,因此JDK为我们提供了Set类型,表示一个集合的概念,与数学中集合的概念是类似的,Set集合中存储中不重复的元素。下图中的EnumSet仅仅用于处理枚举类型,使用位向量实现,根据枚举值的多少提供两种实现方式,这两种实现方式都是包私有的,我们需要通过EnumSet抽象类进行操作。Set类型也分为线程安全和非线程安全,另外,根据内置的集合是否有序,也可以分为两种实现。如果需要使用有序的集合,集合中的元素需要实现Comparable接口,或者让容器在构造时加入比较器对象。在有序的集合中,TreeSet是非线程安全的,内部使用TreeMap 实现,ConcurrentSkipListSet是一个有序且线程安全的集合,是基于ConcurrentSkipListMap实现的,使用ConcurrentSkipListSet进行新增,删除,查询,操作的时间复杂度能达到O(logn)。如果不需要整体有序的集合,我们可以选择别的实现,例如HashSet,它是一个非线程安全且无序的集合,基于HashMap实现。HashSet派生出的一个实现,LinkedHashSet,其保证在迭代获取时,元素获取的顺序与插入的顺序是一致的。LinkedHashSet在迭代访问Set中全部元素时,其性能优于HashSet,而在插入元素时,性能逊色于HashSet。CopyOnWriteArraySet是线程安全的,基于CopyOnWriteArrayList实现,在添加元素前,检查当前数据结构中是否包含了相同的元素,如果已经包含,就不再重复增加。
Queue:
Queue代表队列,一般用于存取需要处理的任务,提供了在一端进行存取的能力。BlockingQueue接口的实现类额外提供了在存取元素时,阻塞的能力。BlockingQueue提供了多套方法,在获取元素时,如果队列为空,或者在插入元素时,队列已满,BlockingQueue将会抛出异常,或者返回一个特定值让程序去处理,或者让调用者线程无限的等待,或者在有限的时间范围内等待。Deque接口的实现类提供在两端进行存取的能力。PriorityQueue和PriorityBlockingQueue提供优先队列的能力,前者是非线程安全,后者是线程安全的。它们存取的对象需要实现Comparable接口或者提供比较器,否者会抛出异常。DelayQueue是一个延时队列,当队列中的元素达到延时的时间时才会被取出,队列中的元素最终会按照执行的时间在队列中进行排序。SynchronizedQueue并没有容量设置,每一个获取动作必须等待线程的插入动作达到匹配,在线程池中有所应用。TransferQueue接口的实现类的作用和SynchronizedQueue类似,功能有所增强。在典型生产者与消费者模式中,生产者可以采用阻塞或者非阻塞的方式,将对象传递给消费者,并且在队列上提供查询消费者情况的接口。
三、Map:
Map是一种键值对的映射,在java中有多种Map的实现可供选择。实现SortedMap接口的类会按照key进行排序,TreeMap是SortedMap的一种实现,是非线程安全的,内部使用红黑树模型。使用TreeMap进行插入,删除,查找操作的时间复杂度为O(logn)。ConcurrentSkipListMap使用跳表的数据结构,插入,删除,查询操作的时间复杂度为O(logn)。跳表主要采用空间换时间的思想,跳表由多条链构成,是关键字升序排列的数据结构,包含多个级别,一个head引用指向最高的级别,最低(底部)的级别,包含所有的key,每一个级别都是其更低级别的子集,并且是有序的。对于Map,常用的实现为HashMap,concurrentHashMap。HashMap的实现结构是散列表(数组+链表)方式, LinkedHashMap增加了时间和空间上的开销,但是通过维护运行在所有条目上的双向链表,保证元素顺序的一致性。ConcurrentHashMap是线程安全的,提供了分段锁的设计,在同一个分段内,才存在竞态的关系,而不同的分段锁之间,由于不存在竞争,可以并行。相对于在整个Map上加锁的设计,ConcurrentHashMap采用分段锁,大大提高了在高并发环境下处理的能力。HashTable就是在整个Map上加锁,达到线程安全的目的。EnumMap是专门用于枚举类型的映射,提供比较高的效率。WeakHashMap和HashMap比较类似,区别在于WeakHashMap 内部是通过弱引用来管理entry的,虽然弱引用可以用来访问对象,但进行垃圾回收时弱引用并不会被考虑在内,仅有弱引用指向的对象仍然会被GC回收。在IdentityHashMap中,当且仅当两个key严格相等(key1==key2)时,IdentityHashMap才认为两个key相等;相对于普通HashMap而言,只要key1和key2通过equals()方法返回true,且它们的hashCode值相等即可。
四、处理细节注意事项
1.只要重写equals,就必须重写hashcode,如下图所示。
首先equals与hashcode间的关系是这样的:如果两个对象相同(即用equals比较返回true),那么它们的hashCode值一定要相同;如果两个对象的hashCode相同,它们并不一定相同(即用equals比较返回false)。重写hashcode方法的原因在于,为了保证同一个对象,保证在equals相同的情况下hashcode值必定相同,如果重写了equals而未重写hashcode方法,可能就会出现两个没有关系的对象equals相同的(因为equal都是根据对象的特征进行重写的),但hashcode确实不相同的。
2. 使用Map的方法keySet()/values()/entrySet()返回集合对象时,不可以对其进行添加元素操作,否则会抛出UnsupportedOperationException异常。如下图所示。
上图中的返回方法的对象都是内部的实现类,例如EntrySet,Values,因为这些都是实现类,这些类只实现了抽象集合中的部分方法,并没有重写add方法,如果在调用add方法时,实际会调用到AbstractCollection中的add方法,而抽象类中的add方法会抛出UnsupportedOperationException异常。
3. Collections类返回的对象,如:emptyList()/singletonList()等都是immutable list,不可对其进行添加或者删除元素的操作。
与前一条注意事项一样,下图展示了截取的EmptyList类重写的一些方法列表,我们发现并没有重写add方法,所以最终调用的还是AbstractCollection中的add方法,而抽象类中的add方法会抛出UnsupportedOperationException异常。
4. ArrayList的subList结果不不可强转成ArrayList,否则会抛出ClassCastException异常。
下图截取了ArrayList中的SubList,SubList是实际返回的一个类,SubList与ArrayList并没有任何的子类关系,如果强转,java会抛出ClassCastException异常。
5. 在subList场景中,高度注意对父集合元素个数的修改,会导致子列表的遍历、增加、删除均会产生ConcurrentModificationException 异常。
下图展示了ArrayList中的subList的一个Iterator,它的next()方法中,在执行的时候会先调用checkModification()方法,这个方法中会对expectedModcount进行判断,任何父集合元素的修改都会引起这个值的变化,在子集合遍历的时候抛出ConcurrentModificationException 异常。
6. 使用集合转数组的方法,必须使用集合的toArray(T[] array),如下图所示,传入的是类型完全一样的数组,大小就是list.size()。
直接使用toArray()无参数方法时存在问题,这个方法只能返回Object类型的数组,如果强制转换成其他类型的数组,将会出现ClassCastException异常。当我们使用toArray带参数方法时,入参分配的参数空间不够大的话,toArray方法内部将重新分配内存空间,并返回新数组的地址;如果数组元素的个数大于实际所需,数组的下表从list.size(),一直到数组末尾全部元素都会被置为null,其他数组元素保持不变。因此,我们最好将方法入参数组的大小定义成集合元素的个数。
7. 使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常 。Arrays.asList()方法内部会用到ArrayList实现类,这个不同于java.util下的ArrayList类,从下图中可以看到,这个类并没有重写add和remove方法,而这些方法会继承自AbstractList抽象类中的add,remove,clear方法。add和remove操作会抛出UnsupportedOperationException异常。
8. 不要在foreach循环⾥进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。
ArrayList的遍历是通过下面的内部类Iterator来实现的,遍历的时候,每当Iterator获取下一个元素时,如下图所示,都会调用checkForComodification()去检查arrayList是否被修改过。如果被修改了,就会抛出一个ConcurrentModificationException异常。
本文由云栖志愿小组沈金凤整理,编辑百见