一、集合类简介
数组是很常用的一种的数据结构,我们用它可以满足很多的功能,但是,有时我们会遇到如下这样的问题:
1、我们需要该容器的长度是不确定的。
2、我们需要它能自动排序。
3、我们需要存储以键值对方式存在的数据。
如果遇到上述的情况,数组是很难满足需求的,接下来本章将介绍另一种与数组类似的数据结构——集合类,集合类在Java中有很重要的意义,保存临时数据,管理对象,泛型,Web框架等,很多都大量用到了集合类。
常见的集合类有这些种:
实现Collection接口的:Set、List以及他们的实现类。
实现Map接口的:HashMap及其实现类,我们常用的有Map及其实现类HashMap,HashTable,List、Set及其实现类ArrayList、HashSet,因为集合类是很大的一块内容,我们不方便把它的全部内容写出来,只能慢慢的增加,希望各位读者有自己想法的,踊跃向我提出,我们共同打造精美的博客,供广大编程爱好者学习,下面我我们通过一个图来整体描述一下:
实现Collection接口的类,如Set和List,他们都是单值元素(其实Set内部也是采用的是Map来实现的,只是键值一样,从表面理解,就是单值),不像实现Map接口的类一样,里面存放的是key-value(键值对)形式的数据。这方面就造成他们很多的不同点,如遍历方式,前者只能采用迭代或者循环来取出值,但是后者可以使用键来获得值得值。
二、基本方法及使用
实现Map接口的
HashMap
Set的实现类HashSet,底层还是调用Map接口来处理,所以,此处,我将说下Map接口及其实现类的一些方法。Map接口中的原始方法有:
public abstract int size();
public abstract boolean isEmpty();
public abstract boolean containsKey(Object paramObject);
public abstract boolean containsValue(Object paramObject);
public abstract V get(Object paramObject);
public abstract V put(K paramK, V paramV);
public abstract V remove(Object paramObject);
public abstract void putAll(Map<? extends K, ? extends V> paramMap);
public abstract void clear();
public abstract Set keySet();
public abstract Collection values();
public abstract Set<Entry<K, V>> entrySet();
public abstract boolean equals(Object paramObject);
public abstract int hashCode();
此处细心的读者会看到,每个方法前都有abstract关键字来修饰,其实就是说,接口中的每个方法都是抽象的,有的时候我们写的时候不加abstract关键字,但是在编译的过程中,JVM会给加上的,所以这点要注意。抽象的方法意味着它没有方法实现体,同时必须在实现类中重写,接下来我们依次分析一下他们的实现类HashMap中是怎么做的。
重点方法介绍:
首先是构造方法,大多数情况下,我们采取无参的构造函数来构造哈希表,
public HashMap() {
this.entrySet = null;
this.loadFactor = 0.75F;
this.threshold = 12;
this.table = new Entry[16];
init();
}
此处先介绍三个重要的变量:loadFactor、threshold、table,loadFactor是一个加载因子,threshold是临界值,table说明哈希表的底层,其实是个数组。可以看看他们的声明方式:
transient Entry[] table;;
int threshold;
final float loadFactor;
细心的读者似乎又发现一个新问题,为什么table前面采用的是transient关键字呢,那我们得闲来研究下声明为transient关键字的含义:变量如果被声明为transient类型的话,那么在序列化的时候,忽略其的值,就是说此处的table,如果将要进行持久化的话,是不会对table的值进行处理的,直接忽略,为什么此处table会这样处理呢?因为HashMap的存储结构,其实就是一个数组+多个链表,数组里存放对象的地址,链表存放数据,所以对地址进行持久化是没有任何意义的。
HashMap的初始容量为0,每增加一对值,容量曾1,这点好理解,我们通过一个小的例子,来看看HashMap的基本使用方法。
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/*
* HashMap的使用
*/
public class MapTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
/*
* 初始化Map
*/
Map<String, Integer>map=new HashMap<String,Integer>();
System.out.println("HashMap的长度:"+map.size());
System.out.println("HashMap是否为空:"+(map.isEmpty()?"是":"否"));
/*
* 像map中添加元素
*/
map.put("haosiyuan", 1);
map.put("zhangsan", 2);
map.put("lisi", 3);
System.out.println(map.size());
System.out.println("HashMap是否为空:"+(map.isEmpty()?"是":"否"));
/*
* 遍历HashMap中的元素
*/
Set<String>set=map.keySet();
for(String s:set) {
System.out.println(s+" "+map.get(s)+" "+"hashcode"+s.hashCode()+" ");
}
/*
* 检查是否有某个Key
*/
System.out.println(map.containsKey("haosiyuan"));
/*
* 检查是否有某个value
*/
System.out.println(map.containsValue(2));
/*
* 打印hashCode
*/
System.out.println(map.hashCode());
}
}
此处附一个利用HashMap来简单处理问题的例子,需求在注释中已经给出,希望读者好好看看,代码不难,但是很多的面试及面试题都用到这个思路,笔者曾经面试的时候,经常会被问题这题的思想,但是就是没有去亲自实现一下,以致在hashmap的操作上被难住了。
/*
* 打印在数组中出现n/2以上的元素
* 利用一个HashMap来存放数组元素及出现的次数
*/
public class MapTest {
public static void main(String[] args) {
int[] a = { 2, 3, 2, 2, 1, 4, 2, 2, 2, 7, 9, 6, 2, 2, 3, 1, 0 };
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (int i = 0; i < a.length; i++) {
if (map.containsKey(a[i])) {
int tmp = map.get(a[i]);
tmp += 1;
map.put(a[i], tmp);
} else {
map.put(a[i], 1);
}
}
Set<Integer> set = map.keySet();
for (Integer s : set) {
if (map.get(s) >= a.length / 2) {
System.out.println(s);
}
}
}
}
实现Collection接口的
ArrayList
后面的博文,我会以分析和例子为主,毕竟源码这种东西,不适合大量的贴出来,大家都会自己去看,有些不好懂的地方或者容易忽略的知识,我会贴出来,其它的建议读者自己去看JDK源码。ArrayList底层采用数组实现,具有较高的查询速度。
boolean isEmpty():如果容器里面没有保存任何元素,就返回true。
boolean contains(Object):如果容器持有参数Object,就返回true。
Iterator iterator():返回一个可以在容器的各元素之间移动的Iterator。
Object[] toArray():返回一个包含容器中所有元素的数组。
Object[] toArray(Object[] a):返回一个包含容器中所有元素的数组,且这个数组不是普通的Object数组,它的类型应该同参数数组a的类型相同(要做类型转换)。
void clear():清除容器所保存的所有元素。(“可选”)
boolean remove(Object o);
boolean add(Object):确保容器能持有你传给它的那个参数。如果没有把它加进去,就返回false。(这是个“可选”的方法,本章稍后会再作解释。)
boolean addAll(Collection):加入参数Collection所含的所有元素。只要加了元素,就返回true。
boolean containsAll(Collection):如果容器持有参数Collection所含的全部元素,就返回true
boolean removeAll(Collection):删除容器里面所有参数Collection所包含的元素。只要删过东西,就返回true。(“可选”)
boolean retainAll(Collection):只保存参数Collection所包括的元素(集合论中“交集”的概念)。如果发生过变化,则返回true。(“可选”)
boolean equals(Object o);
int hashCode();
int size():返回容器所含元素的数量。
这些方法也就是Set和List及它们的实现类里有的方法。
List接口在Collection接口的基础上,有添加了自己的一系列方法:
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
ListIterator listIterator();
ListIterator listIterator(int index);
List subList(int fromIndex, int toIndex);
先看一下构造函数,和Map一样,我们同样习惯使用默认的无参构造函数,
private transient Object[] elementData;
private int size;
public ArrayList(int paramInt){
if (paramInt < 0)
throw new IllegalArgumentException("Illegal Capacity: " + paramInt);
this.elementData = new Object[paramInt];
}
public ArrayList(){
this(10);
}
此处elementData就是它底层用来存放数据的数组元素,仔细看一下,无论采用无参还有有参的构造
函数,最终都归结于一句话:this.elementData = new Object[paramInt];如果没有传入参数的话,会默认开辟一个10个字节大小的空间,可是当我们用的时候,我们写下如下的语句:
List list = new ArrayList( );
List list = new ArrayList(5);
当我们输出它的size值时:System.out.println(list.size());我们发现,输出的都是0.这让人貌似有一丝迷惑,明明是10或者5,这儿应该用清楚,elementData数组的长度并不是size的值,size是里面元素的个数,上面的10或者是5,意思是向内容开辟10个大小的空间,初始化的时候开辟一定数量的内存,但是里面并没有放任何对象,所以用size()计算得到的结果仍为0.
这时,我们又有新问题了,因为我们知道List是可以自动扩容的,这个功能就取决于如下的方法:
public void ensureCapacity(int paramInt){
this.modCount += 1;
int i = this.elementData.length;
if (paramInt > i) {
Object[] arrayOfObject = this.elementData;
int j = i * 3 / 2 + 1;
if (j < paramInt)
j = paramInt;
this.elementData = Arrays.copyOf(this.elementData, j);
}
}
ensureCapacity(int paramInt)用来初始化或者扩大ArrayList的空间。
从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现。代码如下:
public void trimToSize(){
this.modCount += 1;
int i = this.elementData.length;
if (this.size < i)
this.elementData = Arrays.copyOf(this.elementData, this.size);
}
通过ensureCapacity(int paramInt)方法可以提高ArrayList的初始化速度,请看下面的代码:
import java.util.ArrayList;
public class EnsureCapacityTest {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
final int N = 1000000;
Object obj = new Object();
/*没用调用ensureCapacity()方法初始化ArrayList对象*/
ArrayList list = new ArrayList();
long startTime = System.currentTimeMillis();
for (int i = 0; i <= N; i++) {
list.add(obj);
}
long endTime = System.currentTimeMillis();
System.out.println("没有调用ensureCapacity()方法所用时间:"
+ (endTime - startTime) + "ms");
/*调用ensureCapacity()方法初始化ArrayList对象*/
list = new ArrayList();
startTime = System.currentTimeMillis();
// 预先设置list的大小
list.ensureCapacity(N);
for (int i = 0; i <= N; i++) {
list.add(obj);
}
endTime = System.currentTimeMillis();
System.out.println("调用ensureCapacity()方法所用时间:" + (endTime - startTime)
+ "ms");
}
}
很明显,使用ensureCapacity()能提高不少效率!
下面实现一个简单的例子,来展现下ArrayList的基本使用。
public class ListTest {
public static void main(String[] args) {
/* 新建一个ArrayList */
ArrayList<String> list = new ArrayList<String>();
System.out.println("初始化大小:" + list.size());
/* 添加元素 */
list.add("zzz");
list.add("egg");
list.add("hell");
list.add("child");
System.out.println("当前容量:" + list.size());
/* 将ArrayList的大小和实际所含元素的大小设置一致 */
list.trimToSize();
/* 遍历 */
for (String string : list) {
System.out.println(string);
}
/* 在指定位置插入元素 */
list.add(2, "zhu");
for (String string : list) {
System.out.println(string);
}
System.out.println("--------------");
/* 清空list */
list.clear();
/* 遍历 */
for (String string : list) {
System.out.println(string);
}
System.out.println("--------------");
}
}
ArrayList基于数组实现,所以它具备数组的特点,即查询速度较快,但是修改、插入的速度却有点儿慢,但是,下面将要介绍的LinkedList就是来解决这个问题的,LinkedList基于链表,与ArrayList互补,所以实际开发中我们应该按照自己的需求来定到底用哪一个。
Where there is a will,there is a way.With the boat burned,Qin’s territory final belonged to Chu.
The God won’t cheat the hard working people.As the undergo self-imposed hardships
so as to strengthen his resolve,three thousand soldiers from Yue destroyed the country of Wu!
LinkedList
LinkedList底层采用双向循环列表实现,进行插入和删除操作时具有较高的速度,我们还可以使用LinkedList来实现队列和栈。
这是LinkedList的原始存储模型,因为是双向循环列表,我们可以回忆一下数据结构中双向列表是什么情况:一个数据data,两个指针,一个指向前一个节点,名为previous,一个指向下一个节点,名为next,但是循环怎么体现了,来看下她的无参构造函数:头尾相等,就是说初始化的时候就已经设置成了循环的。仔细观察源码,不难理解,如果熟悉数据结构的读者,一定很快就能掌握她的原理。下面我简单分析一个操作,就是LinkedList的add()。读者可以通过这个自己去理解其他操作。
我们先来观察下上面给出的Entity类,构造方法有三个参数,第二个是她的next域,第三个是她的previous域,所以上述代码1行处将传进来的entry实体,即header对象作为newEntry的next域,而将entry.previous即header.previous作为previous域。也就是说在header节点和header的前置节点之间插入新的节点。看下面的图:
一目了然。
其他的道理一样,下面我会给出一个LinkedList使用的例子,需要注意的地方,我会特别说明。
比较简单,其他的方法,请读者自己去尝试,结合源码。
实现Map接口的
WeakHashMap
理解该集合类之前,建议先去了解Java的垃圾回收机制,WeakHashMap多用于缓存系统,就是说在系统内存紧张的时候可随时进行GC,但是如果内存不紧张则可以用来存放一些缓存数据。因为如果使用HashMap的话,它里面的值基本都是强引用,即使内存不足,它也不会进行GC,这样系统就会报异常。看一下WeakHashMap中Entry的实现:
Entry继承了WeakReference类,且在构造函数中构造了Key的弱引用,当进行put或者get操作时,都会调用一个函数叫expungeStaleEntries(),如下:
就是用来判断,如果key存在弱引用,则进行垃圾回收,所以这个就是WeakHashMap的工作原理。它与HashMap的区别就是:数据量大的时候,它可根据内存的情况,自动进行垃圾回收。如果手动将key置为强引用,那么它将和HashMap变得一样,失去其功能。
三、比较(性能,功能方面)
这一块主要就是对我们平时接触的这些集合类做一个简单的总结,一方面有助于自己整理思路,再者面试的时候,面试官总喜欢问一些他们之间的区别,凡是Java面试,几乎都要问到集合类的东西,问的形式有两种:一、总体介绍下集合类有哪些。这个问题只要把我上文中的图介绍一下就行了。二、比较一下XXX和XXXX。当然了,肯定包括相同点和不同的地方。这个稍微麻烦一点,需要我们彻底理解了,才能回答的比较准确。以下是我对常被比较的一些类的分析:
1、HashMap和HashTable
相同点:二者都实现了Map接口,因此具有一系列Map接口提供的方法。
不同点:
1、HashMap继承了AbstractMap,而HashTable继承了Dictionary。
2、HashMap非线程安全,HashTable线程安全,到处都是synchronized关键字。
3、因为HashMap没有同步,所以处理起来效率较高。
4、HashMap键、值都允许为null,HashTable键、值都不允许有null。
5、HashTable使用Enumeration,HashMap使用Iterator。
这些就是一些比较突出的不同点,实际上他们在实现的过程中会有很多的不同,如初始化的大小、计算hash值的方式等等。毕竟这两个类包含了很多方法,有很重要的功能,所以其他不同点,请感兴趣的读者自己去看源码,去研究。笔者推荐使用HashMap,因为她提供了比HashTable更多的方法,以及较高的效率,如果大家需要在多线程环境中使用,那么用Collections类来做一下同步即可。
2、Set接口和List接口
相同点:都实现了Collection接口
不同点:
1、Set接口不保证维护元素的顺序,而且元素不能重复。List接口维护元素的顺序,而且元素可以重复。
2、关于Set元素如何保证元素不重复,我将在下面的博文中给出。
3、ArrayList和LinkList
相同点:都实现了Collection接口
不同点:ArrayList基于数组,具有较高的查询速度,而LinkedList基于双向循环列表,具有较快的添加或者删除的速度,二者的区别,其实就是数组和列表的区别。上文有详细的分析。
4、SortedSet和SortedMap
二者都提供了排序的功能。 来看一个小例子:
输出:5 2 3 0 1
从结果看得出:SortedMap具有自动排序功能
6、HashSet和LinkedHashSet
HashSet,为快速查找而设计的Set。存入HashSet的对象必须实现hashCode()和equals()。
LinkedHashSet,具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序),于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。
7、TreeSet和HashSet
TreeSet: 提供排序功能的Set,底层为树结构 。相比较HashSet其查询速度低,如果只是进行元素的查询,我们一般使用HashSet。
8、ArrayList和Vector
同步性:Vector是线程安全的,也就是说是同步的,而ArrayList是线程序不安全的,不是同步的。
数据增长:当需要增长时,Vector默认增长为原来一培,而ArrayList却是原来的一半
9、Collection和Collections
Collection是一系列单值集合类的父接口,提供了基本的一些方法,而Collections则是一系列算法的集合。里面的属性和方法基本都是static的,也就是说我们不需要实例化,直接可以使用类名来调用。下面是Collections类的一些功能列表:
生成单元素集合Collections中的单元素集合指的是集合中只有一个元素而且集合只读。
Collections.singletonList——用来生成只读的单一元素的List
Collections.singletonMap——用来生成只读的单Key和Value组成的Map
Collections.singleton——用来生成只读的单一元素的Set
如下面的例子:
Collections.singletonMap(1, 1)生成一个单元素的map,如果加上1处的代码,会报异常。Checked集合Checked集合具有检查插入集合元素类型的特性,例如当我们设定checkedList中元素的类型是String的时候,如果插入其他类型的元素就会抛出ClassCastExceptions异常,Collections中提供了以下生成Checked集合的方法checkedCollection,checkedList,checkedMap,checkedSet,checkedSortedMap,checkedSortedSet同步集合Collections类提供一系列同步方法,为一些非线程安全的集合类提供同步机制。
查找替换fill——使用指定元素替换指定列表中的所有元素。
frequency——返回指定 collection 中等于指定对象的元素数。
indexOfSubList—— 返回指定源列表中第一次出现指定目标列表的起始位置,如果没有出现这样的列表,则返回 -1。
lastIndexOfSubList——返回指定源列表中最后一次出现指定目标列表的起始位置,如果没有出现这样的列表,则返回-1。
max—— 根据元素的自然顺序,返回给定 collection 的最大元素。
min——根据元素的自然顺序 返回给定 collection 的最小元素。
replaceAll——使用另一个值替换列表中出现的所有某一指定值。
附一个小例子:
集合排序
Collections还提供了集中对集合进行排序的方法。
reverse——对List中的元素进行转置
shuffle——对List中的元素随即排列
sort——对List中的元素排序
swap——交换List中某两个指定下标位元素在集合中的位置。
rotate——循环移动
输出:
排序前:
5 2 1 9 0
排序后
0 1 2 5 9
下面是关于rotate(List<?> list, int distance)的一个例子:
读者可以多次换换第二个参数,来观察它的变化。
总结一下,带Tree的集合类,底层一般是基于二叉树的,所以具有自动排序功能。有些功能方面差别不大,具体开发的时候需根据实际情况选择使用哪个类。
上文来源:http://blog.csdn.net/zhangerqing
来源:CSDN
作者:爱猫狗的小郝
链接:https://blog.csdn.net/qq_43518645/article/details/104075886