Java中的容器(集合)

1、Java常用容器:List,Set,Map

List:

  • 继承了Collection接口(public interface List<E> extends Collection<E> ),有序且允许出现重复值。

Set:

  • 继承了Collection接口(public interface Set<E> extends Collection<E> ),无序且不允许出现重复值。

Map:

  • 是一个使用键值对存储的容器(public interface Map<K,V> )。

2、Collection 和 Collections 的区别

Collection:

  • Collection是一个集合类的通用接口(源码:public interface Collection<E> extends Iterable<E>)。
  • 通过查看源码可以发现,其中包含的都是一些通用的集合操作接口,他的直接继承接口有List和Set。

Collections:

  • Collections是一个集合工具类(源码:public class Collections)。
  • 其中提供一系列对集合操作的静态方法,比如排序:sort(),集合安全:synchronizedCollection(),反转:reverse()等等。

3、ArrayList 和 LinkedList 的区别

ArrayList:

  • 底层数据结构是一个数组,查询效率比较高,添加删除较慢(默认添加在末尾,在指定位置添加元素效率比较低,因为需要将指定位置后续的元素都往后移位)。

LinkedList:

  • 底层数据结构是一个双向链表(prev指向前节点,next指向后节点),查询效率比较慢,添加删除比较快。

4、ArrayList 和 Vector 的区别

ArrayList:

  • 非线程安全,读取效率较高。

Vector:

  • 线程安全(源码中显示该类的方法使用了synchronized),读取效率较低(推荐使用CopyOnWriteArrayList,该类适合读多写少的场景)。

5、HashMap 和 Hashtable 的区别

HashMap:

  • 非线程安全,允许空键值,执行效率相对较高(底层使用的数据结构是数组+链表+红黑树(jdk8)或者数组+链表(jdk7))。

Hashtable:

  • 线程安全,不允许空键值,执行效率相对较低(推荐使用ConcurrentHashMap)。

6、HashMap 和 TreeMap 的使用场景

HashMap:

  • 一般情况下进行插入,删除,定位元素的话,使用HashMap(常用)。

TreeMap:

  • 如果需要使用有序的集合,推荐用TreeMap。

7、HashMap 实现原理

以put操作为例:

  • 首先会根据key的hashCode得到hash值(部分源码:return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)),依据hash值得到该元素在数组的位置(下标),如果该位置不存在元素,则将该元素直接放入此位置上;否则判断元素是否相等,如果是,则覆盖,否则使用拉链法解决冲突(创建一个链表,先加入的放到链尾,后加入的放在链头,超过8位时,使用红黑树存储)。
  • 放入的元素是包含了键值对的元素,而非仅仅只有值。

8、HashSet 实现原理

以add操作为例:

  • 进入add源码(return map.put(e, PRESENT)==null),可以看到其底层是用map来实现的,只是传入的值当做了map的key,而map的value使用的是统一的PRESENT。

9、迭代器:Iterator

Iterator:

  • 是一个轻量级的对象(创建代价小),主要用来对集合进行遍历移除等操作。
  • 示例代码如下

package com.spring.test.service.demo;

import java.util.*;

/**
 * @Author: philosopherZB
 * @Date: 2019/10/1
 */
public class Demo {
    public static void main(String[] args){
        List<String> list = new ArrayList<>(5);
        for(int i=0;i<5;i++){
            list.add("Test" + i);
            System.out.println("输入:Test" + i);
        }
        //利用iterator()返回一个Iterator对象
        Iterator<String> it = list.iterator();
        //判断是否还存在元素
        while(it.hasNext()){
            //第一次调用next()会返回集合中的第一个元素,之后返回下一个
            String s = it.next();
            if("Test3".equals(s))
                //移除某个元素
                it.remove();
        }
        list.forEach(l->{
            System.out.println(l);
        });
    }
}

10、ArrayList 扩容源码解析(JDK8)

