《OnJava8》精读(五) 集合

在这里插入图片描述

@

介绍


《On Java 8》是什么?

它是《Thinking In Java》的作者Bruce Eckel基于Java8写的新书。里面包含了对Java深入的理解及思想维度的理念。可以比作Java界的“武学秘籍”。任何Java语言的使用者,甚至是非Java使用者但是对面向对象思想有兴趣的程序员都该一读的经典书籍。目前豆瓣评分9.5,是公认的编程经典。

为什么要写这个系列的精读博文?

由于书籍读起来时间久,过程漫长,因此产生了写本精读系列的最初想法。除此之外,由于中文版是译版,读起来还是有较大的生硬感(这种差异并非译者的翻译问题,类似英文无法译出唐诗的原因),这导致我们理解作者意图需要一点推敲。再加上原书的内容很长,只第一章就多达一万多字(不含代码),读起来就需要大量时间。

所以,如果现在有一个人能替我们先仔细读一遍,筛选出其中的精华,让我们可以在地铁上或者路上不用花太多时间就可以了解这边经典书籍的思想那就最好不过了。于是这个系列诞生了。

一些建议

推荐读本书的英文版原著。此外,也可以参考本书的中文译版。我在写这个系列的时候,会尽量的保证以“陈述”的方式表达原著的内容,也会写出自己的部分观点,但是这种观点会保持理性并尽量少而精。本系列中对于原著的内容会以引用的方式体现。
最重要的一点,大家可以通过博客平台的评论功能多加交流,这也是学习的一个重要环节。

第十二章 集合


本章总字数:19000

关键词:

  • 集合的概念
  • List
  • Set
  • Map
  • 队列
  • 集合与迭代器

如果你读过本系列的前几篇博文,你会发现本篇(第五篇)是唯一一个专注讲一个知识点的。前几篇都是以概括一系列内容为主,而这一篇只讲集合。原因很简单——因为Java中的集合是如此的重要。

由于原著的本章节内容讲解太过偏向理论且知识点比较分散。所以本章将通过我自己的总结来阐述。但是知识是相通的。如果需要可以参读原著内容。

集合的由来

在OOP的相关内容中曾经讲过,我们可以使用 new关键词来创建任意一个对象。有些时候,我们可能需要同一种类型的一系列对象,比如多个 String对象。用以前的知识,我们可以使用数组。

但是数组也有自己的局限性——它的数量是固定的。这样就引出了另一个问题,我们在不清楚一个对象的个数时,如何来创建?

这是一个很常见的问题,比如:学校今天要接待一些学生,如果你知道学生的数量是10位可以这么做:

class Student{}
...
Student[] students=new Student[10];

但是如果你不知道具体数量,只知道会来一些学生,这么做就不合理。这时候就需要一个新概念,一个非固定长度的一系列对象——集合。

集合的概念

在说集合概念前需要先提一下另外一个概念——泛型。泛型指代某一种固定类型,在与集合搭配后可以限制传入集合的对象类型。在之后的章节有专门的泛型相关讲解。

通过使用泛型,就可以在编译期防止将错误类型的对象放置到集合中。

Java中集合的概念分为两种——集合与映射。

  • 集合(Collection) :一个独立元素的序列,这些元素都服从一条或多条规则。List 必须以插入的顺序保存元素, Set 不能包含重复元素, Queue 按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。
  • 映射(Map) : 一组成对的“键值对”对象,允许使用键来查找值。 ArrayList 使用数字来查找对象,因此在某种意义上讲,它是将数字和对象关联在一起。 map 允许我们使用一个对象来查找另一个对象,它也被称作关联数组(associative array),因为它将对象和其它对象关联在一起;或者称作字典(dictionary),因为可以使用一个键对象来查找值对象,就像在字典中使用单词查找定义一样。 Map 是强大的编程工具。

集合的关系(图片来自百度)

集合的关系(图片来自百度)

如图,Collection接口下的都是集合,Map接口下的都是映射。而这两个接口下又分别有不同的接口继承。下面就来逐一详细介绍。

列表List

List是一系列有序的对象集合。 List分为两种: ArrayList LinkedList

  • 基本的 ArrayList ,擅长随机访问元素,但在 List 中间插入和删除元素时速度较慢。
  • LinkedList ,它通过代价较低的在 List 中间进行的插入和删除操作,提供了优化的顺序访问。LinkedList 对于随机访问来说相对较慢,但它具有比 ArrayList 更大的特征集。

接下来我们来看一下的源码:

public interface List<E> extends Collection<E> {
....

List是一个继承自Collection的接口, List在后者的基础上加入了不少拓展方法,比如:

void add(int index, E element);

这些拓展方法丰富了原有接口的内容。

接下来是 ArrayList

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...

ArrayList是一个继承自抽象类AbstractList并实现了 List、RandomAccess、Cloneable等接口的类。这里需要着重提一下Serializable接口,一个类只有实现了Serializable才能被序列化。Serializable接口的内部是空的,没有任何方法,它的作用更多的是给类作出序列化的“标记”。

在C#中,这种方式被优化为了[Serializable]的类标记,而不需要专门继承。

LinkedList 中,也继承了List、Serializable,但是对比 ArrayList你会发现一些区别:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
...

比如在接口的继承中, ArrayList继承了RandomAccess接口,而 LinkedList 继承了Deque接口。参照官方说明文档,RandomAccess接口和Serializable接口一样是一个标记接口(它的内部也同样没有任何方法),它用来标记随机快速访问实现。也就是说它的作用是让 ArrayList的访问速度得以提升,但是牺牲了插入数据与删除数据的速度。

相反的,由于 LinkedList 继承了Deque(Deque继承自Queue接口),使得其数据的查询速度变慢但是增删的速度变快。

所以总结之后我们发现:
继承自 List的两个集合:ArrayList、LinkedList,他们都是有序的。但是 ArrayList更擅长查询,而 LinkedList 增删速度更快

了解了这些有助于我们在项目开发时选择更合适的集合。

再回过头看学生的例子,我们就可以这么做:

List<Student> list = new ArrayList<Student>();
list.add(new Student());
        
List<Student> list2 = new LinkedList<Student>();
list2.add(new Student());

集合Set

首先,与 List最大的不同是, Set不允许存储重复的值

首先看源码,与 List一致, Set也继承自Collection接口:

public interface Set<E> extends Collection<E> {
...

再来看看HashSet 的实现:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable{
    ...

HashSet 与上文ArrayList及LinkedList继承关系类似,但是少了关于RandomAccess和Deque的继承。

至于 TreeSet,它的区别就更大了,少了Set接口的直接继承,而是继承了NavigableSet接口:

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable

不过NavigableSet本身是继承自SortedSet,而SortedSet继承自Set接口。

public interface NavigableSet<E> extends SortedSet<E> {
...

SortedSet的存在,这也就导致了TreeSet与HashSet最大的不同——TreeSet 可以排序的。

可以排序不等于一定有序,只是意味着你可以为你的自定义类型实现排序的功能。只有你实现了排序规则,它才能按照规则排序。Comparator (比较器)排序相关内容会在后期的章节详细介绍。

再来说说 LinkedHashSet,很明显它是HashSet的派生类:

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
    ...

虽然是派生类,但是两者的数据存储方式有一些不同。

HashSet 使用了散列。由 HashSet 维护的顺序与 TreeSet 或 LinkedHashSet 不同,因为它们的实现具有不同的元素存储方式。
TreeSet 将元素存储在红-黑树数据结构中,而 HashSet 使用散列函数。 LinkedHashSet 因为查询速度的原因也使用了散列,但是看起来使用了链表来维护元素的插入顺序。

上文提到了 Set是不允许存储重复数据的,但是如果强制插入重复数据呢?

        Set<Integer> set2=new HashSet<Integer>();
        set2.add(100);
        set2.add(100);

        Set<Integer> set3=new TreeSet<Integer>();
        set3.add(100);
        set3.add(100);

结果:
在这里插入图片描述
不会报错,只是结果不会出现重复的数据。

映射Map

Map接口下主要有HashMap TreeMapLinkedHashMap 。与 List Set不同的是, Map由一系列<key,value>键值对构成,且key不能重复。

我们来统一看一遍源码中的继承关系:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    ...
public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
    ...
public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>{
    ...

有了之前的经验,我们从源码能看得出 TreeMap似乎与 TreeSet更相似, HashMap HashSet更相似(从他们命名上你也能看出一些规律)。比如, TreeMap继承了NavigableMap接口,而 TreeSet继承了NavigableSet接口。这两个接口都与排序相关(所以很明显的,TreeMap同样支持排序)。

有了键值对的概念,我们再次回头看学生的例子,比如我们可以为学生进行编号(学号是唯一的):

Map<String,Student> map=new HashMap<String,Student>();
map.put("001",new Student());
map.put("002",new Student());

在很多情况下,集合与映射可以搭配使用。比如,我们现在要为学生分配班级:

Set<Student> students=new HashSet<Student>();
students.add(new Student());
students.add(new Student());
students.add(new Student());

Set<Student> students2=new HashSet<Student>();
students2.add(new Student());
students2.add(new Student());
students2.add(new Student());

Map<String, Set<Student>> map = new HashMap<String, Set<Student>>();
map.put("一班", students);
map.put("二班", students2);

集合的其他知识

在原著中,作者用不少篇幅解释了队列、迭代器的知识。

队列是一个典型的“先进先出”(FIFO)集合。 即从集合的一端放入事物,再从另一端去获取它们,事物放入集合的顺序和被取出的顺序是相同的。LinkedList 实现了 Queue 接口,并且提供了一些方法以支持队列行为,因此 LinkedList 可以用作 Queue 的一种实现。

出于对队列“先进先出”的理解,我们可以做出尝试:

        Set<Integer> set2 = new HashSet<Integer>();
        set2.add(1);
        set2.add(3);
        set2.add(2);

        Set<Integer> set3 = new TreeSet<Integer>();
        set3.add(1);
        set3.add(3);
        set3.add(2);

        Set<Integer> set4 = new LinkedHashSet<Integer>();
        set4.add(1);
        set4.add(3);
        set4.add(2);

结果:
在这里插入图片描述
结果与预期相同,LinkedHashSet的结果是按照插入数据的方式展现的。而HashSet和TreeSet则不一样。

迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不知道或不关心该序列的底层结构。另外,迭代器通常被称为轻量级对象(lightweight object):创建它的代价小。因此,经常可以看到一些对迭代器有些奇怪的约束。例如,Java 的 Iterator 只能单向移动。这个 Iterator 只能用来:

  • 使用 iterator() 方法要求集合返回一个 Iterator。 Iterator 将准备好返回序列中的第一个元素。
  • 使用 next() 方法获得序列中的下一个元素。
  • 使用 hasNext() 方法检查序列中是否还有元素。
  • 使用 remove() 方法将迭代器最近返回的那个元素删除。

总结

本篇是本系列很重要的一部分内容。对实际编程影响最大,使用也是最多的。几乎在任何时候我们都会遇到集合的使用。深入了解每个集合的特点,我们才能清楚在不同场景下如何选择合适的集合类型。

posted @ 2021-01-23 21:11  Hi-Jimmy  阅读(307)  评论(0编辑  收藏  举报