java集合-Collection接口

本文一共两篇

Collection接口
Map接口

1. Java集合

java集合大致可分为Set、List和Map三种体系。

  • 其中Set代表无序、不可重复的集合;
  • List代表有序、可重复的集合;
  • 而Map则代表具有映射关系的集合。

Java 5之后,增加了Queue体系集合,代表一种队列集合实现。

顶层接口分别是Collection和Map。但是这2个接口都不能直接被实现使用,分别代表两种不同类型的容器。

简单来看,Collection代表的是单个元素对象的序列,(可以有序/无序,可重复/不可重复 等,具体依据具体的子接口Set,List,Queue等);Map代表的是“键值对”对象的集合(同样可以有序/无序 等依据具体实现)
在这里插入图片描述

2. Collection接口

2.1 Collection介绍

Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。一些Collection允许相同的元素而另一些不行。一些能排序而另一些不行。Java SDK不提供直接继承自Collection的类,Java SDK提供的类都是继承自Collection的子接口,如List和Set。

根据Java官方文档对Collection的解释

The root interface in the collection hierarchy. A collection represents a group of objects, known as its elements. Some collections allow duplicate elements and others do not. Some are ordered and others unordered. The JDK does not provide any direct implementations of this interface: it provides implementations of more specific subinterfaces like Set and List. This interface is typically used to pass collections around and manipulate them where maximum generality is desired.

大概意思就是

是容器继承关系中的顶层接口。是一组对象元素组。有些容器允许重复元素有的不允许,有些有序有些无序。 JDK不直接提供对于这个接口的实现,但是提供继承与该接口的子接口比如 List Set。这个接口的设计目的是希望能最大程度抽象出元素的操作。

接口定义:


public interface Collection<E> extends Iterable<E> {
	 // Query Operations
	 ...
} 
   

Iterable类

public interface Iterable<T> {
    
    Iterator<T> iterator();

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
 
    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}
2.2 Collection接口继承树

在这里插入图片描述

2.3 Collection的方法

Collection接口是Set、List和Queue接口的父接口。

基本操作包括:

add(Object o):增加元素
  
addAll(Collection c)...

clear()...

contains(Object o):是否包含指定元素

containsAll(Collection c):是否包含集合c中的所有元素

iterator():返回Iterator对象,用于遍历集合中的元素

remove(Object o):移除元素

removeAll(Collection c):相当于减集合c

retainAll(Collection c):相当于求与c的交集

size():返回元素个数

toArray():把集合转换为一个数组

所有实现Collection接口的类都必须提供两个标准的构造函数:

无参数的构造函数用于创建一个空的Collection,有一个Collection参数的构造函数用于创建一个新的Collection,这个新的Collection与传入的Collection有相同的元素。后一个构造函数允许用户复制一个Collection。

如何遍历Collection中的每一个元素?

不论Collection的实际类型如何,它都支持一个iterator()的方法,该方法返回一个迭代子,使用该迭代子即可逐一访问Collection中每一个元素。典型的用法如下:

Iterator it = collection.iterator(); // 获得一个迭代子 
while(it.hasNext()) { 
	Object obj = it.next(); // 得到下一个元素 
} 

子类介绍如下:

3. Set子接口

Set集合不允许包含相同的元素,而判断两个对象是否相同则是根据equals方法。

3.1 HashSet类

HashSet类是Set接口的典型实现类。特点:

不能保证元素的排列顺序,加入的元素要特别注意hashCode()方法的实现。
HashSet不是同步的,多线程访问同一步HashSet对象时,需要手工同步。
集合元素值可以是null。

HashSet的核心概念。Java文档中描述

This class implements the Set interface, backed by a hash table (actually a HashMap instance). It makes no guarantees as to the iteration order of the set; in particular, it does not guarantee that the order will remain constant over time. This class permits the null element.

大概意思是

HashSet实现了Set接口,基于HashMap进行存储。遍历时不保证顺序,并且不保证下次遍历的顺序和之前一样。HashSet中允许null元素。
进入到HashSet源码中我们发现,所有数据存储在

private transient HashMap<E,Object> map;

private static final Object PRESENT = new Object();

意思就是HashSet的集合其实就是HashMap的key的集合,然后HashMap的val默认都是PRESENT。HashMap的定义即是key不重复的集合。使用HashMap实现,这样HashSet就不需要再实现一遍。

