Java集合
Java的集合类主要由两个接口派生:Collection和Map
对于Set、List、Queue和Map四种集合,最常用的实现类在两张图中都以灰色背景色覆盖,分别是HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList和HashMap、TreeMap等实现类。
使用Stream操作集合
Java8新增了Stream、IntStream、LongStream、DoubleStream等流式API,这些API代表多个支持串行和并行聚集操作的元素。Stream是一个通用的流接口,而IntStream、LongStream、DoubleStream则代表元素集合类型为int、long、double的流。
-
独立使用Stream的步骤如下:
-
- 使用Stream或XxxStream的builder()类方法创建该Stream对应的Builder。
-
- 重复调用Builder的add()方法向流中添加多个元素。
-
- 调用Builder的build()方法获取对应的Stream。
-
- 调用Stream的聚合方法。
对于大部分聚集方法,每个Stream只能执行一次
@Test
public void t14() {
IntStream is = IntStream.builder().add(52).add(78).add(-90).build();
//下面调用聚集方法每个方法只能单独使用
System.out.println("is里的最大值:"+is.max().getAsInt());
System.out.println("is里的最小值:"+is.min().getAsInt());
System.out.println("is里的所有元素的总值:"+is.sum());
System.out.println("is里一共有多少元素:"+is.count());
System.out.println("is里的所有元素的平均值:"+is.average());
System.out.println("is里的所有元素的平方是否都大于20? "+is.allMatch(i->i*i>20));
System.out.println("is里是否有平方小于0的元素? "+is.anyMatch(i->i*i<0));
IntStream mapIs = is.map(i->i*2);//对is中的所有元素都乘2
mapIs.forEach(System.out::println);//遍历mapIs
}
Set集合
HashSet类
HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能。
-
HashSet特点:
-
▶不能保证元素的排序顺序,顺序可能与添加顺序不同,顺序也有可能发生改变。
▶HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或者两个以上线程同时修改HashSet集合时,则必须通过代码来保证其同步。
▶集合元素值可以是null。
当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中存储的位置。如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将会把它们存储在不同的位置,依然可以添加成功。也就是说:HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等。 如果需要把某个类的对象保存到HashSet集合中,重写这个类的equals()方法和hashCode()方法时,应该尽量保证两个对象通过equals()方法比较返回true时,它们的hashCode()方法返回值也相等。HashSet中每个能存储元素的"槽位"(slot)通常称为"桶"(bucket),如果有多个元素的hashCode值相等,但它们通过equals()方法比较返回false,就需要在一个"桶"里放多个元素,这样会导致性能下降。
-
重写hashCode()方法的基本规则:
-
▶在程序运行过程中,同一个对象多次调用hashCode()方法应该返回相同的值。
▶在两个对象通过equals()方法比较返回true时,这两个对象的hashCode()方法应返回相等的值。
▶对象中用作equals()方法比较标准的实例变量,都应该用于计算hashCode值。
当向HashSet中添加可变对象时,必须小心:如果修改HashSet集合中的对象,有可能导致该对象与集合中的其他对象相等,从而导致HashSet无法准确访问该对象。
LinkedHashSet类
HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。当遍历LinkedHashSet集合里的集合时,LinkedHashSet将会按元素的添加元素来访问集合里的元素。 虽然LinkedHashSet使用了链表记录集合元素的添加顺序,但LinkedHashSet依然是HashSet,因此LinkedHashSet依然不允许集合元素重复。
TreeSet类
TreeSet是SortSet接口的实现类,TreeSet可以确保集合元素处于排序状态。有访问第一个、前一个、后一个、最后一个元素的方法。
@Test
public void t14() {
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(5);
treeSet.add(2);
treeSet.add(10);
treeSet.add(-9);
System.out.println(treeSet);
System.out.println(treeSet.first());//输出集合中第一个元素
System.out.println(treeSet.last());//输出集合中最后一个元素
System.out.println(treeSet.headSet(6));//返回所有小于6的元素(不包括6)
System.out.println(treeSet.tailSet(0));//返回所有大于0的元素(不包括0)
System.out.println(treeSet.subSet(0, 6));//返回(2,5)区间的元素
}
TreeSet并不是根据元素的插入顺序进行排序的,而是根据元素实际值的大小来进行排序的。与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet采用红黑树的数据结构来存储集合元素。TreeSet支持两种排序方法:自然排序和定制排序(默认采用自然排序)
- 自然排序
TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排序,这种方式就是自然排序。
当把一个对象加入TreeSet集合中时,TreeSet调用该对象的compareTo(Object obj)方法与容器中的其他对象比较大小,然后根据红黑树结构找到它的存储位置。如果两个对象通过compareTo(Object obj)方法比较相等,新对象将无法添加到TreeSet集合中。
对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object obj)方法比较是否返回0——如果通过compareTo(Object obj)方法比较返回0,TreeSet则认为它们相等;否则就认为它们不相等。因此应该注意:当需要把一个对象放入TreeSet集合中,重写该对象对应类的equals()方法时,应保证该方法与compareTo(Object obj)方法有一致的结果,其规则是:如果两个对象通过equals()方法比较返回true时,这两个对象通过compareTo(Object obj)方法比较应返回0。
与HashSet类似的是,如果TreeSet中包含了可变对象,当可变对象的实例对象被修改时,TreeSet在处理这些对象时将非常复杂,而且容易出错,因此建议不要修改放入HashSet和TreeSet集合中元素的关键实例变量。 - 定制排序
TreeSet的自然排序是根据集合元素的大小,TreeSet将它们以升序排序。如果需要实现定制排序,例如以降序排列,则可通过Comparator接口的帮助。该接口里包含一个int compare(T o1,T o2)方法,该方法用于比较o1和o2的大小:如果该方法返回正整数,则表明o1大于o2;如果该方法返回0,则表示o1等于o2;如果该方法返回负整数,则表明o1小于o2。
现在如果定制一个降序排列,就颠倒下判断的规则,改为:如果o1大于o2,则该方法返回负整数;如果o1等于o2,则该方法返回0;如果o1小于o2则该方法返回正整数。
如下就是实现定制倒序排序:
@Test
public void t14() {
//Comparator是一个函数式接口
TreeSet<Integer> treeSet = new TreeSet<>((o1,o2)->{
return o1>o2?-1:o1<o2?1:0;
}) ;
treeSet.add(21);
treeSet.add(-98);
treeSet.add(58);
treeSet.add(100);
System.out.println(treeSet);
}
各Set实现类的性能分析
HashSet和TreeSet是Set的两个典型实现,HashSet的性能总是比TreeSet好(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的Set时,才应该使用TreeSet,否则都该使用HashSet。HashSet的子类LinkedHashSet,对于普通的插入、删除操作,LinkedHashSet比HashSet要略慢一点,这是由维护链表所带来的额外开销造成的,但由于有了链表,遍历LinkedHashSet会更快。
List集合
List集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List集合允许使用重复元素。可以通过索引来访问指定位置的集合元素,List集合默认按元素的添加顺序设置元素的索引。List判断两个对象相等只要通过equals()方法比较返回true即可。
Java8为List集合增加了两个常用的默认方法sort()和replaceAll()。
@Test
public void t14() {
List<String> arr = new ArrayList<>();
arr.add(new String("一二"));
arr.add(new String("一"));
arr.add(new String("一二三四"));
arr.add(new String("一二三"));
//o1-o2表示升序排;o2-o1表示降序排
arr.sort((o1,o2)->o1.length()-o2.length());
System.out.println(arr.toString());
//replaceAll方法里需要一个UnaryOperator函数式接口
arr.replaceAll(i->"我被替换了");
System.out.println(arr.toString());
}
ArrayList和Vector实现类
ArrayList和Vector作为List类的两个典型实现,完全支持List接口的全部功能。ArrayList和Vector类都是基于数组实现的List类,所以ArrayList和Vector类封装了一个动态的、允许再分配的Object[]数组。如果创建空的ArrayList或Vector集合时不指定该数组的长度,则Object[]数组的长度默认为10。Vector类是一个古老的集合,具有很多缺点,通常尽量少用Vector实现类。
-
ArrayList和Vector实现类的区别是:
- ArrayList是线程不安全的,当多个线程访问同一个ArrayList集合时,如果有超过一个线程修改了ArrayList集合,程序则必须手动保证该集合的同步性。而Vector是线程安全的,所以Vector的性能比ArrayList的性能低。即使需要保证list集合线程安全也不推荐使用Vector实现类。因为Collections工具类可以把ArrayList封装成线程安全的集合,或者直接使用CopyOnWriteArrayList对象。
固定长度的List
Arrays是一个可以操作数组的工具类,该工具类里提供了一个asList(Object… a)方法,该方法可以把一个数组或者指定个数的对象转换成一个List集合,这个List集合既不是ArrayList实现类的实例,也不是Vector实现类的实例,而是Arrays的内部类ArrayList的实例。
Arrays.ArrayList是一个固定长度的List集合,程序只能遍历访问该集合里的元素,不可增加、删除该集合里的元素。
Queue集合
Queue用于模拟队列这种数据结构,队列通常是指"先进先出"(FIFO)的容器。队列的头部保存在队列中存放时间最长的元素,队列的尾部保存在队列中存放时间最短的元素。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。
Queue接口中定义的方法 | 诠释 |
---|---|
void add(Object e) | 将指定元素加入此队列的尾部 |
Object element() | 获取队列头部的元素,但是不删除该元素 |
boolean offer(Object e) | 将指定元素加入此队列的尾部。当使用有容量限制的队列时,此方法通常比add(Object e)方法更好 |
Object peek() | 获取队列头部的元素,但是不删除该元素。如果此队列为空,则返回null。 |
Object poll() | 获取队列头部的元素,并删除该元素。如果此队列为空,则返回null。 |
Object remove() | 获取队列队列头部的元素,并删除该元素。 |
Queue接口有一个PriorityQueue实现类,Queue还有一个Deque接口,Deque代表一个"双端队列",双端队列可以同时从两端来添加、删除元素,因此Deque的实现类既可当成队列使用,也可以当成栈使用。Java为Deque提供了ArrayDeque和LinkedList两个实现类。
PriorityQueue实现类
PriorityQueue是一个比较标准的队列实现类。之所以说它是比较标准的队列实现,而不是绝对标准的队列实现,是因为PriorityQueue保存队列元素的顺序并不是按加入队列的顺序,而是按队列元素的大小进行重新排序。因此当调出peek()方法或者poll()方法取出队列中的元素时,并不是取得最先进入队列的元素,而是取出队列中最小的元素。从这点来看,PriorityQueue违反了队列的最基本的规则:先进先出(FIFO)。
Deque接口和ArrayDeque实现类
Deque接口是Queue接口的子接口,它代表一个双端队列。
Deque接口的方法 | 诠释 |
---|---|
void addFirst(Object e) | 将指定的元素插入该双端队列的开头。 |
void addLast(Object e) | 将指定的元素插入该双端队列的末尾。 |
Object getFirst() | 获取但不删除双端队列的第一个元素。 |
Object getLast() | 获取但不删除双端队列的最后一个元素。 |
boolean offerFirst(Object e) | 将指定元素插入该双端队列的开头。 |
boolean offerLast(Object e) | 将指定元素插入该双端队列的末尾。 |
Object peekFirst() | 获取但不删除该双端队列的第一个元素;如果此双端队列为空,则返回null。 |
Object peekLast() | 获取但不删除该双端队列的最后一个元素;如果此双端队列为空,则返回null。 |
Object pollFirst() | 获取并删除该双端队列的第一个元素;如果此双端队列为空,则返回null。 |
Object pollLast() | 获取并删除该双端队列的最后一个元素;如果此双端队列为空,则返回null。 |
(栈方法) Object pop() | pop出该双端队列所表示的栈的栈顶元素。相当于removeFirst()。 |
(栈方法) void push(Object e) | 将一个元素push进该双端队列所表示的栈的栈顶。相当于addFirst(e)。 |
Deque不仅可以当作双端队列使用,而且可以被当作栈来使用。因为Deque类里还包含了pop(出栈)、push(入栈)两个方法。
Deque接口提供了一个典型的实现类ArrayDeque。ArrayDeque是一个基于数组实现的双端队列,创建Deque时同样可制定一个参数来用于指定Object[]数组的长度,不指定参数时,Deque底层数组长度默认为16。
ArrayDeque当成"栈"使用:
@Test
public void t14() {
ArrayDeque<String> ad = new ArrayDeque<>();
ad.push("第一个元素");
ad.push("第二个元素");
ad.push("第三个元素");
ad.push("第四个元素");
ad.push("第五个元素");
//先进后出
System.out.println(ad);
//获取队列第一个元素,但是不删除该元素。如果此队列为空,则返回null。
System.out.println(ad.peek());
//获取队列第一个元素并删除该元素
System.out.println(ad.pop());
System.out.println(ad);
}
ArrayDeque也可以当作队列使用,此时ArrayDeque将按"先进先出"的方式操作集合元素:
@Test
public void t14() {
ArrayDeque<String> ad = new ArrayDeque<>();
ad.offer("第一个元素");
ad.offer("第二个元素");
ad.offer("第三个元素");
ad.offer("第四个元素");
ad.offer("第五个元素");
//先进先出
System.out.println(ad);
//获取队列第一个元素,但是不删除该元素。如果此队列为空,则返回null。
System.out.println(ad.peek());
//获取队列第一个元素并删除该元素
System.out.println(ad.poll());
System.out.println(ad);
}
LinkedList实现类
LinkedList类是List接口的实现类——是一个List集合,可以根据索引来随机访问集合中的元素。而且 LinkedList还实现了Deque接口,可以被当成双端队列来使用,因此既可以被当成"栈"来使用,也可以当成队列来使用。
@Test
public void t14() {
LinkedList<String> arr = new LinkedList<>();
//将字符串元素加入队列的尾部
arr.offer("队列最后一个");
//将字符串元素加入栈的顶部
arr.push("栈的栈顶");
//将字符串元素加入队列的头部(相当于栈的栈顶)
arr.addFirst("小汉");
//以List按索引访问的方式获取元素
System.out.println(arr.get(1));
//访问并不删除栈顶的元素
System.out.println(arr.peekFirst());
//访问并不删除队列的最后一个元素
System.out.println(arr.peekLast());
//将栈顶的元素pop(删除)出"栈"
System.out.println(arr.pop());
//访问并删除队列的最后一个元素
System.out.println(arr.pollLast());
System.out.println(arr);
}
LinkedList与ArrayList、ArrayDeque的实现机制完全不同,ArrayList、ArrayDeque内部以数组的形式来保存集合中的元素,因此随机访问集合元素时有较好的性能;而LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能比较好。
各种线性表的性能分析
Java提供的List就是一个线性表接口,而ArrayList、LinkedList又是线性表的两种典型实现:基于数组的线性表和基于链的线性表。Queue代表队列,Deque代表双端队列(即可作为队列使用、也可作为栈使用)。由于数组以一块连续的内存区来保存所有的数组元素,所以数组在随机访问时性能比较好,所有的内部以数组作为底层实现的集合在随机访问时性能都比较好;而内部以链表作为底层实现的集合在执行插入、删除操作时有比较好的性能。
增强的Map集合
Map用于保存具有映射关系的数据,因此Map集合里是键值对的形式保存数据。Map的key不允许重复,即同一个Map对象的任意两个key通过equals方法比较总是返回false。
Java是先实现了Map,然后通过包装一个所有value都为空对象的Map就实现了Set集合。Map集合添加kv对时,允许多个value重复,但如果添加kv时添加有多个重复的k,那么新添加的value会覆盖该key原来对应的value。
HashMap
HashMap是Map接口的典型实现类,线程不安全,key不能重复,用作key的对象必须实现hashCode()方法和equals()方法。与HashSet集合不能保证元素的顺序一样,HashMap也不能保证其中kv对的顺序。判断两个key相等的标准是两个key通过equals()方法比较返回true时,两个key的hashCode值也相等。
如果使用可变对象作为HashMap的key,并且程序修改了作为key的可变对象,则可能出现无法准确访问到Map中被修改过的key。
LinkedHashMap实现类
HashSet有个LinkedHashSet子类,HashMap也有一个LinkedHashMap子类;LinkedHashMap也使用双向链表来维护kv对的顺序,该链表负责维护Map的迭代顺序,迭代顺序与kv对的插入顺序保持一致。
SortedMap接口和TreeMap实现类
SortMap接口有一个TreeMap实现类。TreeMap是一个红黑树数据结构,每个kv对即作为红黑树的一个节点。TreeMap存储kv对(节点)时,需要根据key对节点进行排序。TreeMap可以保证所有的kv对处于有序状态。TreeMap和TreeSet一样也有两种排序方式:自然排序和定制排序。
默认按自然排序:(升序)
@Test
public void t14() {
TreeMap<Integer,String> treeMap=new TreeMap<>();
treeMap.put(58, "五十八");
treeMap.put(10, "十");
treeMap.put(100, "一百");
treeMap.put(-32, "负三十二");
System.out.println(treeMap.toString());
}
定制排序:(降序)
@Test
public void t14() {
TreeMap<Integer,String> treeMap=new TreeMap<>((o1,o2)->{
return o1>o2?-1:o1<o2?1:0;
});
treeMap.put(58, "五十八");
treeMap.put(10, "十");
treeMap.put(100, "一百");
treeMap.put(-32, "负三十二");
System.out.println(treeMap.toString());
}
HashSet和HashMap的性能
对于HashSet及其子类而言,它们采用hash算法来决定集合中元素的存储位置,并通过hash算法来控制集合的大小;对于HashMap、Hashtable及其子类而言,它们采用hash算法来决定Map中key的存储,并通过hash算法来增加key集合的大小。
hash表里可以存储元素的位置被称为"桶(bucket)",在通常情况下,单个"桶"里存储一个元素时具有最好的性能:hash算法可以根据hashCode值计算出"桶"的存储位置,接着从"桶"里取出元素。但hash表的状态是open的:在发生"hash冲突"时,单个桶会存储多个元素,这些元素以链表的形式存储,必须按顺序搜索。下面是hash表保存各元素,且发生"hash冲突"的示意图。
-
因为HashSet和HashMap、Hashtable都使用hash算法来决定其元素(HashMap只考虑key)的存储,因此HashSet、HashMap的hash表包含如下属性:
-
➤容量(capacity):hash表中桶的数量。
➤初始化容量(initial capacity):创建hash表时桶的数量。HashMap和HashSet都允许在构造器中指定初始化容量。
➤尺寸(size):当前hash表中记录的数量。
➤负载因子(load factor):负载因子等于"size/capacity"。负载因子为0,表示空的hash表,0.5表示半满的hash表……轻负载的hash表具有冲突少、适宜插入与查询的特点。
hash表里还有一个"负载极限","负载极限"就是一个0~1的数值,"负载极限"决定了hash表的最大填满程度。当hash表中的负载因子达到指定的"负载极限"时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。
HashSet和HashMap、Hashtable的构造器允许指定一个负载极限,HashSet和HashMap、Hashtable默认的"负载极限"为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing。
"负载极限"的默认值(0.75)是时间和空间成本上的一种折中:较高的"负载极限"可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作;较低的"负载极限"会提高查询数据的性能,但会增加hash表所占用的内存开销。
操作集合的工具类:Collections
排序操作
以下是对List集合元素进行排序的常用类方法
类方法 | 诠释 |
---|---|
void reverse(List list) | 反转指定List集合中元素的顺序 |
void shuffle(List list) | 对List集合元素进行随机排序(模拟"洗牌"动作) |
void sort(List list) | 根据元素的自然排序对指定的List集合的元素按升序进行排序 |
void sort(List list,Comparator c) | 根据指定Comparator产生的顺序对List集合元素进行排序 |
void swap(List list,int i,int j) | 将指定List集合中i处的元素和j处的元素进行交换 |
@Test
public void t14() {
List<Integer> intArr=new ArrayList<>();
intArr.add(82);
intArr.add(10);
intArr.add(43);
intArr.add(-92);
intArr.add(100);
intArr.add(100);
intArr.add(100);
intArr.add(95);
//输出集合中最大的元素
System.out.println(Collections.max(intArr));
//输出集合中最小的元素
System.out.println(Collections.min(intArr));
//输出指定元素在集合中出现的次数
System.out.println(Collections.frequency(intArr, 100));
}
包装线程不安全的集合
ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的,当多个并发线程向这些集合中存、取元素时,就可能会破坏这些集合的数据完整性。
-
可以使用Collections提供的类方法把这些集合包装成线程安全的集合:
-
Collections.synchronizedCollection():返回指定collection对应的线程安全的collection。
Collections.synchronizedList():返回指定List对象对应的线程安全的List对象。
Collections.synchronizedMap():返回指定Map对象对应的线程安全的Map对象。
Collections.synchronizedSet():返回指定Map对象对应的线程安全的Set对象。
线程安全的集合类:
-
以Concurrent开头的集合类:ConcurrentHashMap、ConcurrentSkipListMap等。
以CopyOnWrite开头的集合类:CopyOnWriteArrayList等。