带着问题学Java:1.Java中的集合
原创文章,未经本人允许不得转载

上图参考http://blog.csdn.net/zhangerqing/article/details/8122075自己重新画了一幅Collection家族的梳理
1.我们知道Collection是所有集合类实现的最终接口,可是在图上可以看到它又继承自Iterable这个接口,为什么Iterable不能说所有集合的最终接口?
虽然Collectino接口又继承了Iterable这个接口,但是对于集合的规则是定义Collecion中的,Iterable这个接口的作用是用来区分哪些类是可迭代的,对于可迭代的类,我们才可以进行迭代。
在这个Iterable中只有三个方法:
Iterator<T> iterator(); default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } default Spliterator<T> spliterator() { return Spliterators.spliteratorUnknownSize(iterator(), 0); }
应当注意到,这里边除了iterator这个迭代器以外,还有两个方法,其中forEach可以说我们非常熟悉了,所以,实现了Iterable的类不光可以创建迭代器进行迭代,还可以使用ForEach这个方法进行迭代。(迭代器的作用是用来的迭代,但迭代这个词本身含义是依次获取全部元素)
2.List,Set,和Queue有什么分别?
对于区别我想直接拿JDK的官方注释来说
对于List:
* An ordered collection (also known as a <i>sequence</i>). The user of this * interface has precise control over where in the list each element is * inserted. The user can access elements by their integer index (position in * the list), and search for elements in the list.<p> * * Unlike sets, lists typically allow duplicate elements. More formally, * lists typically allow pairs of elements <tt>e1</tt> and <tt>e2</tt> * such that <tt>e1.equals(e2)</tt>, and they typically allow multiple * null elements if they allow null elements at all. It is not inconceivable * that someone might wish to implement a list that prohibits duplicates, by * throwing runtime exceptions when the user attempts to insert them, but we * expect this usage to be rare.<p>
List是一个有序集合,所有实现这个集合的类都可以通过索引来获取到对应的元素,在添加元素的时候可以是重复元素,甚至是多个null也没问题
我们看set怎么说:
* A collection that contains no duplicate elements. More formally, sets * contain no pair of elements <code>e1</code> and <code>e2</code> such that * <code>e1.equals(e2)</code>, and at most one null element. As implied by * its name, this interface models the mathematical <i>set</i> abstraction.
Set这个集合的特色就是元素不能重复,它可以往里边存放null,但是仅仅只能存放一个null,需要注意的是,在Set中并没有提供直接获取某个元素的方法,仅仅提供了迭代器供我们获取全部元素。
Queue:
* <p>Queues typically, but do not necessarily, order elements in a * FIFO (first-in-first-out) manner. Among the exceptions are * priority queues, which order elements according to a supplied * comparator, or the elements' natural ordering, and LIFO queues (or * stacks) which order the elements LIFO (last-in-first-out). * Whatever the ordering used, the <em>head</em> of the queue is that * element which would be removed by a call to {@link #remove() } or * {@link #poll()}. In a FIFO queue, all new elements are inserted at * the <em>tail</em> of the queue. Other kinds of queues may use * different placement rules. Every {@code Queue} implementation * must specify its ordering properties.
Queue的作用其实相当于给集合这个概念多提供了一种数据存放方式,它能保证数据都在内存中先进先出,保证了数据的存放顺序。队列是一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。在上图我们看LinkedList间接实现了Queue,所以LinkedList也常常看作是一个队列。

3我们知道数组可以存放任意数据类型,那么我们为什么还需要ArrayList?
ArrayList是我们最熟悉的一个集合类,它的底层是数组。和数组一样通过索引获取对应元素。对于数组来说,可以存放4类八种基本类型,也可以存放各种各样的对象(ArrayList只可以存放对象,但是由于底层是Object数组,所以对一传入的基本类型数据有自动进行了装箱)。可以说是相当灵活,但是我们看一个数组的创建我们就会发现问题
int [] arr = new int[10];
我们使用数组,必须要给数组指定容量,而且一旦指定以后就不能动态进行扩展。我们在实际中经常遇到的情况是完全不确定会又多少个元素,这个时候如果直接给数组设置一个非常大的容量,有很有可能造成内存溢出。ArrayList的出现就很好的解决了这个问题。它在添加元素时可以动态的对集合容量进行扩展。
我们以一个例子来分析:
List<Integer> list =new ArrayList<>(3); list.add(null); list.add(null); list.add(3); list.add(3); System.out.println(list);
这是最简单的创建一个初始大小为3的list,然后往里边存入四个数据进行打印,那么它的底层是如何实现的呢?
第一步:创建一个Object数组
public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
其中:
private static final int DEFAULT_CAPACITY = 10; private static final Object[] EMPTY_ELEMENTDATA = {}; transient Object[] elementData;
可以看到,如果我们指定了长度,那么初始容量就是我们指定的大小,如果没有指定,就会使用默认值10.
第二步:添加元素
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
集合的size是在原来容量的基础上进行加1,这个地方调用了一个方法ensureCapacityInternal
private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
获取当前元素个数,如果当前个数已经大于我们定义ArrayList时指定的容量大小,就执行扩容的方法。
第三步:扩容
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1);//右移一位相当于原来的数除以2 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); }
我们看到,一旦超过原来长度,ArrayList就会创建一个长度是原来1.5倍的新集合,然后把元素拷贝过去.于是这样我们就实现了ArrayList的动态扩容。
4.ArrayList和LinkedList有什么不同?

