Java基础内容集合

这部分是Java中的基础内容,集合,也叫做Java容器,用在很多的地方。

集合是用来存储数据的,简称为容器,其中这里的存储指内存层面的存储,不是持久化存储。

1. 数组的特点:

  • 指定长度后,长度不可以更改
  • 声明了类型后,数组只能存放这个类型的数据。
  • 数组的查询效率高,删除、增加元素的效率低
  • 数组中实际元素的数量无法获取,没有提供具体的方法来获取
  • 数组存储数据是有序可重复的,这里的有序不是指实际数据排序,指是线性排列的意思

综上所述,数组的灵活性低,引入了集合,有Set、List、Map等等,不同集合底层数据结构不一样,所以每个集合都有各自的特点,适用于不同的场景。

2. 集合体系结构图

3. 集合的应用场合:

集合是用来存储数据的,在整个开发过程中从始到终一直在用到,根据各个集合的特点选用对应合适的集合使用。其中集合只能存放引用数据类型的数据,不能是基本数据类型,基本数据类型自动装箱。当使用泛型时候,为了方便统一管理,一般只会存入一种数据类型。

  • 大致总结一下如:
    • 如果需要频繁访问元素,使用ArrayList
    • 如果需要频繁插入和删除元素,使用LinkedList
    • 如果不允许重复元素且无序,使用HashSet
    • 如果需要自动排序,使用TreeSet
    • 如果需要快速查找和存储键值对,使用HashMap
    • 如果需要按键自动排序,使用TreeMap

区别:
List接口特点:不唯一,有序的
Set接口特点:唯一,无序的(无序不等于随机,是相对于List接口部分来说的)

4. ArrayList

ArrayList是一个可变大小的数组实现,适用于需要频繁访问元素的场景,查询快,增删慢,是多线程不安全的。
在提前知道list长度的时候,可以提前指定集合的容量,这样可以减少扩容的次数,提高性能(数据量越大,效果越明显)

ArrayList源码类似StringBuilder,其中jdk1.7 中和jdk1.8版本中ArrayList实现有所不同(以下使用jdk1.8版本做说明)。

新建一个ArrayList对象,未指定大小时候,使用的数组为空。调用add后,才会确定数组长度,存实际数据

新增到空间不够了,再进行扩容

ArrayList底层的数据结构分为物理结构和逻辑结构,物理结构对应紧密结构,在内存中是连续着的,形成的逻辑结构是线性表中的数组。

    public static void main(String[] args) {
        List<Object> arrayList = new ArrayList<>();
        arrayList.add(10);
        arrayList.add(8);
        arrayList.add(13);
        System.out.println("arrayList中数据:" + arrayList);
        arrayList.add(1, 24);
        System.out.println("arrayList中数据2:" + arrayList);
        arrayList.set(3, 12);
        System.out.println("arrayList中数据3:" + arrayList);
        arrayList.add(2);
        System.out.println("arrayList中数据4:" + arrayList);
        arrayList.remove(2);
        System.out.println("arrayList中数据5:" + arrayList);
    }

输出

arrayList中数据:[10, 8, 13]
arrayList中数据2:[10, 24, 8, 13]
arrayList中数据3:[10, 24, 8, 12]
arrayList中数据4:[10, 24, 8, 12, 2]
arrayList中数据5:[10, 24, 12, 2]

5. LinkedList

底层使用的双向链表,使用链表查找元素,所以查询慢,增删只需要替换掉指针的指向即可,所以增删快,元素按照插入的顺序排列,元素可以重复。

新增数据时候,创建新Node节点信息,指向上一个元素的地址

Node对象:存前一个元素的地址,当前存入的元素,下一个元素的地址,整个对象是链表中的一个节点。

遍历LinkList的方式:

        for (Object node : linkedList) {
            System.out.println(node);
        }

        Iterator<Object> iterator = linkedList.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }


        // 这种方式,it在for循环这个作用域内有效,for循环结束时候,it这个对象的生命周期结束,就自动消失了
        for (Iterator<Object> it = linkedList.iterator(); it.hasNext(); ) {
            System.out.println(it.next());
        }
  • LinkedList的数据结构:
    • 物理结构:内存中是跳转结构,不连续的
    • 逻辑结构:是线性表中的链表

6. HashSet

Set接口里没有跟索引相关的方法,意味着不能用普通for循环遍历

  • 遍历方式:
    • 迭代器
    • 增强for循环(底层还是迭代器来的)

存放基本数据类型的数据时候,都满足唯一,无序的特点。
存放自定义类型的数据时候,没有满足唯一的特点。

HashSet集合存入的数据(以Integer类型为例):

