Java集合篇:集合类介绍

版权声明:本文为原创文章,如有不足之处可以指出,欢迎大家转载,记得标明出处。 https://blog.csdn.net/a745233700/article/details/83028574

上面的图展示了整个集合大家族的成员以及他们之间的关系。下面就上面的各个接口、基类做一些简单的介绍(主要介绍各个集合的特点,区别)。

一、Collection接口:

llection接口是最基本的集合接口,它不提供直接的实现,Java SDK提供的类都是继承自Collection的“子接口”如List和Set。Collection所代表的是一种规则,它所包含的元素都必须遵循一条或者多条规则。如有些允许重复而有些则不能重复、有些必须要按照顺序插入而有些则是散列,有些支持排序但是有些则不支持。

在Java中所有实现了Collection接口的类都必须提供两套标准的构造函数,一个是无参,用于创建一个空的Collection,一个是带有Collection参数的有参构造函数,用于创建一个新的Collection,这个新的Collection与传入进来的Collection具备相同的元素。 

 

二、List接口:

List接口为Collection直接接口。List所代表的是有序的Collection,即它用某种特定的插入顺序来维护元素顺序。用户可以对列表中每个元素的插入位置进行精确地控制,同时可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack。

2.1、ArrayList:

(1)ArrayList底层是一个动态数组实现的。

(2)它是非同步实现的,并允许包括null在内的所有元素,ArrayList擅长于随机访问。

(3)每一个ArrayList都有一个初始容量(10),该集合是可变长度数组,数组扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量增长大约是其容量的1.5倍,这种操作的代价很高。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。

(4)采用了Fail-Fast机制,面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

(5)remove方法会让下标到数组末尾的元素向前移动一个单位,并把最后一位的值置空,方便GC。

size、isEmpty、get、set、iterator 和 listIterator 操作都以固定时间运行。add 操作以分摊的固定时间运行,也就是说,添加 n 个元素需要 O(n) 时间(由于要考虑到扩容,所以这不只是添加元素会带来分摊固定时间开销那样简单)。

2.2、LinkedList:

(1)LinkedList是List接口的双向链表,并允许包括null在内的所有元素。

(2)LinkedList是非同步实现的,如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List: List list = Collections.synchronizedList(new LinkedList(...));

(3)LinkedList不能随机访问,它所有的操作都是要按照双重链表的需要执行。在列表中索引的操作将从开头或结尾遍历列表(先判断index是在链表的哪一半,然后再去对应区域查找,这样最多只要遍历链表的一半节点即可找到)。这样做的好处就是可以通过较低的代价在List中进行插入和删除操作。

2.3、Vector:

与ArrayList相似,但是Vector是同步的。所以说Vector是线程安全的动态数组。它的操作与ArrayList几乎一样。

2.4、Stack:

(1)Stack继承自Vector,实现一个后进先出的堆栈。所以用法、线程安全什么的跟Vector都差不多,Stack刚创建后是空栈。

(2)Stack还提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop 方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。

 

三、Map接口:

 

Map与List、Set接口不同,它是由一系列键值对组成的集合,提供了key到Value的映射。同时它也没有继承Collection。在Map中它保证了key与value之间的一一对应关系。也就是说一个key对应一个value,所以它不能存在相同的key值,当然value值可以相同。实现map的有:HashMap、TreeMap、HashTable、Properties、EnumMap。

3.1、HashMap:

(1)HashMap是基于哈希表的Map接口的非同步实现,允许使用null值和null键,但不保证映射的顺序,它是为快速查询而设计的。

(2)底层使用数组实现,数组中每一项是个单向链表,即数组和链表的结合体;当链表长度大于一定阈值时,链表转换为红黑树,这样减少链表查询时间。

(3)HashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Node对象。HashMap底层采用一个Node[]数组来保存所有的key-value对,当需要存储一个Node对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Node时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Node。

(4)HashMap进行数组扩容需要重新计算扩容后每个元素在数组中的位置,很耗性能。

(5)采用了Fail-Fast机制,通过一个modCount值记录修改次数,对HashMap内容的修改都将增加这个值。迭代器初始化过程中会将这个值赋给迭代器的expectedModCount,在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map,马上抛出异常。

3.1.1、HashMap的优化:

HashMap的扩容代价非常大,要生成一个新的桶数组,然后要把所有元素都重新Hash落桶一次,几乎等于重新执行了一次所有元素的put。所以如果我们对Map的大小有一个范围的话,可以在构造时给定大小,一般大小设置为:(int) ((float) expectedSize / 0.75F + 1.0F)。

