概述
集合
- 前言
- 泛型和类型安全的集合
- 基本概念
- 添加元素组
- 集合的打印
- 列表List
- 迭代器Iterators
- ListIterator
- 链表LinkedList
- 堆栈Stack
- 集合Set
- 映射Map
- 队列Queue
- 优先级队列PriorityQueue
- 集合与迭代器
- for-in和迭代器
- 适配器方法惯用法
- 本章小结
- 知识扩展
- ArrayList和LinkedList的区别?
- HashMap和TreeMap、LinkedHashMap的区别?
- HashSet、TreeSet、LinkedHashSet的区别?
前言
如果一个程序只包含固定数量的对象且对象的生命周期都是已知的,那么这是一个非常简单的程序。
因为从来不会知道实际需要多少个这样的引用,大多数编程语言都提供了某种方法来解决这个基本问题。在没有集合类之前,实际上在Java语言里已经有一种方法可以存储对象,那就是数组。数组不仅可以存放基本数据类型也可以容纳属于同一种类型的对象。数组的操作是高效率的,但也有缺点。比如数组的长度是不可以变的,数组只能存放同一种类型的对象(或者说对象的引用)。
java.util 库提供了一套相当完整的集合类(collection classes)来解决这个问题,其中基本的类型有 List 、 Set 、 Queue 和 Map。这些类型也被称作容器类(container classes),但我将使用Java类库使用的术语。集合提供了完善的方法来保存对象,可以使用这些工具来解决大量的问题。因此,与数组不同,在编程时,可以将任意数量的对象放置在集合中,而不用关心集合应该有多大。
泛型和类型安全的集合
使用 Java 5 之前的集合的一个主要问题是编译器允许你向集合中插入不正确的类型。可以把 ArrayList 看作“可以自动扩充自身尺寸的数组”来看待。
下面代码中有两个对象:Teacher和 Student 都被放到了集合中,我们可以使用add()
添加元素,可以使用get()
获取元素,可以用size()
方法来确认这个集合有多少个元素:
public class Student {
private String grade;
public String getGrade(){
return grade;
}
}
public class Teacher {
private String subject;
}
public class Test {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(new Student());
list.add(new Teacher());
for (Object o:list) {
Student student = (Student) o;
System.out.println(student.getName());
}
}
}
Teacher和 Student是两个不同的类,但都继承Object,所以ArrayList会默认保存Object,如果需要调用 Student中的getName()
方法就必须强制类型转换,否则,将会产生语法错误。
如果明确容器的类型,只需要使用 ArrayList<Student>来代替 ArrayList 。尖括号括起来的是类型参数(可能会有多个),它指定了这个集合实例可以保存的类型。这样就可以在编译期防止将错误类型的对象放置到集合中:
public class Test {
public static void main(String[] args) {
ArrayList<Student> list = new ArrayList();
list.add(new Student());
//erro:in ArrayList cannot be applied
//list.add(new Teacher());
for (Student student:list) {
System.out.println(student.getName());
}
}
}
在 Student定义的右侧,可以看到 new ArrayList<>() 。这有时被称为“菱形语法”(diamond syntax)。在 Java 7 之前,必须要在两端都进行类型声明,如下所示:
ArrayList<Student> apples = new ArrayList<Student>();
随着类型变得越来越复杂,这种重复产生的代码非常混乱且难以阅读。程序员发现所有类型信息都可以从左侧获得,因此,编译器没有理由强迫右侧再重复这些。虽然类型推断(type inference)只是个很小的请求,Java 语言团队仍然欣然接受并进行了改进。
使用泛型,从 List 中获取元素不需要强制类型转换。因为 List 知道它持有什么类型,因此当调用 get()
时,它会替你执行转型。因此,使用泛型,你不仅知道编译器将检查放入集合的对象类型,而且在使用集合中的对象时也可以获得更清晰的语法。向上转型也可以像作用于其他类型一样作用于泛型:
public class Test {
public static void main(String[] args) {
List<School> list = new ArrayList();
list.add(new Student());
list.add(new Teacher());
for (School school:list) {
System.out.println(school);
}
/*Output:
Student@4554617c
Teacher@74a14482
*/
}
}
请注意, ArrayList 已经被向上转型为了 List 。因此,可以将子类型添加到被指定为保存父类的集合中。对象后面的散列码的无符号十六进制表示(这个散列码是通过 hashCode()
方法产生的)。
基本概念
Java集合类库采用“持有对象”(holding objects)的思想,并将其分为两个不同的概念,表示为类库的基本接口:
- 集合(Collection) :一个独立元素的序列,这些元素都服从一条或多条规则。List 必须以插入的顺序保存元素, Set 不能包含重复元素, Queue 按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。
- 映射(Map) : 一组成对的“键值对”对象,允许使用键来查找值。 ArrayList 使用数字来查找对象,因此在某种意义上讲,它是将数字和对象关联在一起。 map 允许我们使用一个对象来查找另一个对象,它也被称作关联数组(associative array),因为它将对象和其它对象关联在一起;或者称作字典(dictionary),因为可以使用一个键对象来查找值对象,就像在字典中使用单词查找定义一样。 Map 是强大的编程工具。
Collection 接口概括了序列的概念——一种存放一组对象的方式。创建一个具体类的对象,将其向上转型为对应的接口,就像下面这样:
public static void main(String[] args) {
Collection<Integer> collection = new ArrayList<>();
collection.add(1);
collection.add(2);
collection.add(3);
for (Integer in:collection) {
System.out.println(in);
}
/*Output:
1,2,3,
*/
}
向上转型会导致父类无法包含子类的方法,例如, LinkedList 具有 List 接口中未包含的额外方法。所以要考虑是否转型为更通用的接口。
代码里的add()
方法会在 Collection 中添加一个新元素。在 Set中,只有当元素不存在时才会添加元素。在使用 ArrayList ,或任何其他类型的 List 时,add()
总是表示“把它放进去”,因为 List 不关心是否存在重复元素。
添加元素组
在 java.util 包中的 Arrays 和 Collections 类中都有很多实用的方法,如下:
public static void main(String[] args) {
Integer[] integers = {1,2,3,4};
List<Integer> list = Arrays.asList(integers);
list.add(5);//Exception in thread "main" java.lang.UnsupportedOperationException
Collection<Integer> collection = new ArrayList<>(list);
collection.add(5);
collection.addAll(Arrays.asList(7,8,9));
collection.forEach(item->{
System.out.print(item+ ",");
});
/*Output:
1,2,3,4,5,7,8,9,
*/
}
注意: Arrays.asList()
中间的“暗示”(即 < Integer > ),告诉编译器 List 类型的实际目标类型是什么。这称为显式类型参数说明(explicit type argument specification)。
Arrays.asList()
方法接受一个数组或是逗号分隔的元素列表(使用可变参数),并将其转换为 List 对象。 但是这里的底层实现是数组,没法调整大小。如果尝试在这个 List 上调用 add()
或 remove()
,由于这两个方法会尝试修改数组大小,所以会在运行时得到“Unsupported Operation(不支持的操作)”错误。
Collections.addAll()
方法接受一个 Collection 对象,以及一个数组或是一个逗号分隔的列表,将其中元素添加到 Collection 中。
集合的打印
下面是一个例子,这个例子中介绍了基本的Java集合的两个主要类型,它们的区别在于集合中的每个“槽”(slot)保存的元素个数。:
public class Test {
static Collection getCollection(Collection<String> collection){
collection.add("one");
collection.add("five");
collection.add("three");
collection.add("five");
return collection;
}
static Map getMap(Map map){
map.put("one","1");
map.put("five","4");
map.put("three","3");
map.put("five","5");
return map;
}
public static void main(String[] args) {
System.out.println(getCollection(new ArrayList<>()));
System.out.println(getCollection(new LinkedList<>()));
System.out.println(getCollection(new LinkedHashSet<>()));
System.out.println(getCollection(new HashSet<>()));
System.out.println(getCollection(new TreeSet<>()));
System.out.println(getMap(new HashMap()));
System.out.println(getMap(new LinkedHashMap()));
System.out.println(getMap(new TreeMap()));
/*Output:
[one, five, three, five]
[one, five, three, five]
[one, five, three]
[one, five, three]
[five, one, three]
{one=1, five=5, three=3}
{one=1, five=5, three=3}
{five=5, one=1, three=3}
*/
}
}
Collection 类型在每个槽中只能保存一个元素,这些类型都实现了 add()
方法以添加新元素。此类集合包括:
- ArrayList 和 LinkedList 都是 List 的类型,从输出中可以看出,它们都按插入顺序保存元素。两者之间的区别不仅在于执行某些类型的操作时的性能,而且 LinkedList 包含的操作多于 ArrayList。
- HashSet , TreeSet 和 LinkedHashSet 是 Set 的类型。 且不允许重复元素(如果存储顺序很重要,则可以使用 TreeSet,它将按比较结果的升序保存对象);他们打印输出的内容以方括号括住,每个元素由逗号分隔。
Map 每个槽中存放了两个元素,即键和与值的关联 (也称为关联数组)。对于每个键, Map 只存储一次,就像一个简单的数据库。
- HashMap , TreeMap 和 LinkedHashMap 通过
Map.put(key, value)
添加一个值并与一个键相关联。,可以使用键来查找对象Map.get(key)
。 - 保存在 HashMap 中的顺序不是插入顺序,因为 HashMap 实现使用了非常快速的算法来控制顺序。 TreeMap 通过比较结果的升序来保存键, LinkedHashMap 在保持 HashMap 查找速度的同时按键的插入顺序保存键。打印时以大括号括住,每个键和值用等号连接(键在左侧,值在右侧)。
列表List
List承诺将元素保存在特定的序列中。 List 接口在 Collection 的基础上添加了许多方法,允许在 List 的中间插入和删除元素。
- 基本的 ArrayList ,擅长随机访问元素,但在 List 中间插入和删除元素时速度较慢。
- LinkedList ,它通过代价较低的在 List 中间进行的插入和删除操作,提供了优化的顺序访问。 LinkedList 对于随机访问来说相对较慢,但它具有比 ArrayList 更大的特征集。
调用addAll()
将Arrays.asList()
的多参字符串转换为List添加集合里面,成为新的集合就可以使用正常的对集合进行操作。
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.addAll(Arrays.asList("a","b","c","d","e"));
System.out.println("addList:"+list.toString());
System.out.println("list is contains:"+list.contains("b"));
System.out.println("indexOf:"+list.indexOf("b"));
list.remove("d");
list.remove(1);
System.out.println("remove after List:"+list.toString());
/*Output:
addList:[a, b, c, d, e]
list is contains:true
indexOf:1
remove after List:[a, c, e]
*/
}
使用 contains()
方法确定对象是否在列表中。
如果要删除一个元素或者对象,可以调用remove()
方法,如果没有找到则报错。
使用 indexOf()
在 List 中找到该对象所在位置的下标号,如果没有找到则返回-1。
public static void main(String[] args) {
list.add(2,"b");
System.out.println("indexAdd:"+list.toString());
list.set(3,"f");
System.out.println("indexSet:"+list.toString());
/*Output:
indexAdd:[a, c, b, e]
indexSet:[a, c, b, f]
*/
}
可以在 List 的中间插入一个元素,使用add()
方法和set()
方法一样可以添加元素,但是add()
方法会将指定下标后的元素往后排,而set()
方法则会替换当前下标的元素。
public static void main(String[] args) {
List<String> subList = list.subList(1, 3);
System.out.println("subList"+ subList);
Collections.sort(subList);
System.out.println("sort:"+ subList);
Collections.reverse(subList);
System.out.println("reverse:"+subList.toString());
Collections.shuffle(list,new Random(3));
System.out.println("shuffle:"+list.toString());
System.out.println("get:"+list.get(1));
List<String> s = new ArrayList<>();
s.addAll(Arrays.asList("a","h","j","k","l"));
list.retainAll(s);//保留两个集合中相同的数据
System.out.println("retainAll"+list.toString());
/*Output:
subList[c, b]
reverse:[b, c]
sort:[b, c]
get:a
retainAll[a]
*/
}
subList()
方法 可以截取从下标多少开始到下标多少结束的集合。
如果对集合排序有要求可以使用:Collections.reverse()
将集合倒叙排列或Collections.sort()
将集合升序排列。
Collections.shuffle()
方法可以将集合的顺序打乱。
通过get()
方法可以获取指定下标的元素。
retainAll()
方法实际上是一个“集合交集”操作,保留两个集合中相同元素。
public static void main(String[] args) {
System.out.println("isEmpty:"+list.isEmpty());
Object[] objects = list.toArray();
System.out.println("array:"+objects[0]);
list.clear();
System.out.println("clearList:"+list.toString());
/*Output:
isEmpty:false
array:a
clearList:[]
*/
}
如果我们需要判断这个集合是否为空可以调用 isEmpty()
方法,true代表为空 false代表不为空。
toArray()
方法将任意的 Collection 转换为数组 。如果将指定类型数组传递给这个方法,他就会生成一个指定类型的数组。如果参数数组太小而无法容纳 List 中的所有元素,则 toArray()
会创建一个具有合适尺寸的新数组。
clear()
方法会清空所有元素。
迭代器Iterators
迭代器(也是一种设计模式)的概念实现了这种抽象。迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不知道或不关心该序列的底层结构。另外,迭代器通常被称为轻量级对象(lightweight object):创建它的代价小。因此,经常可以看到一些对迭代器有些奇怪的约束。例如,Java 的 Iterator 只能单向移动。这个 Iterator 只能用来:
- 使用
iterator()
方法要求集合返回一个 Iterator。 Iterator 将准备好返回序列中的第一个元素。 - 使用
next()
方法获得序列中的下一个元素。 - 使用
hasNext()
方法检查序列中是否还有元素。 - 使用
remove()
方法将迭代器最近返回的那个元素删除。
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.addAll(Arrays.asList("a","b","c","d","e"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String n=iterator.next();
System.out.println(n);
if("b".equals(n)){
iterator.remove();
}
}
System.out.println(list);
/*Output:
[a, c, d, e]
*/
}
}
有了 Iterator ,就不必再为集合中元素的数量操心了。这是由 hasNext()
和 next()
关心的事情。在调用 remove()
之前必须先调用 next()
。在集合中的每个对象上执行操作,这种思想十分强大,并且贯穿于本书。
ListIterator
ListIterator 是一个更强大的 Iterator 子类型,它只能由各种 List 类生成。 Iterator 只能向前移动,而 ListIterator 可以双向移动。它可以生成迭代器在列表中指向位置的后一个和前一个元素的索引,并且可以使用 set()
方法替换元素。可以通过调用 listIterator()
方法来生成指向 List 开头处的 ListIterator ,还可以通过调用 listIterator(n)
创建一个指向列表索引号为 n 的元素处的 ListIterator 。 下面的示例演示了所有这些能力:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.addAll(Arrays.asList("a","b","c","d","e"));
ListIterator<String> iterator = list.listIterator(1);//list.listIterator()默认从下标0开始
while (iterator.hasNext()){
String next = iterator.next();
if("b".equals(next)) {
iterator.set("a");
}
System.out.println("next:"+next+",nextIndex:"
+iterator.nextIndex()+","+"previousIndex:"+iterator.previousIndex());
}
System.out.println("list:"+list);
/*Output:
next:b,nextIndex:2,previousIndex:1
next:c,nextIndex:3,previousIndex:2
next:d,nextIndex:4,previousIndex:3
next:e,nextIndex:5,previousIndex:4
list:[a, a, c, d, e]
*/
}
链表LinkedList
LinkedList 也实现了基本的 List 接口,但它在 List 中间执行插入和删除操作时比 ArrayList 更高效。然而,它在随机访问操作效率方面却要逊色一些。
LinkedList 还添加了一些方法,使其可以被用作栈、队列或双端队列(deque) 。在这些方法中,有些彼此之间可能只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 Queue 中)。例如:
-
getFirst()
和element()
是相同的,它们都返回列表的头部第一个元素,getLast()
则返回最后一个,如果 List 为空,则抛出 NoSuchElementException 异常。peek()
方法与这两个方法只是稍有差异,它在列表 为空时返回 null。 -
removeFirst()
和remove()
也是相同的,它们删除并返回列表的头部元素,removeLast()
则删除最后一个并返回此元素,并在列表为空时抛出 NoSuchElementException 异常。poll()
稍有差异,它在列表为空时返回 null。 -
addFirst()
在列表的开头插入一个元素。offer()
与add()
和addLast()
相同。 它们都在列表的尾部(末尾)添加一个元素。
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.addAll(Arrays.asList("a","b","c","d","e"));
System.out.println("linkedList:"+list);
System.out.println("getFirst:"+list.getFirst()
+",element:"+list.element()
+",getLast:"+list.getLast());
System.out.println("peekLast:"+list.peekLast());
System.out.println("removeFirst:"+list.removeFirst()
+",removeIndex:"+list.remove(1)
+",removeLast:"+list.removeLast());
System.out.println("poll:"+list.poll());
System.out.println("remove after linkedList:"+list);
list.addFirst("a");
list.add(2,"c");
list.addLast("e");
list.offer("f");
System.out.println("add after linkedList:"+list);
System.out.println("offerFirst:"+list.offerFirst("o"));
System.out.println("end linkedList:"+list);
/*Output:
linkedList:[a, b, c, d, e]
getFirst:a,element:a,getLast:e
peekLast:e
removeFirst:a,removeIndex:c,removeLast:e
poll:b
remove after linkedList:[d]
add after linkedList:[a, d, c, e, f]
offerFirst:true
end linkedList:[o, a, d, c, e, f]
*/
}
如果查看 Queue 接口就会发现,它在 LinkedList 的基础上添加了 element()
, offer()
, peek()
, poll()
和 remove()
方法,以使其可以成为一个 Queue 的实现。
堆栈Stack
堆栈是“后进先出”(LIFO)集合。它有时被称为叠加栈(pushdown stack),因为最后“压入”(push)栈的元素,第一个被“弹出”(pop)栈。经常用来类比栈的事物是带有弹簧支架的自助餐厅托盘。最后装入的托盘总是最先拿出来使用的。
Java 1.0 中附带了一个 Stack 类,结果设计得很糟糕(为了向后兼容,我们永远坚持 Java 中的旧设计错误)。Java 6 添加了 ArrayDeque ,其中包含直接实现堆栈功能的方法:
public static void main(String[] args) {
Deque<String> stack = new ArrayDeque<>();
stack.push("a");
stack.push("b");
stack.push("c");
while (!stack.isEmpty()){
System.out.print(stack.pop()+" ");
}
/*Output:
c b a
*/
}
即使它是作为一个堆栈在使用,我们仍然必须将其声明为 Deque 。有时一个名为 Stack 的类更能把事情讲清楚尽管:
public class Stack<T> {
private Deque<T> storage = new ArrayDeque<>();
public void push(T v) { storage.push(v); }
public T peek() { return storage.peek(); }
public T pop() { return storage.pop(); }
public boolean isEmpty() { return storage.isEmpty(); }
@Override
public String toString() {
return storage.toString();
}
}
这里引入了使用泛型的类定义的最简单的可能示例。无论是请求参数还是返回参数都是一个T参数,而其中的类型参数 T 会在使用类时被实际类型替换。基本上,这个类是在声明“我们在定义一个可以持有 T 类型对象的 Stack 。” 其方法通过push()
添加元素pop()
删除并返回此元素,其他和我们之前的讲解无差别。
下面展示了Stack的使用:
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
stack.push("a");
stack.push("b");
stack.push("c");
while (!stack.empty()){
System.out.println(stack.pop() +" ");
}
/*Output:
c b a
*/
}
但是 ArrayDeque 可以产生更好的 Stack ,因此更可取。
集合Set
Set 不保存重复的元素。 如果试图将相同对象的多个实例添加到 Set 中,那么它会阻止这种重复行为。因此,查找通常是 Set 最重要的操作,因此通常会选择 HashSet 实现,该实现针对快速查找进行了优化。Set 就是一个 Collection ,只是行为不同。下面是使用存放 Integer 对象的 HashSet 的示例:
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
set.add(3);
set.add(2);
set.add(2);
set.add(4);
set.add(1);
System.out.println(set);
/*Output:
1
2
3
4
*/
}
早期 Java 版本中的 HashSet 产生的输出没有可辨别的顺序。这是因为出于对速度的追求, HashSet 使用了散列,与 TreeSet 或 LinkedHashSet 不同,它们具有不同的元素存储方式。 TreeSet 将元素存储在红-黑树数据结构中,而 LinkedHashSet 因为查询速度的原因也使用了散列,但是看起来使用了链表来维护元素的插入顺序。看起来散列算法好像已经改变了,现在 Integer 按顺序排序。但是,您不应该依赖此行为:
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("b");
set.add("f");
set.add("s");
set.add("o");
set.add("f");
System.out.println(set);
/*Output:
[b, s, f, o]
*/
}
String 对象似乎没有排序。要对结果进行排序,一种方法是使用 TreeSet 而不是 HashSet :
public static void main(String[] args) {
Set<String> set = new TreeSet<>();
set.add("b");
set.add("f");
set.add("s");
set.add("o");
set.add("f");
System.out.println(set);
/*Output:
[b, f, o, s]
*/
}
和 List 一样 可以使用 contains()
来判断是否包含某个元素:
public static void main(String[] args) {
Set<String> set = new TreeSet<>();
set.add("b");
set.add("f");
System.out.println("contains:"+set.contains("f"));
/*Output:
contains:true
*/
}
能够产生每个元素都唯一的列表是相当有用的功能。例如,假设想要列出某个Java文件中的所有单词,通过使用本书后面介绍的 java.nio.file.Files.readAllLines()
方法,可以打开一个文件,并将其作为一个 List<String> 读取,每个 String 都是输入文件中的一行:
public static void main(String[] args) {
try {
List<String> list = Files.readAllLines(
Paths.get("D:\study\src\com\study\test\container\Student.java")
);
Set<String> strings = new TreeSet<>();
for (String s:list){
for (String s1:s.split("\W+")){
if(s1.trim().length()>0){
strings.add(s1);
}
}
}
System.out.println(strings);
} catch (IOException e) {
e.printStackTrace();
}
/*Output:
[School, String, Student, class, com, container, extends, getGrade,
grade, package, private, public, return, setGrade, study, test, this, void]
*/
}
我们逐步浏览文件中的每一行,并使用 String.split()
将其分解为单词,这里使用正则表达式依据一个或多个非单词字母来拆分字符串。每个结果单词都会添加到 TreeSet 并对结果进行排序。这里,排序是按字典顺序(lexicographically)完成的,因此大写和小写字母位于不同的组中。如果想按字母顺序(alphabetically)对其进行排序,可以向 TreeSet 构造器传入 String.CASE_INSENSITIVE_ORDER 比较器(比较器是一个建立排序顺序的对象):
public static void main(String[] args) {
try {
List<String> list = Files.readAllLines(
Paths.get("D:\study\src\com\study\test\container\Student.java")
);
Set<String> strings = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
for (String s:list){
for (String s1:s.split("\W+")){
if(s1.trim().length()>0){
strings.add(s1);
}
}
}
System.out.println(strings);
} catch (IOException e) {
e.printStackTrace();
}
/*Output:
[class, com, container, extends, getGrade, grade, package, private,
public, return, School, setGrade, String, Student, study, test, this, void]
*/
}
映射Map
将对象映射到其他对象的能力是解决编程问题的有效方法。
public static void main(String[] args) {
Map<String,Integer> map = new HashMap<>();
map.put("one",1);
map.put("two",2);
map.put("three",3);
System.out.println(map);
/*Output:
{one=1, two=2, three=3}
*/
}
接下来展示通过使用 containsKey()
和 containsValue()
方法去测试一个 Map ,以查看它是否包含某个键或某个值:
public static void main(String[] args) {
Map<String,Integer> map = new HashMap<>();
map.put("one",1);
map.put("two",2);
map.put("three",3);
System.out.println(map);
System.out.println("containsKey:"+map.containsKey("one"));
System.out.println("containsValue:"+map.containsValue(4));
/*Output:
{one=1, two=2, three=3}
containsKey:true
containsValue:false
*/
}
Map 与数组和其他的 Collection 一样,可以轻松地扩展到多个维度,只需要创建一个值为 Map 的 Map(这些 Map 的值可以是其他集合,甚至是其他 Map)。因此,能够很容易地将集合组合起来以快速生成强大的数据结构。例如,假设你想知道每个组里面有多少个人只需要一个 Map<String, List<String>> 即可:
public static void main(String[] args) {
Map<String,List<String>> map = new HashMap<>();
map.put("first_group",Arrays.asList("张一","张二"));
map.put("second_group",Arrays.asList("李一","李二"));
map.put("three_group",Arrays.asList("王一","王二"));
System.out.println("map:"+map);
System.out.println("keySet:"+map.keySet());
System.out.println("containsValue:"+map.values());
for (String s:map.keySet()){
List<String> list = map.get(s);
System.out.println("list:"+list);
}
/*Output:
map:{three_group=[王一, 王二], second_group=[李一, 李二], first_group=[张一, 张二]}
keySet:[three_group, second_group, first_group]
containsValue:[[王一, 王二], [李一, 李二], [张一, 张二]]
list:[王一, 王二]
list:[李一, 李二]
list:[张一, 张二]
*/
}
Map 可以返回由其键组成的 Set ,由其值组成的 Collection ,或者其键值对的 Set 。 keySet()
方法生成由在 map 中的所有键组成的 Set ,它在 for-in 语句中被用来遍历该 Map 。
队列Queue
队列是一个典型的“先进先出”(FIFO)集合。 即从集合的一端放入事物,再从另一端去获取它们,事物放入集合的顺序和被取出的顺序是相同的。队列通常被当做一种可靠的将对象从程序的某个区域传输到另一个区域的途径。队列在并发编程中尤为重要,因为它们可以安全地将对象从一个任务传输到另一个任务。
LinkedList 实现了 Queue 接口,并且提供了一些方法以支持队列行为,因此 LinkedList 可以用作 Queue 的一种实现。 通过将 LinkedList 向上转换为 Queue ,下面的示例使用了在 Queue 接口中与 Queue 相关(Queue-specific)的方法:
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
queue.offer("a");
queue.offer("b");
queue.offer("c");
while (queue.peek()!=null){
System.out.print(queue.remove()+" ");
}
/*Output:
a b c
*/
}
offer()
和 peek()
、element()
、remove()
、poll()
等方法和前面讲解的链表LinkedList大致相同。
Queue 接口窄化了对 LinkedList 方法的访问权限,因此只有适当的方法才能使用,因此能够访问到的 LinkedList 的方法会变少(这里实际上可以将 Queue 强制转换回 LinkedList ,但至少我们不鼓励这样做)。
优先级队列PriorityQueue
先进先出(FIFO)描述了最典型的队列规则(queuing discipline)。队列规则是指在给定队列中的一组元素的情况下,确定下一个弹出队列的元素的规则。先进先出声明的是下一个弹出的元素应该是等待时间最长的元素。
优先级队列声明下一个弹出的元素是最需要的元素(具有最高的优先级)。例如,在机场,当飞机临近起飞时,这架飞机的乘客可以在办理登机手续时排到队头。如果构建了一个消息传递系统,某些消息比其他消息更重要,应该尽快处理,而不管它们何时到达。在Java 5 中添加了 PriorityQueue ,以便自动实现这种行为。
当在 PriorityQueue 上调用 offer()
方法来插入一个对象时,该对象会在队列中被排序。默认的排序使用队列中对象的自然顺序(natural order),但是可以通过提供自己的 Comparator
来修改这个顺序。 PriorityQueue
确保在调用 peek()
, poll()
或 remove()
方法时,获得的元素将是队列中优先级最高的元素。
在下面的示例中,可以看到它们从 PriorityQueue 中弹出的顺序与前一个示例不同:
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(2, 2, 6, 4, 89, 1, 23, 4, 4);
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
priorityQueue.addAll(integers);
remove(priorityQueue);
priorityQueue = new PriorityQueue<>(integers.size(),Collections.reverseOrder());
priorityQueue.addAll(integers);
remove(priorityQueue);
String str = "EDUCATION SHOULD ESCHEW OBFUSCATION";
List<String> list = Arrays.asList(str.split(""));
PriorityQueue<String> stringPriorityQueue = new PriorityQueue<>(list);
remove(stringPriorityQueue);
stringPriorityQueue = new PriorityQueue<>(list.size(),Collections.reverseOrder());
stringPriorityQueue.addAll(list);
remove(stringPriorityQueue);
/*Output:
1 2 2 4 4 4 6 23 89
89 23 6 4 4 4 2 2 1
A A B C C C D D E E E F H H I I L N N O O O O S S S T T U U U W
W U U U T T S S S O O O O N N L I I H H F E E E D D C C C B A A
*/
}
private static void remove(PriorityQueue priorityQueue){
while (priorityQueue.size() > 0){
System.out.print(priorityQueue.remove() +" ");
}
System.out.println();
}
PriorityQueue 是允许重复的,最小的值具有最高的优先级(如果是 String ,空格也可以算作值,并且比字母的优先级高)。为了展示如何通过提供自己的 Comparator 对象来改变顺序,第二个对 PriorityQueue<Integer> 构造器的调用,和第四个对 PriorityQueue<String> 的调用使用了由 Collections.reverseOrder()
(Java 5 中新添加的)产生的反序的 Comparator 。
集合与迭代器
Collection 是所有序列集合共有的根接口。它可能会被认为是一种“附属接口”(incidental interface),即因为要表示其他若干个接口的共性而出现的接口。此外,java.util.AbstractCollection 类提供了 Collection 的默认实现,使得你可以创建 AbstractCollection 的子类型,而其中没有不必要的代码重复。
如果所编写的方法接受一个 Collection ,那么该方法可以应用于任何实现了 Collection 的类。标准 C++ 类库中的集合并没有共同的基类——集合之间的所有共性都是通过迭代器实现的。在 Java 中,遵循 C++ 的方式看起来似乎很明智,即用迭代器而不是 Collection 来表示集合之间的共性。但是,这两种方法绑定在了一起,因为实现 Collection 就意味着需要提供 iterator()
方法:
public class Test {
public static void main(String[] args) {
List<String> list = Arrays.asList("Ralph, Eric, Robin, Lacey,"+
"Britney, Sam, Spot, Fluffy");
display(list);
Map<String,String> map = list.stream().collect(Collectors.toMap(s->s,s->s));
display(map.values().iterator());
/*Output:
Ralph, Eric, Robin, Lacey,Britney, Sam, Spot, Fluffy
Ralph, Eric, Robin, Lacey,Britney, Sam, Spot, Fluffy
*/
}
public static void display(Iterator<String> iterator){
while (iterator.hasNext()){
System.out.print(iterator.next() + " ");
}
System.out.println();
}
public static void display(Collection<String> collections){
for (String str:collections) {
System.out.print(str + " ");
}
System.out.println();
}
}
两个版本的 display()
方法都可以使用 Map 或 Collection 的子类型来工作。 而且Collection 接口和 Iterator 都将 display() 方法与低层集合的特定实现解耦。事实上, Collection 要更方便一点,因为它是 Iterable 类型,因此在 display(Collection)
的实现中可以使用 for-in 构造,这使得代码更加清晰。
实现Collection,必须实现它所有的方法,即使我们不在 display()
方法中使用它们,也必须这样做。虽然这可以通过继承 AbstractCollection 而很容易地实现,但是无论如何还是要被强制去实现 iterator() 和 size() 方法,这些方法 AbstractCollection 没有实现,但是 AbstractCollection 中的其它方法会用到:
public class CollectionSequence extends AbstractCollection<String> {
private String[] arr = {"A","B","C","D"};
@Override
public Iterator<String> iterator() {
return new Iterator<String>() {
private int index=0;
@Override
public boolean hasNext() {
return index < arr.length;
}
@Override
public String next() {
return arr[index++];
}
};
}
@Override
public int size() {
return arr.length;
}
public static void main(String[] args) {
CollectionSequence collectionSequence = new CollectionSequence();
Test.display(collectionSequence);
Test.display(collectionSequence.iterator());
/*Output:
A B C D
A B C D
*/
}
}
remove()
方法是一个“可选操作”,这里可以不必实现它,如果你调用它,它将抛出异常。
如果类已经继承了其他的类,那么就不能再继承 AbstractCollection 了。在这种情况下,要实现 Collection ,就必须实现该接口中的所有方法。此时,继承并提供创建迭代器的能力要容易得多:
class ArrSequence{
protected String[] arr = {"A","B","C","D"};
}
public class CollectionSequence extends ArrSequence {
public Iterator<String> iterator() {
return new Iterator<String>() {
private int index=0;
@Override
public boolean hasNext() {
return index < arr.length;
}
@Override
public String next() {
return arr[index++];
}
};
}
public static void main(String[] args) {
CollectionSequence collectionSequence = new CollectionSequence();
Test.display(collectionSequence.iterator());
/*Output:
A B C D
*/
}
}
生成 Iterator 是将序列与消费该序列的方法连接在一起耦合度最小的方式,并且与实现 Collection 相比,它在序列类上所施加的约束也少得多。
for-in和迭代器
我们都知道for-in 可以用来遍历不仅用于数组,但它也适用于任何 Collection 对象。Java 5 引入了一个名为 Iterable 的接口,该接口包含一个能够生成 Iterator 的 iterator()
方法。for-in 使用此 Iterable 接口来遍历序列。因此,如果创建了任何实现了 Iterable 的类,都可以将它用于 for-in 语句中:
public class IterableClass implements Iterable<String> {
private String[] arr = {"A","B","C","D"};
@Override
public Iterator<String> iterator() {
return new Iterator<String>() {
private int index = 0;
@Override
public boolean hasNext() {
return index < arr.length;
}
@Override
public String next() { return arr[index++]; }
@Override
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
for (String s:new IterableClass()) {
System.out.println(s);
}
/*Output:
A B C D
*/
}
}
for-in 语句适用于数组或其它任何 Iterable ,但这并不意味着数组肯定也是个 Iterable ,也不会发生任何自动装箱:
public static void main(String[] args) {
List<String> list = Arrays.asList("Ralph, Eric, Robin");
display(list);
String[] split = "Ralph, Eric, Robin".split(",");
//display
//(java.util.Collection<java.lang.String>)
//in Test cannot be applied
//to
//(java.lang.String[])
//display(split);
}
public static void display(Collection<String> collections){
for (String str:collections) {
System.out.print(str + " ");
}
System.out.println();
}
尝试将数组作为一个 Iterable 参数传递会导致失败。这说明不存在任何从数组到 Iterable 的自动转换; 必须手工执行这种转换。
适配器方法惯用法
如果现在有一个 Iterable 类,你想要添加一种或多种在 for-in 语句中使用这个类的方法,应该怎么做呢?
一种解决方案是所谓适配器方法(Adapter Method)的惯用法。“适配器”部分来自于设计模式,因为必须要提供特定的接口来满足 for-in 语句。 在这里,若希望在默认的正向迭代器的基础上,添加产生反向迭代器的能力,因此不能使用覆盖,相反,而是添加了一个能够生成 Iterable 对象的方法,该对象可以用于 for-in 语句。这使得我们可以提供多种使用 for-in 语句的方式:
public class ReversalList<T> extends ArrayList<T> {
public ReversalList(Collection t){
super(t);
}
public Iterable<T> reversal(){
return new Iterable<T>() {
@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
int current = size()-1;
@Override
public boolean hasNext() {
return current > -1;
}
@Override
public T next() {
return get(current--);
}
};
}
};
}
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("a","b","c"));
ReversalList<String> reversalList = new ReversalList<>(list);
for (String s:reversalList) {
System.out.print(s +" ");
}
System.out.println();
for (String s:reversalList.reversal()) {
System.out.print(s +" ");
}
/*Output:
a b c
c b a
*/
}
在主方法中,如果直接将 reversalList 对象放在 for-in 语句中,则会得到(默认的)正向迭代器。但是如果在该对象上调用 reversal()
方法,它会产生不同的行为。
public static void main(String[] args) {
Random random = new Random(25);
Integer[] arr = {1,2,3,4,5,6,7};
List<Integer> list = new ArrayList<>(Arrays.asList(arr));
System.out.println("before:" +list);
Collections.shuffle(list,random);
System.out.println("after:" +list);
System.out.println("arr:"+Arrays.toString(arr));
System.out.println("=====================");
List<Integer> list1 = Arrays.asList(arr);
System.out.println("before:" +list1);
Collections.shuffle(list1,random);
System.out.println("after:" +list1);
System.out.println("arr:"+Arrays.toString(arr));
/*Output:
before:[1, 2, 3, 4, 5, 6, 7]
after:[5, 6, 7, 4, 3, 1, 2]
arr:[1, 2, 3, 4, 5, 6, 7]
=====================
before:[1, 2, 3, 4, 5, 6, 7]
after:[1, 6, 3, 5, 2, 4, 7]
arr:[1, 6, 3, 5, 2, 4, 7]
*/
}
在第一种情况下, Arrays.asList()
的输出被传递给了 ArrayList 的构造器,这将创建一个引用 arr的元素的 ArrayList ,因此打乱这些引用不会修改该数组。但是,如果直接使用 Arrays.asList(arr)
的结果,这种打乱就会修改 arr 的顺序。重要的是要注意 Arrays.asList()
生成一个 List 对象,该对象使用底层数组作为其物理实现。如果执行的操作会修改这个 List ,并且不希望修改原始数组,那么就应该在另一个集合中创建一个副本。
本章小结
- Collection 保存单一的元素,而 Map 包含相关联的键值对。使用 Java 泛型,可以指定集合中保存的对象的类型,因此不能将错误类型的对象放入集合中,并且在从集合中获取元素时,不必进行类型转换。各种 Collection 和各种 Map 都可以在添加更多的元素时,自动调整其尺寸大小。集合不能保存基本类型,但自动装箱机制会负责执行基本类型和集合中保存的包装类型之间的双向转换。
- 像数组一样, List 也将数字索引与对象相关联,因此,数组和 List 都是有序集合。
- 如果要执行大量的随机访问,则使用 ArrayList ,如果要经常从表中间插入或删除元素,则应该使用 LinkedList 。
- 队列和堆栈的行为是通过 LinkedList 提供的。
- Map 是一种将对象(而非数字)与对象相关联的设计。 HashMap 专为快速访问而设计,而 TreeMap 保持键始终处于排序状态,所以没有 HashMap 快。 LinkedHashMap 按插入顺序保存其元素,但使用散列提供快速访问的能力。
- Set不接受重复元素。 HashSet 提供最快的查询速度,而 TreeSet 保持元素处于排序状态。 LinkedHashSet 按插入顺序保存其元素,但使用散列提供快速访问的能力。
- 不要在新代码中使用遗留类 Vector ,Hashtable 和 Stack
浏览一下Java集合的简图(不包含抽象类或遗留组件)会很有帮助。这里仅包括在一般情况下会碰到的接口和类。
可以看到,实际上只有四个基本的集合组件: Map , List , Set 和 Queue ,它们各有两到三个实现版本(Queue 的 java.util.concurrent 实现未包含在此图中)。最常使用的集合用黑色粗线线框表示。
虚线框表示接口,实线框表示普通的(具体的)类。带有空心箭头的虚线表示特定的类实现了一个接口。实心箭头表示某个类可以生成箭头指向的类的对象。例如,任何 Collection 都可以生成 Iterator , List 可以生成 ListIterator 。除 TreeSet 之外的所有 Set 都具有与 Collection 完全相同的接口。List 和 Collection 存在着明显的不同,尽管 List 所要求的方法都在 Collection 中。另一方面,在 Queue 接口中的方法是独立的,在创建具有 Queue 功能的实现时,不需要使用 Collection 方法。最后, Map 和 Collection 之间唯一的交集是 Map 可以使用 entrySet()
和 values()
方法来产生 Collection 。
知识扩展
ArrayList和LinkedList的区别?
- 底层数据结构:
他们都实现了List接口,由他的实现类各自进行实现size()
、get()
等方法,List继承Collection接口,代表有序的队列(省略部分代码)。
ArrayList基于动态数组实现,继承AbstractList抽象类实现了List接口中除了size()
、get()
之外的方法。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
}
LinkedList基于双向链表实现,继承AbstractSequentialList抽象类,里面大部分方法都是通过迭代器实现的,LinkedList还实现了Deque接口,可以被当作栈、队列或双端队列来使用。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
}
- 查询、新增、删除效率:
理论上对于新增和删除操作LikedList比ArrayList的效率更高,而LikedList的随机查询效率相对低,实际并非如此(亲测简单操作:新增1000w条数据,ArrayList只慢了200ms左右,而随机新增竟然时间差不多,甚至比LinkedList还要快一点,随机查询LinkedList也只慢了几十毫秒,删除操作两个耗时都差不多),所以不同问题不同分析。为了更符合实际开发场景,稍微复杂一点的方式区分他们。
尾部新增:遍历输入10w条数据ArrayList稍微慢一点(每次结果不一),因为每次会判断是否需要扩容。
指定位置新增:给指定位置新增的时候LinkedList还是要比ArrayList快,因为ArrayList不仅还要考虑扩容,还要向后位移,这样一来所消耗的时间就越大了。
public static void main(String[] args) {
List<Integer> aList = new ArrayList<>();
add(aList);//Output: add:4
addIndex(aList);//Output: addIndex:2
List<Integer> linkedList = new LinkedList<>();
add(linkedList);//Output: add:3
addIndex(aList);//Output: addIndex:1
}
private static void add(List<Integer> list){
long s = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
long e = System.currentTimeMillis();
System.out.println("add:"+ (e-s));
}
private static void addIndex(List<Integer> list){
long s = System.currentTimeMillis();
Object[] objects = list.toArray();
for (int i = 0; i < objects.length; i++) {
if(i == 8888) {
list.add(i, 9969);
}
}
long e = System.currentTimeMillis();
System.out.println("addIndex:"+ (e-s));
}
indexOf()
查询:两个源码差别也不大,通过源码去遍历获取下标,两个集合所耗时差距也不是很大。可能是数据少的原因,数据量越大ArrayList的耗时就越小。
get()
查询:ArrayList的速度还是要优于LinkedList当数据量越大两个耗时差距也就越大,毕竟ArrayList底层是数组,不像LinkedList一样通过遍历去找到元素。
public static void main(String[] args) {
List<Integer> aList = new ArrayList<>();
indexOf(aList);//Output: indexOf:6
get(aList);//Output: get:0
List<Integer> linkedList = new LinkedList<>();
indexOf(linkedList);//Output: indexOf:4
get(linkedList);//Output: get:1
}
private static void indexOf(List<Integer> list){
long s = System.currentTimeMillis();
for (Integer str:list) {
if(str.equals(45678)) {
int i = list.indexOf(str);
}
}
long e = System.currentTimeMillis();
System.out.println("indexOf:"+ (e-s));
}
private static void get(List<Integer> list){
long s = System.currentTimeMillis();
for (Integer i:list) {
if(i == 45678) {
list.get(i);
}
}
long e = System.currentTimeMillis();
System.out.println("get:"+ (e-s));
}
下标删除:ArrayList需要将数据向前位移,数据量大位移操作也就越慢,LinkedList只需要遍历下标拿到数据重新建立连接再将当前节点置为null即可。
内容删除:ArrayList则需要遍历完以后再将数据向前位移,这样一来消耗的时间更多了,LinkedList只需要遍历一遍即可。
public static void main(String[] args) throws InterruptedException {
List<Integer> aList = new ArrayList<>();
removeIndex(aList);//Output: removeIndex:75
removeObj(aList);//Output: removeObj:8
List<Integer> linkedList = new LinkedList<>();
removeIndex(aList);//Output: removeIndex:49
removeObj(aList);//Output: removeObj:2
}
private static void removeIndex(List<Integer> list){
long s = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
if(i % 8 ==0)
list.remove(i);
}
long e = System.currentTimeMillis();
System.out.println("removeIndex:"+ (e-s));
}
private static void removeObj(List<Integer> list){
long s = System.currentTimeMillis();
for (Integer i:list) {
if(i % 9 ==0)
list.remove(i);
}
long e = System.currentTimeMillis();
System.out.println("removeObj:"+ (e-s));
}
通过实践结论得出:当你对列表更多的进行查询,即获取某个位置的元素时,应当优先使用ArrayList;当你对列表需要进行频繁的删除和增加,而很少使用查询时,优先使用LinkedList;
- 是否保证线程安全
ArrayList和 LinkedList都是不同步的,也就是不保证线程安全。如果需要可以使用Collections.synchronizedList()
和创捷Vector对象实现线程安全。
public static void main(String[] args) {
List<Integer> aList = new ArrayList<>();
List<Integer> integers = Collections.synchronizedList(aList);
Vector vector = new Vector(aList);
}
- 内存空间占用
ArrayList的空间浪费主要体现在在列表的结尾会预留一定的容量空间, 而LinkedList
的空间花费则体现在它的每一个元素(存放直接后继和直接前驱以及数据)都需要消耗比ArrayList更多的空间。
HashMap和TreeMap、LinkedHashMap的区别?
HashMap继承AbstractMap类,实现Map接口,HashMap的底层是数组+链表的结构
- 插入的顺序按照公式:
(n - 1) & hash
计算出所存放的位置,所以HashMap的插入位置是无序的。 - 删除通过公式:
(n - 1) & hash
找到对应数组位置再遍历找到当前节点位置,将上下节点重新连接即可。 - 查询通过公式:
(n - 1) & hash
找到对应数组位置再遍历找到对应节点位置。 - 当链表长度大于8且数组长度等于64的时候开始转换为红黑树结构。树结构的插入、删除过程需要维护树的颜色和平衡。
TreeMap继承AbstractMap类,实现NavigableMap接口,TreeMap的底层是树结构,相等HashMap较简单。
- 插入的顺序按照比较器的大小(ascii码顺序)来判断是存放再左子节点还是右子节点,所以TreeMap的插入是有序的。
- 删除时,先获取到节点,然后将父节点和子节点重新连接,通过平衡操作将节点挪到最后一个节点进行删除。
- 查询通过比较器,判断从左子节点还是右子节点查找,直到最后相等,返回对应节点。
LinkedHashMap继承HashMap类,实现Map接口,底层毋庸置疑和HashMap一致,唯一不同的是查询的顺序是按照插入顺序进行排序的,这是因为每次创建一个节点的时候LinkedHashMap会将节点与上一个节点建立关系,保持一个链表的结构,所以遍历的顺序和插入顺序是一致的;HashMap是数组结构,从0开始遍历每个一节点,所以遍历的顺序和插入顺序不一致,这是由公式计算决定的。
HashSet、TreeSet、LinkedHashSet的区别?
HashSet继承AbstractSet类,实现Set接口,底层调用HashMap;TreeSet继承AbstractSet类,实现NavigableSet接口,底层调用NavigableMap,但还是使用TreeMap;LinkedHashSet继承HashSet类,实现Set接口,底层调用HashSet的构造方法,其本质是LinkedHashMap。它们之间的有个共同的地方:将值成为Map的key,value是Object对象,这也保证了它们的数据的唯一性。
最后
以上就是还单身自行车为你收集整理的重拾Java基础知识:集合前言知识扩展的全部内容,希望文章能够帮你解决重拾Java基础知识:集合前言知识扩展所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复