调用对应的hashCode方法计算哈希值,哈希值是int类型的数据,再通过哈希值和一个表达式计算在数组中存放的位置。如位置冲突,则维护一个链表来存储数据,如果存放的数据重复了,不会再次放入(使用equals方法比较)
HashSet底层原理:数组+链表,即哈希表
所以,放入HashSet中的数据,一定要重写两个方法:hashCode和equals

其中底层用到了HashMap的结构

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    private transient HashMap<E,Object> map;
    private static final Object PRESENT = new Object();
    // 所以说HashSet底层是用HashMap来完成的
    public HashSet() {
        map = new HashMap<>();
    }
    public boolean add(E e) {
        // value都是一样的,用来占位
        return map.put(e, PRESENT)==null;
    }
}

示例:

    public static void hashSetTest() {
        HashSet<Object> set = new HashSet<>();
        set.add(12);
        set.add(8);
        set.add(9);
        set.add(42);
        set.add(14);
        System.out.println(set);
    }
输出:
[8, 9, 42, 12, 14]

7. LinkedHashSet

HashSet是无序的,所以出现LinkedHashSet,是唯一,有序的(按照输入顺序进行输出)
底层原理:其中就是再HashSet的基础上,多了一个总的链表,这个总链表将放入的元素串在一起,方便有序的遍历

    public static void hashSetTest() {
        HashSet<Object> set = new LinkedHashSet<>();
        set.add(12);
        set.add(8);
        set.add(9);
        set.add(42);
        set.add(14);
        System.out.println(set);
    }
输出:
[12, 8, 9, 42, 14]

8. TreeSet

首先看一下比较器的内容先:
内部比较器java.lang.Comparable#compareTo,返回int类型,判断>0 =0 <0 来确定数据的大小

String类型也可以比较大小,因为实现了Comparable方法,重写了其中的compareTo方法
外部比较器:实现Comparator接口,重写compare方法
外部比较器比内部比较器好用些,因为用了多态,好扩展

然后是TreeSet的内容:
特点:唯一的,有序:可以按照升序进行遍历,没有按照输入顺序进行输出
底层是用的二叉树(逻辑结构)
其中二叉树有三种遍历方式,中序遍历、先序遍历、后序遍历
当前的TreeSet底层的二叉树使用的中序遍历,所以输出是升序的。

例如:Integer 实现了Comparable接口,所以可以比较(基本数据类型)

    public static void test() {
        TreeSet<Integer> set = new TreeSet<>();
        set.add(6);
        set.add(4);
        set.add(34);
        set.add(12);
        System.out.println(set);
    }
输出:
[4, 6, 12, 34]

如果放入自定义类型时候,自定义类型必须实现比较器,否则使用TreeSet添加数据会失败,可以使用内部比较器或者外部比较器。

    public static void test() {
        Comparator<User> comparator = new Comparator<User>() {
            @Override
            public int compare(User o1, User o2) {
//                return o1.getName().compareTo(o2.getName());
                return o1.getAge() - o2.getAge();
            }
        };
        TreeSet<User> set = new TreeSet<>(comparator);
        set.add(new User(15, "娜娜"));
        set.add(new User(12, "苗苗"));
        set.add(new User(13, "文文"));
        set.add(new User(15, "红红"));
        System.out.println(set);
        System.out.println(set.size());
    }
输出:
[User{age=12, name='苗苗'}, User{age=13, name='文文'}, User{age=15, name='娜娜'}]
3

底层原理和TreeMap类似,通过TreeMap的key来控制TreeSet的元素

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;
    }
    //底层创建了一个TreeMap
    public TreeSet() {
        this(new TreeMap<E,Object>());
    }
    // 通过add增加数据,实际上是通过TreeMap的put方法放数据
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
}

9. HashMap

首先Map是一个泛型接口,存储是key-value形式,称为键值对,键用key表示,值用value表示,是一对信息一起存储

