Java 集合(底层解析)

  • 使用数组存储对象具有一些弊端,而Java集合就像一种容器,可以动态的把多个对象的引用放入容器中
  • 数组在内存中存储的特点:
  • 数组初始化以后,长度就确定了
  • 数组声明的类型,就决定了进行元素初始化的类型
  • 数组在存储数据方面的弊端:
  • 数组初始化以后,长度就不可变了,不便于扩展
  • 数组中提供的属性和方法少,不便于进行添加,删除,插入等操作,且效率不高,同时无法直接获取存储元素的个数
  • 数组存储的数据是有序的,可以重复的。 存储数据的特点单一
  • Java集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组

1、Collection 接口

单列数据,定义了存取一组对象的方法的集合

iterator() :返回 Iterator 接口的实例,用于遍历集合元素

  • Iterator 对象称为迭代器(设计模式的一种),主要用于遍历Collection 集合中的元素
  • 迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的细节。迭代器模式,就是为容器而生。
  • Collection接口继承了Iterable接口,该接口有一个iterator() 方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用于返回一个实现了Iterator接口的对象
  • Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合
  • 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认指针都在集合的第一个元素之前。

1.1 List

元素有序、可重复的集合。

  • ArrayList:作为List的主要实现类,线程不安全的,效率高,底层使用数组存储
// 底层分析
// JDK 1.7
ArrayList list = new ArrayList();// 底层创建了长度是10的Object[]数组
list.add(123)...;// 如果此次的添加导致底层elementData数组容量不够,则扩容
// 默认情况下,扩容为原来容量的1.5倍,同时将原有数组中的数据复制到新的数组中
// 开发中建议使用带参的构造器:指定初始长度,避免不必要的扩容,增加效率

// JDK 1.8
ArrayList list = new ArrayList();// 底层Object[] elementData初始化为{},并没有创建长度为10的数组
list.add(123);// 第一次调用add()时,底层才创建了长度为10的数组,并将数据添加到elementData数组中,后期扩容一样
// 延迟了数组的创建,节省内存·
  • LinkedList:底层使用双向链表存储,对于频繁的插入、删除操作,比ArrayList效率高
// 底层分析
LinkedList list = new LinkedList();// 内部声明了Node类型的first和last属性,默认值为null
list.add(123);// 将123封装到Node中,创建了Node对象。
// 其中,Node定义为:
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;
    }
}
  • Vector:线程安全的,效率低,底层使用数组存储
// Vector底层源码分析:通过构造器创建对象时,底层都是创建了长度为10的数组
// 在扩容方面,默认扩容为原来数组的2倍。

1.2 Set

元素无序、不可重复的集合。

以HahSet为例:

  1. 无序性:不等于随机性。存储的数据在底层数组(初始长度16,当使用率超过0.75,则扩容为原来的2倍)中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的
  2. 不可重复性:保证添加的元素按照equals() 判断时,不能返回true。即:相同的元素只能添加一个
  3. 向Set中添加的数据,其所在的类一定要重写hashCode()和equals()方法:相等的对象必须有相等的散列码(哈希值)

添加元素的过程:以HashSet为例

首先通过散列算法确定元素需添加在数组中的具体位置,如果该位置上有元素了,则比较哈希值,如果哈希值不一样,则该元素以链表的形式添加在该位置,如果哈希值一样,则通过equals()方法比较,如果还是一样则不会添加到集合

  • HashSet:Set接口的主要实现类,线程不安全,可以存储null值
  • LinkedHashSet:作为HashSet的子类,遍历其内部数据时,可以按照添加的顺序遍历
    将数据封装到了Node节点中,对于频繁的遍历操作,LinkedHashSet效率高于HashSet
  • TreeSet:可以按照添加对象的指定属性,进行排序。
  1. 向TreeSet中添加的数据,要求是相同类的对象
  2. 两种排序:自然排序和定制排序
class User{
    String name;
    int age;
}
// 自然排序 需要排序的类需要实现Comparable接口,重写compareTo()方法
@Override
public int compareTo(Object o){
    if(o instanceof User){
        User user = (User)o;
        int compare = -.compareTo();//按照姓名排序
        if(compare != 0){
            return compare;
        }else{
            return Integer.compare(this.age,user.age);// 同名再按照年龄排序
        }
    }else{
        throw new RuntimeException("输入的类型不匹配")
    }
}
// 自然排序中,比较两个对象是否相同的标准为:compareTO() 返回0,不再是equals().

// 定制排序
public void test(){
    Comparator<User> com = (o1,o2) -> {
		if (o1 != null && o2 != null){
			return Integer.compare(o1.getAge(), o2.getAge());// 按照年龄,年龄一样就不能添加
		} else {
			throw new RuntimeException("传参不能为空");
		}
	};
	Set set = new TreeSet(com);
}

2、Map 接口

双列数据,保存具有映射关系“key-value”的集合

Map 中的key:无序的、不可重复的,使用Set存储所有的key ;key所在的类要重写equals() 和 hashCode()

Map 中的value:无序的、可重复的,使用Collection存储所有的value

一个键值对:key-value构成了一个Entry对象。

Map 中的entry:无序的、不可重复的,使用set存储所有的entry

  • HashMap:作为Map的主要实现类;线程不安全,效率高;可存储null的key-value
// 数组加链表(jdk7)
// 数组加链表加红黑树(jdk8)
// HashMap的底层实现原理。以jdk7为例
HashMap map = new HashMap();// 在实例化以后,创建了长度是16的一维数组 Entry[] table;
map.put(key1,value1);// 首先调用key1所在类的hashCode()计算key1哈希值,此哈希值经过计算后,得到在Entry数组中存放的位置。
// 如果此位置上的数据为空,此时key1-value1添加成功。
// 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表方式存在)),比较key1和已经存在的一个或多个数据的哈希值:如果key1的哈希值与已存在的数据的哈希值都不相同,此时添加key1-value1成功。如果key1的哈希值和已存在的某一个数据的哈希值相同,继续比较:调用key1所在类的equals()方法,比较,如果equals()返回false,添加成功,如果equals()返回true:使用value1替换相同key的value值,即修改了值
// 在不断的添加过程中,会涉及扩容问题,默认的扩容方式:扩容为原来容量的2倍,并将原来的数据复制过来。

// jdk8 相较于jdk7在底层实现方面的不同:
new HashMap(); // 底层没有创建一个长度为16的数组
// 底层的数组是:Node[] ,而非Entry[]
// 首次调用put()方法时,底层创建长度为16的数组
// 数组加链表加红黑树。当某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组长度 > 64时,此索引位置上的所有数据改为使用红黑树存储。
  • LinkedHashMap:作为HashMap的子类:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素,对于频繁的遍历操作,执行效率高于HashMap
// 源码中相较于HashMap
static class Entry<K,V> extends HashMap.Node<K,V>{
    Entry<K,V> before,after;// 能够记录添加的元素的先后顺序
    Entry(int hash,K key,V value,Node<K,V> next){
        super(hash,key,value,next);
    }
}
  • TreeMap:可按照key对集合进行排序,实现排序遍历。底层使用红黑树
// 向TreeMap中添加key-value,要求key必须是同一个类创建的对象,因为要按照key进行排序
// 也是自然排序和定制排序
  • HashTable:作为Map的古老实现类;线程安全,效率低;不可存储null的key-value
  • Properties:作为Hashtable的子类,常用于处理配置文件,key和value都是String类型的