Java 基础回顾-5:容器类

本文深入探讨Java中的容器类,包括Collection和Map两大类,并详细解析ArrayList、LinkedList、HashSet、TreeSet及HashMap等核心容器的实现原理与应用场景。

0、基础

数组的效率比容器高,但是我们应该优先选择容器而不是数组,只有在已证明性能成为问题时,才应该将程序重构为使用数组。

1、容器的框架

下面是 Java 中的容器框架的基础关系图,

Java 容器框架的继承关系

根据容器的存储方式,我们可以将 Java 容器类划分成下面两个大类:

  1. Collection:一组独立元素的列表,它有三个子类型 List、Set 和 Queue. 它们之间的区别是 List 必须按照插入的顺序保存元素,Set 不能有重复元素,Queue 是队列性质的列表。
  2. Map:表示存储的是键值对类型的对象。

此外,还有两个工具类可以使用:ArraysCollections. 前者提供了适用于数组类型的一系列静态工具方法,后者提供了许多适用于各种类型的静态工具方法。

2、Collection

顾名思义,它的作用效果与数学中的 “集合” 的概念是相似的。

这里给出 Java 中 Collection 接口的定义。Collection 作为框架中顶层的基接口,规定了其实现类应该实现哪些功能,

	public interface Collection<E> extends Iterable<E> {
	    int size();
	    boolean isEmpty();
	    boolean contains(Object o);
	    Iterator<E> iterator();
	
	    // 返回列表元素组成的数组
	    Object[] toArray();
	
	    // 返回列表元素组成的数组,不同的是它可以根据传入的泛型数组,返回指定类型的数组
	    <T> T[] toArray(T[] a);
	
	    boolean add(E e);
	    boolean remove(Object o);
	    boolean containsAll(Collection<?> c);
	    boolean addAll(Collection<? extends E> c);
	    boolean removeAll(Collection<?> c);
	
	    // 只保留该集合中处于c中的元素,即只保留当前集合与c的交集元素
	    boolean retainAll(Collection<?> c);
	
	    void clear();
	}

2.1 List

List 在 Collection 的基础之上又增加了几个方法。从下面的方法的定义中,我们可以看出这些方法是针对列表类型的容器定义的方法:

    boolean addAll(int index, Collection<? extends E> c);
    E get(int index);
    E set(int index, E element);
    void add(int index, E element);
    E remove(int index);
    int indexOf(Object o);
    int lastIndexOf(Object o);
    ListIterator<E> listIterator();
    ListIterator<E> listIterator(int index);
    List<E> subList(int fromIndex, int toIndex);

这里的前面的几个方法从字面意思中就可以看出大致的作用。后面的 listIterator() 方法的作用是返回一个迭代器,这个迭代器不仅能向后遍历,还能向前遍历;而 subList() 的作用是返回容器中元素的一个视图(如果在视图中添加元素,添加的元素也会反应到原来的容器上)。

subList 视图实现插入元素的具体方式是(以 ArrayList 为例):在创建 SubList(通常继承自 AbstractList,作为一个 List 实现)的时候,将外部类作为参数传入到 Sublist 的构造方法中,然后当用户调用视图的添加元素的方法的时候,它调用外部类 ArrayListadd() 方法,同时将元素添加到外部类容器中。(装饰器模式)

List 的一个顶层实现是 AbstractList,它是一个抽象的类,为我们实现了 List 中定义的一些方法。我们熟知的 ArrayList 和 LinkedList 是在它的基础之上拓展的。

2.1.1 ArrayList

它内部是基于 动态数组 来实现的,所以它具有数组的一些特性。比如,访问固定位置的某一元素效率比较高,而对容器中的元素查找效率比较低(需要遍历数组进行查找)。

默认的初始容量是10,每次加入一个新的元素的时候,会将加入元素之前的容量与新加入的元素数量的和与加入元素之前的容量的 3/2 进行对比,选择较大的一个作为将要拓充到的容量。具体的调整数组大小的操作是由 System.arrayCopy() 来实现的。

至于在 ArrayList 中的删除操作,也是先将元素置为 null,之后使用动态 System.arrayCopy() 来调整数组的大小。

在 Arrays 中还有一个 ArrayList 的实现 Arrays.ArrayList,它跟这里的 ArrayList 是不同的。虽然后者也继承了 AbstractList,但是它在实现的时候没有实现 add() 等方法,所以是不能向其中添加新元素的。(不可变)

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    private static final int DEFAULT_CAPACITY = 10;

    private static final Object[] EMPTY_ELEMENTDATA = {};

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    transient Object[] elementData; // 数据
    
    private int size;

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
        }
    }

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    // ...
}
2.1.2 LinkedList

