Java8编程思想精粹(十)-容器(上)


点击上方「蓝字」关注我们

如果一个程序只包含固定数量的对象且对象的生命周期都是已知的,那么这是一个非常简单的程序。
  • 编程痛点


    通常程序总是根据运行时才知道某些条件,从而去创建新的对象。

    在此之前,无法知道所需对象的数量甚至确切类型。

  • 解决方案


    需要在任意时刻和任意位置创建任意数量的对象。

    因此,不能再简单地这样依靠创建命名的引用来持有每一个对象:

    MyType aReference;
    

    因为你不会知道实际上到底需要多少个这样的引用。

Java有多种方式保存对象的引用。例如数组,这种编译器支持的类型,是保存一组对象的最有效的方式,如果想要保存一组基本类型数据,也推荐使用数组。
但数组具有固定容量,而在更一般情况下,写程序时我们并不知道

  • 将需要多少个对象

  • 是否需要更复杂的方式来存储对象

因此数组这一限制过于受限。

java.util 库提供了一套相当完整的集合类(collection classes)来解决这个问题,其中基本的类型有 ListSetQueueMap。也称作容器类(container classes)。集合提供了完善的方法来保存对象,可以使用这些工具来解决大量的问题。

集合还有一些其它特性。例如,

  • Set 对于每个值都只保存一个对象

  • Map 是一个关联数组,允许将某些对象与其他对象关联起来

Java集合类都可动态调整容量。可将任意数量的对象放置在集合中,而不用关心集合应该多大。

尽管在 Java 中没有直接的关键字支持,但集合类仍然是可以显著增强编程能力的基本工具。

1 泛型和类型安全的集合

Java5 之前的集合的一个主要问题是编译器准许SE向集合中插入不正确类型。

例如, Apple 对象的集合,使用最基本最可靠的 ArrayList ,可自动扩充自身容量的数组。
创建一个实例,用 add() 插入对象;get() 访问对象,此时需要使用索引,就像数组那样,但无需方括号。size() 方法说明集合中包含了多少个元素,所以不会不小心因数组越界而引发错误。

如下示例, AppleOrange 都被放到集合,然后取出。

正常情况下,Java编译器会给出警告,因为这里没有使用泛型。使用@SuppressWarning 注解及其参数表示只抑制“unchecked”类型的警告.

  • 运行结果



    AppleOrange 是截然不同的,它们除了都是 Object 外没有任何共同点。

    因为 ArrayList 保存的是 Object ,所以不仅可以通过 ArrayListadd() 方法将 Apple 对象放入这个集合,而且可以放入 Orange 对象,这无论在编译期还是运行时都不会有问题。


    当使用 ArrayListget() 方法来取出你认为是 Apple 的对象时,得到的其实是 Object 引用,必须转型为 Apple

    然后将整个表达式用括号括起来,以便在调用 Appleid() 方法之前,强制执行转型。

    否则,将会产生语法错误。


    在运行时,当尝试将 Orange 对象转为 Apple 时,会出现输出中显示的错误。

使用 Java 泛型来创建类可能很复杂。但是,使用预先定义的泛型类却相当简单。例如,要定义一个用于保存 Apple 对象的 ArrayList ,只需要使用 ArrayList\ 来代替 ArrayList
尖括号括起来的是类型参数(可能会有多个),指定了这个集合实例可以保存的类型

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

泛型版本示例

new ArrayList<>() ,“菱形语法”(diamond syntax)。在 Java7 之前,必须要在两端都进行类型声明,如下:

ArrayList<Apple> apples = new ArrayList<Apple>();

随着类型变得越来越复杂,这种重复产生的代码非常混乱且难以阅读。程序员发现所有类型信息都可以从左侧获得,因此,编译器没有理由强迫右侧再重复这些。
虽然类型推断(type inference)只是个很小的请求,Java 语言团队仍然欣然接受并进行了改进。