3.2、Hashtable:

(1)Hashtable是基于哈希表的Map接口的同步实现,不允许使用null值和null键;

(2)底层使用数组实现,数组中每一项是个单链表,即数组和链表的结合体;

(3)synchronized是针对整张Hash表的,即每次锁住整张表让线程独占;

(4)Hashtable在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象。Hashtable底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

3.3、ConcurrentHashMap:

(1)ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同段进行的修改,每个段其实就是一个小的hashtable,它们有各自的锁。只要多个并发发生在不同的段上,它们就可以并发进行。

(2)与HashMap不同的是,ConcurrentHashMap使用多个子Hash表,也就是段(Segment)

ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。

(3)ConcurrentHashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象。Hashtable底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

3.4、TreeMap:

键以某种排序规则排序,内部以red-black(红-黑)树数据结构实现,实现了SortedMap接口。

3.5 LinkedHashMap:

(1)LinkedHashMap继承于HashMap,底层使用哈希表和双向链表来保存所有元素,并且它是非同步,允许使用null值和null键。

(2)基本操作与父类HashMap相似,通过重写HashMap相关方法,重新定义了数组中保存的元素Entry,来实现自己的链接列表特性。该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而构成了双向链接列表。

3.6、WeakHashMap:

WeakHashMap与HashMap的用法基本相同,区别在于:后者的key保留对象的强引用,即只要HashMap对象不被销毁,其对象所有key所引用的对象不会被垃圾回收,HashMap也不会自动删除这些key所对应的键值对对象。但WeakHashMap的key所引用的对象没有被其他强引用变量所引用,则这些key所引用的对象可能被回收。WeakHashMap中的每个key对象保存了实际对象的弱引用,当回收了该key所对应的实际对象后,WeakHashMap会自动删除该key所对应的键值对。

3.7、IdentifyHashMap:

IdentityHashMap与HashMap基本相似,只是当两个key严格相等时,即key1==key2时,它才认为两个key是相等的 。IdentityHashMap也允许使用null,但不保证键值对之间的顺序。

 

四、Set接口:

Set是一种不包括重复元素的Collection。它维持它自己的内部排序,所以随机访问没有任何意义。与List一样,它同样运行null的存在但是仅有一个。由于Set接口的特殊性,所有传入Set集合中的元素都必须不同,同时要注意任何可变对象,如果在对集合中元素进行操作时,导致e1.equals(e2)==true,则必定会产生某些问题。实现了Set接口的集合有:EnumSet、HashSet、TreeSet。

3.1、HashSet:

(1)HashSet基于HashMap实现,API也是对HashMap的行为进行了封装。

(2)HashSet堪称查询速度最快的集合,因为其内部是以HashCode来实现的。它内部元素的顺序是由哈希码来决定的,所以它不保证set 的迭代顺序,特别是它不保证该顺序恒久不变,并允许使用null元素。

3.2、LinkedHashSet:

(1)对于LinkedHashSet而言,它继承与HashSet、又基于LinkedHashMap来实现的。

(2)LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承与HashSet,其所有的方法操作上又与HashSet相同。

3.3、TreeSet:

基于TreeMap,生成一个总是处于排序状态的set,内部以TreeMap来实现。它是使用元素的自然顺序对元素进行排序,或者根据创建Set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。

3.4、EnumSet:

是枚举的专用Set。所有的元素都是枚举类型。

 

五、Queue:

队列,它主要分为两大类,一类是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要包括ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。另一种队列则是双端队列,支持在头、尾两端插入和移除元素,主要包括:ArrayDeque、LinkedBlockingDeque、LinkedList。

 

六、异同点:

6.1、Vector和ArrayList:

       (1)vector是线程同步的,所以它也是线程安全的,而arraylist是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用arraylist效率比较高,实际中一般也不怎么用Vector,可以自己做线程同步,也可以用Collections配合ArrayList实现线程同步。
       (2)如果集合中的元素的数目大于目前集合数组的长度时,vector增长率为目前数组长度的100%,而arraylist增长率为目前数组长度的50%。如过在集合中使用数据量比较大的数据,用vector有一定的优势。
       (3)如果查找一个指定位置的数据,vector和arraylist使用的时间是相同的,都是0(1),这个时候使用vector和arraylist都可以。而如果移动一个指定位置的数据花费的时间为0(n-i)n为总长度,这个时候就应该考虑到使用linklist,因为它移动一个指定位置的数据所花费的时间为0(1),而查询一个指定位置的数据时花费的时间为0(i)。

       ArrayList 和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,都允许直接序号索引元素,但是插入数据要设计到数组元素移动等内存操作,所以索引数据快插入数据慢,Vector由于使用了synchronized方法(线程安全)所以性能上比ArrayList要差,LinkedList使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项的前后项即可,所以插入数度较快!

 6.2、Aarraylist和Linkedlist:

(1)都实现了Collection接口。

(2)ArrayList是实现了基于动态数组的数据结构,LinkedList基于双向循环链表的数据结构。

(3)对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。

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

6.3、HashMap与TreeMap:

(1)HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的),HashMap中元素的排列顺序是不固定的。

(2)在Map 中插入、删除和定位元素,HashMap 是最好的选择。但如果要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。使用HashMap要求添加的键类明确定义了hashCode()和 equals()的实现。 这个TreeMap没有调优选项,因为该树总处于平衡状态。

6.4、Hashtable与HashMap:

相同点:二者都实现了Map接口,因此具有一系列Map接口提供的方法。

不同点:

(1)HashMap继承了AbstractMap,而HashTable继承了Dictionary。

(2)HashMap非线程安全,HashTable线程安全,到处都是synchronized关键字,因为HashMap没有同步,所以处理起来效率较高。

(3)HashMap键、值都允许为null,HashTable键、值都不允许有null。

(4)HashTable使用Enumeration,HashMap使用Iterator。

这些就是一些比较突出的不同点,实际上他们在实现的过程中会有很多的不同,如初始化的大小、计算hash值的方式等等。

6.5、TreeSet和HashSet:

     TreeSet: 提供排序功能的Set,底层为树结构 。相比较HashSet其查询速度低,如果只是进行元素的查询,我们一般使用HashSet。

 

七、对集合类的选择:

7.1、对List的选择:

(1)对于随机查询与迭代遍历操作,数组比所有的容器都要快。所以在随机访问中一般使用ArrayList;

(2)LinkedList使用双向链表对元素的增加和删除提供了非常好的支持,而ArrayList执行增加和删除元素需要进行元素位移。

(3)对于Vector而已,我们一般都是避免使用。

(4)将ArrayList当做首选,毕竟对于集合元素而已我们都是进行遍历,只有当程序的性能因为List的频繁插入和删除而降低时,再考虑LinkedList。

7.2、对Set的选择:

(1)HashSet由于使用HashCode实现,所以在某种程度上来说它的性能永远比TreeSet要好,尤其是进行增加和查找操作。

(2)虽然TreeSet没有HashSet性能好,但是由于它可以维持元素的排序,所以它还是存在用武之地的。

7.3、对Map的选择:

(1)HashMap与HashSet同样,支持快速查询。虽然HashTable速度的速度也不慢,但是在HashMap面前还是稍微慢了些,所以HashMap在查询方面可以取代HashTable。

(2)由于TreeMap需要维持内部元素的顺序,所以它通常要比HashMap和HashTable慢。

 

八、相关问题:

8.1、Set集合是如何保证对象不重复的?

Set集合不允许有重复出现的对象,且最终的判断是根据equals()的。其实原理是这样的:HashSet的底层采用HashMap来存放数据,HashMap的put()方法是这样的:

  public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());//----------1----------
        int i = indexFor(hash, table.length);//-----------2---------
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {//-----------3---------
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }//------------------4--------------------
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

当向HashMap中添加元素的时候,首先计算元素的hashcode值,然后根据1处的代码计算出Hashcode的值,再根据2处的代码计算出这个元素的存储位置,如果这个位置为空,就将元素添加进去;如果不为空,则看3-4的代码,遍历索引为i的链上的元素,如果key重复,则替换并返回oldValue值。

8.2、集合类排序问题:

一种情况是集合类本身自带排序功能,如前面说过的TreeSet、SortedSet、SortedMap等,另一种就是本身不带排序功能,我们通过为需要排序的类实现Comparable或者Comparator接口来实现。

先来看两个例子,一个是实现Comparable的,一个是实现Comparator的,为了方便,我将类都写在了一个文件中。

(1)实现Comparable的:

package com.xtfggef.list.test;
 
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
 