源码解析:

  • 首先我们使用 ArrayList<String> list = new ArrayList<>(5)创建一个ArrayLsit,这表明创建的ArrayList初始容量为5.
  • 源码如下:
    //默认初始容量10
    private static final int DEFAULT_CAPACITY = 10;
    //一个空的默认对象数组,当ArrayList(int initialCapacity),ArrayList(Collection<? extends E> c)中的容量等于0的时候使用
    private static final Object[] EMPTY_ELEMENTDATA = {};
    //一个空的默认对象数组,用于ArrayList()构造器
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    //一个对象数组,transient表示不能序列化
    transient Object[] elementData;
    //数组大小
    private int size;

    //以传入的容量构造一个空的list
    public ArrayList(int initialCapacity) {
        //如果传入值大于0,则创建一个该容量大小的数组。
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //否则如果传入值等于0,则创建默认空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //如果小于0则抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                    initialCapacity);
        }
    }
  • 接着我们使用add方法添加一个字符串到该list中,list.add("Test")。进入add源码会发现,真正的扩容是发生在add操作之前的。
  • 源码如下:
    //默认添加在数组末尾
    public boolean add(E e) {
        //添加之前先确认是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //新加入的元素是添加在了数组的末尾,随后数组size自增。
        elementData[size++] = e;
        return true;
    }
  • 进入ensureCapacityInternal()方法查看对应源码如下:
    private void ensureCapacityInternal(int minCapacity) {
        //先通过calculateCapacity方法计算最终容量,以确认实际容量
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
  • 到这一步,我们需要先进入calculateCapacity()方法看看他是如何计算最后容量的,源码如下:
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //如果elementData为默认空数组,则比较传入值与默认值(10),返回两者中的较大值
        //elementData为默认空数组指的是通过ArrayList()这个构造器创建的ArrayList对象
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //返回传入值
        return minCapacity;
    }
  • 现在我们确认了最终容量,那么进入ensureExplicitCapacity,查看源码如下:
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        //如果最终确认容量大于数组容量,则进行grow()扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
  • 可以看到,只有当最终容量大于数组容量时才会进行扩容。那么以我们上面的例子而言具体分析如下:
  • 首先因为我们创建的时候就赋了初始容量5,所以elementData.length = 5。
  • 当我们add第一个元素的时候,minCapacity是等于size + 1 = 1的。
  • 此时minCapacity - elementData.length > 0条件不成立,所以不会进入grow(minCapacity)方法进行扩容。
  • 以此类推,只有添加到第五个元素的时候,minCapacity = 6 大于 elementData.length = 5,这时就会进入grow(minCapacity)方法进行扩容。
  • grow()以及hugeCapacity()源码如下:
    //可分配的最大数组大小
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    //扩容
    private void grow(int minCapacity) {
        // overflow-conscious code
        //oldCapacity表示旧容量
        int oldCapacity = elementData.length;
        //newCapacity表示新容量,计算规则为旧容量+旧容量的0.5,即旧容量的1.5倍。如果超过int的最大值会返回一个负数。
        //oldCapacity >> 1表示右移一位,对应除以2的1次方。
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果新容量小于最小容量,则将最小容量赋值给新容量(有时手动扩容可能也会返回<0,对应方法为ensureCapacity())
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //如果新容量大于MAX_ARRAY_SIZE,则执行hugeCapacity(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);
    }

    private static int hugeCapacity(int minCapacity) {
        //如果最小容量超过了int的最大值,minCapacity会是一个负数,此时抛出内存溢出错误
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        //比较最小容量是否大于MAX_ARRAY_SIZE,如果是则返回Integer.MAX_VALUE,否则返回MAX_ARRAY_SIZE
        return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }

(以上所有内容皆为个人笔记,如有错误之处还望指正。)

原文地址:https://www.cnblogs.com/xihuantingfeng/p/11616389.html

时间: 2024-10-04 17:37:36

Java中的容器(集合)的相关文章

Java中的容器(集合)之HashMap源码解析

