1、Set
HashSet概述
对于HashSet而言,它是基于HashMap实现的,底层采用HashMap来保存元素,所以如果对HashMap比较熟悉了,那么学习HashSet也是很轻松的。
我们先通过HashSet最简单的构造函数和几个成员变量来看一下,证明咱们上边说的,其底层是HashMap:
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
其实在英文注释中已经说的比较明确了。首先有一个HashMap的成员变量,我们在HashSet的构造函数中将其初始化,默认情况下采用的是initial capacity为16,load factor为0.75。
HashSet的实现
对于HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成,我们应该为保存到HashSet中的对象覆盖hashCode()和equals()。
构造方法
/**
* 默认的无参构造器,构造一个空的HashSet。
*
* 实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。
*/
public HashSet() {
map = new HashMap<E,Object>();
}
/**
* 构造一个包含指定collection中的元素的新set。
*
* 实际底层使用默认的加载因子0.75和足以包含指定collection中所有元素的初始容量来创建一个HashMap。
* @param c 其中的元素将存放在此set中的collection。
*/
public HashSet(Collection<? extends E> c) {
map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
/**
* 以指定的initialCapacity和loadFactor构造一个空的HashSet。
*
* 实际底层以相应的参数构造一个空的HashMap。
* @param initialCapacity 初始容量。
* @param loadFactor 加载因子。
*/
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<E,Object>(initialCapacity, loadFactor);
}
/**
* 以指定的initialCapacity构造一个空的HashSet。
*
* 实际底层以相应的参数及加载因子loadFactor为0.75构造一个空的HashMap。
* @param initialCapacity 初始容量。
*/
public HashSet(int initialCapacity) {
map = new HashMap<E,Object>(initialCapacity);
}
/**
* 以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。此构造函数为包访问权限,不对外公开,
* 实际只是是对LinkedHashSet的支持。
*
* 实际底层会以指定的参数构造一个空LinkedHashMap实例来实现。
* @param initialCapacity 初始容量。
* @param loadFactor 加载因子。
* @param dummy 标记。
*/
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}
add方法
/**
* @param e 将添加到此set中的元素。
* @return 如果此set尚未包含指定元素,则返回true。
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
如果此set中尚未包含指定元素,则添加指定元素。更确切地讲,如果此 set 没有包含满足(enull ? e2null : e.equals(e2))的元素e2,则向此set 添加指定的元素e。如果此set已包含该元素,则该调用不更改set并返回false。但底层实际将将该元素作为key放入HashMap。思考一下为什么?
由于HashMap的put()方法添加key-value对时,当新放入HashMap的Entry中key与集合中原有Entry的key相同(hashCode()返回值相等,通过equals比较也返回true),新添加的Entry的value会将覆盖原来Entry的value(HashSet中的value都是PRESENT),但key不会有任何改变,因此如果向HashSet中添加一个已经存在的元素时,新添加的集合元素将不会被放入HashMap中,原来的元素也不会有任何改变,这也就满足了Set中元素不重复的特性。
该方法如果添加的是在HashSet中不存在的,则返回true;如果添加的元素已经存在,返回false。其原因在于我们之前提到的关于HashMap的put方法。该方法在添加key不重复的键值对的时候,会返回null。
其余方法
/**
* 如果此set包含指定元素,则返回true。
* 更确切地讲,当且仅当此set包含一个满足(o==null ? e==null : o.equals(e))的e元素时,返回true。
*
* 底层实际调用HashMap的containsKey判断是否包含指定key。
* @param o 在此set中的存在已得到测试的元素。
* @return 如果此set包含指定元素,则返回true。
*/
public boolean contains(Object o) {
return map.containsKey(o);
}
/**
* 如果指定元素存在于此set中,则将其移除。更确切地讲,如果此set包含一个满足(o==null ? e==null : o.equals(e))的元素e,
* 则将其移除。如果此set已包含该元素,则返回true
*
* 底层实际调用HashMap的remove方法删除指定Entry。
* @param o 如果存在于此set中则需要将其移除的对象。
* @return 如果set包含指定元素,则返回true。
*/
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
/**
* 返回此HashSet实例的浅表副本:并没有复制这些元素本身。
*
* 底层实际调用HashMap的clone()方法,获取HashMap的浅表副本,并设置到HashSet中。
*/
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone();
newSet.map = (HashMap<E, Object>) map.clone();
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError();
}
}
}
LinkedHashSet概述
LinkedHashSet首先我们需要知道的是它是一个Set的实现,所以它其中存的肯定不是键值对,而是值。此实现与HashSet的不同之处在于,LinkedHashSet维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。
看到上面的介绍,是不是感觉其与HashMap和LinkedHashMap的关系很像?
注意,此实现不是同步的。如果多个线程同时访问链接的哈希Set,而其中至少一个线程修改了该Set,则它必须保持外部同步。
LinkedHashSet的实现
对于LinkedHashSet而言,它继承与HashSet、又基于LinkedHashMap来实现的。
LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承与HashSet,其所有的方法操作上又与HashSet相同,因此LinkedHashSet的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个LinkedHashMap来实现,在相关操作上与父类HashSet的操作相同,直接调用父类HashSet的方法即可。LinkedHashSet的源代码如下:
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
private static final long serialVersionUID = -2851667679971038690L;
/**
* 构造一个带有指定初始容量和加载因子的新空链接哈希set。
*
* 底层会调用父类的构造方法,构造一个有指定初始容量和加载因子的LinkedHashMap实例。
* @param initialCapacity 初始容量。
* @param loadFactor 加载因子。
*/
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
/**
* 构造一个带指定初始容量和默认加载因子0.75的新空链接哈希set。
*
* 底层会调用父类的构造方法,构造一个带指定初始容量和默认加载因子0.75的LinkedHashMap实例。
* @param initialCapacity 初始容量。
*/
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
/**
* 构造一个带默认初始容量16和加载因子0.75的新空链接哈希set。
*
* 底层会调用父类的构造方法,构造一个带默认初始容量16和加载因子0.75的LinkedHashMap实例。
*/
public LinkedHashSet() {
super(16, .75f, true);
}
/**
* 构造一个与指定collection中的元素相同的新链接哈希set。
*
* 底层会调用父类的构造方法,构造一个足以包含指定collection
* 中所有元素的初始容量和加载因子为0.75的LinkedHashMap实例。
* @param c 其中的元素将存放在此set中的collection。
*/
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
}
以上几乎就是LinkedHashSet的全部代码了,那么读者可能就会怀疑了,不是说LinkedHashSet是基于LinkedHashMap实现的吗?那我为什么在源码中甚至都没有看到出现过LinkedHashMap。不要着急,我们可以看到在LinkedHashSet的构造方法中,其调用了父类的构造方法。我们可以进去看一下:
/**
* 以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。
* 此构造函数为包访问权限,不对外公开,实际只是是对LinkedHashSet的支持。
*
* 实际底层会以指定的参数构造一个空LinkedHashMap实例来实现。
* @param initialCapacity 初始容量。
* @param loadFactor 加载因子。
* @param dummy 标记。
*/
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}
在父类HashSet中,专为LinkedHashSet提供的构造方法如下,该方法为包访问权限,并未对外公开。
由上述源代码可见,LinkedHashSet通过继承HashSet,底层使用LinkedHashMap,以很简单明了的方式来实现了其自身的所有功能。
总结
以上就是关于LinkedHashSet的内容,我们只是从概述上以及构造方法这几个方面介绍了,并不是我们不想去深入其读取或者写入方法,而是其本身没有实现,只是继承于父类HashSet的方法。
所以我们需要注意的点是:
LinkedHashSet是Set的一个具体实现,其维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。
LinkedHashSet继承与HashSet,并且其内部是通过LinkedHashMap来实现的。有点类似于我们之前说的LinkedHashMap其内部是基于Hashmap实现一样,不过还是有一点点区别的(具体的区别大家可以自己去思考一下)。
如果我们需要迭代的顺序为插入顺序或者访问顺序,那么LinkedHashSet是需要你首先考虑的。
HashSet、TreeSet总结
Set:集合,元素是无序的(因为没有索引),元素不可以重复。可以有null元素。
|—>HashSet(JDK1.2):底层数据结构是哈希表、存取速度快、元素唯一、线程不同步。
保证性元素唯一的原理:
先判断元素的hashCode值是否相同,再判断两元素的equals方法是否为true。(往HashSet里面存的自定义元素要复写hashCode和equals方法,以保证元素的唯一性!)
|—>TreeSet:底层数据结构式二叉树。可以对Set集合中的元素进行排序。元素有序、线程不同步。
TreeSet排序的第一种方式:
让元素自身具备比较性。
定义对象类,实现Compareble接口,复写compareTo方法,此方式是元素的自然顺序。
TreeSet排序的第二种方式:
当元素自身不具备比较性(比如存储学生对象时)或者具备的比较性不是我们所需要的比较性时(比如想字符串的长度排序),此时就需要让集合自身具备自定义的比较性。
那如何让集合自身具备比较性呢?可在集合初始化时,就让集合具备比较方式。即定义一个类,实现Comparator接口,覆盖compare方法。
(1)HashSet:
通过new的方式往HashSet里面存的元素的hashCode都不同,但通常我们定义对象,比如学生对象时,虽然是new的两个学生对象,但是当他们name和age一样时,我们认为是同一个对象,所以为了保证元素的唯一性,我们通常在往HashSet集合里面存储元素时,在定义对象的类中通常复写hashCode和equals方法。
public int hashCode()
{
return name.hashCode()+age*39;
}
public boolean equals(Object obj)
{
if(!(obj instanceof Student))
return false;
Student stu = (Student)obj;
return this.name.equals(stu.name)&&this.age==stu.age;
}
HashSet是如何保证元素唯一性的呢?
如果两元素的hashCode值不同,则不会调用equals方法。
如果两元素的hashCode值相同,则继续判断equals是否返回true。
hashCode和equals方法虽然定义在自定义对象类里面,但不是我们手动调用,而是往HashSet集合里面存储元素的时候,集合底层自己调用hashCode和equals,它自己拿对象去判断,自己判断两元素是否是同一个元素。
(2)TreeSet:
TreeSet要求往里面存的元素具备比较性,否则会报错。
TreeSet排序的第一种方式:让元素自身具备比较性。
定义对象类,实现Compareble接口,复写compareTo方法,此方式是元素的自然顺序。
class Student implements Comparable
{
private String name;
private int age;
public Student(String name,int age)
{
this.name=name;
this.age=age;
}
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
public int compareTo(Object obj)
{
if(!(obj instanceof Student))
throw new RuntimeException("不是学生对象!");
Student stu = (Student)obj;
int num = this.age-stu.age;
if(num==0)
return this.name.compareTo(stu.name);
return num;
}
}
TreeSet排序的第二种方式:
当元素自身不具备比较性(比如存储学生对象时)或者具备的比较性不是我们所需要的比较性时(比如想字符串的长度排序),此时就需要让集合自身具备自定义的比较性。
那如何让集合自身具备比较性呢?可在集合初始化时,就让集合具备比较方式。即定义一个类,实现Comparator接口,覆盖compare方法。
class StringLengthComparator implements Comparator
{
public int compare(Object obj1,Object obj2)
{
String s1 = (String)obj1;
String s2 = (String)obj2;
int num = new Integer(s1.length()).compareTo(new Integer(s2.length()));
if(num==0)
return s1.compareTo(s2);
return num;
}
}
class TreeSetTest
{
public static void main(String[] args)
{
TreeSet ts = new TreeSet(new StringLengthComparator());
ts.add("addfg");
ts.add("dfg");
ts.add("agtuug");
ts.add("vgjkg");
sop(ts);
}
}
基本数据类型或字符串对象均实现了Comparable接口,故同种类型基本数据间具备比较性,即自然顺序。
HashSet和HashMap的区别
- HashMap实现了Map接口 HashSet实现了Set接口。
- HashMap储存键值对 HashSet仅仅存储对象。
- 使用put()方法将元素放入map中 使用add()方法将元素放入set中。
- HashMap中使用键对象来计算hashcode值 HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false。
- HashMap比较快,因为是使用唯一的键来获取对象 HashSet较HashMap来说比较慢。
迭代器
1、Iterator(Map集合没有迭代器)
对collection进行迭代的迭代器.迭代器取代了Enumeration。
(1)迭代器就是取出集合元素的方式
(2)迭代器的作用
因为每个集合中元素的取出方式都不一样,于是就把元素的取出方式进行抽取,并定义在集合内部,
这样取出方式就可以直接访问集合内部的元素。而每个容器的数据结构不同,所以取出动作的细节也不一样,但是有共性内容:判断和取出。
那么就将共性内容进行抽取,从而形成了接口Iterater。
(3)获取迭代器的方法:
Iterator iterator() 返回在此 collection 的元素上进行迭代的迭代器。
Iterator iterator() 返回在此 set 中的元素上进行迭代的迭代器。
(4)迭代器方法:
boolean hasNext() 如果仍有元素可以迭代,则返回 true。
E next() 返回迭代的下一个元素。
void remove() 从迭代器指向的collection中移除迭代器返回的最后一个元素(可选操作)。
2、列表迭代器:ListIterator
(1)List集合特有的迭代器ListIterator是Iterator的子接口,在迭代时,不可以通过集合对象的方法操作集合中的元素,因为会发生ConcurrentModificationException(当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常)
(2)Iterator方法有限,只能对元素进行判断、取出和删除的操作。
ListIterator可以对元素进行添加和修改动作等。
(3)获取列表迭代器方法:
ListIterator listIterator() 返回此列表元素的列表迭代器(按适当顺序)。
ListIterator listIterator(int index)
返回此列表中的元素的列表迭代器(按适当顺序),从列表中指定位置开始。
(4)列表迭代器方法:
void add(E e) 将指定的元素插入列表(可选操作)。
boolean hasPrevious() 如果以逆向遍历列表,列表迭代器有多个元素,则返回 true。
int nextIndex() 返回对 next 的后续调用所返回元素的索引。
E previous() 返回列表中的前一个元素。
int previousIndex() 返回对 previous 的后续调用所返回元素的索引。
void set(E e) 用指定元素替换 next 或 previous 返回的最后一个元素(可选操作)。
迭代器和枚举的区别:
迭代器允许调用者利用定义良好的语义在迭代期间从迭代器所指向的collection移除元素方法名称得到了改进,简化书写 。
LisIterator:系列表迭代器,允许程序员按任一方向遍历列表、迭代期间修改列表。
Comparable:此接口强行对实现它的每个类的对象进行整体自然排序。使元素具备比较性。
Comparator:强行对某个对象collection进行整体排序的比较函数,使集合具备比较性。
Collections:此类完全由在 collection 上进行操作或返回 collection 的静态方法组成。
Arrays:此类包含用来操作数组(比如排序和搜索)的各种静态方法。