什么是Collection
首先打开Collection这个类的源代码,查看第一段话
The root interface in the collection hierarchy. A collection
represents a group of objects, known as its elements. Some
collections allow duplicate elements and others do not. Some are ordered
and others unordered. The JDK does not provide any direct
implementations of this interface: it provides implementations of more
specific subinterfaces like Set and List. This interface
is typically used to pass collections around and manipulate them where
maximum generality is desired.
翻译一下大概意思就是:
当前的类是 Collection 继承体系当中的根接口。
一个 Collection 代表一组对象,每个对象被称之为它的元素。
有一些集合允许有重复的元素出现,有一些则不允许。
有一些集合是有序的,有一些是无序的。
JDK不提供对这个接口的直接实现,但是提供了一些子接口的实现,例如 Set、List。
这个接口通常在一些比较通用的地方,当作参数传递时使用。
Collection 接口就是Java中所说的多态的一种实现,它只给出了一些集合需要用到的方法,自己不提供实现,由具体的实现子类去实现,如下图,都是Collection定义的方法,
看方法名字大概就知道是在做什么了,这也是代码命名规范带来的好处。
add是添加一个元素。
addAll是将另外一个集合的元素全部添加到当前集合里来。
remove是删除一个元素。
removeAll是从当前集合删除参数集合中包涵的元素。
更多读者可以直接去阅读这个类的源代码注释,源代码注释文档写的很详细。
List
An ordered collection (also known as a sequence). The user of this
interface has precise control over where in the list each element is
inserted. The user can access elements by their integer index (position in
the list), and search for elements in the list.
List 是 Collection 集合下的子接口, 相对于Collection,List接口提供了可以通过下标索引对元素进行查找和保存的方法一系列方法。
ArrayList
ArrayList是List接口的一个具体实现类,我们使用这个类可以直接进行元素的增删改查操作,例如:
List<String> strArr = new ArrayList<String>();
strArr.add("1");
strArr.add("2");
strArr.add("3");
...一直无限添加
问题,上面的代码中 ArrayList 对象真的可以无限添加输入吗,是怎么做到的?
ArrayList的本质其实就是数组,在它的类中有一个Object[] elementData的属性,这个属性用来保存通过add等方法不停添加进来的元素,当 elementData 数组长度不够用的时候,就创建一个更大的数组,把之前数组的数据都拷贝过去,然后将最新的数据添加到新数组里面去,这种方式叫做扩容。
// 自己写的代码
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("1");
}
// ArrayList的add实现方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// 调用add方法时 ArrayList的扩容方法
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 调用add方法时 ArrayList的扩容方法
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 这里算出当前对象的数组是不是初始化时的默认的,
// 如果是,则在 minCapacity 和 DEFAULT_CAPACITY中取出一个最大的作扩容数组的长度。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 调用add方法时 ArrayList的扩容方法
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
// 这里如果+1后的最小容量大于等于当前数组里的长度时,将执行真正的扩容操作
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 调用add方法时 ArrayList的扩容方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 默认按照1.5倍扩容
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 执行将老数据拷贝到一个新数组,并将新数组赋值给老数组变量操作
// 这一步是最关键的地方
elementData = Arrays.copyOf(elementData, newCapacity);
}
我们可以查看ArrayList源码中的 grow 方法,这个方法则是最关键的地方,新老数组的接替就在这里,ArrayList帮我们做了对数组直接操作的封装,
我们只需要调用add方法就可以不停的添加元素了,这就是封装的好处之一,正常情况下,ArrayList是可以添加符合你业务场景下的多个对象的,当然如果不合理的使用会造成内存溢出,只要内存不溢出,可以一直往里面添加元素。
Set
A collection that contains no duplicate elements. More formally, sets
contain no pair of elements e1 and e2 such that
e1.equals(e2), and at most one null element. As implied by
its name, this interface models the mathematical set abstraction.
Set 是一个不允许用重复相等的元素的集合,且只会包含一个null,适合用于存储不重复元素的场景或者需要高效率的查找元素。
HashSet
HashSet是Set接口的实现子类之一,这个类除了实现了Set的特性外,并且不保证元素的存储是按照顺序进行存储的。
与List效率对比之快速查找示例
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
HashSet<Integer> set = new HashSet<>();
for (int i = 0; i < 1000_0000; i++) {
list.add(i);
set.add(i);
}
// list查找元素
Date listStartTime = new Date();
list.contains(9999_9999);
Date listEndTime = new Date();
System.out.println((listEndTime.getTime() - listStartTime.getTime()) + "ms");
// set查找元素
Date setStartTime = new Date();
set.contains(9999_9999);
Date setEndTime = new Date();
System.out.println((setEndTime.getTime() - setStartTime.getTime()) + "ms");
}
list用了34毫秒,set仅仅用了不到1毫秒的时间,这就是效率的差距。
存储唯一性示例
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
HashSet<Integer> set = new HashSet<>();
for (int i = 0; i < 10; i++) {
list.add(1);
set.add(1);
}
System.out.println(list);
System.out.println(set);
}
可以看到,list里有十个1,但是Set里只有一个。
为什么HashSet比ArrayList快那么多之HashCode
HashCode在Java中的约定:
1. 同一个对象必须始终返回相同的HashCode
2. 两个对象的equals返回true,必须返回相同的HashCode
3. 两个对象不等,也可能返回相同的HashCode
为什么要有HashCode?说到这里我们举一个例子,
如果一个HashSet中有一百万个元素,这时候新增一个的时候,如果去判断是否重复?
就需要循环这一百万个元素一个一个去对比,效率非常低下,
如果我们将这一百万个元素分到相应的哈希桶里面去,例如张三、张四、张五分配到姓张的桶里,姓李的就放到姓李的桶里去,这样下次来一个姓张的我就去姓张的桶里找,姓李的就去姓李的里面找,这样就效率就会快很多,
Java中的HashCode就相当于把一个对象放到了一个桶里去,可以根据对象去找到他的HashCode桶,在桶里判断是否有内存地址和自己相等或者值相等的对象,就可以极大效率的判断是否存在和自己一样的对象了。
无序性存储示例
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
set.add("3");
set.add("9");
set.add("7");
set.add("13");
set.add("1");
System.out.println(set);
}
可以看到,元素并没有按照我们添加的顺序进行输出。
Map
An object that maps keys to values. A map cannot contain duplicate keys;
each key can map to at most one value.
This interface takes the place of the Dictionary class, which
was a totally abstract class rather than an interface.
Map是一个由键映射到值的对象,一个map不允许有重复的键,每个键最多可以映射到一个值。
Map 这个接口替换了 Dictionary 这个类。
Map与Dictionary的区别是,Map一个key只能映射到一个value,但是Dictionary可以映射到多个value。
当然Map只是一个接口,具有很多种实现,下面说一下最常用的实现HashMap。
HashMap
Hash table based implementation of the Map interface.
This implementation provides all of the optional map operations, and permits null values and the null key.
The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.
This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.
HashMap是一个基于Hash表的Map接口实现。
他实现了map中提供的所有操作,并且允许null作为key或者value。
HashMap和Hashtable基本是相同的,只是HashMap不是同步的(线程不安全),并且允许有null值。
HashMap不保证映射数据的保存顺序,而且要注意,随着时间的变化,原本的顺序也可能发生变化。
HashMap通过put(K key, V value)保存一个元素
public static void main(String[] args) {
Map<String,String> hashMap = new HashMap<>();
hashMap.put("A","1");
hashMap.put("B","2");
System.out.println(hashMap);
}
HashMap通过get(Object key)获取一个元素
public static void main(String[] args) {
Map<String,String> hashMap = new HashMap<>();
hashMap.put("A","1");
hashMap.put("B","2");
System.out.println(hashMap);
// get获取
System.out.println(hashMap.get("A"));
}
HashMap的keySet()
Returns a Set view of the keys contained in this map.
The set is backed by the map, so changes to the map are reflected in the set, and vice-versa.
keySet() 方法返回一个Set集合,集合中是当前map的key,对这个Set的更改会影响到当前map,反之亦然。(因为Set是不允许重复的,map中的key也是不允许重复的,所以key采用Set存储。)
public static void main(String[] args) {
Map<String,String> hashMap = new HashMap<>();
hashMap.put("A","1");
hashMap.put("B","2");
// 接收map的key set
Set<String> keySet = hashMap.keySet();
// 输出结果
System.out.println(keySet);
// 从map中删除一个key
hashMap.remove("A");
// 此刻输出结果,发现Set中少了一个A
System.out.println(keySet);
}
从上面的代码和结果来验证keySet方法返回的Set对象,只要map的key做了CRUD操作,这个Set对象也会随之更改。
HashMap的entrySet()
public static void main(String[] args) {
Map<String,String> hashMap = new HashMap<>();
hashMap.put("A","1");
hashMap.put("B","2");
// 循环键值对
for (Map.Entry<String, String> entry : hashMap.entrySet()) {
// 输出key和value
System.out.println("key: " + entry.getKey() + " value:" + entry.getValue());
}
}
entrySet和keySet相同,对map的更改也会影响到他,只是keySet方法只返回key,entrySet方法把key和value都返回了。
HashMap常见的面试题
- HashMap的扩容
HashMap的扩容和ArrayList相似,就是创建一个更大的数组,将原本数组里的值放进去,并且拥有足够的空间后,将最新的值放进去。
- HashMap的线程不安全性
HashMap在多线程环境下,同时进行resize扩容时,会出现死循环问题,所以并发环境下可以使用ConcurrentHashMap。
- HashMap在1.7+之后的改变
在Java1.7+版本之后,Hash桶由链表变成了红黑树。
原因是一个HashMap中,存在十万个桶,这时候我存放100万的数据,但是由于这个100万个数据返回的HashCode都是相同的,所以都被存放在了一个桶中,
这时候Hash桶的优势就没有了,同一个桶中在1.7之前是用链表存储的,所以这时候在100万个对象中查找一个,效率就非常的低,
所以在1.7+之后,JDK将原本的链表改为红黑树。
Set的排序
首先我们先编写一块代码,然后我们根据输出的结果进行分析总结:
public static void main(String[] args) {
List<Integer> list = Arrays.asList(10000, 196, -2 , -334422 , 23332 , 16);
Set set1 = new HashSet();
Set set2 = new LinkedHashSet();
Set set3 = new TreeSet();
for (Integer i : list) {
set1.add(i);
set2.add(i);
set3.add(i);
}
set1.forEach(System.out::println);
System.out.println("-------------");
set2.forEach(System.out::println);
System.out.println("-------------");
set3.forEach(System.out::println);
}
HashSet 存储顺序随机
HashSet的输出结果:
10000
-334422
16
-2
196
23332
这里可以看到,HashSet的输出结果完全是无序的,没有规则的,随机打乱。
LinkedHashSet 和插入的先后顺序一致
LinkedHashSet的输出结果:
10000
196
-2
-334422
23332
16
可以看到这里和我们在代码里的添加顺序是一致的。
TreeSet 有序存储
TreeSet的输出结果:
-334422
-2
16
196
10000
23332
TreeSet则是由从小到大进行了排序,特别说明TreeMap和TreeSet是一样的,只是TreeMap存储的是键值对。
红黑树简介(是二叉树的一个分支)
首先看下ArrayList的存储结构:
ArrayList本质是一个很大的数组,如果我们想到上百万的数据中查找一个元素,就得循环去对比查找。
这个查找的复杂度在算法中称为线性复杂度O(n),如果有n个数字最坏的结果要查找n次,这个成本是和规模的增加成正比的。
下面看下树的存储结构:
树的子节点,左边都比上级小,右边都比上级大。
在一个数组中,1、2、3、4、5、6,6个元素中,如果用数组形式查找6,则需要找6次,
但是如果用上面的树的话,第一次找到3这个根节点,和6对比,发现根节点比6小,就去根节点的右边查找,
随后找到5,发现5还是比6小,就继续去右边找,最后找到6,查找次数为3,效率的对比就出来了。
所以用树的话,算法复杂度就从O(n)变成了O(log n),也就是把复杂度由线性时间变成了对数时间。
如果在树中插入一个值为0的元素,那么树会保持左小右大的原则,如下:
Collection工具方法集合
先说一个小知识,在java的集合中,如果你想搜索Collection相关的工具类,就搜索Collections,
如果是Set,就搜索Sets,在集合类名称后面添加一个s,就是它的工具类,这是一个小规范。
-
Collections.emptySet():返回一个空集合
-
Collections.synchronizedCollection():将一个集合改为线程安全的
-
Collections.unmodifiableCollection():将一个集合改为不可变的(也可以使用Guava的Immutable)
Collection的其他实现
- Queue/Deque
Queue(队列)
A collection designed for holding elements prior to processing.
Besides basic Collection operations, queues provide additional insertion, extraction, and inspection operations.
Queue是用来存储一系列带有优先级元素的集合。
除了提供Collection的基本操作以外,还提供了插入、提取、检查等操作。
队列是一种常见的数据结构,例如去排队买票,第一个进去的先买到票也是第一个出来,
所以队列就是这种模式,先进先出(LILO: Last in Last out),先进入的元素第一个被处理,后面则进行排队。
Deque(双端队列)
A linear collection that supports element insertion and removal at both ends.
The name deque is short for "double ended queue" and is usually pronounced "deck".
Deque是一个线性的集合,支持在两端,头部尾部进行新增和删除的操作。
他的名字是双端队列的缩写。
- Vector/Stack (已被JDK抛弃使用)
Vector
If a thread-safe implementation is not needed, it is recommended to use ArrayList in place of Vector.
Vector是ArrayList的前身,现在JDK已经不推荐使用他,而是推荐ArrayList。
Stack
A more complete and consistent set of LIFO stack operations is
provided by the {@link Deque} interface and its implementations,
which should be used in preference to this class. For example:
Deque<Integer> stack = new ArrayDeque<Integer>();
Stack是一个支持先进后出的队列,不过现在JDK也不支持了,推荐使用Deque。
- LinkedList
LinkedList是一种链表的实现,和ArrayList的区别是,ArrayList使用数组排列他们的先后顺序,
LinkedList是前一个元素会指向到下一个元素,每个元素都保存着下一个元素的位置,所以叫做链表。
- ConcurrentHashMap
ConcurrentHashMap是HashMap的线程安全实现。
- PriorityQueue
An unbounded priority Queue queue based on a priority heap.
基于堆实现的无边界优先级队列。
这个队列好比手机里的闹铃,是一个列表,比如现在是早上7点,有一个9点的闹钟在列表中在最后一位,其他的都是下午的闹铃,
但是到了9点以后,还是9点的闹铃最先响,因为它的优先级在当前环境是最高的,和存放顺序无关。
Guava
Guava是由谷歌开发的一组核心Java类库,里面对Java原生集合做了很多填充和拓展,
感兴趣的读者可以去github了解一下:github.com/google/guav…
首先在Maven项目中引入jar包
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
Multiset
public static void main(String[] args) {
Multiset multiset = HashMultiset.create();
multiset.add(1);
multiset.add(2);
multiset.add(3);
multiset.add(3);
System.out.println(multiset);
}
如果是HashSet的话,输出的结果就是1,2,3,但是Multiset输出的是 [1, 2, 3 x 2] ,
因为Multiset会帮我们统计相同的对象共放入了多少次,这就是Guava帮我们拓展的Set集合。
Multimap
public static void main(String[] args) {
Multimap multimap = HashMultimap.create();
multimap.put(1,1);
multimap.put(1,2);
multimap.put(1,3);
System.out.println(multimap);
}
传统的map一个key只能保存一个值,Multimap可以一个key对应多个值。
BiMap
public static void main(String[] args) {
BiMap biMap = HashBiMap.create();
biMap.put(1,"我是1");
System.out.println(biMap.inverse().get("我是1"));
}
传统的map只能通过key查找value,BiMap可以通过value去查找key。
以上是几个Guava典型的例子,其他的读者自行去发现。
总结
数据结构和算法无处不在,Java以及Java第三方库中,对数据结构的实现算法都非常的优秀,本文做一个抛砖引玉的文章,希望读者可以多去发现Java体系中其他数据结构的实现,以及思考其中的原理,加固自身这方面的知识。