一、认识集合框架
引言
前言:为了方便对对象操作,我们会将多个对象集合到一起,再没学习使用集合之前我们使用的是数组来存储对象,而使用数组存储对象的话有着许多的弊端,例如我们想直接添加一个对象或是删除、修改一个对象使用数组只能通过下标来对数组进行操作,比较有局限性。数组只给我们提供了length()、clone()的方法。
数组存储方面的特点:
数组初始化以后,长度就确定了。
数组声明的类型,就决定了进行元素初始化的类型。
弊端:
初始化数组之后,长度确定就不便于扩展。
数组中提供的方法少不便于增删改查,效率不高,且无法直接获得数组中真实的元素个数。
数组存储的数据是有序的、可以重复的。
介绍集合
Java中给我们提供了现成的各种存储结构的集合,其中可以存储数量不等的多个对象,并且可以保存具有映射关系的数组。
场景:在服务器端中我们可以将由对象构成的List集合转换为JSON对象或JSON数组来进行传递关键的信息内容。
Java的集合中分为Collection、Map两个体系,这两个都是最顶层的接口。
Collection接口:单列数据,定义了存取一组对象的方法集合。
List:元素有序、可重复的集合。
Set:元素无序、不可重复的集合。
Map接口:双列数据,保存具有映射关系“key-value对”的集合。
Collection接口继承树:
Map接口继承树:
神图:网上搜集
二、 Iterator迭代器接口
介绍Iteractor接口
Iteractor对象:称为迭代器(设计模式中的一种),主要用于遍历Collection集合元素。仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建 Iterator 对象,则必须有一个被迭代的集合。
GOF(设计模式作者)这样定义迭代器:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节,为容器而生。
集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前
public interface Iterable<T> { ... }
三个主要方法
hasNext():判断是否还有下一个元素。
next():返回从第1个位置开始的元素。
remove():需要跟next()搭配,直接使用remove会报错。
遍历方法:三种
public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("123"); list.add("456"); Iterator<String> iterator = list.iterator(); //方式一:直接使用next()单个遍历 //System.out.println(iterator.next()); //System.out.println(iterator.next()); //方式二:通过hasNext()与next()配合使用(确定Iterator中的个数并输出) //while(iterator.hasNext()){ // System.out.println(iterator.next()); //} //方式三:通过原本集合的size()获取到指定个数,接着输出即可 // for(int i=0;i<list.size();i++){ // System.out.println(iterator.next()); // } //方式四:通过foreach遍历 for (String s : list) { System.out.println(s); } }
这里使用到了hasNext()与next()方法
删除元素:
public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("123"); list.add("456"); Iterator<String> iterator = list.iterator(); //删除所有元素 while(iterator.hasNext()){ iterator.next(); //需要在next()之后调用 iterator.remove(); } System.out.println(list);//[] }
源码分析(ArrayList中的迭代器)
直接从上面三个方法例子中看源码:
首先我们是通过ArraryList实例中的Iterator()获得迭代器的:
public Iterator<E> iterator() { //实际上返回的是自己的内部类Itr return new Itr(); }
Iterator()是经过重写的,其ArraryList继承了AbstractList类,又该类继承了List类,List类继承了Collection类,Collection类实现了Iterable接口,就有Iteractor()方法了。
Itr这个内部类继承了Iterator接口,所以上面是多态返回。
接着我们开始代码简略分析一下:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{ //数据都存储在这个数组之中 transient Object[] elementData; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //初始构造 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } //返回迭代器 public Iterator<E> iterator() { return new Itr(); } //内部类继承了Iterator接口 private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; Itr() {} //调用这个方法实际上就是判断当前下标是否已经到达最后一个位置了,size指当前存放对象数组中的数量 public boolean hasNext() { return cursor != size; } //主要就是返回在elementData数组的lastRet位置的数据 @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor;//首次为0 if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1;//接着加+1,表示下一次要读取的位置 return (E) elementData[lastRet = i];//这里实际上就是之前cursor的位置数 } //移除lastRet位置的对象 public void remove() { //因为初始lastRet或者执行一次remove()之后为-1,所以之前说单独调用remove()就会报错 if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { //实际上就是调用了ArrayList本身的remove()方法 ArrayList.this.remove(lastRet); cursor = lastRet;//倒退1 lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } ... } }
简单总结一下:hasNext()实际上就是判断一下当前位置是否不等于总元素个数(有点像之前遍历方法三中的方法)、next()实际上就是返回ArrayList中实际保存数据数组里指定位置的元素,remove()实际也就是调用ArrayList本身的Remove()方法,前提就是需要先next()才能执行该方法。
三、顶级接口及实现类
认识各个接口
Collection接口:顶层接口,是List、Set 和 Queue 接口的父接口。JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List) 实现。(单列集合,用来存储一个个对象)
需要记住:在 Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容 器中对象的数据类型。
List接口:存储有序,可重复的数据,顶层接口,继承了Collection接口,同时也是ArrayList、LinkedList等集合元素的父类。
Set接口:无序,无重复,与List接口同级,也继承了Collection接口,对于add、equals、hashCode方法提供额外标准。
Queue接口:是List、Set接口并列的Collection的三大接口之一。用来保持元素访问次序,除了Collection接口提供方法,还有额外的插入、读取、检查操作。
SortSet接口:直接继承于Set接口,能够使用Comparable对元素进行自然排序或使用Comparator在创建时元素提供定制排序规则,Set迭代器按升序元素遍历集合。
Map接口:支持key-value存储的对象,Map不包含重复的key,每个键最多映射一个值。该接口代替Dictionary类,Dictionary是一个抽象类而不是接口。
List接口及常用实现类
List接口
List接口:存储有序、可重复的数据,"动态"的数组替换原有的数组。
ArrayList:作为List接口的有效实现类,线程不安全,效率高,底层使用Object[] elementData存储。
LinkedList:底层使用双向链表存储,对于频繁插入、删除操作效率比ArraryList高。
Vector:List接口古老实现类,线程安全,效率低,底层使用Object[] elementData存储。
除Collection集合继承的方法外,List包含其他操作集合元素方法
void add(int index, Object ele):在index位置插入ele元素
boolean addAll(int index, Collection eles):从index位置开始将eles中 的所有元素添加进来
Object get(int index):获取指定index位置的元素
int indexOf(Object obj):返回obj在集合中首次出现的位置
int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
Object remove(int index):移除指定index位置的元素,并返回此元素
Object set(int index, Object ele):设置指定index位置的元素为ele
List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex 位置的子集合
Arrays的asList()方法
//Arrays工具类中的方法 @SafeVarargs @SuppressWarnings("varargs") public static <T> List<T> asList(T... a) { return new ArrayList<>(a); }
通过该方法得到的List集合既不是 ArrayList 实例,也不是 Vector 实例,见下:
//该类是Arrays工具类的内部类,同样也是继承了AbstractList,所以也算是List的子类 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) { a = Objects.requireNonNull(array); } ... }
该类简单实现了一个List集合,其中并没有重新实现add()方法、remove()方法(假设调用会跑异常,因为调用的是AbstractList的重写方法,就只是抛出异常,可见源码)。
它的目的就是简单的将一个数组转为一个list集合并对现有集合中的内容进行简单查询操作。
相关面试
面试题:ArraryList、LinkedList、Vector三者异同?
相同点:三者都实现了List接口,都是存储有序、可重复的数据。
不同点:如上。
ArrayList
重点:ArrayList不是线程安全的容器,有线程安全的问题,我们可以使用Collections.synchronizedList(...)来获得线程安全的List集合。
例如:List list = Collections.synchronizedList(new ArrayList(...))。获取一个单独的线程安全的类,该类创建在Collections类中,实际上就是给每个方法包了一个同步代码块。
ArrayList具有fail-fast机制,对ArrayList做出失败检测,即可抛出ConcurrentModificationException异常。
fail-fast:多个线程对同一个集合的内容进行操作,会产生fail-fast事件,例如:当线程A通过iteractor去遍历某个集合的过程中,该集合内容被其他线程改变,那么线程A访问集合时,就会出现异常,从而产生fail-fast事件。
ArrayList不同JDK版本描述
JDK7.0:
ArrayList list = new ArrayList():底层创建了长度为10的Object[]数组elementData。
list.add(666);:相当于elementData[0]=new Integer(666); 若是add()方法操作导致容量不够会进行自动扩容,默认情况下,扩容为原来的1.5倍,同时将原有数组内容复制到新数组中。
JDK8.0:
ArrayList list = new ArrayList():底层只是初始化Object[] elementData={}。
list.add(666);:第一次调用add()方法时,才创建长度为10 的数组,并将666添加到数组中,后序若是容量不够也是有扩容操作为1.5倍。
注意:
①开发中最好使用带参的构造器ArrayList(int initialCapacity),指定容量大小。
②jdk7中ArraryList里对象创建类似于单例的饿汉式,直接创建好指定10容量的数组;而在jdk8中如单例懒汉式,只有add()方法调用时才创建,延迟了数组创建,节省内存。
源码分析
JDK7源码:
无参构造器:直接创建容量为10的数组。
add()操作:简单添加操作,一旦容量不够,重新开辟并复制到一个1.5倍容量数组,接着进行添加操作。
有参构造器:直接创建容量为initialCapacity的数组。
JDK8源码:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; //无参构造器:并没有开辟指定容量大小,直接elementData = {}; public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } //首次add()方法会开辟10个容量大小的数组,接着进行添加操作;超过容量大小重新扩容创建数组 public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } //这里给出一个关键的方法(在add()中调用),其中就可以看到扩容为1.5倍 private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1);//>>1是位运算相当于除以2,所以扩容为1+0.5=1.5倍 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } }
LinkedList
对于频繁的插入与删除元素使用LinkedList集合,效率高。
LinkedList:是一个双向链表,允许存储任何元素(包含null)。
内部并没有声明数组,而是通过定义内部类Node来构成链表。
该集合所有的操作都可以表示为双向性的,索引链表的操作可以从前到后,或者往前遍历。
非线程安全的,当多个线程并发访问该集合时,一旦有线程更改链表的结构,就需要在外部上锁,我们可以通过使用Collections.synchronizedList方法获取。
源码:内部链表类Node
private static class Node<E> { E item; Node<E> next;//记录当前Node节点的前一个元素 Node<E> prev;//记录当前Node节点的后一个元素 Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
接着看下LinkedList集合中的构造器以及add()添加方法:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{ //无参构造器:没有做任何事 public LinkedList() { } //add添加方法:直接调用linkLast()方法 public boolean add(E e) { linkLast(e); return true; } void linkLast(E e) { final Node<E> l = last; //定义一个新的节点并且赋值 final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode;//若是第一次创建链表,该Node节点为第一个节点 else l.next = newNode;//不是第一个节点,那么将刚创建的节点放置到最后一个Node节点的Next()接收 size++; modCount++; } ... }
通过链表方式来存储数据。
对于链表其频繁插入以及删除元素效率是很高的,但是想要查看指定元素效率就没有ArrayList中使用数组来得快。
Vector
Vector:JDK1.0的时候就出现了,底层使用的是Object[] elementData数组来存储数据,是线程安全的。
无参构造器会直接创建长度为10的数组。
使用add()方法扩容时,会扩容原来数组长度的2倍。
内部的方法都是简单粗暴的上锁,使用时开销较大。
源码分析
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{ protected Object[] elementData; //无参构造器默认创建大小为10的数组 public Vector() { this(10); } //add()方法:扩容为2倍,可以看到其中的方法都是上锁了的 public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; } //grow是add()中的一个关键方法,用于创建一个新的数组,一旦添加时超过现有容量时,会扩容为2倍 private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);//这里1+1=2 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
Stack
Stack:栈类,我们的程序方法调用执行也是使用的栈。该类继承了Vector类,提供了push()、pop()(入栈、出栈)的方法,peek()获取栈顶数据、empty()判断栈是否为空,search(Object o)查询指定数据与栈顶的距离。
单独一个Stack类其中的方法比较少,一个更完善、可靠性更强的LIFO(后进先出)栈操作由Deque接口及其实现类,应该优先使用ArrayDeque类,如:Deque<Integer> deque = new ArrayDeque<>();
Deque是双端队列,可以作为队列,也可以使用于栈操作。
测试一下:
public class Main { public static void main(String[] args) { Deque<Integer> deque = new ArrayDeque<>(); deque.push(123); deque.push(456); System.out.println(deque.pop());//456 System.out.println(deque.pop());//123 } }
总结及相关面试题
总结
一般情况下我们都是使用ArrayList来操作数据;对于频繁插入、删除操作的话使用LinkedList;Vector是线程安全的,相关操作与ArrayList无异,效率低,尽量避免使用。若是对于多线程的话我们一般使用Collections.synchronizedList(List<T> list)获取线程的集合,实际上就是包裹了一层synchronized。
面试题
问:请问ArrayList/LinkedList/Vector的异同?谈谈你的理解?ArrayList底层是什么?扩容机制?Vector和ArrayList的最大区别?
①ArrayList和LinkedList的异同
二者都线程不安全,相对线程安全的Vector,执行效率高。
此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于 随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。对于新增 和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。
②ArrayList和Vector的区别
Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类,线程安全的。因此开销就比ArrayList要大,访问要慢。
正常情况下,大多数的Java程序员使用 ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。
Set接口及常用实现类
Set接口
Set接口:是Collection的子接口,set接口没有提供额外的方法,都是Collection中的抽象方法。
Set集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。
Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法。
接口及实现类描述
Set接口:存储无序的、不可重复的数据。
HashSet:作为Set接口的主要实现类,线程不安全,可存储null值。
LinkedHashSet:为HashSet子类,可按照添加顺序遍历。
TreeSet:可以按照添加对象的指定属性进行排序。
都是线程不安全的,除排序类可通过Collections.synchronizedSet()获取到Set集合进行外部加锁。
排序类如TreeSet可通过Collections.synchronizedSortedSet(new TreeSet(...))来获取SortedSet集合进行外部加锁。
HashSet
初步认识HashSet
HashSet:是Set接口的典型实现,大多使用Set集合都使用该实现类,按Hash算法来存储集合中的元素,对存取、查找、删除有着很好的性能。
特点:无序、不是线程安全的,集合元素可为null。
无序性:不等于随机性,存储的数据在底层数组中并非按照数组索引的顺序依次添加,而是根据数据的哈希值来作为索引位置。
不重复性:保证添加的元素按照equals()判断时,不返回true,及相同的元素只能添加一个。
HashSet集合判断两个元素相等的标准:两个对象通过hashCode()方法比较相等,并且两个对象的 equals() 方法返回值也相等。
对于存放在Set容器中的对象,该类需要重写equals()、hashCode(Object obj)方法,以实现对象相等规则,即相等的对象必须具有散列码。
实际遍历一下:
public class Main { public static void main(String[] args) { Set<String> set = new HashSet<>(); set.add("changlu"); set.add("love"); set.add("love"); set.add("liner"); //遍历Set集合,使用Iterator Iterator<String> iterator = set.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } //love //changlu //liner } }
通过迭代器进行遍历
HashSet添加过程介绍
通过调用add()方法,过程如下:
向HashSet中添加元素,首先会调用元素a所在类的hashCode()方法,计算元素a的哈希值(通过某种算法计算出在HashSet底层数组中存放的位置,即索引位置),判断数组在此位置上是否有元素。
若该位置上没有其他元素,直接将元素a添加进来。
若是该位置上有其他元素b(是以链表形式存在的多个元素),则会比较元素a和元素b的hash值
若是hash值不相同,则元素a添加成功!
若hash值相同,接着会调用元素a所在类的equals()方法
若equals()返回true,则元素a添加失败!
若equals()返回false,则元素a添加成功!
大致过程如上,对于元素a实际上是在已经存在指定索引位置上以数组+链表形式存储的。
底层也是数组,初始容量为16,当如果使用率超过0.75(即若是使用了16*0.75=12)时就会扩大容量为原来2倍。(之后依次推即可,16扩容为32,32扩容为64)
JDK不同版本说明:总结为7上8下
JDK7:将元素a放到数组中,指向原来的元素
JDK8:将元素a放在链表最后位置。
看下源码:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable{ //内部实际上使用的是HashMap private transient HashMap<E,Object> map; //添加元素 public boolean add(E e) { //直接使用了HashMap中的put()操作。 return map.put(e, PRESENT)==null; } }
这里暂时不展示HashMap相关源码,在HashMap中是使用一个Node<K,V>[] table;数组,其中存放了一个个Node节点(即链表),配合存储,其中还相关hash算法。