java集合类深度解析

一、为什么要使用集合类

我们想要装一些元素,首先想到的是数组,但是数组有局限性,一是必须装相同类型的数据,二是其长度是不可变的,在很多场合上,比如员工管理,假设我们需要一个容器来存储员工的基本信息,因为一个公司的员工可能有辞职跳槽新进的等等,所以要是选择数组来装这组数据就会非常麻烦,所以,为了解决这个问题,我们引出了集合这种容器,因为集合是专门用来解决引用类型的数据,所以其并不能装八种基本数据类型,只能装对象的引用,但可以装基本数据类型对应的包装类。

二、集合的分类

java的集合类主要包含三种类型,Set、List、Map,他们主要的特点是set中的数据最像数学中的集合,数据无序,但是是不可重复的;List集合最像数组,按索引进行存储对象,允许有重复对象;而Map中是按键值对进行存储对象的,它的键是唯一的,但他的值是可以重复的。下面是他们的家族图解:


三、Set集合

Set集合是最简单的一种集合,它的数据无序且唯一,它是一个继承了Collection接口的接口,他的实现类有两个,一个是HashSet,一个是TreeSet,Set集合中的对象的比较方法都是利用equals方法进行比较。

①HashSet类

HashSet类是Set接口(Set接口是继承了Collection接口的)最常用的实现类,顾名思义,底层是用了哈希表(散列/hash)算法。其底层其实也是一个数组,存在的意义是提供查询速度,插入的速度也是比较快,但是适用于少量数据的插入操作。

1)其add方法在添加运算的时候,先调用HashCode方法看两个对象的HashCode值是否相同,要是不同,直接添加进来,要是相同在再调用equals方法,看equals方法返回的是否为true,若返回的是true则表示两个对象为同一对象,则该对象不会添加到set集合里。   说明判断两个对象为同一对象的充要条件就是 HashCode相同且equals方法返回为true。因为Object类中定义了以上两个方法,其中,Object类中的equals方法是按照内存地址来比较两个对象是否相等的,若一个对象引用和另外一个对象的引用使用equals方法进行比较的话,其实两个变量引用的就是一个对象,他们两个对象的HashCode也一定相同,所以当用户 重写了equals方法却没有重写HashCode方法的时候,当两个对象调用equals方法返回true时,其哈希码不一定一样,这样HashSet集合就不会吧该对象存进来,这样就违背了我们的本意,所以,当我们重写equals方法的时候一定要重写HashCode方法,保证两者的一致性,其实HashCode决定了其在哈希表中的存储位置,equals方法区判断他们的值。小demo看一下:

import java.util.HashSet;

/**
 * Set 数据是无序 不重复
 * Set集合是如何保证里面的数据不重复的呢?
 * 如果一个类没有重写equals方法 那么其两个对象equals方法为object类的方法 相当于==判断两个引用变量地址
 * 这种情况在实际开发当中 与某些情况不符 所以需要重写equals方法
 * 
 * 如果此时需要创建多个学生类型的的对象 需要一个容器 进行管理
 * 考虑数组长度不可变 所以选择HashSet集合
 * Set集合有一个特点 无序 不可重复
 * 如何保证不重复:每次存入数据都要进行equals验证 要存入的信息与该集合的所有元素都进行一次equals对比 结过都为true是则存入 否则不存入
 * 但是 当数据非常多时,对比次数会非常多为属性数*数据数 还不包括判断两个对象的地址和判断对象类型的次数
 * 效率非常低
 * 
 * 结论:不能直接使用equals方法进行比较 需要一个方式既能保证对比结果的准确性 又能减少equals方法的使用次数
 * 
 * 解决方案:先对每一个对象多的一个特征进行判断 以便于区分两个对象是否相同
 * 最适合的的方法是比较每个对象的hashcode值 而hashcode需要通过Object类提供的方法进行获取 也存在不适用的情况需要重写
 * 
 * 规则:hashcode方法的实现原理就是根据对象的属性进行运算 得出一个值 这个值每个对象基本不会重复 但也存在特殊情况
 * 
 * 重写hashcode方法不一定能完全保证完成两个对象的比较(只用hashcode不能保证准确性) 所以是先比较hashcode 若相同 再比较equals(),
 * 这样可以兼顾效率和结果的准确性
 * 若想保证hashSet正常工作 我们需要再重写equals方法的同时 去重写hashCode方法
 * @author zhmm
 *
 */
