关闭

Java容器类必须掌握知识合集

554人阅读 评论(0) 收藏 举报
分类:

1.  ArrayList

底层实现:数组

容量:默认初始为10,可自己设定

扩容:1.5倍旧容量与新添加的元素个数相比,选大的

添加元素:判断容量,添加至尾部

添加元素到中间位置i:判断元素位置合法,判断容量,即把原数组从i个元素位置数据后移一位

移除i位置元素:把i后的元素移动到i

 

2.  ArrayList和LinkedList

1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。

2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。

3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

这一点要看实际情况的。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList. 因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。

LinkedList实现了Deque接口,即双端队列,stack被遗留,常用LinkedList代替。

 

3.  HashMap

内部类

 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
   ....
}

          实现Map接口,底层实现为:链式数组

          三个构造函数:

1.  HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。

2.  HashMap(intinitialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。

3.  HashMap(intinitialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。、

 ■ 初始容量:其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,即数组的默认长度。

 ■  加载因子:是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75。

■   Entry为HashMap内部类,它包含了键key、值value、下一个节点next,以及hash值。

 ■  存储数据的过程:

    通过源码我们可以清晰看到HashMap保存数据的过程为:首先判断key是否为null,若为null,则直接调用putForNullKey方法。若不为空则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置,如果table数组在该位置处有元素,则通过比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。若table在该处没有元素,则直接保存。

■  如何保障数据在数组中分布均匀:

int hash = hash(key.hashCode());             // 两次哈希保证随机性

 static int indexFor(int hash, int length) { 

            return hash &(length-1); 

        } 

根据哈希值与(长度-1)做与运算保证数组中分布均匀


HashMap的底层数组长度总是2的n次方,当length为2的n次方时,h&(length- 1)就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化。取值为2^n时不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。

注意:

★  链的产生:这是一个非常优雅的设计。系统总是将新的Entry对象添加到bucketIndex处。如果bucketIndex处已经有了对象,那么新添加的Entry对象将指向原有的Entry对象,形成一条Entry链,但是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。

扩容问题:随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度*加载因子。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

★  resize(2 * table.length);   若HashMap中元素的个数超过极限了,则容量扩大两倍

 

■  取数据

通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可

HashMap中允许存在null键和null值,而HashTable中不允许

 

 

4.  TreeMap:

基于红黑树实现,查看“键”或“键值对”时,他们会被排序(次序由Comparable或者Comparator决定),特点在于所得到的结果是经过排序的,TreeMap可以返回一个子树。

 

5.  HashSet

底层实现为HashMap,数据存储在Key中,value里面存放finalstatic的Object,Set接口是一种不包括重复元素的Collection,它维持它自己的内部排序,所以随机访问没有任何意义。

 

6.  TreeSet:

基于TreeMap实现,元素必须实现Comparable接口,保持有序。

 

 

7.  HashMap和TreeMap在性能上有什么样的差别呢?你比较倾向于使用哪一个?

    A:一个平衡树的性能是O(logn)。Java里的TreeMap用一个红黑树来保证key/value的排序。红黑树是平衡二叉树。保证二叉树的平衡性,使得插入,删除和查找都比较快,时间复杂度都是O(log n)。不过它没有HashMap快,HashMap的时间复杂度是O(1),但是TreeMap的优点在于它里面键值是排过序的,这样就提供了一些其他的很有用的功能。

   

    Q:怎么去选择该使用哪一个呢?

A:使 用无序的HashSet和HashMap,还是使用有序的TreeSet和TreeMap,主要取决于你的实际使用场景,一定程度上还和数据的大小以及运行环境有关。比较实际的一个原因是,如果插入和更新都比较频繁的话,那么保证元素的有序可以提高快速和频繁查找的性能。如果对于排序操作(例如产生一个报表合作者运行一个批处理程序)的要求不是很频繁的话,那么把数据以无序的方式存储,然后在需要排序的时候用Collections.sort(…)来进行排序,会比用有序的方式来存储可能会更加高效。这个只是一种可选的方式,没人能给你一个确切的答案。即使是复杂度的理论,例如O(n),成立的前提也是在 n足够大的情况下。只要在n足够小的情况下,就算是O(n)的算法也可能会比O(log n)的算法更加高效。另外,一个算法可能在AMD处理器上的速度比在Intel处理器上快。如果你的系统有交换区的话,那么你还要考虑磁盘的性能。唯一可以确定的性能测试途径是用大小合适的数据来测试和衡量程序的性能和内存使用量。在你所选择的硬件上来测试这两种指标,是最合适的方法。

 

对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选 择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。

 

8.  为何Collection不从Cloneable和Serializable接口继承?

    Collection接口指定一组对象,对象即为它的元素。如何维护这些元素由Collection的具体实现决定。例如,一些如List的Collection实现允许重复的元素,而其它的如Set就不允许。很多Collection实现有一个公有的clone方法。然而,把它放到集合的所有实现中也是没有意义的。这是因为Collection是一个抽象表现。重要的是实现。

    当与具体实现打交道的时候,克隆或序列化的语义和含义才发挥作用。所以,具体实现应该决定如何对它进行克隆或序列化,或它是否可以被克隆或序列化。

在所有的实现中授权克隆和序列化,最终导致更少的灵活性和更多的限制。特定的实现应该决定它是否可以被克隆和序列化。

 

9.  Iterator是什么?

Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者在迭代过程中移除元素。

   

10. Iterator和ListIterator主要区别有:

 interface ListIterator<E> extends Iterator<E>


一、ListIterator有add()方法,可以向List中添加对象,而Iterator不能。

二、ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历。但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。

三、ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator 没有此功能。

四、都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iterator仅能遍历,不能修改。因为ListIterator的这些功能,可以实现对LinkedList等List数据结构的操作。

 

11. 为何Iterator接口没有具体的实现?

Iterator接口定义了遍历集合的方法,但它的实现则是集合实现类的责任。每个能够返回用于遍历的Iterator的集合类都有它自己的Iterator实现内部类。

这就允许集合类去选择迭代器是fail-fast还是fail-safe的。比如,ArrayList迭代器是fail-fast的,而CopyOnWriteArrayList迭代器是fail-safe的。

 

12. fail-fast

“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug。


13. fail-safe<juc下面的集合类>

fail-safe任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException。

fail-safe机制有两个问题

(1)需要复制集合,产生大量的无效对象,开销大

(2)无法保证读取的数据是目前原始数据结构中的数据。


 

14. HashSet和TreeSet有什么区别

       HashSet有以下特点:

       A. 无序(不能保证元素的排列顺序,顺序有可能发生变化)B. 不同步C. 允许空值(集合元素可以是null,可以放入多个null,但会自动覆盖)

       当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置。简单的说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值相 等注意,如果要把一个对象放入HashSet中,重写该对象对应类的equals方法,也应该重写其hashCode()方法。其规则是如果两个对象通过equals方法比较返回true时,其hashCode也应该相同。另外,对象中用作equals比较标准的属性,都应该用来计算 hashCode的值。

       TreeSet有以下特点:

        A. 有序

        1.TreeSet是由一个树形的结构来实现的(数据结构是二叉树),它里面元素是有序的

         2.TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态,支持两种排序方式,自然排序和定制排序。其中自然排序为默认的排序方式;定制排序,TreeSet中的对象元素需要实Comparabl接口

TreeSet类中跟HashSet类一样也没有get()方法来获取列表中的元素,所以也只能通过迭代器方法来获取二叉树:

        B . 不允许空值

         1.HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的,只不过Set用的只是Map的key

         2.Map的key和Set都有一个共同的特性就是集合的唯一性.TreeMap更是多了一个有序性.

         3.TreeSet类中跟HashSet类一样也没有get()方法来获取列表中的元素,所以也只能通过迭代器方法来获取

         4.HashSet是基于hash算法实现的,性能优于TreeSet,通常使用HashSet。在我们需要对其中元素排序的时候才使用TreeSet。

 

15.  Comparator接口与Comparable接口的区别

相同点:都是接口,用于对自定义的class进行大小比较

不同点:Comparable接口定义在类内部,即  X..Class implements Comparable

并且需要重写compareTo方法,实现Comparable接口的类可以直接用Collections.sort(personList) 来实现排序

 

两种方法各有优劣, 用Comparable 简单, 只要实现Comparable接口的对象直接就成为一个可以比较的对象,

但是需要修改源代码, 用Comparator 的好处是不需要修改源代码, 而是另外实现一个比较器, 当某个自定义

的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了, 并且在Comparator 里面用户可以自

己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了

 

链接:http://www.cnblogs.com/sunflower627/p/3158042.html

 

16. Collection.sort()原理:

实现调用Arrays.Sort()方法,其使用策略设计模式,用了两种排序算法,快速排序和优化的合并排序,快速排序主要是对哪些基本类型数据(int,short,long等)排序, 而合并排序用于对对象类型进行排序。

使用不同类型的排序算法主要是由于快速排序是不稳定的,而合并排序是稳定的。这里的稳定是指比较相等的数据在排序之后仍然按照排序之前的前后顺序排列。对于基本数据类型,稳定性没有意义,而对于对象类型,稳定性是比较重要的,因为对象相等的判断可能只是判断关键属性,最好保持相等对象的非关键属性的顺序与排序前一直;另外一个原因是由于合并排序相对而言比较次数比快速排序少,移动(对象引用的移动)次数比快速排序多,而对于对象来说,比较一般比移动耗时。

补充一点合并排序的时间复杂度是n*logn, 快速排序的平均时间复杂度也是n*logn,但是合并排序的需要额外的n个引用的空间 ......


17.LinkedList遍历

	public static void main(String[] args) {

		LinkedList<String> list = new LinkedList<String>();
		list.add("First");
		list.add("Second");
		list.add("Thrid");
		System.out.println(list);
		ListIterator<String> itr = list.listIterator();
		while (itr.hasNext()) {
			System.out.println(itr.next());
		}
		try {
			System.out.println(itr.next());
		} catch (Exception e) {
		}
		itr = list.listIterator();
		System.out.println(list);
		System.out.println(itr.next());
		itr.add("new node1");
		System.out.println(list);
		itr.add("new node2");
		System.out.println(list);
		System.out.println(itr.next());
		itr.set("modify node");
		System.out.println(list);
		itr.remove();
		System.out.println(list);
	}

输出:

[First, Second, Thrid]
First
Second
Thrid
[First, Second, Thrid]
First
[First, new node1, Second, Thrid]
[First, new node1, new node2, Second, Thrid]
Second
[First, new node1, new node2, modify node, Thrid]
[First, new node1, new node2, Thrid]

18. HashMap遍历



19. Set遍历



20. 迭代器遍历与for遍历

使用for each循环语句的优势在于更加简洁,更不容易出错,不必关心下标的起始值和终止值。

forEach不是关键字,关键字还是for,语句是由iterator实现的,他们最大的不同之处就在于remove()方法上。

一般调用删除和添加方法都是具体集合的方法,例如:

List list = new ArrayList(); list.add(...); list.remove(...);

但是,如果在循环的过程中调用集合的remove()方法,就会导致循环出错,因为循环过程中list.size()的大小变化了,就导致了错误。 所以,如果想在循环语句中删除集合中的某个元素,就要用迭代器iterator的remove()方法,因为它的remove()方法不仅会删除元素,还会维护一个标志,用来记录目前是不是可删除状态,例如,你不能连续两次调用它的remove()方法,调用之前至少有一次next()方法的调用。

forEach就是为了让用iterator循环访问的形式简单,写起来更方便。当然功能不太全,所以但如有删除操作,还是要用它原来的形式。

 使用for循环与使用迭代器iterator的对比

效率上的各有有事

采用ArrayList对随机访问比较快,而for循环中的get()方法,采用的即是随机访问的方法,因此在ArrayList里,for循环较快

采用LinkedList则是顺序访问比较快,iterator中的next()方法,采用的即是顺序访问的方法,因此在LinkedList里,使用iterator较快

数据结构角度分析,for循环适合访问顺序结构,可以根据下标快速获取指定元素.而Iterator 适合访问链式结构,因为迭代器是通过next()和Pre()来定位的.可以访问没有顺序的集合.

而使用 Iterator 的好处在于可以使用相同方式去遍历集合中元素,而不用考虑集合类的内部实现(只要它实现了 java.lang.Iterable 接口),如果使用 Iterator 来遍历集合中元素,一旦不再使用 List 转而使用 Set 来组织数据,那遍历元素的代码不用做任何修改,如果使用 for 来遍历,那所有遍历此集合的算法都得做相应调整,因为List有序,Set无序,结构不同,他们的访问算法也不一样.点击打开链接



0
1

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:42417次
    • 积分:1443
    • 等级:
    • 排名:千里之外
    • 原创:97篇
    • 转载:41篇
    • 译文:0篇
    • 评论:22条
    最新评论