业内经常说的一句话是不要重复造*,但是有时候,只有自己造一个*了,才会深刻明白什么样的*适合山路,什么样的*适合平地!
我将会持续更新java基础知识,欢迎关注。
往期章节:
JAVA基础第三章-类与对象、抽象类、接口
说起集合框架,很多面试官在面试初级javaer的时候也是很喜欢问的一个知识点
我们先上一张图看看
从上面的关系图中,我们可以看到从上往下分呢~最上面的是接口,中间是抽象类,最下面就是各个具体的实现类,这个在我们上一章节中说到的抽象类与接口之间的关系的时候有提到过。
再从左往右看呢,也是大致分三块,Iterator,Collection,Map 三个最*的接口,但是最主要的部分还是Collection,Map 2个接口,而Iterator更多的像是附加产品。
Collection
我们先看看Collection接口,从这个接口往下,他的子接口有List、Set、Queue。
List
List的具体实现类有 ArrayList、LinkedList,Vector。
ArrayList
顾名思义,是一个“数组型”的集合,对于数组,我们应该知道的是,数组在定义的时候就确定了大小,不可更改。那优点就是数组的元素是通过索引访问的,效率较高,因为数组是一块连续的存储空间。
所以呢,ArrayList 就是在数组的基础上,增加了可以改变大小的接口(方法),如 add 、remove 等方法,方便我们去操作修改当前集合中的数据元素,当集合中新添加的数据超过了当前的存储空间大小时
会申请一个新的存储空间,然后将这些已有的数据拷贝过去,再添加新的数据 ,扩容后的集合的大小等于扩容前集合的1.5倍。
当我们删除一个元素的时候,会将当前被删除元素之后的元素统一向前移动一位,然后将最后的一位元素置为null,以便于gc回收。
所以,如果我们对一个ArrayList 有频繁的增删操作,这样对性能是一个极大的损耗。
ArrayList 的数据存储结构示意图如下:
假设上图中每一个黄色的格子都代表一个ArrayList 的存储空间
步骤1:在我们第一次调用add方法增加一个元素1的时候,那么list会直接扩容为默认的大小10,我们也可以在调用ArrayList 构造函数的时候传入参数,指定初始化空间大小;
步骤2:我们再继续添加数据,直到添加到11时,会判断当前的存储空间放不下要增加的数据了,这个时候会继续扩容,之后再放入数据11;
步骤3:在这一步,我们决定删除数据2,2的下标为1(数组的下标都是从0开始),也就是调用remove方法;
注意:当我们调用size方法获取到的是实际的存储的数据大小,而不是整个ArrayList 获得的存储空间大小,例如 ,步骤2中调用size方法返回的会是11,而不是15。
LinkedList
从这个名字上,我们也可以大概知道,link是关联的意思。LinkedList 和ArrayList 不同的一点是,他实现了Deque接口 这是一个双向链表的接口。
我们先看下存储结构示意图:
如上图中所示,每一个节点都是一个Node对象,其中每个Node都有三个属性,item 实际存储的数据元素,如上图中的绿色格子,next和prev,这样就构成了一个链表结构。
而要注意的是next 和prev 也是一个Node对象,而Node是LinkedList 中的静态内部类。如下图中代码所示:
在这个链表中还存在2个属性 first 和 last,分别用于存放整个集合链表中的头和尾,如果只有一个元素,那么first 和last就指向同一个元素。
数据的添加
当我们在链表中添加一个元素的时候,最后一个元素的null位置会设置引用新的Node节点,然后新添加的节点的prev会指向前一个元素的位置
我们从LinkedList 源码中做一些简单的分析
1 /** 2 * Appends the specified element to the end of this list. 3 * 4 * <p>This method is equivalent to {@link #addLast}. 5 * 6 * @param e element to be appended to this list 7 * @return {@code true} (as specified by {@link Collection#add}) 8 */ 9 public boolean add(E e) { 10 linkLast(e); 11 return true; 12 }
如上所示,从 “Appends the specified element to the end of this list.” 这句注释中,我们就大致可以明白其意,当我们调用add方法增加元素的时候,默认是在末尾追加数据。
这个时候add方法中会调用linkLast方法,具体代码如下:
1 /** 2 * Links e as last element. 3 */ 4 void linkLast(E e) { 5 final Node<E> l = last; 6 final Node<E> newNode = new Node<>(l, e, null); 7 last = newNode; 8 if (l == null) 9 first = newNode; 10 else 11 l.next = newNode; 12 size++; 13 modCount++; 14 }
上述代码中,首先会将当前的last赋给l,然后新建一个Node对象,传入新添加的数据,以及将当前集合中的last赋值给新添加节点的prev属性。
然后将新建的对象赋值给last,之后再判断最开始的last,也就是当前的l是否为null,如果是null,也就代表集合是空的,这是第一个元素,那么就把它赋给frist,否则,那么就说明已经有元素存在了,让上一个元素的next指向当前新建的对象。
最后再进行调整大小等操作。
数据添加的操作示意图如下:
数据的删除
下面我们再来看一下,LinkedList 的删除操作,也就是我们默认调用remove方法。
源码如下所示:
1 /** 2 * Retrieves and removes the head (first element) of this list. 3 * 4 * @return the head of this list 5 * @throws NoSuchElementException if this list is empty 6 * @since 1.5 7 */ 8 public E remove() { 9 return removeFirst(); 10 }
同样,从“Retrieves and removes the head (first element) of this list.”注释中,我们大致可以明白,大意是检索并移除list头部的元素。
在这个方法中直接调用了removeFirst方法,下面我们看一下removeFirst代码:
1 /** 2 * Removes and returns the first element from this list. 3 * 4 * @return the first element from this list 5 * @throws NoSuchElementException if this list is empty 6 */ 7 public E removeFirst() { 8 final Node<E> f = first; 9 if (f == null) 10 throw new NoSuchElementException(); 11 return unlinkFirst(f); 12 }
如上所示,在这个代码中,直接判断是不是存在first,也就是集合是不是空的,不是那就继续调用unlinkFirst方法,
unlinkFirst代码如下所示:
1 /** 2 * Unlinks non-null first node f. 3 */ 4 private E unlinkFirst(Node<E> f) { 5 // assert f == first && f != null; 6 final E element = f.item; 7 final Node<E> next = f.next; 8 f.item = null; 9 f.next = null; // help GC 10 first = next; 11 if (next == null) 12 last = null; 13 else 14 next.prev = null; 15 size--; 16 modCount++; 17 return element; 18 }
如上所示,从“Unlinks non-null first node f”注释中,可知,解开非空的第一个节点的关联。首先将first节点的f.item 以及f.next设置为null,以便于gc回收。在将f.next置为null之前赋值给了临时的next。
然后判断next是否为null,如果是,则说明后面没有元素了,这是集合中的唯一一个元素,将last也设置为null;否则,将next中的prev设置为null。
数据删除操作示意图如下:
所以呢,当我们对linkedList进行增删操作的时候只需要对2个节点进行修改,而对其他节点没有任何影响。
Vector
这个类和ArrayList基本相似,不同的点在于他是线程安全的,也就是在同一个时刻,只能有一个线程访问Vector;另外Vector扩容不同于ArrayList,他每次扩容默认都是按2倍,而ArrayList是1.5倍。
ArrayList、LinkedList、 Vector三者之间的异同
ArrayList与LinkedList相比查询速度快,增删速度慢。
所以如果只是查询,建议用前者,反之建议用后者,因为后者再增删的时候,只需要修改2个节点的 prev和next ,而不存在复制当前已有的元素到新的存储空间。
Vecor和ArrayList基本相似,区别是前者是线程安全的,后者不是。但是2个底层实现都是数组,LinkedList底层实现是链表。
集合的遍历
经常用到有三种方式,代码示意如下:
1 /* 第一种遍历方式 2 for循环的遍历方式 3 */ 4 for (int i = 0; i < lists.size(); i++) { 5 System.out.print(lists.get(i)); 6 } 7 8 9 /* 第二种遍历方式 10 foreach的遍历方式 11 */ 12 for (Integer list : lists) { 13 System.out.print(list); 14 } 15 16 17 /* 第三种遍历方式 18 Iterator的遍历方式 19 */ 20 for (Iterator<Integer> list = lists.iterator(); list.hasNext();) { 21 System.out.print(list.next()); 22 }
for循环效率高于Iterator循环,高于foreach循环,因为我们都知道他们的底层实现都是数组,而for循环是通过下标查询是最适合的遍历方式; 而foreach循环是在Iterator基础上进行的,所以最慢。
另外,迭代器遍历方式, 适用于连续内存存储方式,比如数组、 ArrayList,Vector。 缺点是只能从头开始遍历, 优点是可以一边遍历一边删除。
for循环这种方式遍历比较灵活,可以指定位置开始遍历。性能最高,但有一个缺点就是遍历过程中不允许删除元素,否则会抛ConcurrentModificationException。
(注:但是曾经发现在删除倒数第2个元素的时候,并不会抛出异常,详见 记一次list循环删除元素的突发事件!)
Set
Set不允许包含相同的元素,如果试图把两个值相同元素加入同一个集合中,add方法返回false。
Set判断两个对象相同不是使用==运算符,而是根据equals方法。也就是说,只要两个对象用equals方法比较返回true,Set就不会再存储第二个元素。
set的实现类有HashSet、TreeSet、LinkedHashSet
HashSet
不能保证元素的排列顺序;不是线程安全的;集合元素可以是null,但只能放入一个null,其他相同数据也只能有一份存在;
对于HashSet我们要知道的是,他是依靠HashMap中的key去维护存放的数据,所以HashSet的这些特性都是和HashMap的key相关的。
hashSet默认构造函数代码如下:
1 /** 2 * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has 3 * default initial capacity (16) and load factor (0.75). 4 */ 5 public HashSet() { 6 map = new HashMap<>(); 7 }
如上代码所示,他是调用了HashMap的默认构造函数。
LinkedHashSet
LinkedHashSet和HashSet的区别是前者是有序的,也就是当你插入数据的时候会按顺序排放,这样我们遍历时就可以按照之前插入的顺序获取数据。
和HashSet相似也是在构造函数中调用了LinkedHashMap构造方法,代码如下所示:
1 /** 2 * Constructs a new, empty linked hash set with the default initial 3 * capacity (16) and load factor (0.75). 4 */ 5 public LinkedHashSet() { 6 super(16, .75f, true); 7 }
因为LinkedHashSet继承了HashSet,所以他调用super,就是调用的HashSet的构造器,在HashSet中再调用了LinkedHashMap,代码如下所示:
1 /** 2 * Constructs a new, empty linked hash set. (This package private 3 * constructor is only used by LinkedHashSet.) The backing 4 * HashMap instance is a LinkedHashMap with the specified initial 5 * capacity and the specified load factor. 6 * 7 * @param initialCapacity the initial capacity of the hash map 8 * @param loadFactor the load factor of the hash map 9 * @param dummy ignored (distinguishes this 10 * constructor from other int, float constructor.) 11 * @throws IllegalArgumentException if the initial capacity is less 12 * than zero, or if the load factor is nonpositive 13 */ 14 HashSet(int initialCapacity, float loadFactor, boolean dummy) { 15 map = new LinkedHashMap<>(initialCapacity, loadFactor); 16 }
TreeSet
TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。
TreeSet支持两种排序方式,自然排序 和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象。
自然排序不用多说,那定制排序的意思就是,我们可以自己通过实现Comparator接口覆写其中比较方法,然后按照我们意愿进行排序,比如自然排序是升序,我们通过覆写这个排序方法,可以修改成降序。
TreeSet带比较器的构造函数代码如下:
1 /** 2 * Constructs a new, empty tree set, sorted according to the specified 3 * comparator. All elements inserted into the set must be <i>mutually 4 * comparable</i> by the specified comparator: {@code comparator.compare(e1, 5 * e2)} must not throw a {@code ClassCastException} for any elements 6 * {@code e1} and {@code e2} in the set. If the user attempts to add 7 * an element to the set that violates this constraint, the 8 * {@code add} call will throw a {@code ClassCastException}. 9 * 10 * @param comparator the comparator that will be used to order this set. 11 * If {@code null}, the {@linkplain Comparable natural 12 * ordering} of the elements will be used. 13 */ 14 public TreeSet(Comparator<? super E> comparator) { 15 this(new TreeMap<>(comparator)); 16 }
TreeSet默认构造函数代码如下:
1 /** 2 * Constructs a new, empty tree set, sorted according to the 3 * natural ordering of its elements. All elements inserted into 4 * the set must implement the {@link Comparable} interface. 5 * Furthermore, all such elements must be <i>mutually 6 * comparable</i>: {@code e1.compareTo(e2)} must not throw a 7 * {@code ClassCastException} for any elements {@code e1} and 8 * {@code e2} in the set. If the user attempts to add an element 9 * to the set that violates this constraint (for example, the user 10 * attempts to add a string element to a set whose elements are 11 * integers), the {@code add} call will throw a 12 * {@code ClassCastException}. 13 */ 14 public TreeSet() { 15 this(new TreeMap<E,Object>()); 16 }
从上面的源码注释中,我们大致可以明白,其意是构造一个新的空的树形set,排序按照元素的自然顺序排序,所有要插入到set中的元素必须实现Comparable接口,同时,这些元素还必须是互相可以比较的。
如果使用者尝试添加一个string类型的数据到integer类型的set中,那么会抛出ClassCastException 异常。
关于Map接口,我们将在下一章节中做一个详细的分析
文中若有不正之处,欢迎批评指正!