所以所有的add,remove等操作其实都是HashMap的add、remove操作。遍历操作其实就是HashMap的keySet的遍历,举例如下

...
public Iterator<E> iterator() {
    return map.keySet().iterator();
}

public boolean contains(Object o) {
    return map.containsKey(o);
}

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

public void clear() {
    map.clear();
}
...
3.2 LinkedHashSet类

LinkedHashSet的核心概念相对于HashSet来说就是一个可以保持顺序的Set集合。HashSet是无序的,LinkedHashSet会根据add,remove这些操作的顺序在遍历时返回固定的集合顺序。这个顺序不是元素的大小顺序,而是可以保证2次遍历的顺序是一样的。
类似HashSet基于HashMap的源码实现,LinkedHashSet的数据结构是基于LinkedHashMap。

与HashSet相比,特点:

对集合迭代时,按增加顺序返回元素。
性能略低于HashSet,因为需要维护元素的插入顺序。但迭代访问元素时会有好性能,因为它采用链表维护内部顺序。

3.3 SortedSet接口及TreeSet实现类

TreeSet类是SortedSet接口的实现类。
TreeSet即是一组有次序的集合,如果没有指定排序规则Comparator,则会按照自然排序。(自然排序即e1.compareTo(e2) == 0作为比较)
注意:TreeSet内的元素必须实现Comparable接口。
TreeSet源码的算法即基于TreeMap,具体算法在说明TreeMap的时候进行解释。
因为需要排序,所以性能肯定差于HashSet。与HashSet相比,额外增加的方法有:

first():返回第一个元素

last():返回最后一个元素

lower(Object o):返回指定元素之前的元素

higher(Obect o):返回指定元素之后的元素

subSet(fromElement, toElement):返回子集合

可以定义比较器(Comparator)来实现自定义的排序。默认自然升序排序。

3.4 EnumSet类

EnumSet类是专为枚举类设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值。《Effective Java》第32条,用EnumSet代替位域,示范:


// EnumSet - a modern replacement for bit fields - Page 160
import java.util.*;

public class Text {
    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }

    // Any Set could be passed in, but EnumSet is clearly best
    public void applyStyles(Set<Style> styles) {
        // Body goes here
    }

    // Sample use
    public static void main(String[] args) {
        Text text = new Text();
        text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
    }
}

4. List子接口

Java文档中介绍

An ordered collection (also known as a sequence). The user of this interface has precise control over where in the list each element is inserted. The user can access elements by their integer index (position in the list), and search for elements in the list.
Unlike sets, lists typically allow duplicate elements. More formally, lists typically allow pairs of elements e1 and e2 such that e1.equals(e2), and they typically allow multiple null elements if they allow null elements at all. It is not inconceivable that someone might wish to implement a list that prohibits duplicates, by throwing runtime exceptions when the user attempts to insert them, but we expect this usage to be rare.

The List interface provides a special iterator, called a ListIterator, that allows element insertion and replacement, and bidirectional access in addition to the normal operations that the Iterator interface provides. A method is provided to obtain a list iterator that starts at a specified position in the list.

大概意思是

一个有序的Collection(或者叫做序列)。使用这个接口可以精确掌控元素的插入,还可以根据index获取相应位置的元素。
不像Set,list允许重复元素的插入。有人希望自己实现一个list,禁止重复元素,并且在重复元素插入的时候抛出异常,但是我们不建议这么做。

List提供了一种特殊的iterator遍历器,叫做ListIterator。这种遍历器允许遍历时插入,替换,删除,双向访问。 并且还有一个重载方法允许从一个指定位置开始遍历。
然后我们再看下List接口新增的接口,会发现add,get这些都多了index参数,说明在原来Collection的基础上,List是一个可以指定索引,有序的容器。在这注意以下添加的2个新Iteractor方法。

ListIterator<E> listIterator();

ListIterator<E> listIterator(int index);

我们再看ListIterator的代码

public interface ListIterator<E> extends Iterator<E> {
    // Query Operations
    
    boolean hasNext();

    E next();

    boolean hasPrevious();

    E previous();

    int previousIndex();

    void remove();

    void set(E e);

    void add(E e);
}

