Java集合(上)

本文详细探讨了Java集合框架的设计原则,包括接口与实现的分离,重点介绍了队列、链表、散列表等数据结构的实现,以及ArrayList、LinkedList、HashSet、TreeSet等集合类的特点与应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java 集合框架

将集合的接口与实现分离

Java集合类库将接口(interface)实现(implementation)分离。
以队列接口为例,队列接口需要实现的有:

  1. 在头部删除元素
  2. 在尾部增加元素
  3. 查找队列中元素的个数

所以队列接口的最简单形式可能类似下面这样:

java.util.Queue

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Queue<E> extends Collection<E> {
    boolean add(E var1);

    boolean offer(E var1);

    E remove();

    E poll();

    E element();

    E peek();
}

Queue接口规定了队列的基本功能,具体实现有两种方式:循环数组和链表,可以通过实现Queue接口创建队列。
Java类库中,需要循环数组可以使用ArrayDeque类,需要链表队列可以使用LinkedList类。
一般将集合的引用赋值给接口类型:
Queue<Customer> expressLane = new CircularArrayQueue<>(100)

Java类库中还有一个AbstractQueue类,是在Queue上的进一步封装,当我们需要编写自己实现的队列时,继承AbstractQueue会比实现Queue接口好得多
java.util.AbstractQueue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public abstract class AbstractQueue<E> extends AbstractCollection<E> implements Queue<E> {
    protected AbstractQueue() {
    }

    public boolean add(E var1) {
        if (this.offer(var1)) {
            return true;
        } else {
            throw new IllegalStateException("Queue full");
        }
    }

    public E remove() {
        Object var1 = this.poll();
        if (var1 != null) {
            return var1;
        } else {
            throw new NoSuchElementException();
        }
    }

    public E element() {
        Object var1 = this.peek();
        if (var1 != null) {
            return var1;
        } else {
            throw new NoSuchElementException();
        }
    }

    public void clear() {
        while(this.poll() != null) {
        }

    }

    public boolean addAll(Collection<? extends E> var1) {
        if (var1 == null) {
            throw new NullPointerException();
        } else if (var1 == this) {
            throw new IllegalArgumentException();
        } else {
            boolean var2 = false;
            Iterator var3 = var1.iterator();

            while(var3.hasNext()) {
                Object var4 = var3.next();
                if (this.add(var4)) {
                    var2 = true;
                }
            }

            return var2;
        }
    }
}

Collection接口

Java类库中集合类的基本接口时Collection接口,这个接口有两个基本方法:

1
2
3
4
5

public interface Collection<E> extends Iterable<E> {
    boolean add(E var1);
    Iterator<E> iterator();
}

iterator()方法 返回一个已经实现的迭代器,可以遍历集合。

迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface Iterator<E> {
    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    default void forEachRemaining(Consumer<? super E> var1) {
        Objects.requireNonNull(var1);

        while(this.hasNext()) {
            var1.accept(this.next());
        }

    }
}

next方法获取下个元素,如果没有下个元素则报NoSuchElementException异常,所以在使用next方法前需要使用hasNext()判断。
foreach可以与任何实现了Iterable接口的对象一起工作,Collection接口扩展了Iterable接口。因此,对于标准类库中的的任何集合都可以使用foreach
remove方法会伤处上次调用next方法时返回的元素,不能连续两次使用remove,使用remove前必须先使用next越过需要删除的元素。

集合框架中的接口

Collectionmap是集合的两个基本接口,对集合来说,添加元素只需要给定一个元素类型,所以使用add方法,对于映射来说,则需要Key的类型和Value的类型所以用put方法。
List是有序集合,元素会 被添加到特定位置,具体实现有ArrayListLinkedList,前者是基于数组实现,在遍历时推荐使用整数索引(又称随机访问Random Access),后者基于链表实现,遍历时最好使用迭代器访问。

查看Java类库中ArrayList类的定义,发现:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable

ArrayList类实现了RandomAccess接口,这个接口时标记接口,不含有任何方法,只是用以表明这个类是否支持快速随机访问。

通过:

1
2
3
4
5
if(c instanceof RandomAccess){
    //使用随机访问
}else{
    //使用迭代器访问
}

就可以采用适合的访问方式。

Set接口等同于Collection接口,不过set中的add方法不允许增加重复的元素。源码中Set接口扩展了Collection接口,但是它本身并没有对这些接口做真正的改动,真正做了改动的是AbstractSet类,其中重写了hashCodeequalremoveAll方法.
说下前两个重写,因为集合是无序存储,所以集合的判等应该是其中所有元素都是相同的就视为相等,因此在使用equal时需要处理一下,看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {
    protected AbstractSet() {
    }

    public boolean equals(Object var1) {
        if (var1 == this) {
            return true;
        } else if (!(var1 instanceof Set)) {
            return false;
        } else {
            Collection var2 = (Collection)var1;
            if (var2.size() != this.size()) {
                return false;
            } else {
                try {
                    return this.containsAll(var2);
                } catch (ClassCastException var4) {
                    return false;
                } catch (NullPointerException var5) {
                    return false;
                }
            }
        }
    }

    public int hashCode() {
        int var1 = 0;
        Iterator var2 = this.iterator();

        while(var2.hasNext()) {
            Object var3 = var2.next();
            if (var3 != null) {
                var1 += var3.hashCode();
            }
        }

        return var1;
    }

    public boolean removeAll(Collection<?> var1) {
        Objects.requireNonNull(var1);
        boolean var2 = false;
        Iterator var3;
        if (this.size() > var1.size()) {
            for(var3 = var1.iterator(); var3.hasNext(); var2 |= this.remove(var3.next())) {
            }
        } else {
            var3 = this.iterator();

            while(var3.hasNext()) {
                if (var1.contains(var3.next())) {
                    var3.remove();
                    var2 = true;
                }
            }
        }

        return var2;
    }
}