1.HashMap源码解析(JDK8) 基础原理: 对比上一篇<Java中的容器(集合)之ArrayList源码解析>而言,本篇只解析HashMap常用的核心方法的源码. HashMap是一个以键值对存储的容器. hashMap底层实现为数组+链表+红黑树(链表超过8时转为红黑树,JDK7为数组+链表). HashMap会根据key的hashCode得到对应的hash值,再去数组中找寻对应的数组位置(下标). hash方法如下: static final int hash(Object key

初识Java中的容器

记得第一次听到java中的容器是一个师哥说的,当时听着十分神秘.那么今天就来揭开那层神秘的面纱. 首先什么是容器呢? 在书写程序的时候,我们常常需要对大量的对象引用进行管理.为了实现有效的归类管理,我们常常将同类的引用放置在同一数据容器中.由于数据容器中存放了我们随时可能需要使用到的对象引用,所以一般的数据容器要都要能能提供方便的查询.遍历.修改等基本接口功能. 早期的OOP语言都通过数组的方式来实现对引用集的集中管理和维护. 但是数组方式下,数组大小需要提前被确定,并不允许修改大小,导致其作为

java中的容器解释

解释一:容器(Container)Spring 提供容器功能,容器可以管理对象的生命周期.对象与对象之间的依赖关系,您可以使用一个配置文件(通常是XML),在上面定义好对象的名称.如何产生(Prototype 方式或Singleton 方式).哪个对象产生之后必须设定成为某个对象的属性等,在启动容器之后,所有的对象都可以直接取用,不用编写任何一行程序代码来产生对象,或是建立对象与对象之间的依赖关系.换个更直白点的说明方式:容器是一个Java 所编写的程序,原先必须自行编写程序以管理对象关系,现在

Java中如何克隆集合——ArrayList和HashSet深拷贝

编程人员经常误用各个集合类提供的拷贝构造函数作为克隆List,Set,ArrayList,HashSet或者其他集合实现的方法.需要记住的是,Java集合的拷贝构造函数只提供浅拷贝而不是深拷贝,这意味着存储在原始List和克隆List中的对象是相同的,指向Java堆内存中相同的位置.增加了这个误解的原因之一是对于不可变对象集合的浅克隆.由于不可变性,即使两个集合指向相同的对象是可以的.字符串池包含的字符串就是这种情况,更改一个不会影响到另一个.使用ArrayList的拷贝构造函数创建雇员List

java中数组、集合、字符串之间的转换,以及用加强for循环遍历

java中数组.集合.字符串之间的转换,以及用加强for循环遍历: 1 @Test 2 public void testDemo5() { 3 ArrayList<String> list = new ArrayList<String>(); 4 list.add("甲乙1"); 5 list.add("甲乙2"); 6 list.add("甲乙3"); 7 list.add("甲乙4"); 8 //

JAVA中所有与集合有关的实现类都是这六个接口的实现类

JAVA中所有与集合有关的实现类都是这六个接口的实现类. Collection接口:集合中每一个元素为一个对象,这个接口将这些对象组织在一起,形成一维结构. List接口代表按照元素一定的相关顺序来组织(在这个序列中顺序是主要的),List接口中数据可重复. Set接口是数学中集合的概念:其元素无序,且不可重复.(正好与List对应) SortedSet会按照数字将元素排列,为"可排序集合". Map接口中每一个元素不是一个对象,而是一个键对象和值对象组成的键值对(Key-Value)

Java中数组和集合容器的剖析

java中常用的存储容器就是数组的集合,每种容器存储的形式和结构又有所不同. 数组,是最基础的容器,在创建数组的时候有三种方式分别如下: int[] arr = new int[5]; int[] arr = new String[]{1,2,3,4,5}; int[] arr = {1,2,3,4,5}; 从上面的三种方式可以看出,在定义数组的时候有个共同的特点就是能够直接看出数组的长度,这也是数组的一大特点,就是定义的时候指定长度,同时数组一旦定义完成后长度就不可以变化,这也是数组在后期开发

Java中集合类容器初步了解

容器(Collection) 数组是一种容器,集合也是一种容器 java编程中, 装其他各种各样的对象(引用类型)的一种东西, 叫容器 (图书馆里所有的书, 要想管理图书馆里所有的书, 就需要先把这些书放到一个东西里面, 目前掌握的知识来说, 只能是数组, 数组的长度是固定的, 这就出现 一个问题, 数组的长度该定义成多长 ? 长度是不固定的, 因为不知道有多少本书, 这个时候需要这样一种机制: 定义一种东西, 长度不固定, 可以随时添加和删除, 这种东西就是Collection, 只要不超出内

Java中泛型在集合框架中的应用

泛型是Java中的一个重要概念,上一篇文章我们说过,当元素存入集合时,集合会将元素转换为Object类型存储,当取出时也是按照Object取出的,所以用get方法取出时,我们会进行强制类型转换,并且通过代码也可以看出来,我们放入其他类型时,如字符串,编译器不会报错,但是运行程序时会抛出类型错误异常,这样给开发带来很多不方便,用泛型就解决了这个麻烦 泛型规定了某个集合只能存放特定类型的属性,当添加类型与规定不一致时,编译器会直接报错,可以清楚的看到错误 当我们从List中取出元素时直接取出即可,不

Java中list&lt;Object&gt;集合去重实例

一:Java中list去重的方法很多,下面说一下其中一种方法:把list里的对象遍历一遍,用list.contain(),如果不存在就放入到另外一个list集合中: 二:实例 这里需要注意的是:使用contains方法的时候,list中里面的对象是否相等的问题,我们知道对象是否相等,有两层意思,对象的地址相等和对象的属性值相等.而contains比对的时候调用的是object类中的equals方法: 我们可以看到,比对的是对象的地址.而实际中可能我们想要的结果是,对象里面的值想等,我们就认为这两