public class HashSetTest {
	public static void main(String[] args) {
		Student student1 = new Student();
		Student student2 = new Student();
		Student student3 = new Student();
		student1.setName("张三");
		student2.setName("张三");
		student3.setName("张三");
		student3.setAge(20);
		student3.setNo(10);
		HashSet s = new HashSet();
		s.add(student1);
		s.add(student2);
		s.add(student3);
//		System.out.println(student1.equals(student2));
		
	}
}
class Student{
	private int no;
	private int age;
	private String name;
	private int scoreJava;
	public int getNo() {
		return no;
	}
	public void setNo(int no) {
		this.no = no;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getScoreJava() {
		return scoreJava;
	}
	public void setScoreJava(int scoreJava) {
		this.scoreJava = scoreJava;
	}
	public boolean equals(Object obj){
		System.out.println("调用了equals方法");
		//如果两个对象地址相同 则相当于比较值
		if(this == obj){
			return true;
		}
		//如果两个对象地址不同 判断该对象是否为Student的实例
		if(obj instanceof Student){
			//如果是,则强转成Student类型
			Student student = (Student) obj;
			//如果两个对象名字相同 再去比较其它属性是否相同
			if(this.getName().equals(student.getName())){
				return this.getAge()==student.getAge()
						&&this.getNo()==student.getNo()
						&&this.getScoreJava()==student.getScoreJava();
			}
			//如果两个对象名字不同 返回false
		}else{
			return false;
		}
		return false;
		
	}/*
	如果两个对象的hashcode值不同其为不同对象 若相同 也不能完全证明其为同一个对象
	*/
	@Override
	public int hashCode() {
		System.out.println("调用了hashCode方法");
		return this.getNo()*16+this.getScoreJava()*19;
	}
}

运行结果

2)因为我们知道哈希表能够保证数据的唯一性,但不能记录数据的先后顺序,链表结构能够保证数据的先后顺序,但是不能保证数据的唯一性,如果我们有这样的需求,就是既想数据无重复性,又想数据能够保证添加的先后顺序,这样我们需要哈希表和链表结构双向实现的一种集合,这就是LinkedHashSet。LinkedHashSet继承了HashSet类,所以它的底层用的也是哈希表的数据结构,但因为保持数据的先后添加顺序,所以又加了链表结构,但因为多加了一种数据结构,所以效率较低,不建议使用。

②TreeSet类

TreeSet也是Set接口的实现类,也拥有set接口的一般特性,但是不同的是他也实现了SortSet接口,它底层采用的是红黑树算法(红黑树就是满足一下红黑性质的二叉搜索树:①每个节点是黑色或者红色②根节点是黑色的③每个叶子结点是黑色的④如果一个节点是红色的,那么他的两个子节点是黑色的⑤对每个节点,从该节点到其所有的后代叶子结点的简单路径上,仅包含相同数目的黑色结点,红黑树是许多“平衡”搜索树的一种,可以保证在最坏情况下的基本操作集合的时间复杂度为O(lgn)。普及:二叉搜索树的性质:它或者是一棵空树;或者是具有下列性质的二叉树:若左子树不空,则左子树上所有结点的值均小于它的根结点的值;若右子树不空,则右子树上所有结点的值均大于它的根结点的值;左、右子树也分别为二叉排序树。若子树为空,查找不成功。),要注意的是在TreeSet集合中只能存储相同类型对象的引用。

Tree最重要的就是它的两种排序方式:自然排序和客户端排序

1)自然排序

因为TreeSet实现了Comparable接口,所以TreeSet可以调用对象的ComparableTo()方法来比较集合的大小,然后进行升序排序,这种排序方式叫做自然排序。其中实现了Comparable接口的还有BigDecimal、BigInteger、Byte、Double、Float、Integer、Long、Short(按照数字大小排序)、Character(按照Unicode值的数字大小进行排序)String(按照字符串中字符的Unicode值进行排序)类等。

在使用自然排序的时候,只能向TreeSet类中加入相同类型的对象,并且这些对象均实现了Comparable接口。为了能保证TreeSet正确排序,要求存取的对象必须得需要改类的compareTo方法与equals方法按相同比较规则比较这个类的两个对象,又因为当重写equals方法的时候必然要重写HashCode方法,所以,一个类想要使用TreeSet进行存储,必须实现compareTo方法、equals方法、HashCode方法。要注意的是:当已经存储到TreeSet集合中的对象,当该集合对象的属性被修改时并不会重新对对象进行排序,所以,适合用TreeSet进行排序的是不可变类,也就是说这种类的属性是不能被修改的。

2)客户化排序

其实就是实现java.util.Comparator<Type>接口提供的具体的排序方式,<Type> 是具体要比较对象的类型,他有个compare的方法,如compare(x,y)返回值大于0表示x大于y,以此类推,当我们希望按照自己的想法排序的时候可以重写compare方法。 

综上:

相同点:所有Set集合的共同点为:都不允许元素重复,都不是线程安全的类。解决方案:Set set = Collections.sysnchronizedSet(Set对象);    

各自特点:   

 HashSet:不保证元素的先后添加顺序,底层采用的是哈希表算法,查询效率极高,判断两个对象是否相等的规则:1、equals比较为true;2、hashCode值相同。要求:要求存在在哈希表中的对象元素都得覆盖equals和hashCode方法。


LinkedHashSet:HashSet的子类,底层也采用的是哈希表算法,但是也使用了链表算法来维持元素的先后添加顺序,判断两个对象是否相等的规则和HashSet相同。因为需要多使用一个链表来记录元素的顺序,所以性能相对于HashSet较低;一般少用,如果要求一个集合急要保证元素不重复,也需要记录元素的先后添加顺序,才选择使用LinkedHashSet。   

TreeSet:底层用的红黑树算法,一般用于不可变类的排序,要实现comparable接口、重写HashCode、equals方法,存储的对象只能为同一类型的,要实现客户化排序需要继承java.util.Comparator<Type>接口,并实现compare方法。

四、List集合

List实现Collection接口,它的数据结构是有序可以重复的结合,该结合的体系有索引;它有三个实现类:ArrayList、LinkList、Vector三个实现类;

三个实现类的区别:

ArrayList:底层数据结构使数组结构,查询速度快,增删改慢,

LinkList:底层使用链表结构,增删速度快,查询稍慢;

Vector:底层是数组结构,线程同步ArrayList是线程不同步;

可变长度数组是通过不断new数组来实现的:

ArrayList当初始化容量超过10时,会new一个50%de ,把原来的东西放入这150%中;

Vector:当容量超过10时,会new一个100%的浪费内存;

List接口对Collection进行了简单的扩充,它的具体实现类常用的有ArrayList和LinkedList。你可以将任何东西放到一个List容器中,并在需要时从中取出。ArrayList从其命名中可以看出它是一种类似数组的形式进行存储,因此它的随机访问速度极快,而LinkedList的内部实现是链表,它适合于在链表中间需要频繁进行插入和删除操作。在具体应用时可以根据需要自由选择。前面说的Iterator只能对容器进行向前遍历,而ListIterator则继承了Iterator的思想,并提供了对List进行双向遍历的方法                 

1.ArrayList集合

     ArrayList是List接口的可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。

   每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。 

   注意,此实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。     

   数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。

ArrayList和Vector的区别:

①.Vector是线程同步的,所以它也是线程安全的。而ArratList是线程异步的,不安全。如果不考虑安全因素,一般用Arralist效率比较高,查看JDK文档,给出提示:

如果要实现Arraylist线程同步,可以通过下面方式:

如果多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须 保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)这一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用Collections.synchronizedList 方法将该列表“包装”起来。这最好在创建时完成,以防止意外对列表进行不同步的访问:

 List list = Collections.synchronizedList(new ArrayList(...)); 

②.如果集合中的元素数量大于当前集合数组的长度时,Vector的增长率是目前数组长度的100%,而ArryaList增长率为目前数组长度的50%。所以,如果集合中使用数据量比较大的数据,用Vector有一定优势。 

2.LinkList集合

LinkedList与Collection关系如上面的家系图。

LinkedList的本质是双向链表。

(01) LinkedList继承于AbstractSequentialList,并且实现了Dequeue接口。

(02) LinkedList包含两个重要的成员:header 和 size。

header是双向链表的表头,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量: previous, next, element。其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。

size是双向链表中节点的个数。

LinkedList实际上是通过双向链表去实现的。既然是双向链表,那么它的顺序访问会非常高效,而随机访问效率比较低。

既然LinkedList是通过双向链表的,但是它也实现了List接口{也就是说,它实现了get(int location)、remove(int location)等“根据索引值来获取、删除节点的函数”}。LinkedList是如何实现List的这些接口的,如何将“双向链表和索引值联系起来的”?

    实际原理非常简单,它就是通过一个计数索引值来实现的。例如,当我们调用get(int location)时,首先会比较“location”和“双向链表长度的1/2”;若前者大,则从链表头开始往后查找,直到location位置;否则,从链表末尾开始先前查找,直到location位置。

这就是“双线链表和索引值联系起来”的方法。

总结:

(01) LinkedList 实际上是通过双向链表去实现的。

它包含一个非常重要的内部类:Entry。Entry是双向链表节点所对应的数据结构,它包括的属性有:当前节点所包含的值,上一个节点,下一个节点。

(02) 从LinkedList的实现方式中可以发现,它不存在LinkedList容量不足的问题。