这里看到Set类的hashCode是所有子元素的hashCode和,此外,equals中,只要是扩展了Set接口的类,就可以通过第一层判定,比如HashSet和TreeSet。
然后是一个类型转换,containAll方法(O(n2))调用。

具体的集合

链表 LinkedList

Java中的链表实际上都是双向链表,使用ListIterator可以访问前驱结点。
调用previous后如果调用了remove就会删除光标迭代器右侧的元素。

我们知道链表中使用整数索引访问的话效率会很低,虽然LinkedList提供了整数索引方法,但是其本质还是从头开始一个个访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LinkedList.Node<E> node(int var1) {
        LinkedList.Node var2;
        int var3;
        if (var1 < this.size >> 1) {
            var2 = this.first;

            for(var3 = 0; var3 < var1; ++var3) {
                var2 = var2.next;
            }

            return var2;
        } else {
            var2 = this.last;

            for(var3 = this.size - 1; var3 > var1; --var3) {
                var2 = var2.prev;
            }

            return var2;
        }
    }

数组列表ArrayList

ArrayList封装了一个动态再分配的对象数组,可以使得随机访问更加的快速。
还有另一个动态数组是Vector类,两者区别是,Vector中所有方法都是同步的,可以让两个线程安全地访问同一个对象。但是对单线程来说,在同步上需要耗费很多时间,所以在单线程或者不需要同步的场景还是使用ArrayList比较好。

散列集

散列集基于散列表实现,散列表为每个对象计算一个HashCode,在自定义类中,如果重写了equals方法,那必须重写HashCode以保证,equals结果为true时,hashcode一定相同。
上面说到AbstractSet的重写,就拿AbstractSet举例:

可以看到set1,set2的hashCode相同,equals结果相同,这是因为,AbstractSet重写了这两个方法,使得对含有相同元素的两个set,返回相同的hashCode,并使得equals结果为True。
但是实际上,set1,set2在堆内存中是两个不同的对象。
如果不重写这两个方法的话,默认采用Object类中的equals和hashCode:

1
2
3
4
5
public native int hashCode();

public boolean equals(Object var1) {
    return this == var1;
}

hashCode为本地方法,由虚拟机为我们生成,应该是根据对象的内存地址生成的HashCode,同时equals用==判断,实际上就是比较两个对象的内存地址是否相同,换言之,是否是同一个对象。
如果不重写的话,那么上面截图中的结果都是false,因为这是两个对象,有不同的内存地址。

那么有个问题是,既然equals结果为true,hashcode一定相同,为什么不再equals方法中直接判断hashcode是否相同呢,原因就是,hashcode有可能冲突,就是不同的两个对象产生了相同的hashCode。
HashCode和equals的结果应该满足如下关系:

  1. equals为true时,hashCode一定相等
  2. hashCode相等时,equals不一定为true

所以在重写equals时,可以先比较hashCode是否相同,如果不相同的话,那就直接返回False,如果相同,那就再执行判断。

散列表

Java中散列表用链表数组实现就是数组+链表的形式。每个列表被成为桶,先对存入元素计算hashcode,然后将这个hashcode插入对应的桶中。
取元素时,根据元素对应的hashcode获得所在桶(随机访问数组),然后遍历桶(迭代器访问链表),这样就很好利用了两者的优点,但是同时,hashcode的计算方法也至关重要,一般会将(Java类库中就是)桶的大小设置为2的幂,因为这样的话,可以使用效率极快的位运算来计算hashcode,而位运算的结果可能性就是2的幂。
通俗的说,散列表的原理和字典目录差不多,假设我们在编排字典,插入一个汉字时,先看这个字的拼音首字母(计算Hashcode),然后根据首字母找到对应的一级目录(桶),将这个字插入到这个一级目录下(插入桶);如果需要查某个字,先看这个字的首字母,根据这个首字母找到对应的一级目录(桶的随机访问),然后在这个以及目录下一个个找到那个字(链表的迭代访问)。
当加入的字过多时,桶的大小可能会变得超出预期,所以我们需要设计好编排规则,除了只看拼音首字母,还可以看之后的若干字母,建立多级目录(也就是建立多级桶)。

HashSet

HashSet是Java类库中一个实现了散列表的集,计算hashCode的依据是存入对象的内存地址。

TreeSet

TreeSet较散列集有所改进,树集是一个有序集合,可以以任意顺序将元素插入到集合中,这个排序基于红黑树实现,将一个元素添加到树集中比添加到散列表中慢。
要使用树集,必须要能够比较元素,这些元素必须实现Comparable接口,或者构造集时必须提供一个Comparator。

队列与双端队列

通过继承Deque接口实现一个双端队列, Java类库中,ArrayDeque和LinkedList都提供了双端队列。

优先队列

优先队列总是按照排序的顺序进行检索,无论什么时候调用remove总是会获取当前优先队列中最小的元素。优先队列基于堆实现,总是可以让最小的元素移动到根。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值