九. 初探集合

九. 初探集合

通常,程序总是根据运行时才知道的某些条件去创建新的对象,在此之前,无法知道所需对象的数量甚至确切类型。为了解决这个普遍的编程问题,需要在任意时刻和任意位置创建任意数量的对象。

Java有多种方式保存对象(确切地说,是对象的引用)。例如数组,是保存一组对象的最有效的方式,但是数组具有固定的大小尺寸,在很多时候可能需要一种更灵活的或支持更复杂的存储对象的方式。为此,java.util 库提供了一套相当完整的 集合类(又称容器类)来解决这个问题,其中基本的类型有 List 、 Set 、 Queue 和 Map。

Java集合类都可以自动地调整自己的大小。

1. 基本概念

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

在一些情况下,编写的大部分代码都在与这些接口打交道,并且唯一需要指定所使用的精确类型的地方就是在创建的时候。集合创建示例:

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

ArrayList 已经被向上转型为了 List ,使用接口的目的是,如果想要改变具体实现,只需在创建时修改它就行了,就像下面这样:

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

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

Collection 接口概括了序列的概念——一种存放一组对象的方式。下面是用 Integer 对象填充了一个 Collection ,然后打印集合中的每个元素:

import java.util.*;

public class SimpleCollection {
    
  	public static void main(String[] args) {
    	Collection<Integer> c = new ArrayList<>();
    	for(int i = 0; i < 10; i++)
      		c.add(i); // 自动装箱
    	for(Integer i : c)
      		System.out.print(i + ", ");
  	}
}

输出结果:

image-20200726133725814

2. 添加元素组

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

Arrays.asList() 方法接受一个数组或是逗号分隔的元素列表(使用可变参数),并将其转换为 List 对象。 Collections里的addAll() 方法接受一个 Collection 对象,以及一个数组或是一个逗号分隔的列表,将其中元素添加到 Collection 中。代码示例:

import java.util.*;

public class AddingGroups {
    
  	public static void main(String[] args) {
    	Collection<Integer> collection = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
    	Integer[] moreInts = { 6, 7, 8, 9, 10 };
        
    	collection.addAll(Arrays.asList(moreInts));
    	Collections.addAll(collection, 11, 12, 13, 14, 15);
    	Collections.addAll(collection, moreInts);
    
    	List<Integer> list = Arrays.asList(16,17,18,19,20);
    	list.set(1, 99); 
  	}
}

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

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

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

public class Test {

    public static void main(String[] args) {
        List<Fruit> fruit = Arrays.<Fruit>asList(new Apple(), new Orange());
    }
}

class Fruit {}
class Apple extends Fruit{}
class Orange extends Fruit{}

注意 Arrays.asList() 中间的“暗示”(即 ),告诉编译器 Arrays.asList() 生成的结果 List 类型的实际目标类型是什么,这称为显式类型参数说明。

3. 集合的打印

一般使用 Arrays.toString() 来生成数组的可打印形式。

4. 列表 List

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

有两种类型的 List

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

可以使用 contains() 方法确定对象是否在列表中。如果要删除一个对象,可以将该对象的引用传递给 remove() 方法。同样,如果有一个对象的引用,可以使用 indexOf() 在 List 中找到该对象所在位置的下标号。

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

还有其它许多 api ,直接去看源码。

5. 迭代器 Iterators

迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不用关心该序列的底层结构(不必知晓集合的确切类型,迭代器统一了对集合的访问方式)。另外,迭代器通常被称为轻量级对象:创建它的代价小。Java 的 Iterator 只能单向移动,这个 Iterator 能用来:

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

代码示例:

public class Test {


    public static void main(String[] args) {
        List<Apple> apples = new ArrayList<>(10);
        Iterator<Apple> appleIterator = apples.iterator();
        while (appleIterator.hasNext()){
            Apple a = appleIterator.next();
        }
    }
}

Iterator 可以删除由 next() 生成的最后一个元素,这意味着在调用 remove() 之前必须先调用 next() 。

ListIterator

ListIterator 是一个更强大的 Iterator 子类型,它只能由各种 List 类生成。 Iterator 只能向前移动,而 ListIterator 可以双向移动。

它可以生成迭代器在列表中指向位置的后一个和前一个元素的索引,并且可以使用 set() 方法替换它访问过的最近一个元素。

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

