文章目录
1. 容器概述
1.1. Java容器的引入及容器中的接口
在Java当中,如果有一个类专门用来存放其它类的对象,这个类就叫做容器(或者就叫做集合),集合就是将若干性质相同或相近的类对象组合在一起而形成的一个整体。也就是Java容器是用来对多个数据进行存储操作的结构(这里的存储,指的是内存层面的存储,不涉及到如硬盘、数据库等持久化的存储)。而对于存储多个数据在Java中已经有数组这种数据结构存在, 那为什么要引入容器呢,因为数组具有如下的特点:
- 数组一旦初始化后,其长度就确定了,难以扩容;
- 数组一旦定义后,其元素类型也就确定了,即数组中的元素类型必须相同;
- 数组中提供的方法非常有限,对于添加、删除、插入数据等操作,比较不方便,效率也较差;
- 数组无法获取数组中的实际元素个数(比如一个长度为10的数组保存整数,非0位有效,存了5个整数,剩余位置用0站位,我们无法获得其实际元素个数为5);
- 数组存储的数据是有序、可重复的,对于无序、不可重复数据的需求,数组不能满足。
Java容器和数组的区别:
- 容器不是数组,容器不能通过下标的方式访问容器中的元素;
- 数组的所有功能通过
ArrayList
(可以看作动态数组)容器都可以实现,只是实现的方式不同; - 如果非要将容器当做一个数组来使用,通过
toArray
方法返回的就是一个数组。
Java容器可以分为 Collection
和 Map
两种体系:
Collection
接口(集合):存储的是单列数据,是存储一组对象的容器,Collection
接口没有直接提供其实现类,而是又分为若干个更为详细的接口,下面介绍一些重要的接口:List
接口(列表):存储的元素有序、可重复,主要实现类有ArrayList
、LinkedList
、Vector
等;Set
接口(集):存储的元素无序、不可重复,主要实现类有HashSet
、LinkedHashSet
、TreeSet
等;Queue
接口(队列):对各种队列的实现,主要实现类有ArrayDeque
、PriorityQueue
等。
Map
接口(映射):存储的是双列数据,是存储具有映射关系即存储**键值对(key-value)**的容器,Map
接口直接就提供了其实现类,主要实现类有HashMap
、LinkedHashMap
、TreeMap
、Hashtable
、Properties
等。
在Java容器框架为不同类型的容器定义了大量的接口,其主要接口如下:
1.2. Collection<E>
接口中的抽象方法
Iterator<E> iterator(); //返回一个用于访问集合中各个元素的迭代器
int size(); //返回当前存储在集合中的元素个数
boolean isEmpty(); //判断集合中是否有元素,如果集合中没有元素,返回true
boolean contains(Object obj); //判断集合中是否包含和obj相等的对象,包含则返回true
boolean containsAll(Collection<?> other); //判断集合中是否包含另一个集合中的所有元素,包含则返回true
boolean add(E element); //向集合中添加指定的元素,如果由于该方法改变了集合,返回true
boolean addAll(Collection<?> other); //向集合中添加另一个集合的全部元素,如果由于该方法改变了集合,返回true
boolean remove(Object obj); //从集合中删除等于obj的元素,如果由于该方法改变了集合,返回true
boolean removeAll(Collection<?> other); //从集合中删除另一个集合中存在的所以元素,如果由于该方法改变了集合,返回true
void clear(); //从集合中删除所有的元素
boolean retainAll(Collecion<?> other); //从集合中删除所有与另一个集合中元素不同的元素(求交集),如果由于该方法改变了集合,返回true
Object[] toArray(); //返回这个集合的对象的数组
boolean equals(Object obj); //比较对象是否与集合相等,相等则返回true
int hashCode(); //返回该集合的哈希值
也就是说,所有 Collection
接口体系下的容器,都需要实现上述的抽象方法。需要注意的是,上述设计判断是否包含对象等类似方法,判断的都是对象的内容是否相等(需要对象的类重写了 equals
方法),也就是内部调用的 equals()
进行判断,而不是判断是否为同一个对象(用 ==
判断)。
1.3. Iterable
接口和Iterator
接口
对于 Collection
接口,它继承了 Iterable
接口:
而我们查看 Iterable
接口的源码可以看到,Collection
继承该接口表示可以返回迭代器用于遍历集合中的元素,这也是为什么 Collection
接口中有 Iterator<E> iterator();
这个方法,同时也是集合框架中的实现类可以运用 foreach
循环的原因:
public interface Iterable<T> {
// 返回一个迭代器.
Iterator<T> iterator();
// 定义了 foreach 循环的方法
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
…………………………
}
我们查看 Iterator
接口的源码可以发现其主要有 hasNext()
、next()
、remove()
方法:
public interface Iterator<E> {
// 如果迭代过程还有剩余的元素,则返回true
// 也就是说下面的当next()方法没有抛出异常,而是可以返回元素时,hasNext()返回true
boolean hasNext();
// 返回迭代中的下一个元素
// 如果迭代没有下一个元素可返回则抛出异常:NoSuchElementException
E next();
// 删除上次访问的对象
// 该方法必须紧跟在访问一个元素之后执行
void remove();
……………………
}
Iterator
的对象成为迭代器,主要用于遍历 Collection
集合中的元素,集合对象每次调用 iterator()
方法都会得到一个全新的迭代器对象,对于每个迭代器对象,默认的游标都在集合的第一个元素之前。
每次调用迭代器的 next()
方法,都会返回游标之后的那个元素,然后将游标后移。需要注意的是,remove()
方法必须紧跟在访问一个元素之后执行(即进跟着调用next()
方法后),如果上次访问之后集合已经发生变化,则这个方法会抛出IllegalStateExcepption
异常。下面是对一个集合使用迭代器迭代的例子,下面的测试程序创建了一个集合,并在迭代器遍历集合的过程中删除了值为"davis"的元素:
@Test
public void test() {
Collection<String> c = new ArrayList<>();
c.add("james");
c.add("curry");
c.add("davis");
c.add("durant");
Iterator<String> iterator = c.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if ("davis".equals(next)) {
iterator.remove();
}
}
System.out.println(c.toString());
}
其输出结果为:
[james, curry, durant]
1.4. List
接口
List
(since 1.2)接口中的元素都是有序、且可重复的,可以看做是“动态”数组,即可以扩容的数组,以替换原来是数组,其主要的实现类有ArrayList
、LinkedList
、Vector
(遗留的集合)。
ArrayList
、LinkedList
、Vector
的异同:
ArrayList
:since 1.2,作为List
接口的主要实现类,线程不安全的,效率高,底层使用Object[]
数组存储;LinkedList
:since 1.2,对于频繁的插入、删除操作,使用效率比ArrayList
高,底层使用双向链表存储,也实现了Deque
接口(Deque
接口继承了Queue
接口);Vector
:since 1.0,作为List
接口的古老实现类(出现的比List
接口还早),线程安全的,效率低,共有方法都由synchronized
关键字进行了修饰,底层使用Object[]
数组存储。
在List
接口中除了具有Collection
接口中具有的抽象方法外,还具有其特有的抽象方法,可以在指定位置插入add
或删除remove
元素,也可以查询get
指定位置的元素,还可以修改set
指定位置的元素,且其可以返回一个相反方向的迭代器即 ListIterator
。
1.5. Set
接口
Set
接口是 Collection
的子接口,其无序性不等于随机性,其无序性指的是存储数据时并非向底层结构中按索引顺序添加元素,而是根据元素的哈希值进行添加。其不可重复性指的是两个对象是否相同,其判断两个对象是否相同不是使用 ==
运算符,而是根据 equals()
方法(会先比较其哈希值)。Set
接口没有提供额外的抽象方法,其主要的实现类有HashSet
、LinkedHashSet
、TreeSet
。
HashSet
、LinkedHashSet
、TreeSet
的异同:
HashSet
:作为Set
接口的主要实现类,线程不安全的,可以存储null
值;LinkedHashSet
:是HshSet
子类,增加了链表结构,使的遍历其内部数据时,可以按照添加的顺序遍历;TreeSet
:可以按照添加对象的指定属性进行排序。
向Set
添加元素的过程(以HashSet
为例):向HashSet
中添加新元素,先调用该元素的hashCode()
方法,计算其哈希值,此哈希值通过某种算法计算出在HashSet
底层数组中的存放位置,判断数组在此位置上是否已经有元素,如果没有则直接添加成功,如果有元素,比较新元素和已经存在元素的哈希值,不同则添加成功,相同再调用equals()
方法进行比较。
1.5. Queue
与 Deque
接口
顾名思义,这两个接口分别为队列和双端队列,在 Queue
中定义了从队尾入队、对头出队的方法,而在 Deque
中定义了在对头、队尾分别出队、入队的方法。
如果想用栈话,尽量不要用JDK中的
Stack
类,因为事实上Stack
是Vector
的子类,而Vector
实际上已经不推荐使用了,同样Stack
也不推荐使用,而是可以用实现了Deque
接口的ArrayDeque
或LinkedList
。
1.7. Map
接口
存储key-value
键值对,其主要的实现类有:
HashMap
:作为Map
的主要实现类,线程不安全的,效率高,可以存储null
的键值对(key
、value
都可以是null
),其底层在jdk7及之前为数组+链表,在jdk8中为数组+链表+红黑树;LinkedHashMap
:继承了HashMap
,保证在遍历映射元素时,可以按照添加的顺序实现遍历,对于频繁的 遍历操作,此类执行效率高于HashMap
;
TreeMap
:可以按照添加的键值对(主要是对键key
进行排序)进行排序,实现排序遍历;Hashtable
:作为Map
的古老实现类,线程安全的,效率低,不能存储null
的键值对,属于历史遗留的实现类;Properties
:继承了Hashtable
,key-value
都是String
类型,常用来处理配置文件。
通过一个 Map
进行迭代要比 Collection
复杂,因为 Map
不提供迭代器,而是提供了三种方法,将 Map
对象的视图作为 Collection
对象返回,而这些视图本身就是 Collection
(继承了 Iterable
接口),故可以将它们迭代:
// Views(视图)
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
在 Map
中还定义了其内部接口 Entry
,用于表示 key-value
结构:
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
/**
* Compares the specified object with this entry for equality.
* Returns <tt>true</tt> if the given object is also a map entry and
* the two entries represent the same mapping. More formally, two
* entries <tt>e1</tt> and <tt>e2</tt> represent the same mapping
* if<pre>
* (e1.getKey()==null ?
* e2.getKey()==null : e1.getKey().equals(e2.getKey())) &&
* (e1.getValue()==null ?
* e2.getValue()==null : e1.getValue().equals(e2.getValue()))
* </pre>
* This ensures that the <tt>equals</tt> method works properly across
* different implementations of the <tt>Map.Entry</tt> interface.
*
* @param o object to be compared for equality with this map entry
* @return <tt>true</tt> if the specified object is equal to this map
* entry
*/
boolean equals(Object o);
/**
* Returns the hash code value for this map entry. The hash code
* of a map entry <tt>e</tt> is defined to be: <pre>
* (e.getKey()==null ? 0 : e.getKey().hashCode()) ^
* (e.getValue()==null ? 0 : e.getValue().hashCode())
* </pre>
* This ensures that <tt>e1.equals(e2)</tt> implies that
* <tt>e1.hashCode()==e2.hashCode()</tt> for any two Entries
* <tt>e1</tt> and <tt>e2</tt>, as required by the general
* contract of <tt>Object.hashCode</tt>.
*
* @return the hash code value for this map entry
* @see Object#hashCode()
* @see Object#equals(Object)
* @see #equals(Object)
*/
int hashCode();
// 一系列的比较器,比较键值等
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}
public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getValue().compareTo(c2.getValue());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
}
public static <K