HashMap的特点(在key的角度):无序,唯一的(是按照key进行总结的,因为底层key是遵循哈希表的)
哈希表的原理:比如放入集合的数据的那个类,必须重写hashCode和equals方法
一般HashMap的存放的key都是String类型的,String这个类默认重写了hashCode和equals方法
HashMap是无序的,不能按照输入顺序进行输出
首先HashMap底层维护了一个哈希表(数组+链表),使用key的hashCode方法拿到哈希码,通过哈希码和表达式,计算出元素在数组中的位置,就可以放如对应元素。在数组中存入元素的类型是entry类型。
如果存入元素发生哈希冲突了,底层就形成了一个链表,jdk7使用链表的头插法,jdk8 使用链表的后插法新增Node节点(即7上8下)

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    private static final long serialVersionUID = 362498820763181265L;
    //16,底层数组的长度
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    // 最大的容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 定义了一个负载因子,又叫加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //底层主数组
    transient Node<K,V>[] table;
    //集合中的元素数量
    transient int size;
    //默认为0,用来表示数组扩容的边界值/门槛值
    int threshold;
    // 用来接收负载因子
    final float loadFactor;

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    // 存储数据的方法
    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;
        // 底层主数组为空时候,初始化处理一下
        // n是数组的长度
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // i 是元素在数组中的下标位置,通过 (n - 1) & hash 来计算
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 当前位置没有发生哈希冲突时候,直接新建Node,放进数组中,null指的是没有下一个Node节点
            tab[i] = newNode(hash, key, value, null);
        else {
            // p 不等于null,说明当前位置上有Node了
            Node<K,V> e; K k;
            //发生哈希碰撞时,先比较哈希值,再比较key是否是一个对象,如果key是一个对象,不使用equals比较
            //如果不是同一个对象,使用equals方法比较是否一样
            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循环,直到找到链表的最后一个节点,使用后插法
                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;
                }
            }
            // key存在处理,使用新的value替换
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                // 会返回oldValue
                return oldValue;
            }
        }
        ++modCount;
        // 如果下一次新增元素可能会大于临界值,需要扩容
        // 数组长度扩2倍,然后将老数组的元素放到新数组里,以后使用新数组
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

    // 二次hash,没有直接用hashCode的值,为了解决哈希冲突
    // 右移,使用扰动函数,用来增加值的不确定性,让最终得到的数组尽量不一样(让元素所在位置不一样)
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

面试题(题外话):

  1. 负载因子为什么是0.75?
    如果负载因子增大(设置为1):空间利用率得到了很多的满足(数组的16个位置都用到了),很容易碰撞,就会产生链表,导致查询效率低
    如果负载因子减小(设置为0.5):碰撞的概率低,直接进行扩容,产生链表的几率低,导致查询效率高,但是空间利用率太低
    所以取中间值,做折中。
  2. 主数组的长度为什么必须为2^n?
    原因1:(n - 1) & hash 等效于 hash对n取余操作,等效的前提就是:n必须是2的整数倍(n是主数组的长度,hash是key的哈希值)
    原因2:防止哈希冲突,元素在主数组上的位置冲突

10. TreeMap

这里可以和TreeSet对比着来学习
特点:唯一,有序(可以按照升序或者降序来输出)
底层原理:二叉树,这里key遵循二叉树的特点
所以放入集合中key的数据所对应的类型内部一定要实现比较器(内部比较器或者外部比较器都可以)

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
    //外部比较器
    private final Comparator<? super K> comparator;
    //树的根节点,节点类型是Entry
    private transient Entry<K,V> root;
    // 集合中元素的数量
    private transient int size = 0;
    // 如果使用空构造器,底层就不使用外部比较器
    public TreeMap() {
        comparator = null;
    }
    //指定了外部比较器
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
    //添加元素
    public V put(K key, V value) {
        Entry<K,V> t = root;
        // 如果放第一个元素,root为null
        if (t == null) {
            compare(key, key); // type (and possibly null) check
            // 根节点为当前元素,确定root
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        // cpr 为外部比较器,创建对象时候传的
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                // 将元素的key值做比较
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    // <0 ,key比t.key小,放在左子树
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    两个元素的key相等,替换value
                    return t.setValue(value);
            } while (t != null);
        }
        //没有指定外部比较器,指定使用内部比较器
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                // 将元素的key值做比较
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        //将key 和value封装成对象
        Entry<K,V> e = new Entry<>(key, value, parent);
        //没有等于0的情况,如果等于0 ,已经被替换过了
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }


    static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;
    }
}

11. LinkedHashMap

底层在哈希表的基础上多维护了一个链表,所以可以按照输入顺序进行输出
特点:唯一(在key的角度),有序(按照输入顺序进行输出)

12. Hashtable

HashTable用起来和HashMap是一样的,当然两个也是有区别的。
区别:
HashTable是jdk1.0开始的,效率低,但是他是线程安全的,key不可以存空值。
HashMap 是jdk1.2开始的,效率高,是线程不安全的,这里key可以存空值,并且key的null值也遵循唯一的特点。

13. Vector

实际上已经很少用了,底层是Object数组,底层数组长度默认为10(当未指定长度时),底层扩容数组长度为2倍,是线程安全的

Vector和ArrayList的区别:
ArrayList底层扩容长度为原数组的1.5倍,Vector底层扩容长度为原数组的2倍。
ArrayList线程不安全,Vector是线程安全的(因为用了synchronized)

posted @ 2024-09-13 23:04  二十四桥冷月夜  阅读(29)  评论(0)    收藏  举报