6. 链表 LinkedList

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

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

  • getFirst() 和 element() 是相同的,它们都返回列表的头部(第一个元素)而并不删除它,如果 List 为空,则抛出 NoSuchElementException 异常, peek() 方法与这两个方法只是稍有差异,它在列表为空时返回 null 。
  • removeFirst() 和 remove() 也是相同的,它们删除并返回列表的头部元素,并在列表为空时抛出 NoSuchElementException 异常,poll() 稍有差异,它在列表为空时返回 null 。
  • addFirst() 在列表的开头插入一个元素。
  • offer() 与 add() 和 addLast() 相同,它们都在列表的尾部(末尾)添加一个元素。
  • removeLast() 删除并返回列表的最后一个元素。

7. 堆栈 Stack

堆栈是“后进先出”集合,有时被称为叠加栈,因为最后“压入”栈的元素,第一个被“弹出”栈,就像“弹夹”一样。

Java 1.0 中附带了一个 Stack 类,Java 6 添加了 ArrayDeque ,其中包含直接实现堆栈功能的方法。代码示例:

public class Test {
    
    public static void main(String[] args) {
        Deque<String> stack = new ArrayDeque<>();
        for (String s : "This is the test of Stack".split(" ")) {
            stack.push(s);
        }
        while (!stack.isEmpty()) {
            System.out.print(stack.pop() + " ");
        }
    }
}

输出结果:

image-20200726162553014

尽管已经有了 java.util.Stack ,但是 ArrayDeque 可以产生更好的 Stack

8. 集合 Set

Set 不保存重复的元素。Set 最常见的用途是测试归属性,可以很轻松地询问某个对象是否在一个 Set 中。因此,查找通常是 Set 最重要的操作,一般会选择 HashSet 实现,该实现针对快速查找进行了优化。

Set 具有与 Collection 相同的接口,没有任何额外的功能,不像前面两种不同类型的 List 那样。实际上, Set 就是一个 Collection ,只是行为不同。

早期 Java 版本中的 HashSet 产生的输出没有可辨别的顺序,出于对速度的追求, HashSet 使用了散列。由 HashSet 维护的顺序与 TreeSetLinkedHashSet 不同,因为它们的实现具有不同的元素存储方式。 TreeSet 将元素存储在红黑树中,而 HashSet 使用散列函数。 LinkedHashSet 因为查询速度的原因也使用了散列,并使用了链表来维护元素的插入顺序。

9. 映射 Map

将对象映射到其他对象的能力是解决编程问题的有效方法。例如,考虑一个程序,它被用来检查 Java 的 Random 类的随机性。理想情况下, Random 会产生完美的数字分布,但为了测试这一点,则需要生成大量的随机数,并计算落在各种范围内的数字个数。 Map 可以很容易地解决这个问题。在本例中,键是 Random 生成的数字,而值是该数字出现的次数:

public class Test {

    public static void main(String[] args) {
        Random rand = new Random(47);
        Map<Integer, Integer> m = new HashMap<>();
        for(int i = 0; i < 10000; i++) {
            int r = rand.nextInt(20);
            Integer freq = m.get(r);
            m.put(r, freq == null ? 1 : freq + 1);
        }
        System.out.println(m);
    }
}

输出结果:

image-20200726165853603

Map 与数组和其他的 Collection 一样,可以轻松地扩展到多个维度,只需要创建一个值为 MapMap(这些 Map 的值可以是其他集合,甚至是其他 Map)。

10. 队列 Queue

队列是一个典型的“先进先出”集合。 即从集合的一端放入事物,再从另一端去获取它们,事物放入集合的顺序和被取出的顺序是相同的。队列在并发编程中尤为重要,因为它们可以安全地将对象从一个任务传输到另一个任务。

LinkedList 实现了 Queue 接口,并且提供了一些方法以支持队列行为,因此 LinkedList 可以用作 Queue 的一种实现。

Queue 相关的方法提供了完整而独立的功能,也就是说,对于 Queue 所继承的 Collection ,在不需要使用它的任何方法的情况下,就可以拥有一个可用的 Queue

优先级队列 PriorityQueue

先进先出描述了最典型的队列规则。队列规则是指在给定队列中的一组元素的情况下,确定下一个弹出队列的元素的规则。先进先出声明的是下一个弹出的元素应该是等待时间最长的元素。

而在Java 5 中添加了 PriorityQueue 优先级队列,可以声明下一个弹出的元素是最需要的元素(具有最高的优先级)。

当在 PriorityQueue 上调用 offer() 方法来插入一个对象时,该对象会在队列中被排序。默认的排序使用队列中对象的自然顺序,但是可以通过提供自己的 Comparator 来修改这个顺序。PriorityQueue 确保在调用 peek() , poll() 或 remove() 方法时,获得的元素将是队列中优先级最高的元素。

参考资料:On Java 8

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值