Java集合分为Set、List、Queue、Map四种体系,集合只能保存对象
1. Set代表无序、不可重复的集合,像一个罐子,但是罐子里面的东西不能重复
2.List代表有序、重复的集合,像数组一样,可以记住每次添加元素的顺序,长度可变
3.Map代表具有映射关系的集合,Map也像一个罐子,只是里面的每项数据都由两个值组成
4.Queue代表队列集合
首先弄明白equals()方法与hashCode()的区别
public boolean equals(Object obj)
Object类中默认的实现方式是 : return this == obj 。那就是说,只有this 和 obj引用同一个对象,才会返回true。
而我们往往需要用equals来判断 2个对象是否等价,而非验证他们的唯一性。这样我们在实现自己的类时,就要重写equals.
按照约定,equals要满足以下规则。
自反性: x.equals(x) 一定是true
对null: x.equals(null) 一定是false
对称性: x.equals(y) 和 y.equals(x)结果一致
传递性: a 和 b equals , b 和 c equals,那么 a 和 c也一定equals。
一致性: 在某个运行时期间,2个对象的状态的改变不会不影响equals的决策结果,那么,在这个运行时期间,无论调用多少次equals,都返回相同的结果。
equals编写指导
Test类对象有2个字段,num和data,这2个字段代表了对象的状态,他们也用在equals方法中作为评判的依据。
在第8行,传入的比较对象的引用和this做比较,这样做是为了 save time ,节约执行时间,如果this 和 obj是 对同一个堆对象的引用,那么,他们一定是equals的。
接着,判断obj是不是为null,如果为null,一定不equals,因为既然当前对象this能调用equals方法,那么它一定不是null,非null 和 null当然不等价。
然后,比较2个对象的运行时类,是否为同一个类。不是同一个类,则不equals。getClass返回的是 this 和obj的运行时类的引用。如果他们属于同一个类,则返回的是同一个运行时类的引用。注意,一个类也是一个对象。
1、有些程序员使用下面的第二种写法替代第一种比较运行时类的写法。应该避免这样做。
它违反了公约中的对称原则。
例如:假设Dog扩展了Aminal类。
dog instanceof Animal 得到true
animal instanceof Dog 得到false
这就会导致
animal.equls(dog) 返回true
dog.equals(animal) 返回false
仅当Test类没有子类的时候,这样做才能保证是正确的。
2、按照第一种方法实现,那么equals只能比较同一个类的对象,不同类对象永远是false。但这并不是强制要求的。一般我们也很少需要在不同的类之间使用equals。
3、在具体比较对象的字段的时候,对于基本值类型的字段,直接用 == 来比较(注意浮点数的比较,这是一个坑)对于引用类型的字段,你可以调用他们的equals,当然,你也需要处理字段为null 的情况。对于浮点数的比较,我在看Arrays.binarySearch的源代码时,发现了如下对于浮点数的比较的技巧:
4、并不总是要将对象的所有字段来作为equals 的评判依据,那取决于你的业务要求。比如你要做一个家电功率统计系统,如果2个家电的功率一样,那就有足够的依据认为这2个家电对象等价了,至少在你这个业务逻辑背景下是等价的,并不关心他们的价钱啊,品牌啊,大小等其他参数。
5、最后需要注意的是,equals 方法的参数类型是Object,不要写错!
public int hashCode()
这个方法返回对象的散列码,返回值是int类型的散列码。
对象的散列码是为了更好的支持基于哈希机制的Java集合类,例如 Hashtable, HashMap, HashSet 等。
关于hashCode方法,一致的约定是:
重写了euqls方法的对象必须同时重写hashCode()方法。
如果2个对象通过equals调用后返回是true,那么这个2个对象的hashCode方法也必须返回同样的int型散列码
如果2个对象通过equals返回false,他们的hashCode返回的值允许相同。(然而,程序员必须意识到,hashCode返回独一无二的散列码,会让存储这个对象的hashtables更好地工作。)
在上面的例子中,Test类对象有2个字段,num和data,这2个字段代表了对象的状态,他们也用在equals方法中作为评判的依据。那么, 在hashCode方法中,这2个字段也要参与hash值的运算,作为hash运算的中间参数。这点很关键,这是为了遵守:2个对象equals,那么 hashCode一定相同规则。
也是说,参与equals函数的字段,也必须都参与hashCode 的计算。
合乎情理的是:同一个类中的不同对象返回不同的散列码。典型的方式就是根据对象的地址来转换为此对象的散列码,但是这种方式对于Java来说并不是唯一的要求的
的实现方式。通常也不是最好的实现方式。
相比 于 equals公认实现约定,hashCode的公约要求是很容易理解的。有2个重点是hashCode方法必须遵守的。约定的第3点,其实就是第2点的
细化,下面我们就来看看对hashCode方法的一致约定要求。
第一:在某个运行时期间,只要对象的(字段的)变化不会影响equals方法的决策结果,那么,在这个期间,无论调用多少次hashCode,都必须返回同一个散列码。
第二:通过equals调用返回true 的2个对象的hashCode一定一样。
第三:通过equasl返回false 的2个对象的散列码不需要不同,也就是他们的hashCode方法的返回值允许出现相同的情况。
总结一句话:等价的(调用equals返回true)对象必须产生相同的散列码。不等价的对象,不要求产生的散列码不相同。
hashCode编写指导
1.把对象内每个有意义的实例变量(每一个参与equal运算的变量)计算出一个int类型的hashCode值,计算方式在校表中
实例变量类型 | 计算方式 | 实例变量类型 | 计算方式 |
boolean | hashCode=(f?0:1) | float | hashCode=Float.floatTolintBits(f) |
byte、short、char、int | hashCode=(int)f | double |
long1=Double.doubleToLongBits(f) hashCode=(int)(1^(1>>>32)) |
long | hashCode=(int)(f^(f>>>32)) | 引用类型 | hashCode=f.hashCode |
2.用第一步计算出来的多个hashCode值组合计算出一个hashCode值返回。
例如:return fi.hashCode+(int)f2;
为了避免偶然相同,可以通过为各实例变量的hashCode值乘以任意一个质数后再相加。
return fi.hashCode*19+(int)f2*31;
一、Java的集合类主要由两个接口派生而出:Collection和Map
1.Collection集合体系的继承树
o-的为接口
Collection接口里面定义了如下操作集合元素的方法
Iterator接口也是Java集合框架的一员,但不同的是Collection系列集合、Map系列集合用于盛放其他对象,Iterator用于遍历Collection集合中的元素,也被称为迭代器
Iterator只是将迭代集合的值传给了book这个迭代变量,并不是集合本身,所以迭代变量的修改对集合没有影响,只有通过Iterator的remove方法才可以删除上一次next方法返回的集合元素才可以,不可以通过迭代变量删除,会引起异常
1)Set集合(无序,不用记住添加元素的位置,不可重复)
1.HashSet类:Set接口的典型实现,按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能
特点:1.不能保证元素的排列顺序
2.不是同步的,如果多个线程同时访问一个HashSet,假设有两个或两个以上线程同时修改了HashSet集合时,必须通过代码来保证其同步
3.集合元素值可以是null
实现:存入对象时,HashSet会调用对象的hashCode()来得到该对象的hashCode值,基本上,每一个对象都有一个默认的散列码,其值就是对象的内存地址。但也有一些对象的散列码不同,比如String对象,它的散列码是对内容的计算结果。然后根据该值决定在HashSet里面的存储位置。如果两个对象通过equals方法比较返回true,但是它们的hashCode方法返回值不同,HashSet会将它们存放在不同位置。也就是说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值也相当。
因为C的对象返回的equals和hashCode都相等,所以当成同一个对象。
注意!:当把一个对象放入hashSet中时,如果需要重写该对象类的equals()方法,那么也应该重写其hashCode()方法,规则是,如果两个对象通过equals方法比较返回true,这两个对象的hashCode值也应该相同。
HashSet中每个能存取元素的“槽位”通常称为“桶”,如果有多个元素的hashCode值相同,但是equals比较的值不同,那就会在一个桶放多个元素,这样会导致性能下降
如果像HashSet中添加了一个可变对象后,后面程序修改了该可变对象的实例变量,则可能导致他与集合中的其他元素相同。
如图中所示,当修改第一个的实例的值后集合中的第一个元素和第二个元素完全相同,这表明两个元素已经重复,此时HashSet会比较混乱:当试图删除count为9的R对象时,HashSet会计算出该对象的hashCode值,从而找出该对象在集合中的保存位置,并与count为-3的R对象通过equals()方法进行比较,如果相等则删除该对象,只有第二个元素才满足该条件(第一个元素还保存在count为5的R对象对应的位置),所以第二个元素被删除,至于第一个count为9的R对象,它保存在count为5的R对象对应的位置,但使用equals()方法拿它与count为5的R对象比较又返回fasle,这将导致集合不能准确访问该元素。因此尽量不要修改该集合元素中参与计算hashCode()、equals()的实例变量。
2.LinkHashSet类
LinkedHashSet是HashSet的子类,也是根据元素的hashCode来决定元素的存储位置,但他同时使用链表来维护元素的次序,这样使得元素看起来是以插入的顺序保存的
也就是说LinkedHashSet依然是HashSet,依然不允许元素重复,元素的保存位置依然如靠hashCode值来决定,但一个一个存储位置靠链表链接起来,使用链表记录下了元素的添加位置
3.TreeSet
SortedSet接口的实现类,确保集合元素处于排序状态。TreeSet中增加‘、’访问第一个、前一个、后一个、最后一个元素的方法,并提供了三个截取子集合的方法
与HashSet集合采用hash算法来决定元素的存取位置不同,TreeSet采用红黑树的数据结构来存储元素。那么TreeSet进行排序的规则是怎样的呢?支持两种排序方法:自然排序和定制排序,默认情况下,采用自然排序
1.自然排序
实现Comparable接口里的compareTo方法,该方法返回一个整数值,继承该接口必须实现该方法(0相等,正整数大于,负整数小于)
已经实现compareTo方法的常用类:BigDecimal、BiInteger以及所有的数值型对应的包装类、Character、Boolean、String、Date、Time等。
如果试图把一个对象添加到TreeSet中,必须实现Comparable接口
如果希望TreeSet能正常运行,只能添加同一种类型的对象
对于TreeSet集合而言,判断两个对象是否相等的唯一标准是:两个对象通过compareTo方法比较是否返回0;
可以看到当第一个元素改变时,第二个元素也改变了,说明两个集合里两个元素指向了同一个对象,因此,当重写equals方法时,要保证和compareTo有一致的结果。
另外,如果向TreeSet中添加了一个可变对象后,后面改变了该可变对象的实例变量,导致其与其他对象的大小顺序发生了改变,但是TreeSet并不会改变他们的顺序,甚至可能导致TreesSet中保存的这两个对象通过compareTo方法比较返回0.
上图中,如果删除了元素没有改变的值以后,集合中会重新索引。
2.定制排序
定制排序顾名思义就是按照自己想要的顺序来排序,通过Comparator接口的帮助,该接口包含compare(T t1,T t2)。
如果需要实现定制排序,则需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联。由该Compatator对象负责集合元素的排序逻辑。
实现降序。
注意的是,依旧不可以添加不同对象的元素
2)List集合
代表一个元素有序,可重复的集合
List判断两个对象相等的标准是,只要两个对象通过equals()方法返回true即可。
当调用set方法改变指定索引处的元素时,必须是List集合的有效索引。
1.ArrayList和Vector
是List的两个典型实现,支持List接口的全部功能。都是基于数组实现的List类,所以两者封装了一个动态的、允许再分配的Object()数组。使用initialCapacity参数来设置该数组的长度,如果添加的元素超出了该数组的长度时,initialCapacity会自动增加,对于一般的程序来说,无需关心initiaCapacity,但如果向集合中添加大量的元素时,可以使用ensureCapactiy()一次性的增加,减少分配次数,提高性能;另外还可以用trimToSize()方法将数组的长度调整为当前元素的个数,减少内存的占用。
两者用法几乎完全相同,不过Vector是一个古老的集合,那时候还没有集合框架,Vector做的是List的工作,提供了一些名字很长的方法,但是从JDK1.2开始,出现了集合框架,就将Vector作为List的实现之一,导致有很多功能重复的方法,很少用Vector。
两者的显著区别是ArrayList是线程不安全的,当多个线程访问同一个ArrayList集合时,如果超过一个线程修改了Arraylist集合,则程序必须手动保证该集合的同步性;Vector集合时线程安全的,无需程序保证该集合的同步性。因为Vector是线程安全的,所以Vector性能更低。
Vector提供了Stack子类,模拟“栈”这种数据结构,提供了如下几个方法:
Object peek();//取出第一个元素,但不出栈
Object pop(); //出栈
void push(Object item); //入栈
因为stack是继承自Vector,同样是比较古老的,如果要使用"栈",考虑使用ArrayDeque。后面会介绍。
数组的工具类里面提供了一个asList(Object...a)方法,可以把一个数组变成list,这个List是Arrays内部类ArrayList的实例
是Arrays的内部类,而不是ArraysList的实现类。
3)Queue集合
用于模拟队列这种数据结构,Queue接口定义了如下几个方法:
void add() 加入元素
Object element() 获取队列头部的元素,但是不删除元素
boolean offer(Object e) 加入元素,在有限容量的队列时,比add更好
Object peek() 获取队列的元素,并不删除,队列为空,返回null
Object poll() 获取队列头部的元素,并删除,如果为空,返回null
Object remove() 获取队列头部的元素,并删除该元素
1.PriorityQueue实现类
是一个比较标准的队列实现类,之所以说是比较标准,因为队列顺序是按照元素大小来进行重新排序的,从中取的时候,并不是取出最先进入队列的元素,从这个意义上看,已经违反了队列“先进先出”得意义
看到并有进行很好的排序,但这是受到PriorityQueue的toString()方法的影响,多次使用poll方法取出的时候,还是按照从小到大的顺序。
PriorityQueue的元素有两种排序方式:自然排序和定制排序
2.Deque接口与ArrayDeque实现类
Deque代表双端队列
Deque可以当做栈来使用,因为含有了push、pop方法
Deque与Queue的方法对照表
Queue | Deque |
add/offer | addlast/offerLast |
remove/poll | removeFirst/pollFirst |
element/peek | getFirst/peekFirst |
Deque与Stack的方法对照表
Stack | Deque |
push | addFirst/offerFirst |
pop | removerFirst/pollFirst |
peek | gerFirst/peekFirst |
Deque接口提供了一个典型的实现类:ArrayDeque ,是一个基于数组实现的双端队列,和ArrayList实现机制相似,采用一个动态的、可重分配的数组来存储几何元素。
把ArrayDeque当做栈来用:
当然,ArrayDeque也可以当做队列来使用
3.LinkedList实现类
既实现了List接口,又实现了Deque接口,说明其既可以根据索引来随机访问集合中的元素,又可以被当成双端队列来使用,也就是既可以被当成“栈”来使用,也可以当成队列来使用
使用链表的形式来时间,使用数组存放的随机访问有较好的性能。使用链表插入、删除时性能比较出色。
关于使用List集合有如下建议:
1.如果需要遍历List集合元素,对于ArrayList、Vector集合,应该使用随机访问方法(get)来遍历集合元素,但是对于LinkedList,应该采用迭代器(Iterator)来遍历
2.如果经常执行插入、删除操作来改变大量数据的List集合的大小,可以考虑使用LinkedList集合,使用ArrayList、Vector集合可能需要经常重新分配内部数组的大小,效果可能更差。
3.如果有多个线程需要同时访问List集合中的元素,开发者可以考虑使用Collection将集合包装成线程安全的集合
二、Map集合体系的继承树
Map用于保存具有映射关系的数据,因此map集合里保存着两组值,一组用于保存Map里的key,另外一组用于保存Map里的value,key和value都可以是任何引用数据类型的数据。
把Map里的所有的key放在一起看,他们就组成了一个set集合(没有顺序,不重复) ,实际上Map确实包含了一个keySet()方法,用于返回Map里所有key组成的set集合
Map子类和Set的子类也惊人的相似.
Set与Map关系密切,虽然Map里面放的是键值对,Set里面放的是单个的对象,但如果把键值对中的value当做key的附庸,key在哪里,value就跟在哪里,这样就可以像对待Set一样对待Map了。从Java源码看,Java是先实现了Map,再包装一个所有value都为null的Map就实现了Set集合。
如果把Map里面所有的value放在一起看,又非常像list,元素与元素之间可以重复,每个元素可以根据索引来查找,只是Map中的索引不再使用整数值,而是以另一个对象作为索引。
1.HashMap和Hashtable实现类
两者关系完全等同于ArrayList与Vector,另外两者典型区别为:
1.Hashtable是一个线程安全的Map实现,HashMap不是,所以HashMap性能会高一点,如果有多个线程访问同一个Map对象时,使用Hashtable实现类会更好。
2.HashTable不允许使用null作为key和value,HashMap可以使用null作为key或者value
因为Map里面的key不能重复,所以只能有一个键值对使用null作为key值
用作key的对象必须实现hashCode()和equals方法
Hashtalbe与HashMap与HashSet类似,也是通过equals和hashCode值判断两个key相等
除此之外,两个还包含一个containsValue()方法,判断是否包含指定的value,判断标准是:只要两个对象通过equals()方法比较返回true即可。
与HashSet类似的是,如果使用可变对象作为HashMap、Hashtable的key,并且程序修改了作为key的可变对象,则可能出现于HashSet类似的情形:程序再也无法准确访问到Map中被修改过得key
2.LinkedHashMap实现类
使用双向链表维护键值对的次序(其实只要考虑key的次序),该链表负责维护Map的迭代顺序,迭代顺序与键值对的插入顺序一致
3.Properties
Hashtable的子类,正如名字所示,该对象在处理属性文件时特别方便。可以把Map对象和属性文件关联起来,从而可以把Map对象中的键值对写入属性文件中,也可以把属性文件中的“属性名=属性值”加载到Map对象中,由于属性文件里的属性名、属性值只能是字符串类型,所以Properties里的key、value都是字符串类型。
方法:
String getProperty(String key) 获取属性名对应的属性值,类似于Map中的get
String getProperty(String key,String defaultValue)该方法与前一个方法基本相似,如果不存在指定的key时,该方法指定默认值
Object setProperty(String key ,String value): 设置属性值,类似于Hashtable的put()方法
读写属性文件的方法:
void load(InputStream inStream):从属性文件(以输入流标识)中加载键值对时,把加载到键值对追加到Properties里
void stroe(OuputStream op)将Properties中的键值对输出到指定的属性文件中
4.SortedMap接口和TreeMap实现类
TreeMap是一个红黑树数据结构,像TreeSet一样,会根据key对节点排序,分为自然排序和定制排序两种
5.各Map实现类的性能分析
HashMap和Hashtalbe实现机制几乎一样,前者线程不安全更快。
TreeMap通常比HashMap、Hashtable慢。使用其有一个好处,总处于有序状态,无需专门进行排序操作。当TreeMap被填充之后,就可以调用keySet(),取得由key组成的Set,然后使用toArray方法生成key的数组,接下来使用Arrays的binarySearch()方法在已排序的数组中快速的查询对象
对于一般的应用场景,多考虑使用HashMap,因为其是为快速查找设计的,底层也是采用数组来存储键值对
6.HashSet和HashMap的性能选项
都是用hash算法来决定存储位置,通过hash算法来控制key集合的代销
hash表里可以存储元素的位置被称为“桶”,单个“桶”里只存一个元素,拥有最好的性能:hash算法可以根据hashCode值计算出“桶”的存储位置,然后取出,但是hash表的状态是open的:在发生“hash冲突”的情况下,单个桶会存储多个元素,这些元素以链表形式存储,必须按顺序搜索
hash表含下面属性:
1.容量:桶的个数
2.初始化容量:创建hash表时桶的数量
3.尺寸:当前hash表中记录的数量
4.负载因子:等于size/capacity ,轻负载的hash表具有冲突少,事宜插入和查询的特点
还有一个负载极限,负载因子达到负载极限时,就会自动成倍的增加容量
HashSet、HashMap、Hashtable的构造器中允许制定一个负载极限,默认是0.75.
三。操作集合的工具类:Collections:Set、List、Map
1.排序操作
2.查找、替换操作
3.同步控制
4.设置不可变集合