今天咱们来探索一下Java集合类中的一些技术细节。主要是对一些比较容易被遗漏和误解的知识点作一些讲解和补充。可能不全面,还请谅解。java
本文参考:http://cmsblogs.com/?cat=5node
具体代码在个人GitHub中能够找到git
https://github.com/h2pl/MyTech程序员
集合是咱们在Java编程中使用很是普遍的,它就像大海,海纳百川,像万能容器,盛装万物,并且这个大海,万能容器还能够无限变大(若是条件容许)。当这个海、容器的量变得很是大的时候,它的初始容量就会显得很重要了,由于挖海、扩容是须要消耗大量的人力物力财力的。github
一样的道理,Collection的初始容量也显得异常重要。因此:对于已知的情景,请为集合指定初始容量。编程
public static void main(String[] args) { StudentVO student = null; long begin1 = System.currentTimeMillis(); List<StudentVO> list1 = new ArrayList<>(); for(int i = 0 ; i < 1000000; i++){ student = new StudentVO(i,"chenssy_"+i,i); list1.add(student); } long end1 = System.currentTimeMillis(); System.out.println("list1 time:" + (end1 - begin1)); long begin2 = System.currentTimeMillis(); List<StudentVO> list2 = new ArrayList<>(1000000); for(int i = 0 ; i < 1000000; i++){ student = new StudentVO(i,"chenssy_"+i,i); list2.add(student); } long end2 = System.currentTimeMillis(); System.out.println("list2 time:" + (end2 - begin2)); }
上面代码两个list都是插入1000000条数据,只不过list1没有没有申请初始化容量,而list2初始化容量1000000。那运行结果以下:后端
list1 time:1638 list2 time:921
从上面的运行结果咱们能够看出list2的速度是list1的两倍左右。在前面LZ就提过,ArrayList的扩容机制是比较消耗资源的。咱们先看ArrayList的add方法:数组
public boolean add(E e) { ensureCapacity(size + 1); elementData[size++] = e; return true; } public void ensureCapacity(int minCapacity) { modCount++; //修改计数器 int oldCapacity = elementData.length; //当前须要的长度超过了数组长度,进行扩容处理 if (minCapacity > oldCapacity) { Object oldData[] = elementData; //新的容量 = 旧容量 * 1.5 + 1 int newCapacity = (oldCapacity * 3)/2 + 1; if (newCapacity < minCapacity) newCapacity = minCapacity; //数组拷贝,生成新的数组 elementData = Arrays.copyOf(elementData, newCapacity); } }
ArrayList每次新增一个元素,就会检测ArrayList的当前容量是否已经到达临界点,若是到达临界点则会扩容1.5倍。然而ArrayList的扩容以及数组的拷贝生成新的数组是至关耗资源的。因此若咱们事先已知集合的使用场景,知道集合的大概范围,咱们最好是指定初始化容量,这样对资源的利用会更加好,尤为是大数据量的前提下,效率的提高和资源的利用会显得更加具备优点。微信
在实际开发过程当中咱们常用asList讲数组转换为List,这个方法使用起来很是方便,可是asList方法存在几个缺陷:网络
使用8个基本类型数组转换为列表时会存在一个比较有味的缺陷。先看以下程序:
public static void main(String[] args) { int[] ints = {1,2,3,4,5}; List list = Arrays.asList(ints); System.out.println("list'size:" + list.size()); } ------------------------------------ outPut: list'size:1
程序的运行结果并无像咱们预期的那样是5而是逆天的1,这是什么状况?先看源码:
public static <T> List<T> asList(T... a) { return new ArrayList<>(a); }
asList接受的参数是一个泛型的变长参数,咱们知道基本数据类型是没法发型化的,也就是说8个基本类型是没法做为asList的参数的, 要想做为泛型参数就必须使用其所对应的包装类型。可是这个这个实例中为何没有出错呢?
由于该实例是将int类型的数组当作其参数,而在Java中数组是一个对象,它是能够泛型化的。因此该例子是不会产生错误的。既然例子是将整个int类型的数组当作泛型参数,那么通过asList转换就只有一个int 的列表了。以下:
public static void main(String[] args) { int[] ints = {1,2,3,4,5}; List list = Arrays.asList(ints); System.out.println("list 的类型:" + list.get(0).getClass()); System.out.println("list.get(0) == ints:" + list.get(0).equals(ints)); }
outPut:
list 的类型:class [I
list.get(0) == ints:true
从这个运行结果咱们能够充分证实list里面的元素就是int数组。弄清楚这点了,那么修改方法也就一目了然了:将int 改变为Integer。
public static void main(String[] args) { Integer[] ints = {1,2,3,4,5}; List list = Arrays.asList(ints); System.out.println("list'size:" + list.size()); System.out.println("list.get(0) 的类型:" + list.get(0).getClass()); System.out.println("list.get(0) == ints[0]:" + list.get(0).equals(ints[0])); } ---------------------------------------- outPut: list'size:5 list.get(0) 的类型:class java.lang.Integer list.get(0) == ints[0]:true
对于上面的实例咱们再作一个小小的修改:
public static void main(String[] args) { Integer[] ints = {1,2,3,4,5}; List list = Arrays.asList(ints); list.add(6); }
该实例就是讲ints经过asList转换为list 类别,而后再经过add方法加一个元素,这个实例简单的不能再简单了,可是运行结果呢?打出咱们所料:
Exception in thread "main" java.lang.UnsupportedOperationException at java.util.AbstractList.add(Unknown Source) at java.util.AbstractList.add(Unknown Source) at com.chenssy.test.arrayList.AsListTest.main(AsListTest.java:10)
运行结果尽然抛出UnsupportedOperationException异常,该异常表示list不支持add方法。这就让咱们郁闷了,list怎么可能不支持add方法呢?难道jdk脑壳堵塞了?咱们再看asList的源码:
public static <T> List<T> asList(T... a) { return new ArrayList<>(a); }
asList接受参数后,直接new 一个ArrayList,到这里看应该是没有错误的啊?别急,再往下看:
private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable{ private static final long serialVersionUID = -2764017481108945198L; private final E[] a; ArrayList(E[] array) { if (array==null) throw new NullPointerException(); a = array; } //................. }
这是ArrayList的源码,从这里咱们能够看出,此ArrayList不是java.util.ArrayList,他是Arrays的内部类。
该内部类提供了size、toArray、get、set、indexOf、contains方法,而像add、remove等改变list结果的方法从AbstractList父类继承过来,同时这些方法也比较奇葩,它直接抛出UnsupportedOperationException异常:
public boolean add(E e) { add(size(), e); return true; } public E set(int index, E element) { throw new UnsupportedOperationException(); } public void add(int index, E element) { throw new UnsupportedOperationException(); } public E remove(int index) { throw new UnsupportedOperationException(); }
经过这些代码能够看出asList返回的列表只不过是一个披着list的外衣,它并无list的基本特性(变长)。该list是一个长度不可变的列表,传入参数的数组有多长,其返回的列表就只能是多长。因此::不要试图改变asList返回的列表,不然你会自食苦果。
咱们常用subString方法来对String对象进行分割处理,同时咱们也可使用subList、subMap、subSet来对List、Map、Set进行分割处理,可是这个分割存在某些瑕疵。
首先咱们先看以下实例:
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<Integer>(); list1.add(1); list1.add(2); //经过构造函数新建一个包含list1的列表 list2 List<Integer> list2 = new ArrayList<Integer>(list1); //经过subList生成一个与list1同样的列表 list3 List<Integer> list3 = list1.subList(0, list1.size()); //修改list3 list3.add(3); System.out.println("list1 == list2:" + list1.equals(list2)); System.out.println("list1 == list3:" + list1.equals(list3)); }
这个例子很是简单,无非就是经过构造函数、subList从新生成一个与list1同样的list,而后修改list3,最后比较list1 == list2?、list1 == list3?。
按照咱们常规的思路应该是这样的:由于list3经过add新增了一个元素,那么它确定与list1不等,而list2是经过list1构造出来的,因此应该相等,因此结果应该是:
list1 == list2:true list1 == list3: false
首先咱们先不论结果的正确与否,咱们先看subList的源码:
public List<E> subList(int fromIndex, int toIndex) { subListRangeCheck(fromIndex, toIndex, size); return new SubList(this, 0, fromIndex, toIndex); }
subListRangeCheck方式是判断fromIndex、toIndex是否合法,若是合法就直接返回一个subList对象,注意在产生该new该对象的时候传递了一个参数 this ,该参数很是重要,由于他表明着原始list。
/**
* 继承AbstractList类,实现RandomAccess接口 */ private class SubList extends AbstractList<E> implements RandomAccess { private final AbstractList<E> parent; //列表 private final int parentOffset; private final int offset; int size; //构造函数 SubList(AbstractList<E> parent, int offset, int fromIndex, int toIndex) { this.parent = parent; this.parentOffset = fromIndex; this.offset = offset + fromIndex; this.size = toIndex - fromIndex; this.modCount = ArrayList.this.modCount; } //set方法 public E set(int index, E e) { rangeCheck(index); checkForComodification(); E oldValue = ArrayList.this.elementData(offset + index); ArrayList.this.elementData[offset + index] = e; return oldValue; } //get方法 public E get(int index) { rangeCheck(index); checkForComodification(); return ArrayList.this.elementData(offset + index); } //add方法 public void add(int index, E e) { rangeCheckForAdd(index); checkForComodification(); parent.add(parentOffset + index, e); this.modCount = parent.modCount; this.size++; } //remove方法 public E remove(int index) { rangeCheck(index); checkForComodification(); E result = parent.remove(parentOffset + index); this.modCount = parent.modCount; this.size--; return result; } }
该SubLsit是ArrayList的内部类,它与ArrayList同样,都是继承AbstractList和实现RandomAccess接口。同时也提供了get、set、add、remove等list经常使用的方法。可是它的构造函数有点特殊,在该构造函数中有两个地方须要注意:
一、this.parent = parent;而parent就是在前面传递过来的list,也就是说this.parent就是原始list的引用。
二、this.offset = offset + fromIndex;this.parentOffset = fromIndex;。同时在构造函数中它甚至将modCount(fail-fast机制)传递过来了。
咱们再看get方法,在get方法中return ArrayList.this.elementData(offset + index);
这段代码能够清晰代表get所返回就是原列表offset + index位置的元素。一样的道理还有add方法里面的:
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
remove方法里面的
E result = parent.remove(parentOffset + index);
this.modCount = parent.modCount;
诚然,到了这里咱们能够判断subList返回的SubList一样也是AbstractList的子类,同时它的方法如get、set、add、remove等都是在原列表上面作操做,它并无像subString同样生成一个新的对象。
因此subList返回的只是原列表的一个视图,它全部的操做最终都会做用在原列表上。
那么从这里的分析咱们能够得出上面的结果应该偏偏与咱们上面的答案相反:
list1 == list2:false
list1 == list3:true
从上面咱们知道subList生成的子列表只是原列表的一个视图而已,若是咱们操做子列表它产生的做用都会在原列表上面表现,可是若是咱们操做原列表会产生什么状况呢?
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<Integer>(); list1.add(1); list1.add(2); //经过subList生成一个与list1同样的列表 list3 List<Integer> list3 = list1.subList(0, list1.size()); //修改list1 list1.add(3); System.out.println("list1'size:" + list1.size()); System.out.println("list3'size:" + list3.size()); }
该实例若是不产生意外,那么他们两个list的大小都应该都是3,可是恰恰事与愿违,事实上咱们获得的结果是这样的:
list1'size:3 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$SubList.checkForComodification(Unknown Source) at java.util.ArrayList$SubList.size(Unknown Source) at com.chenssy.test.arrayList.SubListTest.main(SubListTest.java:17)
list1正常输出,可是list3就抛出ConcurrentModificationException异常,看过我另外一篇博客的同仁确定对这个异常很是,fail-fast?不错就是fail-fast机制,在fail-fast机制中,LZ花了不少力气来说述这个异常,因此这里LZ就不对这个异常多讲了。咱们再看size方法:
public int size() { checkForComodification(); return this.size; }
size方法首先会经过checkForComodification验证,而后再返回this.size。
private void checkForComodification() { if (ArrayList.this.modCount != this.modCount) throw new ConcurrentModificationException(); }
该方法代表当原列表的modCount与this.modCount不相等时就会抛出ConcurrentModificationException。
同时咱们知道modCount 在new的过程当中 "继承"了原列表modCount,只有在修改该列表(子列表)时才会修改该值(先表如今原列表后做用于子列表)。
而在该实例中咱们是操做原列表,原列表的modCount固然不会反应在子列表的modCount上啦,因此才会抛出该异常。
对于子列表视图,它是动态生成的,生成以后就不要操做原列表了,不然必然都致使视图的不稳定而抛出异常。最好的办法就是将原列表设置为只读状态,要操做就操做子列表:
//经过subList生成一个与list1同样的列表 list3
List<Integer> list3 = list1.subList(0, list1.size());
//对list1设置为只读状态
list1 = Collections.unmodifiableList(list1);
在开发过程当中咱们必定会遇到这样一个问题:获取一堆数据后,须要删除某段数据。例如,有一个列表存在1000条记录,咱们须要删除100-200位置处的数据,可能咱们会这样处理:
for(int i = 0 ; i < list1.size() ; i++){ if(i >= 100 && i <= 200){ list1.remove(i); /* * 固然这段代码存在问题,list remove以后后面的元素会填充上来, * 因此须要对i进行简单的处理,固然这个不是这里讨论的问题。 */ } }
这个应该是咱们大部分人的处理方式吧,其实还有更好的方法,利用subList。在前面LZ已经讲过,子列表的操做都会反映在原列表上。因此下面一行代码所有搞定:
list1.subList(100, 200).clear();
简单而不失华丽!!!!!
在Java中咱们常使用Comparable接口来实现排序,其中compareTo是实现该接口方法。咱们知道compareTo返回0表示两个对象相等,返回正数表示大于,返回负数表示小于。同时咱们也知道equals也能够判断两个对象是否相等,那么他们二者之间是否存在关联关系呢?
public class Student implements Comparable<Student>{ private String id; private String name; private int age; public Student(String id,String name,int age){ this.id = id; this.name = name; this.age = age; } public boolean equals(Object obj){ if(obj == null){ return false; } if(this == obj){ return true; } if(obj.getClass() != this.getClass()){ return false; } Student student = (Student)obj; if(!student.getName().equals(getName())){ return false; } return true; } public int compareTo(Student student) { return this.age - student.age; } /** 省略getter、setter方法 */ }
Student类实现Comparable接口和实现equals方法,其中compareTo是根据age来比对的,equals是根据name来比对的。
public static void main(String[] args){ List<Student> list = new ArrayList<>(); list.add(new Student("1", "chenssy1", 24)); list.add(new Student("2", "chenssy1", 26)); Collections.sort(list); //排序 Student student = new Student("2", "chenssy1", 26); //检索student在list中的位置 int index1 = list.indexOf(student); int index2 = Collections.binarySearch(list, student); System.out.println("index1 = " + index1); System.out.println("index2 = " + index2); }
按照常规思路来讲应该二者index是一致的,由于他们检索的是同一个对象,可是很是遗憾,其运行结果:
index1 = 0
index2 = 1
为何会产生这样不一样的结果呢?这是由于indexOf和binarySearch的实现机制不一样。indexOf是基于equals来实现的只要equals返回TRUE就认为已经找到了相同的元素。
而binarySearch是基于compareTo方法的,当compareTo返回0 时就认为已经找到了该元素。
在咱们实现的Student类中咱们覆写了compareTo和equals方法,可是咱们的compareTo、equals的比较依据不一样,一个是基于age、一个是基于name。
比较依据不一样那么获得的结果颇有可能会不一样。因此知道了缘由,咱们就好修改了:将二者之间的比较依据保持一致便可。
对于compareTo和equals两个方法咱们能够总结为:compareTo是判断元素在排序中的位置是否相等,equals是判断元素是否相等,既然一个决定排序位置,一个决定相等,因此咱们很是有必要确保当排序位置相同时,其equals也应该相等。
使其相等的方式就是二者应该依附于相同的条件。当compareto相等时equals也应该相等,而compareto不相等时equals不该该相等,而且compareto依据某些属性来决定排序。
今天咱们来探索一下HashSet,TreeSet与LinkedHashSet的基本原理与源码实现,因为这三个set都是基于以前文章的三个map进行实现的,因此推荐你们先看一下前面有关map的文章,结合使用味道更佳。
具体代码在个人GitHub中能够找到
https://github.com/h2pl/MyTech
文章首发于个人我的博客:
https://h2pl.github.io/2018/0...
更多关于Java后端学习的内容请到个人CSDN博客上查看:
个人我的博客主要发原创文章,也欢迎浏览 https://h2pl.github.io/
本文参考 http://cmsblogs.com/?p=599
HashSet
定义
public class HashSet<E>
extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
HashSet继承AbstractSet类,实现Set、Cloneable、Serializable接口。其中AbstractSet提供 Set 接口的骨干实现,从而最大限度地减小了实现此接口所需的工做。 ==Set接口是一种不包括重复元素的Collection,它维持它本身的内部排序,因此随机访问没有任何意义。==
本文基于1.8jdk进行源码分析。
基本属性
基于HashMap实现,底层使用HashMap保存全部元素
private transient HashMap<E,Object> map;
//定义一个Object对象做为HashMap的value
private static final Object PRESENT = new Object();
构造函数
/**
* 默认构造函数 * 初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。 */ public HashSet() { map = new HashMap<>(); } /** * 构造一个包含指定 collection 中的元素的新 set。 */ public HashSet(Collection<? extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); } /** * 构造一个新的空 set,其底层 HashMap 实例具备指定的初始容量和指定的加载因子 */ public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); } /** * 构造一个新的空 set,其底层 HashMap 实例具备指定的初始容量和默认的加载因子(0.75)。 */ public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); } /** * 在API中我没有看到这个构造函数,今天看源码才发现(原来访问权限为包权限,不对外公开的) * 以指定的initialCapacity和loadFactor构造一个新的空连接哈希集合。 * dummy 为标识 该构造函数主要做用是对LinkedHashSet起到一个支持做用 */ HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
从构造函数中能够看出HashSet全部的构造都是构造出一个新的HashMap,其中最后一个构造函数,为包访问权限是不对外公开,仅仅只在使用LinkedHashSet时才会发生做用。
方法
既然HashSet是基于HashMap,那么对于HashSet而言,其方法的实现过程是很是简单的。
public Iterator<E> iterator() {
return map.keySet().iterator(); }
iterator()方法返回对此 set 中元素进行迭代的迭代器。返回元素的顺序并非特定的。
底层调用HashMap的keySet返回全部的key,这点反应了HashSet中的全部元素都是保存在HashMap的key中,value则是使用的PRESENT对象,该对象为static final。
public int size() {
return map.size(); }
size()返回此 set 中的元素的数量(set 的容量)。底层调用HashMap的size方法,返回HashMap容器的大小。
public boolean isEmpty() {
return map.isEmpty(); } isEmpty(),判断HashSet()集合是否为空,为空返回 true,不然返回false。
public boolean contains(Object o) {
return map.containsKey(o);
}
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
//最终调用该方法进行节点查找
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //先检查桶的头结点是否存在 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //不是头结点,则遍历链表,若是是树节点则使用树节点的方法遍历,直到找到,或者为null if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null;
}
contains(),判断某个元素是否存在于HashSet()中,存在返回true,不然返回false。更加确切的讲应该是要知足这种关系才能返回true:(o==null ? e==null : o.equals(e))。底层调用containsKey判断HashMap的key值是否为空。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
map的put方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //确认初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //若是桶为空,直接插入新元素,也就是entry if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //若是冲突,分为三种状况 //key相等时让旧entry等于新entry便可 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //红黑树状况 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //若是key不相等,则连成链表 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null;
}
这里注意一点,hashset只是不容许重复的元素加入,而不是不容许元素连成链表,由于只要key的equals方法判断为true时它们是相等的,此时会发生value的替换,由于全部entry的value同样,因此和没有插入时同样的。
而当两个hashcode相同但key不相等的entry插入时,仍然会连成一个链表,长度超过8时依然会和hashmap同样扩展成红黑树,看完源码以后笔者才明白本身以前理解错了。因此看源码仍是蛮有好处的。hashset基本上就是使用hashmap的方法再次实现了一遍而已,只不过value全都是同一个object,让你觉得相同元素没有插入,事实上只是value替换成和原来相同的值而已。
当add方法发生冲突时,若是key相同,则替换value,若是key不一样,则连成链表。
add()若是此 set 中还没有包含指定元素,则添加指定元素。若是此Set没有包含知足(e==null ? e2==null : e.equals(e2)) 的e2时,则将e2添加到Set中,不然不添加且返回false。
因为底层使用HashMap的put方法将key = e,value=PRESENT构建成key-value键值对,当此e存在于HashMap的key中,则value将会覆盖原有value,可是key保持不变,因此若是将一个已经存在的e元素添加中HashSet中,新添加的元素是不会保存到HashMap中,因此这就知足了HashSet中元素不会重复的特性。
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
remove若是指定元素存在于此 set 中,则将其移除。底层使用HashMap的remove方法删除指定的Entry。
public void clear() {
map.clear();
}
clear今后 set 中移除全部元素。底层调用HashMap的clear方法清除全部的Entry。
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(); } }
clone返回此 HashSet 实例的浅表副本:并无复制这些元素自己。
后记:
因为HashSet底层使用了HashMap实现,使其的实现过程变得很是简单,若是你对HashMap比较了解,那么HashSet简直是小菜一碟。有两个方法对HashMap和HashSet而言是很是重要的,下篇将详细讲解hashcode和equals。
TreeSet
与HashSet是基于HashMap实现同样,TreeSet一样是基于TreeMap实现的。在《Java提升篇(二七)-----TreeMap》中LZ详细讲解了TreeMap实现机制,若是客官详情看了这篇博文或者多TreeMap有比较详细的了解,那么TreeSet的实现对您是喝口水那么简单。
TreeSet定义
咱们知道TreeMap是一个有序的二叉树,那么同理TreeSet一样也是一个有序的,它的做用是提供有序的Set集合。经过源码咱们知道TreeSet基础AbstractSet,实现NavigableSet、Cloneable、Serializable接口。
其中AbstractSet提供 Set 接口的骨干实现,从而最大限度地减小了实现此接口所需的工做。
NavigableSet是扩展的 SortedSet,具备了为给定搜索目标报告最接近匹配项的导航方法,这就意味着它支持一系列的导航方法。好比查找与指定目标最匹配项。Cloneable支持克隆,Serializable支持序列化。
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
同时在TreeSet中定义了以下几个变量。
private transient NavigableMap<E,Object> m;
//PRESENT会被当作Map的value与key构建成键值对
private static final Object PRESENT = new Object();
其构造方法:
//默认构造方法,根据其元素的天然顺序进行排序
public TreeSet() {
this(new TreeMap<E,Object>());
}
//构造一个包含指定 collection 元素的新 TreeSet,它按照其元素的天然顺序进行排序。
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
//构造一个新的空 TreeSet,它根据指定比较器进行排序。
public TreeSet(Collection<? extends E> c) {
this(); addAll(c);
}
//构造一个与指定有序 set 具备相同映射关系和相同排序的新 TreeSet。
public TreeSet(SortedSet<E> s) {
this(s.comparator()); addAll(s);
}
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
2、TreeSet主要方法
一、add:将指定的元素添加到此 set(若是该元素还没有存在于 set 中)。
public boolean add(E e) {
return m.put(e, PRESENT)==null; }
public V put(K key, V value) {
Entry<K,V> t = root; if (t == null) { //空树时,判断节点是否为空 compare(key, key); // type (and possibly null) check root = new Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; // split comparator and comparable paths Comparator<? super K> cpr = comparator; //非空树,根据传入比较器进行节点的插入位置查找 if (cpr != null) { do { parent = t; //节点比根节点小,则找左子树,不然找右子树 cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; //若是key的比较返回值相等,直接更新值(通常compareto相等时equals方法也相等) else return t.setValue(value); } while (t != null); } else { //若是没有传入比较器,则按照天然排序 if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; do { parent = t; cmp = k.compareTo(t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } //查找的节点为空,直接插入,默认为红节点 Entry<K,V> e = new Entry<>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e; //插入后进行红黑树调整 fixAfterInsertion(e); size++; modCount++; return null;
}
二、get:获取元素
public V get(Object key) {
Entry<K,V> p = getEntry(key); return (p==null ? null : p.value);
}
该方法与put的流程相似,只不过是把插入换成了查找
三、ceiling:返回此 set 中大于等于给定元素的最小元素;若是不存在这样的元素,则返回 null。
public E ceiling(E e) {
return m.ceilingKey(e); }
四、clear:移除此 set 中的全部元素。
public void clear() {
m.clear(); }
五、clone:返回 TreeSet 实例的浅表副本。属于浅拷贝。
public Object clone() {
TreeSet<E> clone = null; try { clone = (TreeSet<E>) super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(); } clone.m = new TreeMap<>(m); return clone; }
六、comparator:返回对此 set 中的元素进行排序的比较器;若是此 set 使用其元素的天然顺序,则返回 null。
public Comparator<? super E> comparator() {
return m.comparator(); }
七、contains:若是此 set 包含指定的元素,则返回 true。
public boolean contains(Object o) {
return m.containsKey(o); }
八、descendingIterator:返回在此 set 元素上按降序进行迭代的迭代器。
public Iterator<E> descendingIterator() {
return m.descendingKeySet().iterator(); }
九、descendingSet:返回此 set 中所包含元素的逆序视图。
public NavigableSet<E> descendingSet() {
return new TreeSet<>(m.descendingMap()); }
十、first:返回此 set 中当前第一个(最低)元素。
public E first() {
return m.firstKey(); }
十一、floor:返回此 set 中小于等于给定元素的最大元素;若是不存在这样的元素,则返回 null。
public E floor(E e) {
return m.floorKey(e); }
十二、headSet:返回此 set 的部分视图,其元素严格小于 toElement。
public SortedSet<E> headSet(E toElement) {
return headSet(toElement, false); }
1三、higher:返回此 set 中严格大于给定元素的最小元素;若是不存在这样的元素,则返回 null。
public E higher(E e) {
return m.higherKey(e); }
1四、isEmpty:若是此 set 不包含任何元素,则返回 true。
public boolean isEmpty() {
return m.isEmpty(); }
1五、iterator:返回在此 set 中的元素上按升序进行迭代的迭代器。
public Iterator<E> iterator() {
return m.navigableKeySet().iterator(); }
1六、last:返回此 set 中当前最后一个(最高)元素。
public E last() {
return m.lastKey(); }
1七、lower:返回此 set 中严格小于给定元素的最大元素;若是不存在这样的元素,则返回 null。
public E lower(E e) {
return m.lowerKey(e); }
1八、pollFirst:获取并移除第一个(最低)元素;若是此 set 为空,则返回 null。
public E pollFirst() {
Map.Entry<E,?> e = m.pollFirstEntry(); return (e == null) ? null : e.getKey(); }
1九、pollLast:获取并移除最后一个(最高)元素;若是此 set 为空,则返回 null。
public E pollLast() {
Map.Entry<E,?> e = m.pollLastEntry(); return (e == null) ? null : e.getKey(); }
20、remove:将指定的元素从 set 中移除(若是该元素存在于此 set 中)。
public boolean remove(Object o) {
return m.remove(o)==PRESENT; }
该方法与put相似,只不过把插入换成了删除,而且要进行删除后调整
2一、size:返回 set 中的元素数(set 的容量)。
public int size() {
return m.size(); }
2二、subSet:返回此 set 的部分视图
/**
* 返回此 set 的部分视图,其元素范围从 fromElement 到 toElement。 */ public NavigableSet<E> subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive) { return new TreeSet<>(m.subMap(fromElement, fromInclusive, toElement, toInclusive)); } /** * 返回此 set 的部分视图,其元素从 fromElement(包括)到 toElement(不包括)。 */ public SortedSet<E> subSet(E fromElement, E toElement) { return subSet(fromElement, true, toElement, false); }
2三、tailSet:返回此 set 的部分视图
/**
* 返回此 set 的部分视图,其元素大于(或等于,若是 inclusive 为 true)fromElement。 */ public NavigableSet<E> tailSet(E fromElement, boolean inclusive) { return new TreeSet<>(m.tailMap(fromElement, inclusive)); } /** * 返回此 set 的部分视图,其元素大于等于 fromElement。 */ public SortedSet<E> tailSet(E fromElement) { return tailSet(fromElement, true); }
最后
因为TreeSet是基于TreeMap实现的,因此若是咱们对treeMap有了必定的了解,对TreeSet那是小菜一碟,咱们从TreeSet中的源码能够看出,其实现过程很是简单,几乎全部的方法实现所有都是基于TreeMap的。
LinkedHashSet
LinkedHashSet内部是如何工做的
LinkedHashSet是HashSet的一个“扩展版本”,HashSet并无论什么顺序,不一样的是LinkedHashSet会维护“插入顺序”。HashSet内部使用HashMap对象来存储它的元素,而LinkedHashSet内部使用LinkedHashMap对象来存储和处理它的元素。这篇文章,咱们将会看到LinkedHashSet内部是如何运做的及如何维护插入顺序的。
咱们首先着眼LinkedHashSet的构造函数。在LinkedHashSet类中一共有4个构造函数。这些构造函数都只是简单地调用父类构造函数(如HashSet类的构造函数)。 下面看看LinkedHashSet的构造函数是如何定义的。
//Constructor - 1
public LinkedHashSet(int initialCapacity, float loadFactor)
{
super(initialCapacity, loadFactor, true); //Calling super class constructor
}
//Constructor - 2
public LinkedHashSet(int initialCapacity)
{
super(initialCapacity, .75f, true); //Calling super class constructor
}
//Constructor - 3
public LinkedHashSet()
{
super(16, .75f, true); //Calling super class constructor
}
//Constructor - 4
public LinkedHashSet(Collection<? extends E> c)
{
super(Math.max(2*c.size(), 11), .75f, true); //Calling super class constructor addAll(c);
}
在上面的代码片断中,你可能注意到4个构造函数调用的是同一个父类的构造函数。这个构造函数(父类的,译者注)是一个包内私有构造函数(见下面的代码,HashSet的构造函数没有使用public公开,译者注),它只能被LinkedHashSet使用。
这个构造函数须要初始容量,负载因子和一个boolean类型的哑值(没有什么用处的参数,做为标记,译者注)等参数。这个哑参数只是用来区别这个构造函数与HashSet的其余拥有初始容量和负载因子参数的构造函数,下面是这个构造函数的定义,
HashSet(int initialCapacity, float loadFactor, boolean dummy)
{
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
显然,这个构造函数内部初始化了一个LinkedHashMap对象,这个对象刚好被LinkedHashSet用来存储它的元素。
LinkedHashSet并无本身的方法,全部的方法都继承自它的父类HashSet,所以,对LinkedHashSet的全部操做方式就好像对HashSet操做同样。
惟一的不一样是内部使用不一样的对象去存储元素。在HashSet中,插入的元素是被当作HashMap的键来保存的,而在LinkedHashSet中被看做是LinkedHashMap的键。
这些键对应的值都是常量PRESENT(PRESENT是HashSet的静态成员变量,译者注)。
LinkedHashSet是如何维护插入顺序的
LinkedHashSet使用LinkedHashMap对象来存储它的元素,插入到LinkedHashSet中的元素其实是被看成LinkedHashMap的键保存起来的。
LinkedHashMap的每个键值对都是经过内部的静态类Entry<K, V>实例化的。这个 Entry<K, V>类继承了HashMap.Entry类。
这个静态类增长了两个成员变量,before和after来维护LinkedHasMap元素的插入顺序。这两个成员变量分别指向前一个和后一个元素,这让LinkedHashMap也有相似双向链表的表现。
private static class Entry<K,V> extends HashMap.Entry<K,V>
{
// These fields comprise the doubly linked list used for iteration. Entry<K,V> before, after; Entry(int hash, K key, V value, HashMap.Entry<K,V> next) { super(hash, key, value, next); }
}
从上面代码看到的LinkedHashMap内部类的前面两个成员变量——before和after负责维护LinkedHashSet的插入顺序。LinkedHashMap定义的成员变量header保存的是 这个双向链表的头节点。header的定义就像下面这样,
接下来看一个例子就知道LinkedHashSet内部是如何工做的了。
public class LinkedHashSetExample
{
public static void main(String[] args) { //Creating LinkedHashSet LinkedHashSet<String> set = new LinkedHashSet<String>(); //Adding elements to LinkedHashSet set.add("BLUE"); set.add("RED"); set.add("GREEN"); set.add("BLACK"); }
}
更多内容请关注微信公众号【Java技术江湖】
一位阿里 Java 工程师的技术小站。做者黄小斜,专一 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!(关注公众号后回复”资料“便可领取 3T 免费技术学习资源以及我我原创的程序员校招指南、Java学习指南等资源)