有了 ArrayList 声明中的类型指定,编译器会阻止将 Orange 放入 apples ,因此,这会成为一个编译期错误而不是运行时错误

  • 好处


    使用泛型,从 List 中获取元素无需强制类型转换。

    因为 List 知道自己持有的啥类型,因此当调用 get() 时,它会替你执行转型。

    因此,使用泛型,你不仅知道编译器将检查放入集合的对象类型,而且在使用集合中的对象时也可以获得更清晰的语法

泛型下的向上转型

当指定了某个类型为泛型参数时,并不仅限于只能将确切类型的对象放入集合中。
向上转型也可以像作用于其他类型一样作用于泛型:


因此,可以将 Apple 的子类型添加到被指定为保存 Apple 对象的集合中。

2 基本概念

Java集合类库采用“持有对象”(holding objects)的思想,并将其分为两个不同的概念,表示为类库的基本接口:

  1. 集合(Collection)

    一个独立元素的序列,这些元素都服从一条或多条规则。

  • List 必须以插入的顺序保存元素

  • Set 不能包含重复元素

  • Queue 按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。

    2. 映射(Map)

一组成对的“键值对”对象,允许使用键来查找值。


ArrayList 使用数字来查找对象,因此在某种意义上讲,它是将数字和对象关联在一起。


map 允许我们使用一个对象来查找另一个对象,它也被称作关联数组(associative array),因为它将对象和其它对象关联在一起;


或者称作字典(dictionary),因为可以使用一个键对象来查找值对象,就像在字典中使用单词查找定义一样。

在理想情况下,大部分代码都在与这些接口打交道,并且唯一需要指定所使用的精确类型的地方就是在创建的时候
因此,可以像下面这样创建一个 List

List<Apple> apples = new ArrayList<>();

ArrayList 已经被向上转型为了 List ,这与之前示例中的处理方式正好相反。
使用接口的目的是,如果想要改变具体实现,只需在创建时修改它即可:

List<Apple> apples = new LinkedList<>();

因此,应该创建一个具体类的对象,将其向上转型为对应的接口,然后在其余代码中都是用这个接口

这种方式并非总是有效的,因为某些具体类有额外的功能。
例如, LinkedList 具有 List 接口中未包含的额外方法,而 TreeMap 也具有在 Map 接口中未包含的方法。
如果需要使用这些方法,就不能将它们向上转型为更通用的接口。

3  添加元素组

java.util 包中的 ArraysCollections 类中都有很多实用的方法,可以在一个 Collection 中添加一组元素。

  • Arrays.asList() 方法接受一个数组或是逗号分隔的元素列表(使用可变参数),并将其转换为 List 对象。

  • Collections.addAll() 方法接受一个 Collection 对象,以及一个数组或是一个逗号分隔的列表,将其中元素添加到 Collection

Collection 的构造器可以接受另一个 Collection,用它来将自身初始化。因此,可以使用 Arrays.asList() 来为这个构造器产生输入。但是, Collections.addAll() 运行得更快,而且很容易构建一个不包含元素的 Collection ,然后调用 Collections.addAll() ,因此这是首选方式。

Collection.addAll() 方法只能接受另一个 Collection 作为参数,没有 Arrays.asList()Collections.addAll() 灵活。这两个方法都使用可变参数列表。

也可以直接使用 Arrays.asList() 的输出作为一个 List ,但是这里的底层实现是数组,没法调整大小。

4 打印集合

必须使用 Arrays.toString() 来生成数组的可打印形式。但打印集合无需任何帮助。

Java集合库中的两个主要类型。它们的区别在于集合中的每个“槽”(slot)保存的元素个数。

Collection 类型在每个槽中只能保存一个元素。
Map 在每个槽中存放了两个元素,即和与之关联的

默认的打印

使用集合提供的 toString() 方法即可生成可读性很好的结果。
Collection 打印出的内容用方括号括住,每个元素由逗号分隔。
Map 则由大括号括住,每个键和值用等号连接(键在左侧,值在右侧)。