那么为什么数组的查询就比链表的查询快,对增删改的支持就不如链表呢?
这一点需要从数据结构这里说起,数据结构可以分为两大类:线性结构和非线性结构,线性结构包括:数组,链表,队列,栈等,非线性结构包括树,图,表等。(引用自:http://blog.csdn.net/zhangerqing/article/details/8796518)
所谓线性表,就是表中数据元素之间的关系是一对一的关系,除第一个和最后一个数据组元素之外,其他数据元素都是首位相接。线性表的概念来自数据结构,而数组是数据结构的一种,线性表可以是静态的就可以理解成数组,而线性表又可以是动态的那就是链表。
数组:

当进行查询时,我们可以很容易的根据索引获取元素,但是当执行增删改操作时,比如我们在索引为2的元素后边添加一个新的元素,那么这个元素之后的全部索引都需要重新建立,性能消耗也就在这里。
单向链表:

除开第一个和最后一个元素,每一个元素在保存时分别保存了下一个元素的位置信息和这个位置上的数据信息。如果我们需要在0x002这个元素后边添加一个新的元素,只需要把指向0x003的指针指向新的数据位置即可。

但是在我们查询数据时,除了第一个和最有一个元素,获取其他位置上的元素都需要根据指针一个个的去找,假如数据量很大,查询效率就会变得非常慢。
双向链表:

5.ArrayList和Vector的区别?
Vector几乎就是另一个ArrayList,最大的区别就是处理多线程问题时。换句话说即使线程安全。这体现在Vector中几乎所有的方法都加了synchronized这个关键字,导致在执行多线程问题时必须同步。
这在多线程问题中是必须的,但是也正因为加锁释放锁,增加了资源消耗,所以在执行效率上Vector要远远不如ArrayList快。
而在扩容时,Vector和ArrayList也不同:
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
Vector在容量满了以后,容量会直接扩容为原来的2倍。
那么有没有什么办法让ArrayList变成线程安全的呢?
在Collections这个工具类中提供了一个synchronizedCollection的方法,通过调用这个方法我们可以让ArrayList变成线程安全。
List<Integer> list = new ArrayList<>(); Collection<Integer> collection = Collections.synchronizedCollection(list);
但是在Oracle的官方文档中有这么一句话:
If you need synchronization, a Vector will be slightly faster than an ArrayList synchronized with Collections.synchronizedList
这是为什么呢?
SynchronizedList<E>类使用了委托(delegation),实质上存储还是使用了构造时传进来的list,只是将list作为底层存储,对它做了一层包装。正是因为多了一层封装,所以就会比直接操作数据的Vector慢那么一点点。(引用自:https://www.jianshu.com/p/a20052ac48f1)
6.HashSet是怎么存储数据的?
先来看看官方给的注释:
* This class implements the <tt>Set</tt> interface, backed by a hash table * (actually a <tt>HashMap</tt> instance). It makes no guarantees as to the * iteration order of the set; in particular, it does not guarantee that the * order will remain constant over time. This class permits the <tt>null</tt> * element.
它的底层是一个hash表,它不保证按顺序存储,也不保证这个顺序不变。那它如何实现这个存储方式的呢?
public HashSet() { map = new HashMap<>(); }
看到了没有,虽然HashSet和HashMap不同,但是在实现HashSet的时候,却是用HashMap来作为一个实例。HashMap是一个K-V形式的存储对象,我们常说HashSet就是HashMap的键,这又是什么原因呢?
首先看HashSet的add方法:
private static final Object PRESENT = new Object(); private transient HashMap<E,Object> map; public boolean add(E e) { return map.put(e, PRESENT)==null; }
Set<Integer> set1 = new HashSet<>(); set1.add(1); set1.add(null);//特别注意,HashSet是可以存入null的,原因就是HashMap的key可以存入Null System.out.println(set1);
当我们向HashSet传入一个值时,首先对这个值进行自动装箱,以这个对象最为HashMap中的key,然后value传入一个Object对象,如果hashmap中没有这个k-v,就返回true。所以上面那个例子,其底层的实现其实相当于这样:
private static final Object PRESENT = new Object();
Map<Integer, Object> hashset = new HashMap<>(); hashset.put(1, PRESENT); hashset.put(null, PRESENT);
我们知道Set没有提供根据索引获取对应元素的方法,所以HashSet也没有,对应的是迭代器和forEach.所以在使用HashSet的迭代器对内部元素进行迭代时,我们实际上只是获取到了这个HashMap的全部key值:
public Iterator<E> iterator() { return map.keySet().iterator(); }
7.LinkedHashSet和HashSet的区别?
先看官方注释:
* <p>Hash table and linked list implementation of the <tt>Set</tt> interface, * with predictable iteration order. This implementation differs from * <tt>HashSet</tt> in that it maintains a doubly-linked list running through * all of its entries. This linked list defines the iteration ordering, * which is the order in which elements were inserted into the set * (<i>insertion-order</i>). Note that insertion order is <i>not</i> affected * if an element is <i>re-inserted</i> into the set. (An element <tt>e</tt> * is reinserted into a set <tt>s</tt> if <tt>s.add(e)</tt> is invoked when * <tt>s.contains(e)</tt> would return <tt>true</tt> immediately prior to * the invocation.)
LinkedHashSet继承了HashSet,底层除了使用hash表以外,还使用了双向链表这种数据结构,通过双向链表把内存中离散的元素按照插入顺序连接起来,保证在迭代时可以按照插入顺序获取元素,但是如果再次插入一个已有的数据,那么它的位置也会改变。
需要值得一提,LinkedHashSet中几乎没有什么方法:

也就是说除了数据变得有顺序这一点,它的操作方法都是继承自父类。并没有因为长的像linkedList就具备了相似的方法。
8.TreeSet的特点
先看源码:
private static final Object PRESENT = new Object(); public TreeSet() { this(new TreeMap<E,Object>()); } public boolean add(E e) { return m.put(e, PRESENT)==null; }
这是TreeSet的构造方法和add方法,和HashSet唯一的不同就是HashSet底层是一个HashMap而我们的TreeSet的底层是一个TreeMap.
由于TreeMap的底层是一个红黑树,其特点是按照键的自然顺序进行排序,所以TreeSet也就拥有了同样的特点。不过需要注意的一点是,TreeSet不可以存储null.这一点也是TreeMap的规定,不允许key为null.

Set下边这三个常用的子类的区别如下:

9.对于自定义对象,当向HashSet中存储时,是如何判断两个对象是相同的?
首先我们定义一个Person类:
public class Person { private String name; private int age; public Person() { super(); // TODO Auto-generated constructor stub } public Person(String name, int age) { super(); this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
然后写一段测试代码:
Set<Person> set1 = new HashSet<>(); Person p1 = new Person(); p1.setAge(14); p1.setName("Sakura"); set1.add(p1); Person p2 = new Person(); p2.setAge(14); p2.setName("Sakura"); set1.add(p2); System.out.println(set1);
我们new了两个对象,尽管我们对这两个对象传入的值是相同的,但是由于地址不同,所以set中仍然会保存两个对象。
输出结果:
[Bean.Person@7852e922, Bean.Person@4e25154f]
实际上我们关心的不是这个地址,而是对象的内容,要实现相同内容的Person对象在HashSet中只保存一份,只需要实现两个方法:
@Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + age; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Person other = (Person) obj; if (age != other.age) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; }
hashcode和equals方法是Object类定义的,所有类都可以对这两个方法进行重写。当两个对象传入的时候,首先调用hashcode方法判它们的hash值是否相同,如果不同,就肯定代表两个对象。但是hash值相同的两个对象未必能够保证对象内容也是相同的,所以调用equals方法进行比较,在equals方法中先对传入的值进行判断是不是地址值相同,相同代表对象也是相同的,然后判断是不是为null,如果为null,返回false,在通过反射判断是不是同一个类加载文件,如果不是返回false.最后才会判断对象的属性是否相同。

所以在添加了hashcode方法和equals方法后,再次运行,就会发现只有一个输出:
[Bean.Person@932b91fa]

浙公网安备 33010602011771号