它内部是使用双向链表来实现的,所以它的一些操作和双向链表逻辑相同。比如查找某个元素的时候需要对链表进行遍历,而插入元素的时候效率比较高。

我们可以很容易地将它改造成队列、栈或双端队列。

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

    transient int size = 0; // 链表大小

    transient Node<E> first; // 链表尾

    transient Node<E> last; // 链表头

    // 结点类
    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;
        }
    }

    // ...
}
2.1.3 Stack

队列数据结构,我们可以使用 LinkedList 来实现一个队列,但是在 Java 提供的 API 中,队列是继承自 Vector 的,而 Vector 内部使用数组来实现。这就导致了 Stack 在性能上,相比于使用链表的方式还有值得提升的地方。

public class Stack<E> extends Vector<E> {

    public Stack() {
    }

    public E push(E item) {
        addElement(item);
        return item;
    }

    public synchronized E pop() {
        E       obj;
        int     len = size();
        obj = peek();
        removeElementAt(len - 1);
        return obj;
    }

    public synchronized E peek() {
        int     len = size();
        if (len == 0) throw new EmptyStackException();
        return elementAt(len - 1);
    }

    // ...
}

Stack 与 Vector 类似,是线程安全的,实现线程安全的方式是对所有的 API 方法进行加锁。虽然可以在多线程环境中运行,但是非多线程环境下效率比较低。

2.2 Set

它不允许插入两个重复的元素。

Java 中的 Set 并没有在 Collection 的基础之上再增加新的方法。它的一个抽象基类的实现是 AbstractSet,不过在 AbstractSet 中并没有实现太多的方法。

它有几个重要的实现:

  1. TreeSet 将元素存储在红黑树数据结构中;
  2. HashSet 使用的是散列函数;
  3. LinkedHashList 使用了散列,但是看起来它使用了链表来维护元素的插入顺序。
2.2.1 HashSet

内部是借助于 HashMap 来实现的:当向 HashSet 中插入一个元素的时候,我们将插入的元素作为 HashMap 的键,一个默认的 Object 作为值插入到 HashMap 中。因为 HashMap 中不会有两个相同的键,所以在 HashSet 中就不会有两个相同的值。

HashSet 没有提供普通的 get() 方法,我们只能对其进行遍历。它在遍历的时候当然返回的是HashMap 的键的迭代器。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
    static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map;

    private static final Object PRESENT = new Object();

    public HashSet() {
        map = new HashMap<>();
    }

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

    public boolean remove(Object o) {
        return map.remove(o) == PRESENT;
    }

    // ...
}
2.2.2 TreeSet

与 HashSet 类似,它内部使用了 TreeMap 来实现自己的功能。

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable {
    private transient NavigableMap<E,Object> m;

    private static final Object PRESENT = new Object();

    TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }

    public TreeSet() {
        this(new TreeMap<E,Object>());
    }

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

    public boolean remove(Object o) {
        return m.remove(o) == PRESENT;
    }

    // ...
}

3、Map

与 Collection 相似,Map 是所有的基于键值对的容器的顶层接口。下面是它内部定义的一些方法:

	public interface Map<K,V> {
	    int size();
	    boolean isEmpty();
	    boolean containsKey(Object key);
	    boolean containsValue(Object value);
	    V get(Object key);
	    V put(K key, V value);
	    V remove(Object key);
	    void putAll(Map<? extends K, ? extends V> m);
	    void clear();
	    Set<K> keySet();
	    Collection<V> values();
	    Set<Map.Entry<K, V>> entrySet();
	    interface Entry<K,V> {
	        K getKey();
	        V getValue();
	        V setValue(V value);
	        boolean equals(Object o);
	        int hashCode();
	        public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
	            return (Comparator<Map.Entry<K, V>> & Serializable)
	                (c1, c2) -> c1.getKey().compareTo(c2.getKey());
	        }
	        public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
	            return (Comparator<Map.Entry<K, V>> & Serializable)
	                (c1, c2) -> c1.getValue().compareTo(c2.getValue());
	        }
	        public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
	            Objects.requireNonNull(cmp);
	            return (Comparator<Map.Entry<K, V>> & Serializable)
	                (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
	        }
	        public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
	            Objects.requireNonNull(cmp);
	            return (Comparator<Map.Entry<K, V>> & Serializable)
	                (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
	        }
	    }
	    boolean equals(Object o);
	    int hashCode();
	}