ArrayListLinkedList 都是 List 的类型,从输出中可以看出,它们都按插入顺序保存元素。两者之间的区别不仅在于执行某些类型的操作时的性能,而且 LinkedList 包含的操作多于 ArrayList

HashSetTreeSetLinkedHashSetSet 的类型。Set 仅保存每个相同项中的一个,并且不同的 Set 实现存储元素的方式也不同。HashSet 使用相当复杂的方法存储元素。现在只需要知道,这种技术是检索元素的最快方法,因此,存储顺序看上去没有什么意义(通常只关心某事物是否是 Set 的成员,而存储顺序并不重要)。如果存储顺序很重要,则可以使用 TreeSet ,它将按比较结果的升序保存对象)或 LinkedHashSet ,它按照被添加的先后顺序保存对象。

Map (也称为关联数组)使用来查找对象,就像一个简单的数据库。所关联的对象称为。假设有一个 Map 将美国州名与它们的首府联系在一起,如果想要俄亥俄州(Ohio)的首府,可以用“Ohio”作为键来查找,几乎就像使用数组下标一样。正是由于这种行为,对于每个键, Map 只存储一次。

Map.put(key, value) 添加一个所想要添加的值并将它与一个键(用来查找值)相关联。Map.get(key) 生成与该键相关联的值。上面的示例仅添加键值对,并没有执行查找。这将在稍后展示。

Map 的三种基本风格:HashMapTreeMapLinkedHashMap

  • HashMap 中的顺序不是插入顺序,其使用了非常快速的查找算法

  • TreeMap 通过比较结果的升序来保存键,

  • LinkedHashMap 在保持 HashMap 查找速度的同时按键的插入顺序保存键。

5 List

将元素保存在特定的序列中。在 Collection 的基础上添加了许多方法,允许在 List 的中间插入和删除元素。

有两种类型 List

  • 基本的 ArrayList ,擅长随机访问元素,但在 List 中间插入和删除元素时速度较慢。

  • LinkedList ,它通过代价较低的在 List 中间进行的插入和删除操作,提供了优化的顺序访问。

    LinkedList 对于随机访问来说相对较慢,但它具有比 ArrayList 更大的特征集。

常用方法

  • 可以使用 contains() 方法确定对象是否在列表中

  • 如果要删除一个对象,可以将该对象的引用传递给 remove() 方法

  • 如果有一个对象的引用,可以使用 indexOf()List 中找到该对象所在位置的下标号

当确定元素是否是属于某个 List ,寻找某个元素的索引,以及通过引用从 List 中删除元素时,都会用到 equals() 方法。

是否永远不应该在 ArrayList 的中间插入元素,并最好转换为 LinkedList
不,它只是意味着你应该意识到这个问题,如果你开始在某个 ArrayList 中间执行很多插入操作,并且程序开始变慢,那么你应该看看你的 List 实现有可能就是罪魁祸首。
优化是一个很棘手的问题,最好的策略就是置之不顾,直到发现必须要去担心它了(尽管去理解这些问题总是一个很好的主意并且国内面试必备)。

  • subList() 方法可以轻松地从更大的列表中创建切片,当将切片结果传递给原来这个较大的列表的 containsAll() 方法时,很自然地会得到 true

  • retainAll() 方法实际上是一个“集合交集”操作,请再次注意,所产生的结果行为依赖于 equals() 方法。

  • 使用索引号来删除元素与通过对象引用来删除元素相比,显得更加直观,因为在使用索引时,不必担心 equals() 的行为。

  • removeAll() 方法也是基于 equals() 方法运行的。顾名思义,它会从 List 中删除在参数 List 中的所有元素。

  • set() 方法的命名显得很不合时宜,因为它与 Set 类存在潜在的冲突。使用“replace”可能更适合,因为它的功能是用第二个参数替换索引处的元素(第一个参数)。

  • 对于 List ,有一个重载的 addAll() 方法可以将新列表插入到原始列表的中间位置,而不是仅能用 CollectionaddAll() 方法将其追加到列表的末尾。

  • toArray() 方法将任意的 Collection 转换为数组。这是一个重载方法,其无参版本返回一个 Object 数组,但是如果将目标类型的数组传递给这个重载版本,那么它会生成一个指定类型的数组(假设它通过了类型检查)。如果参数数组太小而无法容纳 List 中的所有元素(就像本例一样),则 toArray() 会创建一个具有合适尺寸的新数组。

