Java 集合

xiaoxiao2021-02-28  103

Java 集合 一.Collection 接口     Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。     Java SDK不提供直接继承自Collection的类,Java SDK提供的类都是继承自Collection的“子接口”如List和Set。       所有实现Collection接口的类都必须提供两个标准的构造函数:         无参数的构造函数用于创建一个空的Collection,         有一个 Collection参数的构造函数用于创建一个新的Collection,     这个新的Collection与传入的Collection有相同的元素。后一个构造函数允许用户复制一个Collection。       如何遍历Collection中的每一个元素?不论Collection的实际类型如何,它都支持一个iterator()的方法,     该方法返回一个迭代子,使用该迭代子即可逐一访问Collection中每一个元素。典型的用法如下:         Iterator it = collection.iterator(); // 获得一个迭代子         while(it.hasNext()) {           Object obj = it.next(); // 得到下一个元素         }       由Collection接口派生的两个接口是List和Set。 二.List 集合     1.List 接口是Collection 接口的一个子类,在Collection 基础上扩充了方法。       同时可以对每个元素插入的位置进行精确的控制,它的主要实现类有 ArrayList,       Vector,LinkedList。     2.ArrayList 实现了 List 接口,意味着可以插入空值,也可以插入重复的值,非同步 ,它是 基于数组 的一个实现。       部分成员变量如下:         private static final int DEFAULT_CAPACITY = 10;          //默认初始值             transient Object[] elementData;                          //存放数据的数组           这里可以看出,elementData 提示了是基于数组的实现, DEFAULT_CAPACITY 指名了默认容量为 10 个。       下面重点理解 add()方法,这将有助于理解 ArrayList 的应用场景和性能消耗:             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()方法,首先通过 ensureCapacityInternal(size + 1);     判断需不需要扩容;然后再进行 elementData[size++] = e。 插入的时间复杂度O(1)。     是非常高效的。(But,这不是在固定位置插入的情况下),如果在固定位置插入,那么代码:     public void add(int index, E element) {         rangeCheckForAdd(index);         ensureCapacityInternal(size + 1);  // Increments modCount!!         System.arraycopy(elementData, index, elementData, index + 1,size - index);         elementData[index] = element;         size++;     }     System.arraycopy(elementData, index, elementData, index + 1, size - index);     这个可以看出,这时候就是比较低效的了。     那么网上很多说 ArrayList 不适合 增删操作非常多的操作,这是怎么回事呢?     首先可以看到这句话: elementData = Arrays.copyOf(elementData, newCapacity);     需要知道的是, Arrays.copyOf 函数的内部实现是再创建一个数组,     然后把旧的数组的值一个个复制到新数组中。当经常增加操作的时候,容量不够的时候,     就会进行上述的扩容操作,这样性能自然就下来了。 或者说,当我们在固定位置进行增删的时候,     都会进行 System.arraycopy(elementData, index, elementData, index + 1, size - index);     也是非常低效的。     分析了低效出现的原因,那么我们就可以知道:如果我们需要经常进行特定位置的增删操作,     那么最好还是不要用这个了,但是,如果我们基本上没有固定位置的增删操作,最好是要预估数据量的大小,     然后再初始化最小容量,这样可以有效的避免扩容。如下代码:     ArrayList<Integer> arrayList = new ArrayList<Integer>(20);     总结:         1.ArrayList 可以插入空值,也可以插入重复值         2.ArrayList 是基于数组的时候,所以很多数组的特性也直接应用到了 ArrayList。         3.ArrayList 的性能消耗主要来源于扩容和固定位置的增删。         4.ArrayList 创建的时候 需要考虑是否要初始化最小容量,以此避免扩容带来的消耗。     3.Vector       上述的 ArrayList 不是线程安全的。那么 Vector 就可以看作是 ArrayList 的一个线程安全版本。       由于也是实现了 List 接口,所以也是 可以插入空值,可以插入重复的值。       它和 HashTable 一样,是属于一种同步容器,而不是一种并发容器。(参考《Java并发编程实战》,       类似CopyOnWriteArrayList,ConcurrentHashMap这种就属于并发容器)       内部成员变量:         protected Object[] elementData;         public Vector() {         this(10);         }     可以看到,也是基于 数组的实现,初始化也是 10 个容量。 那么,再来看看 add()方法是否和 ArrayList 相同。     public synchronized boolean add(E e) {         modCount++;         ensureCapacityHelper(elementCount + 1);         elementData[elementCount++] = e;         return true;     }     可以看到,和 ArrayList 也是一样的,只是加了 synchronized 进行同步,     其实很多其他方法都是通过加 synchronized 来实现同步。     总结:          1.可以插入空值,也可以插入重复值          2.也是基于数组的时候,所以很多数组的特性也直接应用到了 Vector。          3.性能消耗也主要来源于 扩容。          4.创建的时候 需要考虑是否要初始化最小容量,以此避免扩容带来的消耗。     相当于 ArrayList 的线程安全版本,实现同步的方式 是通过 synchronized。     4.LinkedList      LinkedList 实现了 List 接口,所以LinkedList 也可以放入重复的值,也可以放入空值。      LinkedList不支持同步。LinkedList 不同于ArrayList 和Vector,它是使用链表的数据结构,不再是数组。      成员变量:         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;             }         }          那么,在进行 add() 方法的时候:     public boolean add(E e) {             linkLast(e);             return true;         }        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++;         }         当进行增删的时候,只需要改变指针,并不会像数组那样出现整体数据的大规模移动,复制等消耗性能的操作。          ArrayList,Vector,LinkedList 的整体对比     实现方式:         1.ArrayList,Vector 是基于数组的实现。         2.LinkedList 是基于链表的实现。     同步问题:         1.ArrayList,LinkedList 不是线程安全的。         2.Vector 是线程安全的,实现方式是在方法中加 synchronized 进行限定。     适用场景和性能消耗         1.ArrayList 和 Vector 由于是基于数组的实现,所以在固定位置插入,           固定位置删除这方面会直接是 O(n)的时间复杂度,另外可能会出现扩容的问题,这是非常消耗性能的。         2.LinkedList 不会出现扩容的问题,所以比较适合增删的操作。但是由于不是数组的实现,           所以在 定位方面必须使用 遍历的方式,这也会有 O(n)的时间复杂度 三.Set 集合     1.Set接口同样是Collection接口的一个子接口,它表示数学意义上的集合概念。Set中不包含重复的元素,       即Set中不存两个这样的元素e1和e2,使得e1.equals(e2)为true。由于Set接口提供的数据结构是数学意义       上集合概念的抽象,因此它需要支持对象的添加、删除,而不需提供随机访问     2.HashSet :散列码    存取的无序性(哈希算法)         HashSet 实现了 Set 接口,而 Set 接口是继承于 Collection 接口,所以可以认为 Set 接口是 List 接口的兄弟。         对于 Set 接口,如注释所说:         * A collection that contains no duplicate elements.  More formally, sets         * contain no pair of elements <code>e1</code> and <code>e2</code> such that         * <code>e1.equals(e2)</code>, and at most one null element.  As implied by         * its name, this interface models the mathematical <i>set</i> abstraction.         所以说不重复,而且HashSet 底层使用的是 HashMap,所以也无序。 这两点是和 List 接口下的 ArrayList,LinkedList 等的一大区别。         我们知道,HashMap 是键值对的形式,HashSet 没有什么键值对的概念,那它内部具体又是怎么利用 HashMap 实现的呢?         其实就是 HashSet 用了一个空对象,如 private static final Object PRESENT = new Object         用这个空对象来填充 HashMap 的 value 域         如下面的 add 方法:         private transient HashMap<E,Object> map;         public boolean add(E e) {             return map.put(e, PRESENT)==null;         }         所以从这里就可以看出:         利用了 HashMap 实现。HashSet 的方法就是调用 HashMap 的对应的方法。         用空对象来填充 HashMap 的 value 域         为优化 HashSet 空间的使用,您可以调优初始容量和负载因子     3.TreeSet:二叉树     存取的有序性        这里需要知道,平常我们所使用的 ArrayList,LinkedList 的元素是按照插入的时候的顺序排列的,而不是按照这个元素的大小之类的排序,假如我们需要有这个功能的话,那么 TreeSet 就派上用场了。        当创建一个 TreeSet 的时候:        private transient NavigableMap<E,Object> m;        public TreeSet() {            this(new TreeMap<E,Object>());        }        TreeSet(NavigableMap<E,Object> m) {            this.m = m;        }        可以看到,内部是使用了 TreeMap。        当 add(E e) 方法的时候:        private static final Object PRESENT = new Object();        public boolean add(E e) {            return m.put(e, PRESENT)==null;        }        也是和 HashSet 一样,令对应的 Map 的 value 域为一个随便不要的对象就行。        其他的方法如:        public boolean remove(Object o) {         return m.remove(o)==PRESENT;        }        public void clear() {            m.clear();       }       等等都是类似的,这里不再累赘。            TreeSet 不包含调优选项,因为树总是平衡的,保证了插入、删除、查询的性能为log(n)。 四.Map     1.Map 接口不是 Collection 接口的继承。而是从自己的用于维护键-值关联的接口层次结构入手。按定义,       该接口描述了从不重复的键到值的映射。       我们可以把这个接口方法分成三组操作:改变、查询和提供可选视图。       改变操作允许您从映射中添加和除去键-值对。键和值都可以为 null。但是,您不能把Map 作为一个键或值添加给自身。     2.HashMap :实现了 Map 接口,允许使用 null 值 和 null 键,并且不保证映射顺序       HashMap 有两个参数影响性能:       初始容量:表示哈希表在其容量自动增加之前可以达到多满的一种尺度       加载因子:当哈希表中的条目超过了容量和加载因子的乘积的时候,就会进行重哈希操作。     如下成员变量源码:       static final float DEFAULT_LOAD_FACTOR = 0.75f;       static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;       transient Node<K,V>[] table;     可以看到,默认加载因子为 0.75, 默认容量为 1 << 4,也就是 16。加载因子过高,容易产生哈希冲突,加载因子过小,容易浪费空间,0.75是一种折中。     另外,整个 HashMap 的实现原理可以简单的理解成: 当我们 put 的时候,首先根据 key 算出一个数值 x,然后在 table[x] 中存放我们的值。 这样有一个好处是,以后的 get 等操作的时间复杂度直接就是O(1),因为 HashMap 内部就是基于数组的一个实现。     另外,是怎么算出这个位置的,非常非常推荐看下 JDK 源码中 HashMap 的 hash 方法原理是什么?          put 方法的实现 与 哈希冲突     下面再结合代码重点分析下 HashMap 的 put 方法的内部实现 和 哈希冲突的解决办法:     public V put(K key, V value) {         return putVal(hash(key), key, value, false, true);     }     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                boolean evict) {         Node<K,V>[] tab; Node<K,V> p; int n, i;         if ((tab = table) == null || (n = tab.length) == 0)         n = (tab = resize()).length;         if ((p = tab[i = (n - 1) & hash]) == null)         tab[i] = newNode(hash, key, value, null);         else {         Node<K,V> e; K k;         if (p.hash == hash &&             ((k = p.key) == key || (key != null && key.equals(k))))             e = p;         else if (p instanceof TreeNode)             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);         else {             for (int binCount = 0; ; ++binCount) {             if ((e = p.next) == null) {                 p.next = newNode(hash, key, value, null);                 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                 treeifyBin(tab, hash);                 break;                 }             if (e.hash == hash &&                 ((k = e.key) == key || (key != null && key.equals(k))))                 break;             p = e;             }         }         if (e != null) { // existing mapping for key             V oldValue = e.value;             if (!onlyIfAbsent || oldValue == null)             e.value = value;             afterNodeAccess(e);             return oldValue;         }         }         ++modCount;         if (++size > threshold)         resize();         afterNodeInsertion(evict);         return null;     }     首先我们看到 hash(key) 这个就是表示要根据 key 值算出一个数值,以此来决定在 table 数组的哪一个位置存放我们的数值。     (Ps:这个 hash(key) 方法 也是大有讲究的,会严重影响性能,实现得不好会让 HashMap 的 O(1) 时间复杂度降到 O(n),在JDK8以下的版本中带来灾难性影响。它需要保证得出的数在哈希表中的均匀分布,目的就是要减少哈希冲突)     重要说明一下:     JDK8 中哈希冲突过多,链表会转红黑树,时间复杂度是O(logn),不会是O(n)     然后,我们再看到:     if ((p = tab[i = (n - 1) & hash]) == null)         tab[i] = newNode(hash, key, value, null);     else {         ......     这就表示,如果没有 哈希冲突,那么就可以放入数据 tab[i] = newNode(hash, key, value, null); 如果有哈希冲突,那么就执行 else 需要解决哈希冲突。     那么放入数据 其实就是 建立一个 Node 节点,该 Node节点有属性 key,value,分别保存我们的 key 值 和 value 值,然后再把这个 Node 节点放入到 table 数组中,并没有什么神秘的地方。     static class Node<K,V> implements Map.Entry<K,V> {         final int hash;         final K key;         V value;         Node<K,V> next;         Node(int hash, K key, V value, Node<K,V> next) {         this.hash = hash;         this.key = key;         this.value = value;         this.next = next;         }     上述可以看到 Node 节点中 有一个 Node<K,V> next; ,其实仔细思考下就应该知道这个是用来解决哈希冲突的。下面再看看是如何解决哈希冲突的:     哈希冲突:通俗的讲就是首先我们进行一次 put 操作,算出了我们要在 table 数组的 x 位置放入这个值。那么下次再进行一个 put 操作的时候,又算出了我们要在 table 数组的 x 位置放入这个值,那之前已经放入过值了,那现在怎么处理呢?     其实就是通过链表法进行解决。     首先,如果有哈希冲突,那么:     if (p.hash == hash &&         ((k = p.key) == key || (key != null && key.equals(k))))     e = p;     需要判断 两者的 key 是否一样的,因为 HashMap 不能加入重复的键。如果一样,那么就覆盖,如果不一样,那么就先判断是不是 TreeNode 类型的:      else if (p instanceof TreeNode)         e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);     这里表示 是不是现在已经转红黑树了(在大量哈希冲突的情况下,链表会转红黑树),一般我们小数据的情况下,是不会转的,所以这里暂时不考虑这种情况(Ps:本人也没太深入研究红黑树,所以就不说这个了)。     如果是正常情况下,会执行下面的语句来解决哈希冲突:     for (int binCount = 0; ; ++binCount) {         if ((e = p.next) == null) {         p.next = newNode(hash, key, value, null);         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st             treeifyBin(tab, hash);         break;         }         if (e.hash == hash &&             ((k = e.key) == key || (key != null && key.equals(k))))         break;         p = e;     }     这里其实就是用链表法来解决。并且:     冲突的节点放在链表的最下面。     因为 首先有:p = tab[i = (n - 1) & hash] ,再 for 循环,然后有 if ((e = p.next) == null) { ,并且如果 当前节点的下一个节点有值的话,那么就 p = e;,这就说明了放在最下面。     强烈建议自己拿笔拿纸画画。     总结     一个映射不能包含重复的键,一个键只能有一个值。允许使用 null 值 和 null 键,并且不保证映射顺序。     HashMap 解决冲突的办法先是使用链表法,然后如果哈希冲突过多,那么会把链表转换成红黑树,以此来保证效率。     如果出现了哈希冲突,那么新加入的节点放在链表的最后面。          3.TreeMap:     4.LinkedHashMap     5.“集合框架”提供两种常规的 Map 实现:HashMap 和TreeMap。和所有的具体实现一样,使用哪种实现取决于您的     特定需要。在Map 中插入、删除和定位元素,HashMap 是最好的选择。但如果您要按顺序遍历键,那么TreeMap 会更好。根据集合大小,先把元素添加到 HashMap,再把这种映射转换成一个用于有序键遍历的 TreeMap 可能更快。使用HashMap 要求添加的键类明确定义了 hashCode() 实现。有了TreeMap 实现,添加到映射的元素一定是可排序的。我们将在排序中详细介绍。     为了优化 HashMap 空间的使用,您可以调优初始容量和负载因子。这个TreeMap 没有调优选项,因为该树总处于     平衡状态。     HashMap 和 TreeMap 都实现Cloneable 接口。 五.Java集合框架是最常被问到的Java面试问题,要理解Java技术强大特性就有必要掌握集合框架。这里有一些实用问题,    常在核心Java面试中问到。   1、什么是Java集合API   Java集合框架API是用来表示和操作集合的统一框架,它包含接口、实现类、以及帮助程序员完成一些编程的算法。       简言之,API在上层完成以下几件事:       ● 编程更加省力,提高城程序速度和代码质量       ● 非关联的API提高互操作性       ● 节省学习使用新API成本       ● 节省设计新API的时间         ● 鼓励、促进软件重用   具体来说,有6个集合接口,最基本的是Collection接口,由三个接口Set、List、SortedSet继承,另外两个接口     是Map、SortedMap,这两个接口不继承Collection,表示映射而不是真正的集合。   2、什么是Iterator   一些集合类提供了内容遍历的功能,通过java.util.Iterator接口。这些接口允许遍历对象的集合。依次操作每个元素对象。当使用 Iterators时,在获得Iterator的时候包含一个集合快照。通常在遍历一个Iterator的时候不建议修改集合本省。        3、Iterator与ListIterator有什么区别?   Iterator:只能正向遍历集合,适用于获取移除元素。ListIerator:继承Iterator,可以双向列表的遍历,同样支持               元素的修改。        4、什么是HaspMap和Map?   Map是接口,Java 集合框架中一部分,用于存储键值对,HashMap是用哈希算法实现Map的类。        5、HashMap与HashTable有什么区别?对比Hashtable VS HashMap   两者都是用key-value方式获取数据。Hashtable是原始集合类之一(也称作遗留类)。HashMap作为新集合框架的一部分     在Java2的1.2版本中加入。它们之间有一下区别:       ● HashMap和Hashtable大致是等同的,除了非同步和空值(HashMap允许null值作为key和value,而Hashtable不可以)。       ● HashMap没法保证映射的顺序一直不变,但是作为HashMap的子类LinkedHashMap,如果想要预知的顺序迭代(默认按照插入顺序),你可以很轻易的置换为HashMap,如果使用Hashtable就没那么容易了。       ● HashMap不是同步的,而Hashtable是同步的。       ● 迭代HashMap采用快速失败机制,而Hashtable不是,所以这是设计的考虑点。        6、在Hashtable上下文中同步是什么意思?     同步意味着在一个时间点只能有一个线程可以修改哈希表,任何线程在执行hashtable的更新操作前需要获取对象锁,       其他线程等待锁的释放。        7、什么叫做快速失败特性    从高级别层次来说快速失败是一个系统或软件对于其故障做出的响应。一个快速失败系统设计用来即时报告可能会导致      失败的任何故障情况,它通常用来停止正常的操作而不是尝试继续做可能有缺陷的工作。当有问题发生时,      快速失败系统即时可见地发错错误告警。在Java中,快速失败与iterators有关。如果一个iterator在集合对象      上创建了,其它线程欲“结构化”的修改该集合对象,并发修改异常 (ConcurrentModificationException) 抛出。        8、怎样使Hashmap同步?   HashMap可以通过Map m = Collections.synchronizedMap(hashMap)来达到同步的效果。        9、什么时候使用Hashtable,什么时候使用HashMap   基本的不同点是Hashtable同步HashMap不是的,所以无论什么时候有多个线程访问相同实例的可能时,     就应该使用Hashtable,反之使用HashMap。非线程安全的数据结构能带来更好的性能。   如果在将来有一种可能—你需要按顺序获得键值对的方案时,HashMap是一个很好的选择,因为有HashMap的一个子类     LinkedHashMap。所以如果你想可预测的按顺序迭代(默认按插入的顺序),你可以很方便用LinkedHashMap替换HashMap。     反观要是使用的Hashtable就没那么简单了。同时如果有多个线程访问HashMap,Collections.synchronizedMap()     可以代替,总的来说HashMap更灵活。        10、为什么Vector类认为是废弃的或者是非官方地不推荐使用?或者说为什么我们应该一直使用ArrayList而不是Vector   你应该使用ArrayList而不是Vector是因为默认情况下你是非同步访问的,Vector同步了每个方法,你几乎从不要     那样做,通常有想要同步的是整个操作序列。同步单个的操作也不安全(如果你迭代一个Vector,你还是要加锁,     以避免其它线程在同一时刻改变集合).而且效率更慢。当然同样有锁的开销即使你不需要,这是个很糟糕的方法在     默认情况下同步访问。你可以一直使用Collections.sychronizedList来装饰一个集合。   事实上Vector结合了“可变数组”的集合和同步每个操作的实现。这是另外一个设计上的缺陷。Vector还有些遗留     的方法在枚举和元素获取的方法,这些方法不同于List接口,如果这些方法在代码中程序员更趋向于想用它。     尽管枚举速度更快,但是他们不能检查如果集合在迭代的时候修改了,这样将导致问题。尽管以上诸多原因,     Oracle也从没宣称过要废弃Vector。
转载请注明原文地址: https://www.6miu.com/read-67150.html

最新回复(0)