从上面我们可以看出,在 Map 接口内部,除了定义了一些常用的操作方法之外,还定义了 Map 中键值对的接口 Entry.

3.1 HashMap

因为 HashMap 的源码比较重要,而且需要很长的篇幅来进行分析,我们将其作为一个单独的文章:《Java 基础回顾-6:HashMap 源码分析》

3.2 HashTable

相比于HashMap,HashTable的实现就显得简单得多。它内部同样自己定义了一个结点:

    private static class Entry<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Entry<K,V> next;
    }

而且也是采用了基于拉链法的碰撞处理机制。它定义了一个

    private transient Entry<?,?>[] table;

下面是它的根据哈希码计算数组的索引的方法:

    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;

虽然,当插入的元素的数量超过指定的阈值的时候,它也会重新调整数组的大小。但是它的插入操作中是不存在将链表改变成红黑树的优化的。并且这里直接使用了取余的方式获取索引,不存在扰动,从而也不会强制要求容量必须为2的整数次幂。

因为它的散列的均匀性没有 HashMap 调节得那么好,所以它的性能可能会比 HashMap 要差一些。而且,它的每个方法上面都加了 sychronized 关键字进行修饰,这样虽然保证了在多线程环境中的数据一致性,但是,在非多线程环境中,无疑是一种不必要的开销。

4、同步容器

4.1 Collections.synchronizedXXX() 方法

该方法用来对指定的集合进行装饰,它使用了装饰者模式,我们看下面的一个例子

    Map m = Collections.synchronizedMap(new HashMap(...));

这里使用了一个匿名的 HashMap,因为 HashMap 不是线程安全的,但是这封装了之后返回的集合就是线程安全的了。下面我们看该 API 究竟做了什么操作:

它返回了 SynchronizedMap:

    public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
        return new SynchronizedMap<>(m);
    }

该 Map 的定义是

    private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {
        private final Map<K,V> m;     // Backing Map
        final Object      mutex;        // Object on which to synchronize

        SynchronizedMap(Map<K,V> m) {
            this.m = Objects.requireNonNull(m);
            mutex = this;
        }

        public V get(Object key) {
            synchronized (mutex) {return m.get(key);}
        }

        public V put(K key, V value) {
            synchronized (mutex) {return m.put(key, value);}
        }

        // ...
    }

在上面的代码中,我们只给出了它的一部分内容的定义,从上面我们就很容易地看出来。它实际上就是对我们定义的 Map 进行了一层包装,我们称其为装饰者模式。它的锁不是加在每个方法上面的,而是使用了粒度更小的私有锁——对每个操作,对一个全局的实例进行加锁。这样能够保证每个 put()get() 方法本身是原子的和线程安全的,而实际的值的获取和插入,还是对原始的容器进行操作的。


Java 基础回顾系列文章,给你的 Java 查缺补漏:

本系列以及其他系列的文章均维护在 Github 上面:Github / Awesome-Java,欢迎 Star & Fork. 如果你喜欢这篇文章,愿意支持作者的工作,请为这篇文章点个赞?!

