第7章Java集合
7.1 Java集合概述
集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。
7.2 Collection和Iterator接口
Collection接口是List、Set和Queue接口的父接口,该接口里定义的方法既可用于操作Set集合,也可用于操作List和Queue集合。Collection接口里定义了如下操作集合元素的方法。
- boolean add(Object o):该方法用于向集合里添加一个元素。如果集合对象被添加操作改变了,则返回true。
- boolean addAll(Collection c):该方法把集合c里的所有元素添加到指定集合里。如果集合对象被添加操作改变了,则返回true。
- void clear():清除集合里的所有元素,将集合长度变为0。
- boolean contains(Object o):返回集合里是否包含指定元素。
- boolean containsAll(Collection c):返回集合里是否包含集合c里的所有元素。
- boolean isEmpty():返回集合是否为空。当集合长度为0时返回true,否则返回false。
- Iterator iterator():返回一个Iterator对象,用于遍历集合里的元素。
- boolean remove(Object o):删除集合中的指定元素o,当集合中包含了一个或多个元素o时,该方法只删除第一个符合条件的元素,该方法将返回true。
- boolean removeAll(Collection c):从集合中删除集合c里面包含的所有元素(相当于调用该方法的集合减集合c),如果删除了一个或一个以上的元素,则该方法返回true。
- boolean retainAll(Collection c):从集合中删除集合c里不包含的元素(相当于把调用方法的集合变为该集合和集合c的交集),如果该操作改变了调用该方法的集合,则该方法返回true。
- int size():该方法返回集合里元素的个数。
- Object[] toArray():该方法把集合转换成一个数组,所有的集合元素变成对应的数组元素。
7.2.1 使用Lambda表达式遍历集合
下面程序示范了使用Lambda表达式来遍历集合元素。
import java.util.Collection;
import java.util.HashSet;
public class CollectionEach {
public static void main(String[] args) {
// 创建一个集合
Collection books = new HashSet();
books.add("大家好我是敖武");
books.add("我在学习Java");
books.add("现在实在写代码");
// 调用forEach()方法遍历集合
books.forEach(obj -> System.out.println("迭代集合元素:"+obj));
}
}
7.2.2 使用Java 8 增强的Iterator遍历集合元素
Collection系列集合、Map系列集合主要用于盛装其他对象,而Iterator则主要用于遍历(即迭代访问)Collection集合中的元素,Iterator对象也被称为迭代器。
Iterator接口里定义了如下4个方法:
- boolean hasNext():如果被迭代的集合元素还没有被遍历完,则返回true。
- Obejct next():返回集合里的下一个元素。
- void remove():删除集合里上一次next方法返回的元素。
- void forEachRemaining(Consumer action),这是Java 8 为Iterator新增的默认你方法,该方法可使用Lambda表达式来遍历集合元素。
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
public class IteratorTest {
public static void main(String[] args) {
// 创建一个集合
Collection books = new HashSet();
books.add("大家好我是敖武");
books.add("我在学习Java");
books.add("现在实在写代码");
// 获取books集合对应的迭代器
Iterator it = books.iterator();
while (it.hasNext()){
// it.next()方法返回的数据类型是Object类型,因此需要强制类型转换
String book = (String) it.next();
System.out.println(book);
if (book.equals("我在学习Java")){
// 从集合中删除上一次next()方法返回的元素
it.remove();
}
// 对book变量赋值,不会改变集合元素本身
book = "测试字符串";
}
System.out.println(books);
}
}
7.2.3 使用Lambda表达式遍历Iterator
如下程序示范了使用Lambda表达式来遍历集合元素:
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
public class IteratorEach {
public static void main(String[] args) {
// 创建一个集合
Collection books = new HashSet();
books.add("大家好我是敖武");
books.add("我在学习Java");
books.add("现在实在写代码");
// 获取books集合对应的迭代器
Iterator it = books.iterator();
// 使用Lambda表达式(目标类型是Comsumer)来遍历集合元素
it.forEachRemaining(obj -> System.out.println("迭代集合元素:" + obj));
}
}
7.2.4 使用foreach循环遍历集合元素
import java.util.Collection;
import java.util.HashSet;
public class ForeachTest {
public static void main(String[] args) {
// 创建一个集合
Collection books = new HashSet();
books.add("大家好我是敖武");
books.add("我在学习Java");
books.add("现在实在写代码");
for (Object obj : books){
// 此处的book变量也不是集合元素本身
String book = (String) obj;
System.out.println(book);
if (book.equals("我在学习Java")){
// 下面代码会引发ConcurrentModificationException异常
books.remove(book);
}
}
System.out.println(books);
}
}
7.2.4 使用Java 8 新增的Predicate操作集合
7.2.5 使用Java 8 新增的Predicate集合
7.2.6 使用Java 8 新增的Stream操作集合
独立使用Stream的步骤如下:
- 使用Stream或XxxStream的builder()类方法创建该Stream对应的Builder。
- 重复调用Builder的add()方法向该流中添加多个元素。
- 调用Builder的build()方法获取对应的Stream。
- 调用Stream的聚集方法。
import java.util.stream.IntStream;
public class IntStreamTest {
public static void main(String[] args) {
IntStream is = IntStream.builder()
.add(20)
.add(13)
.add(-2)
.add(18)
.build();
// 下面调用聚集方法的代码每次只能执行一次
System.out.println("is所有元素的最大值:" + is.max().getAsInt());
System.out.println("is所有元素的最小值" + is.min().getAsInt());
System.out.println("is元素的总和" + is.sum());
System.out.println("is所有元素的总数" + is.count());
System.out.println("is所有元素的平均值" + is.average());
System.out.println("is所有元素的平方是否都大于20:" + is.allMatch(ele -> ele*ele > 20));
System.out.println("is是否包含任何元素的平方大于20:" + is.anyMatch(ele -> ele*ele > 20));
// 将is映射成一个新的Stream,新Stream的每个元素是原Stream元素的2倍+1
IntStream newIs = is.map(ele -> ele * 2 + 1);
// 使用方法引用的方式来遍历集合元素
newIs.forEach(System.out::println);
}
}
7.3 set集合
7.3.1 HashSet类
HashSet具有以下特点:
- 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
- HashSet不是同步的,如果多个线程同时访问同一个HashSet,假设有两个或者两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步。
- 集合元素值可以是null。
重写hashCode()方法的基本规则:
- 在程序运行过程中,同一个对象多次调用hashCode()方法应该返回相同的值。
- 当两个对象通过equals()方法比较返回true时,这两个对象的hashCode()方法返回相等的值。
- 对象中作equals()方法比较标准的实例变量,都应该用于计算hashCode值。
下面给出重写hashCode()方法的一般步骤:
1.把对象内每个有意义的实例变量(即每个参与equals()方法比较标准的实例变量)计算出一个int类型的hashCode值。
2.用第一步计算出来的多个hashCode值组合计算出一个hashCode值返回
7.3.2 LinkedHashSet类
import java.util.LinkedHashSet;
public class LinkedHashSetTest {
public static void main(String[] args) {
LinkedHashSet books = new LinkedHashSet();
books.add("疯狂Java讲义");
books.add("轻量级Java EE企业应用实战");
System.out.println(books);
// 删除 疯狂Java讲义
books.remove("疯狂Java讲义");
// 重复添加 疯狂Java讲义
books.add("疯狂Java讲义");
System.out.println(books);
}
}
7.3.3 TreeSet类
与HashSet集合相比,TreeSet还提供了如下几个额外的方法。
- Comparator comparator():如果TreeSet采用了定制排序,则该方法返回定制排序所使用的Comparator;如果TreeSet采用了自然排序,则返回null。
- Object first():返回集合中的第一个元素。
- Object last():返回集合中的最后一个元素。
- Object lower(Obejct e):返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,参考元素不需要时TreeSet集合里的元素)。
- Object heiger(Object e):返回结合中位于指定元素之后的元素(即大于指定元素的最小元素,参考元素不需要是TreeSet集合里的元素)。
- SortSet subSet(Object fromElement, Object toElement):返回此Set的子集合,范围从fromElement(包含)到toElement(不包含)。
- SortedSet headSet(Object toElement):返回此Set的子集,由小于toElement的元素组成。
- SortedSet tailSet(Object fromElement):返回此Set的子集,由大于或等于fromElement的元素组成。
1.自然排序
TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排序,这种方式就是自然排序。
Comparable接口的常用类: - BigDecimal、BigInteger以及所有的数值型对应的包装类:按它们对应的数值大小进行比较。
- Character:按字符的UNICODE值进行比较。
- Boolean:true对应的包装类实例大于false对应的包装类实例。
- String:按字符串中字符的UNICODE值进行比较。
- Date、Time:后面的时间、日期比前面的时间、日期大。
2.定制排序
7.4 List集合
7.4.1 Java 8 改进的List接口和Listlterator接口
List作为Collection接口的子接口,当然可以使用Collection接口里的全部方法。而且由于List是有序集合,因此List集合里增加了一些根据索引来操作集合元素的方法。
下面程序示范了List集合的常规用法:
import java.util.ArrayList;
import java.util.List;
public class ListTest {
public static void main(String[] args) {
List books = new ArrayList<>();
// 向books集合中添加三个元素
books.add(new String("轻量级Java EE企业应用实战"));
books.add(new String("疯狂Java讲义"));
books.add(new String("疯狂Android讲义"));
System.out.println(books);
// 将新字符串对象插入在第二个位置
books.add(1, new String("疯狂Ajax讲义"));
for (int i=0; i<books.size(); ++i){
System.out.println(books.get(i));
}
// 删除第三个元素
books.remove(2);
System.out.println(books);
// 将books集合的第二个元素(包括)
// 到第三个元素(不包括)截取成子集合
System.out.println(books.subList(1, 2));
}
}
7.4.2 ArrayList和Vector是实现类
ArrayList和Vector作为List类的两个典型实现,完全支持前面介绍的List接口的全部功能。
如果开始就知道ArrayList或Vector集合需要保存多少个元素,则可以在创建它们时就指定initialCapacity大小。如果创建空的ArrayList或Vector集合时不指定initialCapcity参数,则Object[]数组长度默认为10。
实际上,Vector具有很多缺点,通常尽量少用Vector实现类。
需要指出的是,由于Stack继承了Vector,因此它也是一个非常古老的Java集合类,它同样是线程安全性能较差的,因此应该尽量少用Stack类。如果程序需要使用“栈”这种数据结构,则可以考虑使用后面将要介绍的ArrayDeque。
7.4.3 固定长度的List
Arrays.ArrayList是一个固定长度的List集合,程序只能遍历访问该集合里的元素,不可增加、删除该集合里的元素。
7.5 Queue集合
Queue用于模拟队列这种数据结构,队列通常是指“先进先出”(FIFO)的容器。通常,队列不允许随机访问队列中的元素。
7.5.1 PriorityQueue实现类
PriorityQueue保存队列元素的顺序并不是按加入队列的顺序,而是按队列元素的大小进行重新排序。因此当调用peek()方法或者poll()方法取出队列中的元素时,并不是去除最先进入队列的元素,而是取出队列中最小的元素。从这个意义上看,PriorityQueue已经违反了队列的最基本规则:先进先出(FIFO)。
7.5.2 Deque接口于ArrayDeque实现类
Deque接口是Queue接口的子接口,它代表一个双端队列,Deque接口里定义了一份双端队列的方法,这些方法从两端来操作队列的元素。
import java.util.ArrayDeque;
public class ArrayDequeQueue {
public static void main(String[] args) {
ArrayDeque queue = new ArrayDeque();
// 依次将三个元素加入队列
queue.offer("疯狂Java讲义");
queue.offer("轻量级Java EE企业应用实战");
queue.offer("疯狂Android讲义");
// 输出[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
System.out.println(queue);
// 访问队列头部的元素,但并不将其poll出队列”栈“,输出:疯狂Java讲义
System.out.println(queue.peek());
// 依然输出[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
System.out.println(queue);
// poll出第一个元素,输出:疯狂Java讲义
System.out.println(queue.poll());
// 输出:[轻量级Java EE企业应用实战, 疯狂Android讲义]
System.out.println(queue);
}
}
import java.util.ArrayDeque;
public class ArrayDequeStack {
public static void main(String[] args) {
ArrayDeque stack = new ArrayDeque();
// 依次将三个元素加入队列
stack.push("疯狂Java讲义");
stack.push("轻量级Java EE企业应用实战");
stack.push("疯狂Android讲义");
// 输出[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
System.out.println(stack);
// 访问队列头部的元素,但并不将其poll出队列”栈“,输出:疯狂Java讲义
System.out.println(stack.peek());
// 依然输出[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
System.out.println(stack);
// poll出第一个元素,输出:疯狂Java讲义
System.out.println(stack.pop());
// 输出:[轻量级Java EE企业应用实战, 疯狂Android讲义]
System.out.println(stack);
}
}
通过上面两个程序可以看出,ArrayDeque不仅可以作为栈使用,也可以作为队列使用。
7.5.3 LinkedList实现类
下面程序简单示范了LinkedList集合的用法:
import java.util.LinkedList;
public class LinkedListTest {
public static void main(String[] args) {
LinkedList books = new LinkedList();
// 将字符串元素加入队列的尾部
books.offer("疯狂Java讲义");
// 将一个字符串元素加入栈的顶部
books.push("轻量级Java EE企业应用实战");
// 将字符串元素添加到队列的头部(相当于栈的顶部)
books.offerFirst("疯狂Android讲义");
// 以List方式(按索引访问的方式)来遍历集合元素
for (int i=0; i<books.size(); ++i){
System.out.println("遍历中:" + books.get(i));
}
// 访问但不删除栈顶元素
System.out.println(books.peekFirst());
// 访问并不删除队列的最后一个元素
System.out.println(books.peekLast());
// 将栈顶的元素弹出“栈”
System.out.println(books.pop());
// 下面输出将看到队列中第一个元素被删除
System.out.println(books);
// 访问并删除队列的最后一个元素
System.out.println(books.pollLast());
// 下面输出:[轻量级Java EE企业应用实战]
System.out.println(books);
}
}
7.5.4 各种线性表的性能分析
Java提供的List就是一个线性表接口,而ArrayList、LinkedList又是线性表的两种典型实现:基于数组的线性表和基于链的线性表。Queue代表了队列,Deque代表了双端队列(既可作为队列使用,也可作为栈使用)。
7.6 Java 8 增强的Map集合
7.6.2 java 8改进的HashMap和Hashtable实现类
Hashtable和HashMap存在两点典型区别:
- Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的实现,所以HashMap比Hashtable的性能高一些;但如果有多个线程访问同一个Map对象,使用Hashtable实现类会更好。
- Hashtable不允许使用null作为key和value,如果试图把null值放进Hashtable中,会引发NullPointerException异常;但HashMap可以使用null作为key或value。
7.6.6 各Map实现类的性能分析
对于Map的常用实现类语言而言,虽然HashMap和Hashtable的实现机制几乎一样,但由于Hashtable是一个古老的、线程安全的集合,因此HashMap通常比Hashtable更快。
TreeMap通常比HashMap、Hashtable更慢(尤其在插入、删除key-value对时更慢),因为TreeMap底层采用红黑树来管理key-value对(红黑树的每个节点就是一个key-value对)。
使用TreeMap有一个好处:TreeMap中的key-value对总是处于有序状态,无须进行专门进行排序操作。当TreeMap被填充之后,就可以调用keySet(),取得key组成的Set,然后使用toArray()方法生成key的数组,接下来使用Arrays的binarySearch()方法在已排序的数组中快速地查询对象。
对于一般地应用场景,程序应该多考虑使用HashMap,因为HashMap正式为快速查询设计地(HashMap底层其实也是采用数组来存储key-value对)。但如果程序需要一个总是排好序的Map时,则可以考虑使用TreeMap。
7.7 HashSet和HashMap的性能选项
因为HashSet和HashMap、Hashtable都使用hash算法来决定其决定元素(HashMap则只考虑key的存储),因此HashSet、HashMap和hash表包含如下属性:
- 容量(capacity):hash表中桶的数量。
- 初始化容量(initial capacity):创建hash表时桶的数量。
HashMap和HashSet都允许在构造器中指定初始化容量。
- 尺寸(size):当前hash表中记录的数量。
- 负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的hash表,依此类推。轻负载的hash表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢)。
7.8 操作集合的工具类:Collections
7.9 烦琐的接口:Enumeration
如果现在编写Java程序,应该尽量采用Iterator迭代器,而不是用Enumerator迭代器。
本章练习
1.创建一个Set集合,并用Set集合保存用户通过控制台输入的20个字符串。
2.创建一个List集合,并随意添加10个元素。然后获取索引为5处的元素;再获取其中某2个元素的索引;再删除索引为3处的元素。
3.给定[“a”, “b”, “a”, “b”, “c”, “a”, “b”, “c”, “b”]字符串数组,然后使用Map的key来保存数组中字符串元素,value保存该字符串元素的出现次数,最后统计处各字符串元素的出现次数。
4.将本章未完成的梭哈游戏补充完整,不断地添加梭哈游戏规则,并开发一个控制台的梭哈游戏。