(03) LinkedList的克隆函数,即是将全部元素克隆到一个新的LinkedList对象中。

(04) LinkedList实现java.io.Serializable。当写入到输出流时,先写入“容量”,再依次写入“每一个节点保护的值”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。

(05) 由于LinkedList实现了Deque,而Deque接口定义了在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。

所以,不要去随机遍历LinkList。

3.Vector类

相对于ArrayList来说,Vector线程是安全的,也就是说是同步的

创建了一个向量类的对象后,可以往其中随意地插入不同的类的对象,既不需顾及类型也不需预先选定向量的容量,并可方便地进行查找。对于预先不知或不愿预先定义数组大小,并需频繁进行查找、插入和删除工作的情况,可以考虑使用向量类。向量类提供了三种构造方法:

public vector()

public vector(intinitialcapacity,int capacityIncrement)

public vector(intinitialcapacity)

使用第一种方法,系统会自动对向量对象进行管理。若使用后两种方法,则系统将根据参数initialcapacity设定向量对象的容量(即向量对象可存储数据的大小),当真正存放的数据个数超过容量时,系统会扩充向量对象的储存容量。

参数capacityIncrement给定了每次扩充的扩充值。当capacityIncrement为0时,则每次扩充一倍。利用这个功能可以优化存储。在Vector类中提供了各种方法方便用户使用。

五、Map集合

java的Map(映射)是一种把键对象和值对象进行映射的集合,其中每一个元素都包含了键对象和值对象,其中值对象也可以是Map类型的数据,因此,Map支持多级映射,Map中的键是唯一的,但值可以不唯一,Map集合有两种实现,一种是利用哈希表来完成的叫做HashMap,它和HashSet都是利用哈希表来完成的,区别其实就是在哈希表的每个桶中,HashSet只有key,而HashMap在每个key上挂了一个value;另一种就是TreeMap,它实现了SortMap接口,也就是使用了红黑树的数据结构,和TreeSet一样也能实现自然排序和客户化排序两种排序方式,而哈希表不提供排序。

1.HashMap

我们知道,哈希表的实现原理(在我另外一篇文章中会详细分析哈希函数和哈希表z)中,先采用一个数组表示位桶,每个位桶的实现在1.8之前都是使用链表,但当每个位桶的数据较多的时候,链表查询的效率就会不高,因此在1.8之后,当位桶的数据超过阈值(8)的时候,就会采用红黑树来存储该位桶的数据(在阈值之前还是使用链表来进行存储),所以,哈希表的实现包括数组+链表+红黑树,在使用哈希表的集合中我们都认为他们的增删改查操作的时间复杂度都是O(1)的,不过常数项很大,因为哈希函数在进行计算的代价比较高。

jdk1.8 HashMap源码,所用的数据结构、构造函数等等链接一位博友的文章:

https://blog.csdn.net/tuke_tuke/article/details/51588156

2.LinkedHashMap

HashMap 是无序的,HashMap 在 put 的时候是根据 key 的 hashcode 进行 hash 然后放入对应的地方。所以在按照一定顺序 put 进 HashMap 中,然后遍历出 HashMap 的顺序跟 put 的顺序不同(除非在 put 的时候 key 已经按照 hashcode 排序号了,这种几率非常小)

JAVA 在 JDK1.4 以后提供了 LinkedHashMap 来帮助我们实现了有序的 HashMap!

LinkedHashMap 是 HashMap 的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用 LinkedHashMap。

LinkedHashMap 是 Map 接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

LinkedHashMap 实现与 HashMap 的不同之处在于,LinkedHashMap 维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。

注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用 get 方法)的链表。默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。

其实也就是使用哈希表结构的都是不能保证输入顺序的,只能完成存储,要是想记住数据的输入顺序就需要再引入一个链表来完成对对输入数据的顺序记录的功能。

3.TreeMap

TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。TreeMap 实现了Cloneable接口,意味着它能被克隆。TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。

TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。

六、总结

1.只使用哈希算法进行存储的集合只能完成存储功能,并不能完成排序,想要完成有序有两种,一种是使用链表,像LinkedHashList、LinkedHashMap完成对输入记录顺序的记忆,而使用了红黑树结构的集合都能完成数据的排序功能像TreeSet、TreeMap,他们都能进行自然排序,也可以通过compare方法进行客户化排序

2.HashSet和HashMap的区别其实就是在实现哈希表的时候,set的链表(或者红黑树)结构只有key,Map的key上还挂了一个value。

3.这些集合都是线程不安全的,但是都可以通过其父类接口的synchronized方法创建线程安全的集合,但HashTable是线程安全的。

发布了41 篇原创文章 · 获赞 120 · 访问量 15万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 创作都市 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览