Java 容器
摘录来至https://www.cnblogs.com/LipeiNet/p/5888513.html
https://www.cnblogs.com/acm-bingzi/p/javaMap.html
前言:在java开发中我们肯定会大量的使用集合,在这里我将总结常见的集合类,每个集合类的优点和缺点,以便我们能更好的使用集合。下面我用一幅图来表示
其中淡绿色的表示接口,红色的表示我们经常使用的类。
1:基本概念
Java容器类类库的用途是保存对象,可以将其分为2个概念。
1.1:Collection
一个独立元素的序列,这些元素都服从一条或多条规则。其中List必须按照插入的顺序保存元素、Set不能有重复的元素、Queue按照排队规则来确定对象的产生顺序(通常也是和插入顺序相同)
1.2:Map
一组成对的值键对对象,允许用键来查找值。ArrayList允许我们用数字来查找值,它是将数字和对象联系在一起。而Map允许我们使用一个对象来查找某个对象,它也被称为关联数组。或者叫做字典。
2:List
List承诺可以将元素维护在特定的序列中。List接口在Collection的基础上加入了大量的方法,使得可以在List中间可以插入和移除元素。下面主要介绍2种List
2.1:基本的ArrayList
它的优点在于随机访问元素快,但是在中间插入和移除比较慢
那么现在我们就一起来看看为什么ArrayList随机访问快,而插入移除比较慢。先说关于ArrayList的初始化。
ArrayList有三种方式进行初始化如下
private transient Object[] elementData;
public ArrayList() { this(10); } public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); size = elementData.length; // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); }
我们可以看出ArrayList其实就是采用的是数组(默认是长度为10的数组)。所有ArrayList在读取的时候是具有和数组一样的效率,它的时间复杂度为1。
插入尾部就是elementData[size++] = e;当然中间会进行扩容。现在主要说插入中间为什么相对来说比较慢源码如下
public void add(int index, E element) { rangeCheckForAdd(index);//验证(可以不考虑) ensureCapacityInternal(size + 1); // Increments modCount!!(超过当前数组长度进行扩容) System.arraycopy(elementData, index, elementData, index + 1, size - index);(核心代码) elementData[index] = element; size++; }
System.arraycopy(elementData, index, elementData, index + 1)第一个参数是源数组,源数组起始位置,目标数组,目标数组起始位置,复制数组元素数目。那么这个意思就是从index索性处每个元素向后移动一位,最后把索引为index空出来,并将element赋值给它。这样一来我们并不知道要插入哪个位置,所以会进行匹配那么它的时间赋值度就为n。
2.2:LinkedList
它是通过代价较低在List中间进行插入和移除,提供了优化的顺序访问,但是在随机访问方面相对较慢。但是他的特性功能要比ArrayList强大的多。支持Queue和Stack
ListedList采用的是链式存储。链式存储就会定一个节点Node。包括三部分前驱节点、后继节点以及data值。所以存储存储的时候他的物理地址不一定是连续的。
我们看下它的中间插入实现
从代码我们可以看出先获取插入索引元素的前驱节点,然后把这个元素作为后继节点,然后在创建新的节点,而新的节点前驱节点和获取前驱节点相同,而后继节点则等于要移动的这个元素。所以这里是不需要循环的,从而在插入和删除的时候效率比较高。
我们在来看看查询(我们可以分析出它的效率要比ArrayList低了不少)
3:Set
Set也是一个集合,但是他的特点是不可以有重复的对象,所以Set最常用的就是测试归属性,很容易的询问出某个对象是否存在Set中。并且Set是具有和Collection完全一样的接口,没有额外的功能,只是表现的行为不同。
3.1:HashSet
HashSet查询速度比较快,但是存储的元素是随机的并没有排序,下面我写一段程序看一下
public static void main(String[] args){ /** * 没有顺序可循,这是因为hashset采用的是散列(处于速度考虑) */ Random random=new Random(47); Set<Integer> intset=new HashSet<Integer>(); for (int i=0;i<10000;i++){ intset.add(random.nextInt(30)); } System.out.print(intset); }
3.2:TreeSet
TreeSet是将元素存储红-黑树结构中,所以存储的结果是有顺序的(所以如果你想要自己存储的集合有顺序那么选择TreeSet)
public static void main(String[] args){ Random random=new Random(47); Set<Integer> intset=new TreeSet<Integer>(); for (int i=0;i<10000;i++){ intset.add(random.nextInt(30)); } System.out.print(intset); }
关于LinkedHashSet后面再说。
4:Queue
Queue是队列,队列是典型的先进先出的容器,就是从容器的一端放入元素,从另一端取出,并且元素放入容器的顺序和取出的顺序是相同的。LinkedList提供了对Queue的实现,LinkedList向上转型为Queue。其中Queue有offer、peek、element、pool、remove等方法
offer是将元素插入队尾,返回false表示添加失败。peek和element都将在不移除的情况下返回对头,但是peek在对头为null的时候返回null,而element会抛出NoSuchElementException异常。poll和remove方法将移除并返回对头,但是poll在队列为null,而remove会抛出NoSuchElementException异常,以下是例子
public static void main(String[] args){ Queue<Integer> queue=new LinkedList<Integer>(); Random rand=new Random(); for (int i=0;i<10;i++){ queue.offer(rand.nextInt(i+10)); } printQ(queue); Queue<Character> qc=new LinkedList<Character>(); for (char c:"HelloWorld".toCharArray()){ qc.offer(c); } System.out.println(qc.peek()); printQ(qc); List<String> mystrings=new LinkedList<String>(); mystrings.add("1"); mystrings.get(0); Set<String> a=new HashSet<String>(); Set<String> set=new HashSet<String>(); set.add("1"); } public static void printQ(Queue queue){ while (queue.peek
从上面的输出的结果我们可以看出结果并不是一个顺序的,没有规则的,这个时候如果想让队列按照规则输出那么这个时候我们就要考虑优先级了,这个时候我们就应该使用PriorityQueue,这个时候如果在调用offer方法插入一个对象的时候,这个对象就会按照优先级在对列中进行排序,默认的情况是自然排序,当然我们可以通过Comparator来修改这个顺序(在下一篇讲解)。PriorityQueue可以确保当你调用peek、pool、remove方法时,获取的元素将是对列中优先级最高的元素。ok我们再次通过代码查看
public static void main(String[] args) { PriorityQueue<Integer> priorityQueue = new PriorityQueue<Integer>(); Random rand = new Random(); for (int i = 0; i < 10; i++) { priorityQueue.offer(rand.nextInt(i + 10)); } QueueDemo.printQ(priorityQueue); List<Integer>ints= Arrays.asList(25,22,20,18,14,9,3,1,1,2,3,9,14,18,21,23,25); priorityQueue=new PriorityQueue<Integer>(ints); QueueDemo.printQ(priorityQueue); }
从输出可以看到,重复是允许的,最小值拥有最高优先级(如果是String,空格也可以算作值,并且比字母具有更高的优先级)如果你想消除重复,可以采用Set进行存储,然后把Set作为priorityQueue对象的初始值即可。
5:Map
Map主要用于存储健值对,根据键得到值,因此不允许键重复(重复了覆盖了),但允许值重复。
HashMap
HashMap 是一个最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。遍历时,取得数据的顺序是完全随机的。
HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null。
HashMap不支持线程的同步(即任一时刻可以有多个线程同时写HashMap),可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。
Hashtable与 HashMap类似,它继承自Dictionary类。不同的是:它不允许记录的键或者值为空;它支持线程的同步(即任一时刻只有一个线程能写Hashtable),因此也导致了 Hashtable在写入时会比较慢。
LinkedHashMap
LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。也可以在构造时带参数,按照应用次数排序。
在遍历的时候会比HashMap慢,不过有种情况例外:当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢。因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。
TreeMap
TreeMap实现SortMap接口,能够把它保存的记录根据键排序。
默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。
三种类型分别在什么时候使用
1、一般情况下,我们用的最多的是HashMap。HashMap里面存入的键值对在取出的时候是随机的,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map 中插入、删除和定位元素,HashMap 是最好的选择。
2、TreeMap取出来的是排序后的键值对。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。
3、LinkedHashMap 是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列,像连接池中可以应用。
其他
1. HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的,只不过Set用的只是Map的key
2. Map的key和Set都有一个共同的特性就是集合的唯一性.TreeMap更是多了一个排序的功能.
3. hashCode和equal()是HashMap用的, 因为无需排序所以只需要关注定位和唯一性即可.
a. hashCode是用来计算hash值的,hash值是用来确定hash表索引的.
b. hash表中的一个索引处存放的是一张链表, 所以还要通过equal方法循环比较链上的每一个对象才可以真正定位到键值对应的Entry.
c. put时,如果hash表中没定位到,就在链表前加一个Entry,如果定位到了,则更换Entry中的value,并返回旧value
4. 由于TreeMap需要排序,所以需要一个Comparator为键值进行大小比较.当然也是用Comparator定位的.
a. Comparator可以在创建TreeMap时指定
b. 如果创建时没有确定,那么就会使用key.compareTo()方法,这就要求key必须实现Comparable接口.
c. TreeMap是使用Tree数据结构实现的,所以使用compare接口就可以完成定位了.
注意:
1、Collection没有get()方法来取得某个元素。只能通过iterator()遍历元素。
2、Set和Collection拥有一模一样的接口。
3、List,可以通过get()方法来一次取出一个元素。使用数字来选择一堆对象中的一个,get(0)...。(add/get)
4、一般使用ArrayList。用LinkedList构造堆栈stack、队列queue。
5、Map用 put(k,v) / get(k),还可以使用containsKey()/containsValue()来检查其中是否含有某个key/value。
HashMap会利用对象的hashCode来快速找到key。哈希码就是将对象的信息经过一些转变形成一个独一无二的int值,这个值存储在一个array中。我们都知道所有存储结构中,array查找速度是最快的。所以,可以加速查找。 发生碰撞时,让array指向多个values。即,数组每个位置上又生成一个梿表。
6、Map中元素,可以将key序列、value序列单独抽取出来。
使用keySet()抽取key序列,将map中的所有keys生成一个Set。
使用values()抽取value序列,将map中的所有values生成一个Collection。
为什么一个生成Set,一个生成Collection?那是因为,key总是独一无二的,value允许重复。
6:Iterator和Foreach
现在foreach语法主要作用于数组,但是他也可以应用于所有的Collection对象。Collection之所以能够使用foreach是由于继承了Iterator这个接口。下面我写段代码供大家查看
public class IteratorClass { public Iterator<String> iterator(){ return new Itr(); } private class Itr implements Iterator<String>{ protected String[] words=("Hello Java").split(" "); private int index=0; public boolean hasNext() { return index<words.length; } public String next() { return words[index++]; } public void remove() { } } }
Iterator iterators=new IteratorClass().iterator(); for (Iterator it=iterator;iterators.hasNext();) { System.out.println(iterators.next()); } while (iterators.hasNext()){ System.out.println(iterators.next()); }
从中我们可以看出foreach循环最终是转换成 for (Iterator it=iterator;iterators.hasNext();)只不过jdk帮我们隐藏我们无法查看。下面我们在来分析一个问题,关于List删除问题。我们大多肯定使用过for循环或者foreach循环去删除,但是结果很明显会出现错误,那么现在我们一起分析为啥会出现错误。
1:使用for循环删除(出现错误分析)
2:foreach循环删除(错误分析)
从上面我们得知foreach最终是会转成Iterator的所以它首先会通过next来获取元素,我们看代码
请看for循环删除那段代码,没删除一次modCount会++,所以第二次在次删除的时候modCount由于增加和expectedModCount不等所以无法获取元素也就无法删除。
3:正确的删除方式
采用迭代器代码如下
Iterator<String> iterator=userList.iterator(); while (iterator.hasNext()){ iterator.next(); iterator.remove(); }
请记住一定要加上iterator.next();这是因为在源码中有一个lastRed,通过它来记录是否是最后一个元素,如果不加上iterator.next()那么lastRed=-1,在删除验证的时候有这么一段代码if (lastRet < 0)throw new IllegalStateException();所以就会抛出异常。
7:Collections和Arrays
这里只介绍2个常用的Collections.addAll和Arrays.asList
addAll:
asList采用的是数组
可以看出最终转换成ArrayList。
8:总结
1):数组是将数字和对象联系起来,它保存明确的对象,查询对象时候不需要对查询结果进行转换,它可以是多维的,可以保存基本类型的数据,但是数组一旦生成,其容量不能改变。所以数组是不可以直接删除和添加元素。
2):Collection保存单一的元素,而Map保存相关联的值键对,有了Java泛型,可以指定容器存放对象类型,不会将错误类型的对象放在容器中,取元素时候也不需要转型。而且Collection和Map都可以自动调整其尺寸。容器不可以持有基本类型。
3):像数组一样,List也建立数字索性和对象的关联,因此,数组和List都是排好序的容器,List可以自动扩容
4):如果需要大量的随机访问就要使用ArrayList,如果要经常从中间插入和删除就要使用LinkedList。
5):各种Queue和Stack由LinkedList支持
6):Map是一种将对象(而非数字)与对象相关联的设计。HashMap用于快速访问,TreeMap保持键始终处于排序状态,所以不如HashMap快,而LinkedHashMap保持元素插入的顺序,但是也通过散列提供了快速访问的能力
7):Set不接受重复的元素,HashSet提供最快的访问能力,TreeSet保持元素排序状态,LinkedHashSet以插入顺序保存元素。