<think>好的,我现在需要帮助用户解决Maven依赖树的问题以及依赖冲突。用户之前提到他们使用的是Tomcat 8.5.84,Java 1.8,Maven 3.6.1,并且遇到了java.lang.AbstractMethodError,这可能由依赖冲突引起。现在用户想查看Maven依赖树并解决可能的冲突。 首先,我应该回顾用户之前的问题和解决方案。用户之前得到关于检查JAX-RS依赖一致性、验证Tomcat类加载机制、升级依赖和清理本地仓库的建议。现在用户更具体地询问如何解析依赖树和解决冲突,特别是提到引用[1]中的Maven依赖冲突说明,即同一groupId和artifactId下只能使用一个版本。 用户需要知道如何生成和分析依赖树。根据Maven的知识,可以使用mvn dependency:tree命令。但用户可能还需要知道如何过滤特定的依赖,例如排除不需要的库或者找到冲突的来源。 接下来,解决依赖冲突的常用方法包括排除传递性依赖、统一版本号等。用户可能需要具体的例子,比如在pom.xml中如何写exclusion标签,或者如何用dependencyManagement来统一版本。引用[1]中的例子显示,如果有多个版本,Maven会选择最近的定义,但用户可能需要明确排除旧版本。 另外,用户的环境是Maven 3.6.1,可能需要确认该版本的Maven是否与其他工具(如IDEA)兼容,尤其是在生成依赖树时是否存在已知问题。比如引用[2]提到的编译错误可能与插件版本有关,但用户现在的问题是关于依赖树,所以可能需要确保插件版本正确,或者检查是否有其他因素导致依赖解析错误。 用户提到Tomcat 8.5.84,可能需要考虑Tomcat自带的库是否与项目依赖冲突。例如,Tomcat可能包含旧的servlet-api,导致与应用中的版本冲突。这时候需要确保scope为provided,或者在依赖中排除相关库。 此外,用户可能对如何解读依赖树的输出不太清楚,需要指导如何识别冲突,例如同一个库的不同版本出现在不同的路径下,或者存在多个不同版本的相同依赖。这时候需要解释Maven的依赖调解机制,如最短路径优先和最先声明优先。 还要考虑用户是否使用了IDE(如IntelliJ IDEA),是否可以通过IDE的图形化工具查看依赖树,这可能比命令行更直观。但用户明确要求使用Maven命令,所以应重点介绍命令行方法,同时可能提到IDE的辅助功能。 最后,确保回答结构清晰,分步骤说明如何生成依赖树、分析输出、解决冲突,并提供示例代码和命令,以及注意事项,比如清理本地仓库、更新项目等。同时,相关问题要引导用户进一步了解相关主题,比如Maven插件配置、依赖管理最佳实践等。</think>### 解决方案:Maven 依赖树解析与冲突解决 针对 **Tomcat 8.5.84 + Java 1.8 + Maven 3.6.1** 环境,依赖冲突可通过以下步骤定位和修复: --- #### 一、生成依赖树并分析 1. **命令行生成依赖树** 在项目根目录执行: ```bash mvn dependency:tree > dependency_tree.txt ``` 生成的 `dependency_tree.txt` 会展示 **所有依赖的层级关系**,例如: ``` [INFO] com.example:demo:jar:1.0 [INFO] +- org.glassfish.jersey.core:jersey-server:jar:2.34:compile [INFO] | \- javax.ws.rs:javax.ws.rs-api:jar:2.1.1:compile [INFO] \- org.apache.commons:commons-math3:jar:3.6.1:compile ``` 2. **过滤关键依赖** 使用 `-Dincludes` 参数缩小范围(支持通配符): ```bash mvn dependency:tree -Dincludes=javax.ws.rs*,org.glassfish.jersey* ``` 输出示例: ``` [INFO] +- javax.ws.rs:javax.ws.rs-api:jar:2.1.1:compile [INFO] \- org.glassfish.jersey.core:jersey-server:jar:2.34:compile ``` 3. **识别冲突标志** 查找以下模式: - 同一依赖的不同版本(如 `javax.ws.rs-api:2.1.1` 和 `javax.ws.rs-api:2.0.1`) - 不同路径引入的重复依赖(如通过 A 依赖 B 间接引入,同时直接声明 B) --- #### 二、依赖冲突解决方法 1. **排除传递性依赖** 在 `pom.xml` 中通过 `<exclusions>` 移除冲突的依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.glassfish.jersey.core</groupId> <artifactId>jersey-server</artifactId> </exclusion> </exclusions> </dependency> ``` 2. **统一版本号** 使用 `<dependencyManagement>` 强制指定版本(覆盖其他传递性依赖): ```xml <dependencyManagement> <dependencies> <dependency> <groupId>javax.ws.rs</groupId> <artifactId>javax.ws.rs-api</artifactId> <version>2.1.1</version> </dependency> </dependencies> </dependencyManagement> ``` 3. **处理 Tomcat 容器冲突** Tomcat 8.5 自带 `servlet-api` 等库,需确保项目依赖中相关库的 `<scope>` 为 `provided`: ```xml <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> ``` --- #### 三、验证与清理 1. **更新本地仓库** 清理旧版本缓存并重新下载: ```bash mvn clean install -U ``` 2. **检查最终依赖树** 确保冲突已解决: ```bash mvn dependency:tree -Dincludes=冲突的groupId:artifactId ``` --- ### 总结 依赖冲突的核心是 **版本不一致** 或 **传递性依赖污染**。通过分析依赖树定位问题,结合排除法或版本管理统一依赖[^1]。对于 Tomcat 环境,特别注意容器自带库与应用依赖的隔离(通过 `<scope>` 配置)[^2]。 --- ### 相关问题 1. 如何通过 Maven 插件可视化依赖关系? 2. `<dependencyManagement>` 和 `<dependencies>` 的区别是什么? 3. Tomcat 如何管理多个应用的类加载隔离?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值