List子接口是有序集合,所以与Set相比,增加了与索引位置相关的操作,前面说的Iterator只能对容器进行向前遍历,而ListIterator则继承了Iterator的思想,并提供了对List进行双向遍历的方法。

List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。

add(int index, Object o):在指定位置插入元素

addAll(int index, Collection c)...

get(int index):取得指定位置元素

indexOf(Obejct o):返回对象o在集合中第一次出现的位置

lastIndexOf(Object o)...

remove(int index):删除并返回指定位置的元素

set(int index, Object o):替换指定位置元素

subList(int fromIndex, int endIndex):返回子集合
4.1 ArrayList和Vector实现类

这两个类都是基于数组实现的List类。
ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。
size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性,可以插入null。

然后我们来简单看下ArrayList源码实现。这里只写部分源码分析。
所有元素都是保存在一个Object数组中,然后通过size控制长度。

transient Object[] elementData;

private int size;

这时候看下add的代码分析

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

其实在每次add的时候会判断数据长度,如果不够的话会调用Arrays.copyOf,复制一份更长的数组,并把前面的数据放进去。我们再看下remove的代码是如何实现的。

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

其实就是直接使用System.arraycopy把需要删除index后面的都往前移一位然后再把最后一个去掉。

Vector提供一个子类Stack,可以挺方便的模拟“栈”这种数据结构(LIFO,后进先出)。
从内部实现机制来讲ArrayList和Vector都是使用数组(Array)来控制集合中的对象。当你向这两种类型中增加元素的时候,如果元素的数目超出了内部数组目前的长度它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%,所以最后你获得的这个集合所占的空间总是比你实际需要的要大。所以如果你要在集合中保存大量的数据那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小来避免不必要的资源开销。

结论: 不推荐使用Vector类,即使需要考虑同步,即也可以通过其它方法实现。同样我们也可以通过ArrayDeque类或LinkedList类实现“栈”的相关功能。所以Vector与子类Stack,建议放进历史吧。

4.2 LinkedList类

LinkedList是一个链表维护的序列容器。和ArrayList都是序列容器,一个使用数组存储,一个使用链表存储。

数组和链表2种数据结构的对比:

1.查找方面。数组的效率更高,可以直接索引出查找,而链表必须从头查找。
2.插入删除方面。特别是在中间进行插入删除,这时候链表体现出了极大的便利性,只需要在插入或者删除的地方断掉链然后插入或者移除元素,然后再将前后链重新组装,但是数组必须重新复制一份将所有数据后移或者前移。
3.在内存申请方面,当数组达到初始的申请长度后,需要重新申请一个更大的数组然后把数据迁移过去才行。而链表只需要动态创建即可。

如上LinkedList和ArrayList的区别也就在此。根据使用场景选择更加适合的List。
下面简单展示LinkedList的部分源码解析。
首先是链表的节点的定义,非常简单的一个双向链表。

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;
    }
}

然后每个LinkedList中会持有链表的头指针和尾指针

transient int size = 0;

transient Node<E> first;

transient Node<E> last;

列举最基本的插入和删除的链表操作

private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
    
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    final Node<E> prev = l.prev;
    l.item = null;
    l.prev = null; // help GC
    last = prev;
    if (prev == null)
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}

E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

上面6个方法就是链表的核心,头尾中间插入,头尾中间删除。其他对外的调用都是围绕这几个方法进行操作的

LinkedList实现了List接口,允许null元素。此外LinkedList还实现了Deque接口,Deque接口是继承Queue的,提供额外的get,remove,insert方法在LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。

注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:
List list =Collections.synchronizedList(new LinkedList(…));

另外还有固定长度的List:Arrays工具类的方法asList(Object… a)可以将数组转换为List集合,它是Arrays内部类ArrayList的实例,特点是不可以增加元素,也不可以删除元素。

5. Queue子接口

Queue用于模拟队列这种数据结构,实现“FIFO”等数据结构。通常,队列不允许随机访问队列中的元素。

Queue 接口并未定义阻塞队列的方法,而这在并发编程中是很常见的。BlockingQueue 接口定义了那些等待元素出现或等待队列中有可用空间的方法,这些方法扩展了此接口。

Queue 实现通常不允许插入 null 元素,尽管某些实现(如 LinkedList)并不禁止插入 null。即使在允许 null 的实现中,也不应该将 null 插入到 Queue 中,因为 null 也用作 poll 方法的一个特殊返回值,表明队列不包含元素。