@SuppressWarnings("unchecked")
public class ComparableTest {
	public static void main(String[] args) {
		// User[] users = { new User("egg", 23), new User("niuniu", 22),
		// new User("qing", 28) };
		// Arrays.sort(users);
		// for (User user : users) {
		// System.out.println(user.getName() + " " + user.getAge());
		// }
		List<User> users = new ArrayList<User>();
		users.add(new User("egg", 23));
		users.add(new User("niu", 22));
		users.add(new User("qing", 28));
 
		Collections.sort(users);
		for (User user : users) {
			System.out.println(user.getName() + " " + user.getAge());
		}
	}
}
 
@SuppressWarnings("unchecked")
class User implements Comparable {
	private String name;
	private int age;
 
	public User(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
 
	public String getName() {
		return name;
	}
 
	public void setName(String name) {
		this.name = name;
	}
 
	public int getAge() {
		return age;
	}
 
	public void setAge(int age) {
		this.age = age;
	}
 
	@Override
	public int compareTo(Object o) {
		return this.age - ((User) o).getAge();
	}
}

(2)下面是实现Comparator接口的:

package com.xtfggef.comparator.test;
 
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
 
public class ComparatorTest {
 
	public static void main(String[] args) {
		List<User> users = new ArrayList<User>();
		users.add(new User("egg", 21));
		users.add(new User("niu", 22));
		users.add(new User("gg", 29));
		UserComparator comparator = new UserComparator();
		Collections.sort(users, comparator);
		for (User user : users) {
			System.out.println(user.getUsername() + " " + user.getAge());
		}
	}
 
}
class User {
	private String username;
	private int age;
 
	public User(String username, int age) {
		super();
		this.username = username;
		this.age = age;
	}
 
	public String getUsername() {
		return username;
	}
 
	public void setUsername(String username) {
		this.username = username;
	}
 
	public int getAge() {
		return age;
	}
 
	public void setAge(int age) {
		this.age = age;
	}
}
class UserComparator implements Comparator<User> {
 
	@Override
	public int compare(User user1, User user2) {
		int age1 = user1.getAge();
		int age2 = user2.getAge();
		if (age1 < age2) {
			return 1;
		}
		return 0;
	}
}

通过上面的这两个小例子,我们可以看出,Comparator和Comparable用于不同的场景,实现对对象的比较从而进行排序。

总结为:

相同点:二者都可以实现对象的排序,不论用Arrays的方法还是用Collections的sort()方法。

不同点:

(1)实现Comparable接口的类,似乎是预先知道该类将要进行排序,需要排序的类实现Comparable接口,是一种“静态绑定排序”。

(2)实现Comparator的类不需要,设计者无需事先为需要排序的类实现任何接口。

(3)Comparator接口里有两个抽象方法compare()和equals(),而Comparable接口里只有一个方法:compareTo()。

(4)Comparator接口无需改变排序类的内部,也就是说实现算法和数据分离,是一个良好的设计,是一种“动态绑定排序”。

(5)Comparator接口可以使用多种排序标准,比如升序、降序等。

8.3、使用for循环删除元素陷阱:

先来看看下面这个程序:

public class Test {
 
	public static void main(String[] args) {
		List<String> list = new LinkedList<String>();
		list.add("A");
		list.add("B");
		list.add("C");
		
		for(int i=0; i<list.size(); i++){
			list.remove(i);
		}
		
		for(String item:list){
			System.out.println(item);
		}
	}
}

读者朋友们可以先猜猜这个程序输出什么?按我们的思路,应该是输不出什么,但是执行它,输出的却是:B。这是为什么呢?我们分部分析下这个程序,当地一步remove完后,集合内还剩2个元素,此时i为1,而list.size()的值为2,从0开始的话,i为1时,正好指向第二个元素,也就是说当remove完A后,直接就跳到C,将B漏了。

public class Test {
 
	public static void main(String[] args) {
		List<String> list = new LinkedList<String>();
		list.add("A");
		list.add("B");
		list.add("C");
		
		for(int i=0; i<list.size(); i++){
			list.remove(i);
			i -= 1;//每次删除完后,i减少1
		}
		
		for(String item:list){
			System.out.println(item);
		}
	}
}

参考博客:

https://blog.csdn.net/chenssy/article/details/17732841

https://blog.csdn.net/zhangerqing/article/details/8122075

https://blog.csdn.net/qq_25868207/article/details/55259978

猜你喜欢

转载自blog.csdn.net/a745233700/article/details/83028574