第9章 集合
集合平时使用的比较多,但是自己很少往深处去看看其实现和原理,这次趁着这个机会了解一下,完全搞懂还是要看自己后续的使用和深挖。
集合框架了解
想要了解集合,就需要先了解集合框架,这里也以最熟悉的队列(queue)为例来介绍集合框架。
-
集合的接口与实现分离
和大多数的设计思想一样,集合的接口和实现也是分离的。队列接口指出了可以在队列的尾部添加元素,在头部删除元素,查找队列中元素的个数。队列的实现通常存在两种形式,一种是使用循环数组,一种是使用链表。这样设计的目的是为了:构建集合时不需要知道使用哪种实现,只有在使用集合对象时,才会确定具体的类。同时也是为了保证:一旦程序改变了想法,只需要在一个地方就可以快捷的改变,即改变调用构造器的地方。
-
Collection
接口集合类的基本接口是
Collection
接口,这个接口有很多方法,详细可以看一下这个网址:https://docs.oracle.com/javase/8/docs/api/.同时,
Collection
接口又继承了Iterable
接口。说到这里就不得不提Iterator
和Iterable
接口的关系,前者是迭代器对象,后者则是定义了如何返回迭代器方法的接口。迭代器接口有四个方法。
hasNext();,next();,remove(),forEachRemaining(Consumer<? super E> action)
.方法的作用看名称就能看出来,不需要多讲什么。需要注意的就是:迭代器的指针是位于两个元素之间的,不是正好指向元素的位置。所以调用next()
方法是先越过下一个元素,再返回越过元素的引用。 -
范型使用方法
不难看出,集合框架中的接口都是范型接口,所以也可以编写一些实用的范型方法,现实也确实这么做了。
Collection
接口中声明了很多方法。作为实现者来说可以通过实现AbstractCollection
接口来定制化自己的需求,同时也不用提供其他不需要的例行方法。具体实现的细节可以看看源码。 -
集合框架中的接口
上图已经很明白这个接口之间的关系了。
具体的集合
下面就来详细介绍一下各个集合的信息和部分集合的用法。
-
链表
链表的优点就是删除和增加元素时消耗不大(只是修改前后元素的引用),但是随机访问时的消耗要比数组大的多。
在Java语言中,所有的链表实际都是双向链表,即后一个元素存放着前一个元素的引用。
Java中的链表中的add方法是将元素添加到链表尾部,但是实际使用中很少遇到这种需求,都是在链表中部添加元素。所以这种对于指定位置添加元素的方法都可以交给迭代器去处理。但是使用迭代球也需要注意:可以给一个容器添加多个迭代器,但是应该只有一个迭代器能够读写,其他迭代器尽量不要读写,只给读的权限即可。光说也体现不出什么,实际撸代码吧。
package collectiontrain; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; /** * LinkedListTrain.java * Description: 链表练习 * * @author Peng Shiquan * @date 2020/8/13 */ public class LinkedListTrain { public static void main(String[] args) { List<String> a = new LinkedList<>(); a.add("A"); a.add("C"); a.add("E"); List<String> b = new LinkedList<>(); b.add("B"); b.add("D"); b.add("F"); b.add("G"); ListIterator<String> aIter = a.listIterator(); Iterator<String> bIter = b.iterator(); while (bIter.hasNext()) { /** * 在当前位置前添加一个元素 */ if (aIter.hasNext()) aIter.next(); aIter.add(bIter.next()); } System.err.println(a); bIter = b.iterator(); while (bIter.hasNext()) { bIter.next(); if (bIter.hasNext()) { bIter.next(); //删除前一个元素 bIter.remove(); } } System.err.println(b); a.removeAll(b); System.err.println(a); } }
可以在敲这段代码的时候先猜一下实际会打印什么,我第一次猜的时候错的离谱。
-
数组列表
这个没有啥好讲的,ArrayList用的比较多,需要注意的就是:需要同步时使用Vector,不需要同步时使用ArrayList。
-
散列集
用处就是随机访问集合中的任意元素,并且概率相同。涉及到数据结构。
原理就是散列表为每一个对象计算一个散列码(hash code),散列码是由对象的实例产生的一个整数。具体使用代码如下:
package collectiontrain; import java.util.HashSet; import java.util.Iterator; import java.util.Scanner; import java.util.Set; /** * SetTrain.java * Description: Set练习 * * @author Peng Shiquan * @date 2020/8/13 */ public class SetTrain { public static void main(String[] args) { Set<String> words = new HashSet<>(); long l = 0; try { Scanner in = new Scanner(System.in); while (!in.hasNext("eof")) { String word = in.next(); //因为电脑的性能不同,这里可能会是0 long callTime = System.currentTimeMillis(); System.err.println(callTime); words.add(word); callTime = System.currentTimeMillis() - callTime; System.err.println(callTime); l += callTime; } } catch (Exception e) { e.printStackTrace(); } Iterator<String> iterator = words.iterator(); for (int i = 0; i <= 20 && iterator.hasNext(); i++) { System.err.println(iterator.next()); } System.err.println("=============="); System.err.println(words.size() + "时间花费" + l + "时间"); } }
-
树集
涉及到数据结构,之前学的都忘的差不多了。和红黑树类似,后续可以算法那部分继续了解,这里只是简单的上代码。
package collectiontrain; import java.util.Objects; /** * ItemTest.java * Description: 树集 * * @author Peng Shiquan * @date 2020/8/16 */ public class ItemTest implements Comparable<ItemTest> { private String description; private int partNumber; public ItemTest(String adescription, int apartNumber) { description = adescription; partNumber = apartNumber; } public String getDescription() { return description; } @Override public String toString() { return "ItemTest{" + "description='" + description + '\'' + ", partNumber=" + partNumber + '}'; } @Override public boolean equals(Object obj) { /** * 比较,需要注意的是对象可能是个null对象,但是仍然要返回true */ if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ItemTest other = (ItemTest) obj; return Objects.equals(description, other.description) && partNumber == other.partNumber; } @Override public int hashCode() { return Objects.hash(description, partNumber); } @Override public int compareTo(ItemTest o) { /** * 比较int的大小和string的长度 */ int diff = Integer.compare(partNumber, o.partNumber); return diff != 0 ? diff : description.compareTo(o.description); } }
package collectiontrain; import java.util.Comparator; import java.util.NavigableSet; import java.util.SortedSet; import java.util.TreeSet; /** * TreeSetTrain.java * Description: 树集练习 * * @author Peng Shiquan * @date 2020/8/16 */ public class TreeSetTrain { public static void main(String[] args) { /** * 代码写完,还是不明白,后续了解。 */ SortedSet<ItemTest> itemTests = new TreeSet<>(); itemTests.add(new ItemTest("A", 123)); itemTests.add(new ItemTest("B", 456)); itemTests.add(new ItemTest("C", 789)); System.err.println(itemTests); NavigableSet<ItemTest> itemTests1 = new TreeSet<>(Comparator.comparing(ItemTest::getDescription)); itemTests1.addAll(itemTests); System.err.println(itemTests1); } }
-
优先级队列
优先级队列就是一个可以自我调整的二叉树,对树执行添加和删除操作时,可以让最小的元素移动到根,而不必花费时间对元素进行排序。代码的使用很简单。
package collectiontrain; import java.time.LocalDate; import java.util.HashMap; import java.util.Map; import java.util.PriorityQueue; /** * PriorityQueueTest.java * Description: 优先级队列练习 * * @author Peng Shiquan * @date 2020/8/16 */ public class PriorityQueueTest { public static void main(String[] args) { PriorityQueue<LocalDate> localDates = new PriorityQueue<>(); localDates.add(LocalDate.MAX); localDates.add(LocalDate.MIN); localDates.add(LocalDate.of(1903, 12, 10)); localDates.add(LocalDate.of(1902, 12, 10)); localDates.add(LocalDate.of(1903, 11, 10)); System.err.println("打印"); for (LocalDate localDate : localDates) { System.err.println(localDate); } System.err.println("删除"); /** * 只取出最小的删除,无论如何操作。 */ while (!localDates.isEmpty()) { System.err.println(localDates.remove()); } } }
映射
有时集合并不是很适合,例如我们知道某些键的信息,但是想查询与键对应的值。映射就是为了解决这类问题或场景而设计的。
-
映射基本用法
映射的两个通用实现分别为:HashMap,TreeMap。名字已经很清楚了。一个时散列映射,一个是树映射。散列只能将键散列,树映射也是对键的整体顺序排序。
映射中有一些常用的方法,例如:
get(k),put(k,v),remove(k),size(),forEach()
,映射中键是唯一的,同一个键重复赋值,后一次的赋值会覆盖掉前一次的赋值。需要注意的就是如果代码中有更新映射的需求,需要先判断这个键是否存在,在执行对应的逻辑,代码中很容易就实现了,这里不在列举出详细代码。
使用起来也是比较简单,这个还是比较常用的,代码如下。
Map<String, String> stringStringMap = new HashMap<>(); stringStringMap.put("1", "A"); stringStringMap.put("2", "B"); stringStringMap.put("3", "C"); stringStringMap.put("4", "D"); System.err.println(stringStringMap); stringStringMap.remove("4"); stringStringMap.put("1", "AA"); System.err.println(stringStringMap.get("1")); /** * 还是需要再了解和使用lambda表达式 */ stringStringMap.forEach((k, v) -> System.err.println("key" + k + "===" + "value" + v));
-
视图和其他映射
视图就是将映射中的元素存放到集合中(官方认为映射不是一个集合)。有三种映射,键集、值集合和键/值对集。需要注意的时键集中删除某个键,映射中也会删除这个键和其对应的值,但是添加则不行。
弱散列映射,当对键的唯一引用来自于散列条目时,这一数据结构将与垃圾回收器协同工作一起删除键/值对。WeakHashMap使用弱引用来保存键。弱引用其实是将强引用包装,想要获得该对象需要用get方法,具体这里的实现原理可以看一下WeakHashMap的源码,这里水平太差,估计要放到后面留个坑了。
链接散列集与映射,类似于一个双向链表,调用get和put时,会影响该元素在映射中的位置。其实可以修改这个映射,将用的多的元素放到缓存中。这里不再列举代码了。
EnumSet
是枚举元素集,实现是用位序列,如果对应的值在集中,则相应的位置被置位1。标志散列映射,
IdentityHashMap
类比较特殊,键的散列值不是用hashCode()
计算的,是使用identityHashCode()
计算的,这个方法和hashCode()
的区别看一看看源码,大概就是比较时一个使用的是==
,一个是equals()
。
视图和包装器
这一部分感觉设计到设计模式,这一块目前还不是很熟悉,很多用法不知道为什么这样设计以及这样做的好处。就简单介绍记录一下吧。
视图有几种,子范围的视图、不可修改的视图、同步视图、受查视图。作用名称已经很清楚了。
算法
上面也说过了,映射和集合是一种数据结构,自然离不开算法,看到现在也大致了解了,这些的集合和映射的实现离不开算法。但是感觉现在自己学起来还是比较空,就再给自己留个坑吧。
就这样吧,结束。