基本操作:

boolean add(E e) : 将元素加入到队尾,不建议使用

boolean offer(E e): 将指定的元素插入此队列(如果立即可行且不会违反容量限制),当使用有容量限制的队列时,此方法通常要优于 add(E),后者可能无法插入元素,而只是抛出一个异常。推荐使用此方法取代add

E remove(): 获取头部元素并且删除元素,不建议使用

E poll(): 获取头部元素并且删除元素,队列为空返回null;推荐使用此方法取代remove

E element(): 获取但是不移除此队列的头

E peek(): 获取队列头部元素却不删除元素,队列为空返回null

@Test
public void testQueue() {
      Queue<String> queue = new LinkedList<String>();
      queue.offer("1.你在哪儿?");
      queue.offer("2.我在这里。");
      queue.offer("3.那你又在哪儿呢?");
      String str = null;
      while ((str = queue.poll()) != null) {
          System.out.println(str);
      }
}

打印结果:

1.你在哪儿?
2.我在这里。
3.那你又在哪儿呢?
5.1 PriorityQueue类

Java中PriorityQueue实现了Queue接口,不允许放入null元素;其通过堆实现,具体说是通过完全二叉树(complete binary tree)实现的小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue的底层实现。当调用peek()或者是poll()方法时,返回的是队列中最小的元素。当然你可以与TreeSet一样,可以自定义排序。自定义排序的一个示范:

    @Test
    public void testPriorityQueue() {
        PriorityQueue<Integer> pq = new PriorityQueue<Integer>(20, new Comparator<Integer>() {
            public int compare(Integer i, Integer j) {
                // 对数字进行奇偶分类,然后比较返回;偶数有较低的返回值(对2取余数然后相减),奇数直接相减。
                int result = i % 2 - j % 2;
                if (result == 0)
                    result = i - j;
                return result;
            }
        });

        // 倒序插入测试数据
        for (int i = 0; i < 20; i++) {
            pq.offer(20 - i);
        }

        // 打印结果,偶数因为有较低的值,所以排在前面
        for (int i = 0; i < 20; i++) {
            System.out.println(pq.poll());
        }
    }

输出:

2,4,6,8,10,12,14,16,18,20,1,3,5,7,9,11,13,15,17,19,
5.2 Deque子接口与ArrayDeque类

Deque代表一个双端队列,可以当作一个双端队列使用,也可以当作“栈”来使用,因为它包含出栈pop()与入栈push()方法。

ArrayDeque类为Deque的实现类,数组方式实现。方法有:

addFirst(Object o):元素增加至队列开头

addLast(Object o):元素增加至队列末尾

poolFirst():获取并删除队列第一个元素,队列为空返回null

poolLast():获取并删除队列最后一个元素,队列为空返回null

pop():“栈”方法,出栈,相当于removeFirst()

push(Object o):“栈”方法,入栈,相当于addFirst()

removeFirst():获取并删除队列第一个元素

removeLast():获取并删除队列最后一个元素

代码:

ArrayDeque stack=new ArrayDeque();
stack.push("jim");
stack.push("tom");
stack.push("lulu");
//栈先进后出,所以输出结果是反向的
System.out.println(stack);
System.out.println(stack.peek());
System.out.println(stack);
System.out.println(stack.pop());
System.out.println(stack);

打印结果:

[lulu, tom, jim]
lulu
[lulu, tom, jim]
lulu
[tom, jim]
5.3 实现List接口与Deque接口的LinkedList类

LinkedList类是List接口的实现类,同时它也实现了Deque接口。因此它也可以当做一个双端队列来用,也可以当作“栈”来使用。并且,它是以链表的形式来实现的,这样的结果是它的随机访问集合中的元素时性能较差,但插入与删除操作性能非常出色。

6. 各种线性表选择策略

数组:是以一段连续内存保存数据的;随机访问是最快的,但不支持插入、删除、迭代等操作。
ArrayList与ArrayDeque:以数组实现;随机访问速度还行,插入、删除、迭代操作速度一般;线程不安全。
Vector:以数组实现;随机访问速度一般,插入、删除、迭代速度不太好;线程安全的。
LinkedList:以链表实现;随机访问速度不太好,插入、删除、迭代速度非常快。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值