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的步骤如下:
  1. 使用Stream或XxxStream的builder()类方法创建该Stream对应的Builder。
  1. 重复调用Builder的add()方法向流中添加多个元素。
  1. 调用Builder的build()方法获取对应的Stream。
  1. 调用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支持两种排序方法:自然排序和定制排序(默认采用自然排序)

  1. 自然排序
    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集合中元素的关键实例变量。
  2. 定制排序
    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等。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Brrby

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值