集合
基本概念
Java集合类库采用“持有对象”(holding objects)的思想,并将其分为两个不同的概念,表示为类库的基本接口:
- 集合(Collection) :一个独立元素的序列,这些元素都服从一条或多条规则。List 必须以插入的顺序保存元素, Set 不能包含重复元素, Queue 按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。
- 映射(Map) : 一组成对的“键值对”对象,允许使用键来查找值。 ArrayList 使用数字来查找对象,因此在某种意义上讲,它是将数字和对象关联在一起。 map 允许我们使用一个对象来查找另一个对象,它也被称作关联数组(associative array),因为它将对象和其它对象关联在一起;或者称作字典(dictionary),因为可以使用一个键对象来查找值对象,就像在字典中使用单词查找定义一样。 Map 是强大的编程工具。
Java 集合框架简图,黄色为接口,绿色为抽象类,蓝色为具体类。虚线箭头表示实现关系,实线箭头表示继承关系。
Collection接口
Collection接口方法
向Collectioan接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals()
- add()
- addAll()
- size()
- clear()
- isEmpty()
- contains() 调用当前类的equals()方法进行判断。
- containsAll()
- remove()
- removeAll() 求差集
- retainAll() 求交集
- equals() 判断两个集合
- hashCode()
- toArray() 集合转化成数组 数组——>集合 Arrays.asList()
Collections工具类
Collections是一个操作Set、List和Map等集合的工具类
Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。
排序操作: (均为static方法)
reverse(List): 反转List 中元素的顺序
shuffle(List): 对List集合元素进行随机排序
sort(List): 根据元素的自然顺序对指定List集合元素按升序排序
sort(List, Comparator): 根据指定的Comparator产生的顺序对List集合元素进行排序
swap(List, int, int): 将指定list集合中的i处元素和j处元素进行交换
查找、替换:
Object max(Collection): 根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection, Comparator): 根据Comparator指定的顺序,返回给定集合中的最大元素
Object min(Collection)
Object min(Collection, Comparator)
int frequency(Collection, Object): 返回指定集合中指定元素的出现次数
void copy(List dest,List src): 将src中的内容复制到dest中
boolean replaceAll(List list, Object oldVal, Object newVal): 使用新值替换List对象的所有旧值
怎么确保一个集合不能被修改?
可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。
原因:每一个修改方法中都throw异常
迭代器Iterators
迭代器通常被称为轻量级对象(lightweight object):创建它的代价小。因此,经常可以看到一些对迭代器有些奇怪的约束。例如,Java 的 Iterator 只能单向移动。
这个 Iterator 只能用来:
- 使用
iterator()
方法要求集合返回一个 Iterator。 Iterator 将准备好返回序列中的第一个元素。 - 使用
next()
方法获得序列中的下一个元素。 - 使用
hasNext()
方法检查序列中是否还有元素。 - 使用
remove()
方法将迭代器最近返回的那个元素删除。
Iterator 的真正威力:能够将遍历序列的操作与该序列的底层结构分离。出于这个原因,我们有时会说:迭代器统一了对集合的访问方式。
使用Iterable接口
public class CrossCollectionIteration2 {
public static void display(Iterable<Pet> ip) {
Iterator<Pet> it = ip.iterator();
while(it.hasNext()) {
Pet p = it.next();
System.out.print(p.id() + ":" + p + " ");
}
System.out.println();
}
public static void main(String[] args) {
List<Pet> pets = Pets.list(8);
LinkedList<Pet> petsLL = new LinkedList<>(pets);
HashSet<Pet> petsHS = new HashSet<>(pets);
TreeSet<Pet> petsTS = new TreeSet<>(pets);
display(pets);
display(petsLL);
display(petsHS);
display(petsTS);
}
}
ListIterator
ListIterator 是一个更强大的 Iterator 子类型,它只能由各种 List 类生成。 Iterator 只能向前移动,而 ListIterator 可以双向移动。它可以生成迭代器在列表中指向位置的后一个和前一个元素的索引,并且可以使用 set()
方法替换它访问过的最近一个元素。可以通过调用 listIterator()
方法来生成指向 List 开头处的 ListIterator ,还可以通过调用 listIterator(n)
创建一个一开始就指向列表索引号为 n 的元素处的 ListIterator 。
public class ListIteratorTset {
public static void main(String[] args) {
List list = Arrays.asList(1,2,3,4,5);
ListIterator it = list.listIterator(5);
int i = 10;
while(it.hasPrevious()){
System.out.println(it.previous());
it.set(i++);
}
System.out.println(list);
}
}
for-in
到目前为止,for-in 语法主要用于数组,但它也适用于任何 Collection 对象。
Java 5 引入了一个名为 Iterable 的接口,该接口包含一个能够生成 Iterator 的 iterator()
方法。for-in 使用此 Iterable 接口来遍历序列。因此,如果创建了任何实现了 Iterable 的类,都可以将它用于 for-in 语句中:
public class IterableClass implements Iterable<String> {
protected String[] words = ("And that is how " +
"we know the Earth to be banana-shaped."
).split(" ");
@Override
public Iterator<String> iterator() {
return new Iterator<String>() {
private int index = 0;
@Override
public boolean hasNext() {
return index < words.length;
}
@Override
public String next() { return words[index++]; }
@Override
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
for(String s : new IterableClass())
System.out.print(s + " ");
}
}
/* Output:
And that is how we know the Earth to be banana-shaped.
*/
for-in 语句适用于数组或其它任何 Iterable ,但这并不意味着数组肯定也是个 Iterable ,也不会发生任何自动装箱。
尝试将数组作为一个 Iterable 参数传递会导致失败。这说明不存在任何从数组到 Iterable 的自动转换; 必须手工执行这种转换。
List
List承诺将元素保存在特定的序列中。 List 接口在 Collection 的基础上添加了许多方法,允许在 List 的中间插入和删除元素。
具体实现类
ArrayList LinkedList Vector
-
基本的 ArrayList ,擅长随机访问元素,但在 List 中间插入和删除元素时速度较慢。
-
LinkedList ,它通过代价较低的在 List 中间进行的插入和删除操作,提供了优化的顺序访问。 LinkedList 对于随机访问来说相对较慢。
LinkedList 还添加了一些方法,使其可以被用作栈、队列或双端队列(deque) 。在这些方法中,有些彼此之间可能只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 Queue 中)。
如果查看 Queue 接口就会发现,它在 LinkedList 的基础上添加了 element()
, offer()
, peek()
, poll()
和 remove()
方法,以使其可以成为一个 Queue 的实现。
ArrayList、LinkedList、Vector三者的异同?
同:三个类都实现了List接口,存储数据的特点相同,存储有序的、可重复的数据
不同:
ArrayList作为List接口的主要实现类;线程不安全,效率高;底层使用Object[] elementData存储。
LinkedList:线程不安全,对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储。
Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储。 淘汰原因:Vector同步每个单独的操作。这几乎从来不是你想要做的。
ArrayList源码分析:
JDK7情况下:ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData
list.add(123);//elementData[0] = new Integer(123);
…
list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。
默认情况下扩容为原来的1.5倍,同时需要将原有数组中的数据复制到新的数组中。结论:避免扩容,如果直到数组元素的数量,可以使用带参的构造器:ArrayList list = new ArrayList(int capacity)。
JDK8:
ArrayList list = new ArrayList();//底层Object[] elementData初始化为{},并没有创建长度为10的数组。
list.add(123);//第一次调用add()时,底层才创建了长度为10的数组,并将数据加入。小结:延迟了数组的创建,节省了内存。
LinkedList源码分析:
LinkedList list = new LinkedList();
定义了一个名为Node的内部类,作为数据节点。包含first、last首尾两个指针。
且为双向链表,因为Node中存有前后节点引用。private static class Node{}
Set
无序性:不等于随机性,存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值添加的。
不可重复性:保证添加的元素按照equals()判断时,不能返回true。即:相同的元素只能添加一个。
Set 不保存重复的元素。Set 最常见的用途是测试归属性,可以很轻松地询问某个对象是否在一个 Set 中。因此,查找通常是 Set 最重要的操作,因此通常会选择 HashSet 实现,该实现针对快速查找进行了优化。
Set 具有与 Collection 相同的接口,因此没有任何额外的功能,不像前面两种不同类型的 List 那样。实际上, Set 就是一个 Collection ,只是行为不同。(这是继承和多态思想的典型应用:表现不同的行为。)Set 根据对象的“值”确定归属性
向Set中添加的数据,其所在的类一定要重写hashCode()和equals()。可以直接用IDE生成的。
HashSet
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT(new Object()),因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。
作为Set接口的主要实现类;线程不安全的;可以存储null值
LinkedHashSet
作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
在添加数据的同时,每个数据还维护了两个引用,记录前一个数据和后一个数据。
对于频繁的遍历操作,LinkedHashSet效率高于HashSet。
TreeSet
必须属性一致(因为要排序)
自定义类要实现Comparable接口重写compareTo()方法。
TreeSet中比较两个对象是否相同的标准为:compareTo()返回0,不再是equals()。
由 HashSet 维护的顺序与 TreeSet 或 LinkedHashSet 不同,因为它们的实现具有不同的元素存储 方式。 TreeSet 将元素存储在红-黑树数据结构中,而 HashSet 使用散列函数。 LinkedHashSet 因为查询速度的原因也使用了散列,但是看起来使用了链表来维护元素的插入顺序。
TreeSet对结果进行排序
HashSet添加元素的过程:
我们向HashSet中添加元素a,首先调用a所在类的hashCode()方法,计算a的哈希值,
此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即:索引位置),判断数组此位置是否已经有元素:
如果此位置上没有其他元素,则元素a添加成功。——>情况1
如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
如果hash值不相同,则元素a添加成功。——>情况2
如果hash值相同,进而需要调用元素a所在类的equals()方法:
equals()返回true,元素a添加失败
equals()返回false,则元素a添加成功。——>情况3对于添加成功的情况2和情况3而言:元素a与已经存在指定索引位置上的数据以链表的形式存储。
jdk7中:元素a放在数组中,指向原来的元素。
jdk8中:原来的元素放到数组中,指向元素a。
总结:七上八下。HashSet底层: 数组加链表的结构。
Map
将对象映射到其他对象的能力是解决编程问题的有效方法。
Map 与数组和其他的 Collection 一样,可以轻松地扩展到多个维度,只需要创建一个值为 Map 的 Map(这些 Map 的值可以是其他集合,甚至是其他 Map)。因此,能够很容易地将集合组合起来以快速生成强大的数据结构。例如,假设你正在追踪有多个宠物的人,只需要一个 Map<Person, List> 即可。
具体实现类
HashMap:作为Map的主要实现类,线程不安全,效率高;存储null的key和value
底层:数组加链表 jdk7以前 数组+链表+红黑树 jdk8 后LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。
原因:在原有的HashMap底层结构基础上,添加一个一对指针,指向前一个和后一个元素。对于频繁的遍历操作,执行效率高于HashMapTreeMap:保证按照添加的key-value对进行排序,实现排序遍历,此时考虑key的自然排序或定制排序。底层使用红黑树。
Hashtable:作为古老的实现类,线程安全的,效率低,不能存储null的key和value
Properties:常用来处理配置文件。key和value都是String类型。
HashMap的底层实现原理:
jdk7:HashMap map = new HashMap();
在实例化后,底层创建了长度是16的一维数组Entry[] table。
map.put(key1,value1);(讨论一般情况,非第一次)
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
如果此位置上的数据为空,此时key1-value1添加成功。 —>情况1
如果此位置上的数据不为空(意味着此位置上存放一个或多个数据(以链表形式)),比较key1和已经存在的一个或多个数据的哈希值:
如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。—>情况2
如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)
如果equals返回false:此时key1-value1添加成功—>情况3
如果equals返回true:使用value1替换value2。
补充:关于情况2和3:此时key1-value1和原来的数据以链表方式存储。
扩容问题:当超出临界值时(且要存放的位置非空时)默认的扩容方式:扩容为原有容量的2倍。jdk8 相较于jdk7在底层实现方面的不同:
1、new HashMap():底层没有创建一个长度为16的数组
2、jdk8底层的数组是:Node[],而非Entry[]
3、首次调用put()方法时,底层创建长度为16的数组
4、jdk7底层结构只有:数组+链表。jdk8底层结构:数组+链表+红黑树
当数组的某一个索引位置上的元素以链表形式存在的数据个数>8且当前数组长度>64时
此时索引位置上的所有数据改为使用红黑树存储。
向TreeMap中添加key-value,要求key必须是同一个类创建的对象,因为要按照key进行排序:自然排序,定制排序。
Properties:常用来处理配置文件
Map中的常用方法
集合的快速失败机制(fail—fast)
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出 Concurrent Modification Exception。从而产生fail-fast机制。
原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。
解决办法:快速失败的出现是因为并发修改,因此,当需要在并发场景下使用集合时,换成并发包下采用安全失败的集合类即可。
安全失败机制
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
**原理:**由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。
>缺点:基于拷贝内容的优点是避免了 Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
**场景:**java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
本章小结
Java 提供了许多保存对象的方法:
- 数组将数字索引与对象相关联。它保存类型明确的对象,因此在查找对象时不必对结果做类型转换。它可以是多维的,可以保存基本类型的数据。虽然可以在运行时创建数组,但是一旦创建数组,就无法更改数组的大小。
- Collection 保存单一的元素,而 Map 包含相关联的键值对。使用 Java 泛型,可以指定集合中保存的对象的类型,因此不能将错误类型的对象放入集合中,并且在从集合中获取元素时,不必进行类型转换。各种 Collection 和各种 Map 都可以在你向其中添加更多的元素时,自动调整其尺寸大小。集合不能保存基本类型,但自动装箱机制会负责执行基本类型和集合中保存的包装类型之间的双向转换。
- 像数组一样, List 也将数字索引与对象相关联,因此,数组和 List 都是有序集合。
- 如果要执行大量的随机访问,则使用 ArrayList ,如果要经常从表中间插入或删除元素,则应该使用 LinkedList 。
- 队列和堆栈的行为是通过 LinkedList 提供的。
- Map 是一种将对象(而非数字)与对象相关联的设计。 HashMap 专为快速访问而设计,而 TreeMap 保持键始终处于排序状态,所以没有 HashMap 快。 LinkedHashMap 按插入顺序保存其元素,但使用散列提供快速访问的能力。
- Set 不接受重复元素。 HashSet 提供最快的查询速度,而 TreeSet 保持元素处于排序状态。 LinkedHashSet 按插入顺序保存其元素,但使用散列提供快速访问的能力。
- 不要在新代码中使用遗留类 Vector ,Hashtable 和 Stack 。