文章目录
完整的容器分类图
上图介绍了java中容器类之间的关系。其中实线框的表示实体类,稀疏的虚线框即带有abstract的类是抽象类,交密的虚线框表示的是接口。
可以看出Collection和Map接口是Java容器的基础。
Collection & Iterator
- Iterator(迭代器)
Iterator作用是用来遍历集合,是一个接口。看看它的源码:
public interface Iterator<E> {
// 判断是否还有元素
public boolean hasNext();
// 获取元素
public NullFromTypeParam E next();
// 函数
public default void remove() {
throw new RuntimeException("Stub!");
}
// 遍历剩下元素
public default void forEachRemaining(NonNull Consumer<? super NullFromTypeParam E> action) {
throw new RuntimeException("Stub!"); }
}
看上面的类关系图,Collection生产Iteartor,List生产ListIterator,而ListIterator继承Iterator接口,那么ListIterator和Iterator有什么区别?看看ListIterator的源码:
public interface ListIterator<E> extends Iterator<E> {
/**
* 判断是否还有元素
*/
boolean hasNext();
/**
* 获取元素
*/
E next();
/**
*移除元素(前面这三个方法是相同的)
*/
void remove();
/**
* 判断是否有元素,向前遍历
*/
boolean hasPrevious();
/**
* 向前遍历获取元素
*/
E previous();
/**
* 获取下标,向后遍历
*/
int nextIndex();
/**
* 获取下边,向前遍历
*/
int previousIndex();
/**
* 修改元素。替换它访问过的最后一个元素.
*/
void set(E e);
/**
* 添加。在next()方法返回的元素之前或previous()方法返回的元素之后插入一个元素.
*/
void add(E e);
}
总结:
(1) ListIterator有add()方法,可以向List中添加对象,而Iterator不能
(2) ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
(3) ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
(4) 都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。
(5) ListIterator是一个功能更加强大的, 它继承于Iterator接口,只能用于各种List类型的访问,而Iterator还可以用与Set。
- Collection
它是List和Set集合以及Queue队列的基础,是一个接口同时继承了Iterable接口。看看源码:
public interface Collection<E> extends Iterable<E> {
/**
*
*/
int size();
/**
*
*/
boolean isEmpty();
/**
*
*/
boolean contains(Object o);
/**
*
*/
Iterator<E> iterator();
/**
*
*/
Object[] toArray();
/**
*
*/
<T> T[] toArray(T[] a);
/**
*
*/
boolean add(E e);
/**
*
*/
boolean remove(Object o);
/**
*
*/
boolean containsAll(Collection<?> c);
/**
*
*/
boolean addAll(Collection<? extends E> c);
/**
*
*/
boolean removeAll(Collection<?> c);
/**
*
*/
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
/**
*
*/
boolean retainAll(Collection<?> c);
/**
*
*/
void clear();
/**
*
*/
boolean equals(Object o);
/**
*
*/
int hashCode();
/**
*
*/
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
/**
*
*/
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
/**
*
*/
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
}
其中有一个返回Iterator的函数,Iterator iterator()。
Queue
队列是一个典型的先进先出的容器(FIFO)。
public interface Queue<E> extends Collection<E> {
boolean add(E var1);//
boolean offer(E var1);//在队尾添加以一个元素或返回false
E remove();//移除并返回队头,在队列为空的情况下抛出异常NoSuchElementException
E poll();//移除并返回队头,在队列为空的情况下返回null
E element(); // 在不移除的情况下返回队头,在队列为空的情况下抛出异常NoSuchElementException
E peek(); // 在不移除的情况下返回队头,在队列为空的情况下返回null
}
双向队列Deque
Deque也是一个接口,继承了Queue接口。即可以在任何一端添加和移除元素。
优先级队列PriorityQueue
当我们调用offer()方法来插入一个对象时,这个对象会在队列中被排序。默认的排序将使用对象在队列中的自然顺序。但我们可以通过实现Comparator接口而进行控制的。调用peek(),poll(),remove()方法时,获取的元素将是队列中优先级最高的元素。
public class PriorityQueue<E> extends AbstractQueue<E> implements Serializable {
private final Comparator<? super E> comparator;
...
}
public abstract class AbstractQueue<E> extends AbstractCollection<E> implements Queue<E> {
...
}
List
Java中List最主要的有两种。List可以添加重复的元素,元素的顺序是添加的顺序。
ArrayList
底层使用数组结构,所以随机访问很快,但向其中间插入和移除元素时较慢。
LinkedList
LinkedList继承AbstractSequentialList类,还实现了 Deque接口(Deque又继承Queue接口),所以可以使用LinkedList实现队列的功能。
底层基与链表结构,所以随机访问较慢,但向其中间插入和移除元素时较快。
Set
只要是加入Set容器中的元素都必须是唯一的,因为Set不保存重复元素。通过equals()方法以确保元素的唯一,相同的元素只保存一份。
HashSet
HashSet容器结构是散列结构,所以加入该容器的元素必须要有hashCode()方法产生散列码。由于hashCode()产生的散列码是可以重复的,但通过hashCode()和equals()必须能确定一个唯一的元素。容器中的元素是乱序的。
TreeSet
TreeSet容器结构是树结构,不基于散列结构,所以元素可以不用重写hashCode()。基于树结构,所以改容器的中的元素是排好顺序的,而顺序由元素实现Comparable接口决定。
LinkedHashSet
LinkedHashSet继承了HashSet类,所以该容器中的元素也需要重写hashCode()方法,这也LinkedHashSet的随机访问的速度仅次于HashSet容器的原因,使用了散列,但LinkedHashSet的底层结构是链表的,容器里面的元素的顺序是插入顺序,所以在迭代访问时,速度比HashSet的还快。
Map
HashMap
HashMap基于散列结构(它取代了Hashtable)。插入和查询的开销是固定的,可以通过构造器设置和负载因子,以调整容器的性能。容器中元素没有顺序,即是乱序的。
LinkedHashMap
LinkedHashMap继承了HashMap,所以该容器中的元素也需要重写hashCode()方法。LinkedHashMap的结构是链表结构,容器中元素的顺序是插入时的顺序的。所以在迭代时速度快于HashMap容器。
TreeMap
TreeMap容器结构是基于红黑树的,不基于散列结构,所以元素可以不用重写hashCode()。基于树结构,所以改容器的中的元素是排好顺序的,而顺序由元素实现Comparable或Comparator接口决定。TreeMap是唯一具有subMap()方法的Map,它可以返回一个字树。
散列和散列码
散列
散列的目的在于:想要使用一个对象来查找另一个对象。如果单单是为来达到此目的,可以使用TreeMap或者自己实现一个类似Map的功能也可以。但散列有另外的价值:散列使得查询得以快速进行。散列保存键信息,以便能够很快找到。存储一组元素最快的数据结构是数组,所以散列使用数组来表示键的信息(注意:说的是键的信息,而不是键本身)。但是如果键的数量被数组的容量限制了,该怎么办?
答案:数组并不保存键本身。而是通过对象生成一个数字,将其作为数组的下标,这个数字就是散列码,由定义在Object中的,且可以由你覆盖的hashCode()方法生成。
为解决数组容量被固定的问题,不同的键可以产生相同的下标,也就是说hashCode()产生的散列码可以重复。因此,数组多大就显得不那么重要了,任何键总能在数组中找到它的位置。
由于查询过程中首先是计算散列码,然后使用散列码查询数组,如果能够保证散列码没有冲突,那就有了一个完美的散列函数。但如果散列码冲突重复时,通常用外部链接处理:数组并不保存键本身,而是保存一个List,这个List保存的是键对应的值的元素。先通过散列码作为下表从数组中找出这个List,通过equaIs()方法线性遍历List,因此这个过程是比较慢的,但如果散列得好,这List的元素不多,也不需要多久的时间。
散列码
散列码是一个int值,散列码由定义在Object类中的hashCode()产生,Object类中默认产生的散列码是对象在内存中的地址。我们可以覆盖这个方法,重写hashCode()方法。
我们在重写hashCode()时需要遵守几点:
- hashCode()是为了反应快速,而不用过度强调唯一。
- 重写好后,检查 hashCode()最后生成的结果,确保相同的对象有相同的散列码。
equals()方法
Java容器中的元素的x.equals()方法显得格外的重要。List容器调用remove()等方法时,是基于元素的x.equals()方法,这些方法会由x.equals()方法的行为不通而有不同的结果。Set和Map容器通过equal()方法来确保元素和key的唯一。
所以我们在重写equal()方法时必须满足这5个条件:
- 自返性。对任意的x,x.equals(x)一定返回true;
- 对称性。对任意的x,y都有x.equals(y),y.equals(x);
- 传递性。对任意的x,y,z都有x.equals(y)为true,y.equals(z)为true,则有x.equals(z)为true;
- 一致性。对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一致,要么true,要么一直是false。
- 对任何不是null的x,x.equals(null);一直是false。