文章目录
本文内容源自《疯狂Java讲义》第二版:学习笔记总结。
Java集合
- 出现的原因:由于数组的长度不可变化,且必须在初始化数组的时候指定数组的长度,具有较大的局限性。除此之外,数组无法保存具有映射关系的数据。为了保证数量不确定的数据,以及保存具有映射关系的数据,Java提供了集合类。
- 集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。
- 所有集合类都位于java.util包下,后来为了处理多线程环境下的并发安全问题,Java5还在java.util.concurrent包下提供了一些多线程支持的集合类。
- 集合类与数组不同,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量);而集合里只能保存对象,同样保存的也是对象的引用变量。
- Java的集合类主要由两个接口派生而出:Collection和Map,以下分别介绍这两个根接口及其子接口或实现类。
Collection
- 具有添加对象、删除对象、清空容器、判断容器是否为空等操作集合的方法,详情可以查看API文档。
Set
Set集合特点:
- 把一个对象添加到Set集合时,Set集合无法记住添加这个元素的顺序(无序),所以Set集合不可重复。
- 如果视图把两个相同的元素加入同一个Set集合中,则操作失败,add()方法会返回false,且新元素不会被加入。
- 如果访问Set集合中的元素,只能根据元素本身访问。
SortedSet
TreeSet
- 采用红黑树结构存储集合元素。
- 能够确保集合元素处于排序状态:支持两种排序算法:自然排序和定制排序。
- 自然排序:
- 会调用集合元素的compareTo()方法来比较元素之间的大小关系,然后将集合元素按升序排列。
- 如果compareTo()方法返回0,则表明这两个对象相等;如果该方法返回一个正整数,则表面obj1>obj2;如果返回一个负整数,则表明obj1<obj2。
- 如果试图把一个对象添加到TreeSet时,则该对象的类必须实现Comparable接口,否则程序将会抛出异常(向对象中添加第一个元素的时候不会出现任何问题,但添加第二个元素的时候,TreeSet会调用对象的compareTo方法与集合中其他元素进行比较,但此时找不到该方法,就会引发ClassCastException异常)。
- 如果希望TreeSet能正常运作,TreeSet只能添加同一种类型的对象。
- 重写该对象的equals()方法时,应保证该方法与compareTo()方法有一致的结果。如果返回的结果不一致,可能会不小心添加了同一对象进入TreeSet集合中,这时候如果这一对象有引用类型的变量,会造成如下图的情况:TreeSet中两个相同的对象指向同一个引用类型变量,此时其中一个对象修改该变量的值,都会造成另一个对象值的改变。
- 如果向TreeSet中添加一个可变对象后,并且后面程序修改了该可变对象的实例变量,这将导致它与其他对象的大小顺序发生了改变,但TreeSet不会挨次调整它们的顺序,甚至可能导致TreeSet中保存的这两个对象通过comapreTo()方法比较返回0。
- TreeSet可以删除没有被修改的实例变量、且不与其他被修改实例变量的对象重复的对象:一旦改变了TreeSet集合里可变元素的实例变量,当再试图删除该对象时,TreeSet也会删除失败(甚至集合中原有的、实例变量没被修改但与修改后元素相等的元素也无法删除)
- 定制排序
- 通过Comparator接口的帮助,书写compare()方法可以实现定制排序。例如本身是升序排序,可以通过修改比较规则实现降序排序。但判断两个集合元素相等的标准依然是通过compare()方法返回了0。
- 当通过Comparator对象来实现定制排序的时候,依然不可以向TreeSet中添加类型不同的对象,否则会引发ClassCastException异常。
HashSet
- 是Set接口的典型实现。按Hash算法来存储集合中的元素,因此具有良好的存取和查找性能。
- 不能保证元素的排序序列,可能与添加顺序不同。
- HashSet采用每个元素的hashCode值来计算其存储位置,从而可以自由的增加HashSet的长度,并可以根据元素的hashCode值来访问元素。
- 不是同步的。如果多个线程同时访问一个HashSet,假设有两个或者两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步。
- 集合元素值可以是null。
- 当向HashSet集合存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值(若没有重写方法,hashCode的值默认是地址值),然后根据该hashCode值决定该对象在HashSet中的存储位置。
- 判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等。
扩展:
- 如果有多个元素的hashCode值相等,但他们通过equals()返回false,就需要在一个存储元素的位置里放多个元素,会导致性能的下降。
- 由于HashSet集合判断对象是否相等取决于equals()方法和hashCode()方法的返回值,因此我们可以通过重写这两个方法,来保证对象被加入到集合中,但重写方法时需要遵循一定的规则才比较合理。
- 重写hashCode()方法的基本规则
- 同一个对象多次调用hashCode()方法应该返回相同的值。
- 当两个对象通过equals()方法比较返回true时,这两个对象的hashCode()方法应该返回相等的值。
注意:
- 当程序把可变对象添加到HashSet之后,尽量不要去修改该集合元素中参与计算hashCode()、equals()的实例变量,修改后有可能导致该对象与其他对象相等,将会导致HashSet无法正确访问这些集合元素。
LinkedHashSet
- 也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表来维护元素次序,这样使得元素看起来是以插入顺序保存的。
- 当遍历集合是,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。
Queue
- 用于模拟队列这种数据结构。队列的特点:先进先出。
- 新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。
Deque
- 双端队列:可以同时从两端来添加、删除元素,因此Deque的实现类既可以当做队列使用,也可以当做栈使用。
- 创建Deque时可以指定一个numElements参数,该参数用于指定Object[]数组的长度,如果不指定numElements参数,Deque底层数组的长度为16。
- Java为Deque提供了ArrayDeque和LinkedList两个实现类。
ArrayDeque
- 既实现了List接口,也实现了Deque接口。而且底层也是基于数组的实现,因此随机访问集合元素时有较好的性能。
- ArrayDeque既可以当做栈来使用,也可以作为队列使用。
- 当程序需要使用“栈”这种数据结构的时候,推荐使用ArrayDeque,尽量避免使用Stack——因为Stack是古老的集合,性能较差。
注:对于所有内部基于数组的集合实现,例如:ArrayList、ArrayDeque等,使用随机访问的性能比使用Iterator迭代访问的性能要好。
LinkedList
- LinkedList还实现了Deque接口,可以被当成双端队列来使用,因此既可以被当成栈,也可以被当成队列。
- 以链表形式保存元素,因此随机访问性能差,但插入、删除比较出色(只需改变指针所指的地址即可)。
- 线程不安全。
PriorityQueue
- 保存元素是按队列元素的大小进行重新排序,而不是加入的先后顺序,已经违背了队列的最基本规则:先进先出。因此不是绝对标准的队列的实现。
- 不允许插入null值。
- 元素有两种排序方式:自然排序、定制排序。
- 自然排序:集合中的元素必须实现了Comparable接口。而且应该是同一个类的多个实例,否则可能导致ClassCastException异常。
- 定制排序:创建PriorityQueue队列时,传入一个Comparator对象,该对象负责对队列中的所有元素进行排序。这种排序方式不要求元素实现Comparable接口。
List
- List集合非常像一个数组,它可以记住每次添加元素的顺序(有序),因此允许元素重复,且List的长度可变。
- List集合默认按元素的添加顺序设置元素的索引,因此如果要访问List集合中的元素,可以直接根据元素的索引来访问。
- 通过equals()方法来判断两个元素是否相等。
- 当视图删除一个对象时,List会调用该对象的equals()方法依次与集合元素进行比较,如果该equals()方法以某个集合元素作为参数时返回true,List将会删除该元素。
- 额外提供了一个ListIterator,与普通的Iterator相比,增加了向前迭代的功能。
ArrayList
- 线程不安全。需要程序员手动保证集合的同步性。
- 基于数组实现的List类,因此随机访问集合元素时有较好的性能。
- 使用参数initialCapacity来设置数组的长度,初始化一个ArrayList集合时长度为10。当向集合中添加的元素超出了该数组的长度时,该参数会自动增加。如果要向集合中添加大量元素时,可以使用ensureCapacity方法一次性增加initialCapacity,这样可以减少重分配的次数,提高性能。(同Vector)
- 当长度超过原空间长度的时候,会进行数组扩容,即创建一个原空间1.5倍的空间,将原数组内容复制过去。
Vector
- 比较古老的类,有很多缺点,因此不推荐使用。
- 线程安全,因此性能低于ArrayList。
- 基于数组实现的List类。
- 使用参数initialCapacity来设置数组的长度,初始化一个Vector集合时长度为10。当向集合中添加的元素超出了该数组的长度时,该参数会自动增加。如果要向集合中添加大量元素时,可以使用ensureCapacity方法一次性增加initialCapacity,这样可以减少重分配的次数,提高性能。(同ArrayList)
Stack
- 用于模拟“栈”这种数据结构。
- 进栈出栈都是Object类型,因此从栈中取出元素后必须进行类型转换。
- 由于Stack继承了Vector,因此也是一个非常古老的类,它同样是线程安全、性能较差的,应该尽量少使用。如果需要使用栈这种数据结构,可以考虑使用ArrayDeque。
LinkedList
- LinkedList还实现了Deque接口,可以被当成双端队列来使用,因此既可以被当成栈,也可以被当成队列。
- 以链表形式保存元素,因此随机访问性能差,但插入、删除比较出色(只需改变指针所指的地址即可)。
- 线程不安全。
Map
Map集合的共同特征:
- 保存的每项数据都是key-value队。key用于标识集合里的每项数据。
- key值不可以重复:一个Map的任何两个key通过equals方法比较总是返回false。
- 如果要访问Map集合中的元素,可以根据每项元素的key来访问其value。key和value之间存在单向一对一关系,即通过指定的key,总能找到唯一的、确定的value。
- Map包含keySet()方法,用于返回Map里所有key组成的Set集合。
- Map提供了一个Entry内部类来封装key-value对,而计算Entry存储时只考虑Entry封装的key。
- 实际上所有的Map实现类都重写了toString()方法,调用Map对象的toString()方法总是返回如右格式的字符串:{key1=value1,key2=value2…}
从Java源码来看,Java是先实现了Map,然后通过包装一个所有value都为null的Map就实现了Set集合。
EnumMap
- 是性能最好的,但只能使用同一个枚举类的枚举值作为key。
IdentityHashMap
- 与HashMap基本相似,但使用“==”来判断元素是否相等,而不是equals()。
Hashtable
- 比较古老的一个类,从JDK1.0就出现了。
- 尽量少用,即使需要创建线程安全的Map实现类,也无需使用该类,可以使用Collections工具类把HashMap变成线程安全的。
- 线程安全。
- 不允许key或value的值为null。如果试图把null值放进Hashtable中,将会引发NullPointerException异常。
- 不能保证其中key-value对的顺序。
- 判断两个key相等的标准是:两个key通过equals()方法比较返回true,两个key的hashCode值也相等。
- 判断两个value相等:只要两个对象通过equals()方法比较返回true即可。
Properties
- 是Hashtable类的子类。
- 可以把Map对象和属性文件关联起来,从而可以把Map对象中的key-value对写入属性文件中,也可以把属性文件中的“属性名=属性值”加载到Map对象中。由于属性文件里的属性名、属性值只能是字符串类型,所以Properties里的key、value都是字符串类型。该类提供了如下三个方法来修改Properties里的key、value值。
- Properties可以看成key、value都是String类型的集合。
HashMap
- 线程不安全,因此性能比Hashtable高一些。
- 允许key或value值为null,但由于key具有不可重复性,因此至多只能有一个key值为null,但value可以有多个。
- 判断key值是否相等,需要通过equals()方法返回true,且hashCode()的值一样。
- 不能保证其中key-value对的顺序。
- 判断两个key相等的标准是:两个key通过equals()方法比较返回true,两个key的hashCode值也相等。
- 判断两个value相等:只要两个对象通过equals()方法比较返回true即可。
HashMap与Hashtable:
- 当使用自定义类作为HashMap、Hashtable的key时,如果重写该类的quals()和hashCode()方法,则应该保证两个方法的判断标准一致——当两个key通过equals()方法比较返回true时,两个key的hashCode()返回值也应该相同,因为这两个集合保存集合元素的方式与HashSet完全相同,因此对key的要求也相同。
- 如果使用可变对象作为HashMap、HashTable的key,并且程序修改了作为key的可变对象,则也可能出现与HashSet类似的情形:程序再也无法精确访问到Map中被修改过的key。
- 因此尽量不要使用可变对象作为HashMap、Hashtable的key,如果确实需要使用可变对象作为HashMap、Hashtable的key,则尽量不要在程序中修改作为key的可变对象。
LinkedHashMap
- 需要维护元素的插入顺序:使用双向链表来维护key-value对的次序。
- 因为要维护元素的顺序,因此性能略低于HashMap的性能;但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。
SortedMap
TreeMap
- 使用红黑树结构实现的,每个key-value对即作为红黑树的一个节点。
- TreeMap存储key-value对(节点)时,需要根据key对节点进行排序。TreeMap可以保证所有的key-value对处于有序状态。
- 根据Key对节点进行排序,排序依据Comparable接口的compareTo()方法。
- TreeMap也有两种排序方式:
- 自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的key应该是同一个类的对象,否则将会抛出ClassCastException异常。
- 定制排序:创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有key进行排序。采用定制排序时不要求Map的key实现Conparale接口。
- TreeSet中判断两个key相等的标准是:两个key通过compareTo()方法返回0,即认为这两个key是相等的。
- 如果使用自定义类作为TreeMap的key,且想让TreeMap良好的工作,则重写该类的equals()方法和compareTo()方法时应保持一致的返回结果:两个key通过equals()方法比较返回true时,它们通过compareTo()方法比较应该返回0。
各集合类特点及选用
ArrayList与LinkedList
- 由于数组以一块连续内存区来保存所有的数组元素,所以数组在随机访问时性能最好,所有的内部以数组作为底层实现的集合在随机访问时性能都比较好;而内部以链表作为底层实现的集合在执行插入、删除操作时具有较好的性能。但总的来说,ArrayList的性能比LinkedList的性能要好,因此大部分时候都应该考虑使用ArrayList。
- ArrayList底层使用数组实现,存在数组扩容问题。默认初始化长度为10,若超过这个长度,会进行数组扩容:创建一个原长度1.5倍的空间,将原来的元素复制到新的空间处。LinkedList只需修改指针指向即可。
List集合的使用
- 如果需要遍历List集合元素,对于ArrayList、Vector集合,应该使用随机访问方法(get)来遍历集合元素,这样性能更好;对于LinkedList集合,应该采用迭代器(Iterator)来遍历集合元素。
- 如果需要经常执行插入、删除操作来改变包含大量数据的List集合的大小,可考虑使用LinkedList集合。使用ArrayList、Vector集合可能需要经常重新分配内部数组的大小,效果可能较差。
- 如果有多个线程需要同时访问List集合中的元素,开发者可考虑使用Collections将集合包装成线程安全的集合。
Set与Map
- Set与Map的关系十分密切,Java源码就是先实现了HashMap、TreeMap等集合,然后通过包装一个所有的value都为null的Map集合实现了Set集合类。
Map实现类的性能分析
- 虽然HashMap和Hashtable的实现机制几乎一样,但由于Hashtable是一个古老的、线程安全的集合,因此HashMap通常要比Hashtable要快。
- TreeMap通常比HashMap、Hashtable要慢(尤其在插入、删除key-value对时更慢),因为TreeMap低层采用红黑树来管理key-value对(红黑树的每个节点就是一个key-value对)。
- TreeMap中的key-value对总是处于有序状态,无须专门进行排序操作。当TreeMap被填充之后,就可以调用keySet()取得key组成的Set,然后使用toArray()方法生成key的数组,接下来使用Arrays的binarySearch()方法在已排序的数组中可以快速查询对象。
- 对于一般的应用场景,应该多考虑HashMap,HashMap正是为快速查询设计的(底层采用数组存储key-value对)。但如果程序需要一个总是排好序的Map时,应该采用TreeMap。
- LinkedHashMap比HashMap慢一些,因为它需要维护链表来保证添加顺序。
- IdentityHashMap性能没有特别出色之处,因为采用与HashMap基本相似的实现,只是它使用==而不是equals()来判断元素相等。
- EnumMap性能最好,但它只能使用同一个枚举类的枚举值作为key。
Collections工具类
功能:
- 对元素进行排序。
- 能够查找、替换元素。
- 设置不可变集合。
- 同步控制:synchronizedXXX()方法=>保证xxx集合线程安全。