6 迭代器Iterators

在任何集合中,都必须有某种方式可以插入元素并再次获取它们。毕竟,保存事物是集合最基本的工作。

对于 Listadd() 是插入元素的一种方式, get() 是获取元素的一种方式。

如果从更高层次的角度考虑,会发现这里有个缺点:要使用集合,必须对集合的确切类型编程。

如果原本是 List 编码,后来发现 Set 更方便
或者假设一开始就想编写一段通用代码,不关心正在使用什么类型集合,可以用于不同类型集合
即,如何才能不重写代码就可以应用于不同类型的集合
迭代器(也是一种设计模式)的概念实现了这种抽象。

迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不知道或不关心该序列的底层结构。
迭代器通常被称为轻量级对象:创建它的代价小。
Java 的 Iterator 只能单向移动。这个 Iterator 只能用来:

  1. iterator()
    要求集合返回一个 Iterator

    Iterator 将准备好返回序列中的第一个元素。

  2. next()
    获得序列中的下一个元素。

  3. hasNext()
    检查序列中是否还有元素。

  4. remove()
    将迭代器最近返回的那个元素删除。

有了 Iterator ,就不必再为集合中元素的数量操心了。这是 hasNext()next() 关心的。
如果只向前遍历 List ,并不打算修改 List 对象本身,那么使用 for-in 更简洁。
Iterator 还可删除由 next() 生成的最后一个元素,这意味着在调用 remove() 之前必须先调用 next()
在集合中的每个对象上执行操作,这种思想十分强大

Iterator 的真正威力:将遍历序列的操作与该序列的底层结构分离。
基于此,我们说:迭代器统一了对集合的访问方式。

ListIterator

更强大的 Iterator 子类型,只能由各种 List 类生成。

Iterator 只能向前移动,而 ListIterator 可以双向移动。还可以生成相对于迭代器在列表中指向的当前位置的后一个和前一个元素的索引,并且可以使用 set() 方法替换它访问过的最近一个元素。

可通过调用 listIterator() 方法来生成指向 List 开头的 ListIterator ,还可以通过调用 listIterator(n) 创建一个一开始就指向列表索引号为 n 的元素处的 ListIterator

链表LinkedList

LinkedListArrayList 一样实现了基本的 List 接口,但它在 List 中间执行插入和删除操作时比 ArrayList 更高效。然而,它在随机访问操作效率方面却要逊色一些。

LinkedList 还添加了一些方法,使其可以被用作栈、队列或双端队列(deque) 。这些方法有些可能只是名称差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 Queue 中)。例如:

  • getFirst()element() ,返回列表的头部而并不删除它,如果 List 为空,则抛出NoSuchElementException

    peek() 方法与这两个方法只是稍有差异,它在列表为空时返回 null

  • removeFirst()remove() ,删除并返回列表的头部元素,并在列表为空时抛出 NoSuchElementException 异常。

    poll() 稍有差异,它在列表为空时返回 null

  • addFirst() 在列表的开头插入一个元素。

  • offer()add()addLast()

    在列表的尾部(末尾)添加一个元素。

  • removeLast() 删除并返回列表的最后一个元素。

查看 Queue 接口就会发现,它在 LinkedList 的基础上添加了 element()offer()peek()poll()remove() 方法,以使其可以成为一个 Queue 的实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值