Java集合

Collection

Collection中的元素一个一个存放。Collection是顶层接口

1、List

底层都是数组实现:

优点:查询速度快
缺点:删除、插入速度慢
特点:数据可重复

【1】ArrayList :线程不安全,效率高

----ArrayList的源代码。

JDK1.7:
    底层ElementData数组,直接容量初始化为10,
    扩容时,数组长度变为原来的1.5倍:newLength = oldLength + (oldLength >> 1);
    问题:要是没有数据,浪费内存。
JDK1.8:
    底层数组:在调用参数为空构造方法的时,底层数组为[]
    调用add方法后,底层数组才重新赋值为新数组
    新数组长度为10----->节省了内存,在添加元素后才创建长度为10的数组。扩容还是1.5倍

【2】Vector: ---->扩容方法不一样,两倍 ,线程安全,效率低,被淘汰。

【3】LinkedList

泛型

【1】泛型如果不指定,就会被擦除,泛型对应的类型为Object。

【2】推荐指定泛型

【3】泛型类构造器不可以带泛型

public class TestGeneric<A,B,C>{
    //构造器,再这里带泛型这种写法是错误的
    public TestGeneric<A,B,C>(){

    }
}

【4】不同泛型的引用类型不可以相互赋值

【5】继承情况分两种,如下:

public class Father<E>{

}
//1、第一种,父类指定泛型
public class Child extends Father<String>{

}

//2、第二种,父类不指定泛型,那么子类也会成为一个泛型类,具体类型可以在创建子类对象的时候确定。
public class Child<E> extends Father<E>{

}

【6】泛型类中的静态方法不能使用泛型。

泛型具体类型是在创建对象的时候才确定的,而静态方法会优先与对象创建被加载到内存中。

编译的时候就不会通过。

【7】不能通过泛型直接创建数组,但是可以创建引用。

public class Test<E>{
    public void a(){
        E[] e = new E[10];//这样,编译器不会通过
        //解决方案
        E[] e = (E[])new Object[10];
    }
}

【8】泛型方法

泛型方法对应的那个泛型参数类型,和当前所在的这个类是否是泛型类,泛型是什么,无关
public class Father<E>{
    //不是泛型方法
    //这个方法不能是静态方法
    public void a(E e){

    }
    //是泛型方法
    //T的类型在调用方法时确定
    //泛型方法可以是静态方法,具体泛型在调用时确定
    public <T> void b(T t){

    }
}

【9】A和B是子类父类的关系。但是List<“A”>,List<“B”>不存在继承关系,是并列关系。这里的泛型限制了你编译时的操作,List的底层还是Object。
在这里插入图片描述

【10】泛型受限

    public class Student extends Person {
    public static void main(String[] args) {
        List<Object> list1 = new ArrayList<>();
        List<Person> list2 = new ArrayList<>();
        List<Student> list3 = new ArrayList<>();

        /**
         * 泛型的上限:
         * List<? extends Person> 是List<Person>的父类,也是List<Person的子类>的父类</>
         父类引用可以指向子类对象
         */
        List<? extends Person> list = new ArrayList<>();
        list = list1;//会报错,Object类是Person的父类
        list = list2;
        list = list3;

        /**
         * 泛型的下限:
         * List<? super Person> 是List<Person>的父类,是List<Person父类>的父类
         */
        List<? super Person> list4 = new ArrayList<>();
        list4 = list1;
        list4 = list2;
        list4 = list3;//会报错,因为List<Student>不是其子类
    }
}

JAVA序列化:

【1】Java中对象的序列化指的是将对象转换成以字节序列的形式来表示。

【2】一个序列化后的对象可以被写到数据库或文件中,也可用于网络传输。

【3】序列化后的最终目的是为了反序列化,恢复成原先的Java对象。

【4】Java中transient关键字的作用,简单地说,就是让某些被修饰的成员属性变量不被序列化。

为什么要不被序列化呢,主要是为了节省存储空间。

JAVA通配符

public class Test{
    public static void main(String[] args){
        C c = new C();
        //使用通配符可以传递任何你想传递的类型的数组
        c.add(new List<Integer>());
        c.add(new List<String>());
        c.add(new List<Object>());
    }
}

class C{
    public void add(List<?> list){
        //内部遍历的时候,使用Object,不能使用?
        for(Object o : list)
    }
}

Collection总结

在这里插入图片描述

迭代器Iterator

在这里插入图片描述

具体实现:
在这里插入图片描述

|| 增强for循环底层也是通过迭代器实现的。

|| 并发修改异常 ConcurrentModificationException。使用迭代器时产生。

public class Test{
    public static void main(String[] args){
        ArrayList<String> list = new ArrayList<>();
        list.add("aa");
        list.add("bb");
        list.add("cc");
        list.add("dd");

        Iterator it = list.iterator();
        while(it.hasNext()){
            if("cc".equals(it.next)){
                list.add("xx");//此处发生错误
            }
        }
    }
}
产生错误原因:迭代器和list同时操作集合
解决方案:让一个人做。
引入:ListIterator
public class Test{
    public static void main(String[] args){
        ArrayList<String> list = new ArrayList<>();
        list.add("aa");
        list.add("bb");
        list.add("cc");
        list.add("dd");

        Iterator it = list.ListIterator();//此处修改
        while(it.hasNext()){
            if("cc".equals(it.next)){
                it.add("xx");//此处发生错误
            }
        }
    }
}

2、Set(唯一 无序【与List相比】)

---->方法与Collection一致,但没有关于索引的方法,

遍历方法:

1、增强for循环
2、Iterator

【1】HashSet

--HashSet底层简要原理图:

在这里插入图片描述

- 首先是待存放数组。
- 然后根据HashCode方法,计算出相应的Hash值。
- 根据Hash值加相应的匹配方法计算在数组中的下标。
- 必须重写equals方法,才能判断是否为重复的值,才会不重复写入。
- 下标相等的数组元素(不包含上面equals相等的元素),通过链表形式链接出去
- Hash表 = 数组 + 链表(底层实现)。

HashSet源码分析

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    //其实也是用HashMap实现的,只不过value值,都是他给你new的一个Object
    private transient HashMap<E,Object> map;
    // Dummy(虚拟) value to associate with
    // an Object in the backing Map
    private static final Object PRESENT = new Object();

    //无参构造
    public HashSet() {
        map = new HashMap<>();
    }
    //有参构造,参数为初始容量和装填因子
    public HashSet(int initialCapacity, float loadFactor) {
        //后续,同HashMap
        map = new HashMap<>(initialCapacity, loadFactor);
    }
}

内部比较器与外部比较器

public class Student implements Comparable<Student>{
    public String name;
    public int age;
    public double height;

    //实现Comparable接口
    @Override
    public int compareTo(Student o) {
        //比较年龄
        //return this.age - o.getAge();
        //比较身高
        /**
        * 调用了Double的CompareTo方法,直接相减的话,无法得到整数,
        * 强制类型转换也会有问题
        **/
        /**
        *return 
        *((Double)this.height).compareTo((Double)o.getHeight());
        **/
        //比较姓名
        return this.name.compareTo(o.getName());
    }
}
//外部比较器,优点,利用多态,可扩展性增强
class BiJiaoQi1 implements Comparator<Student>{

    //比较年龄
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getAge() - o2.getAge();
    }
}

class BiJiaoQi2 implements Comparator<Student>{
    //比较名字
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getName().compareTo(o2.getAge());
    }
}

class Test{
    public static void main(String[] args){
        Student s1 = new Student("zhangsan",18,160.6);
        Student s2 = new Student("Lisi",19,180.5);
        Comparator bj1 = new BiJiaoQi1();
        bj1.compare(s1,s2);
        Comparator bj2 = new BiJiaoQi2();//利用多态
        bj2.compare(s1,s2);

    }
}

NOTE:LinkedHashSet可以按照输入顺序输出,通过链表链接上了。

【2】TreeSet

  • 还是满足唯一,无序(没有按照输入顺序进行输出)。

  • 有序。按照升序进行遍历输出,所以必须实现比较器才行。

  • 比较器一是实现唯一,二是实现升序。

public class Test01 {
    public static void main(String[] args) {
        //利用内部比较器,Student实现了Comparable接口
        TreeSet<Student> set = new TreeSet<>();
        set.add(new Student("cili",18,176.8));
        set.add(new Student("alili",17,170.2));
        set.add(new Student("blili",15,176.3));
        set.add(new Student("clili",20,176.5));
        set.add(new Student("clili",21,174.0));
        set.add(new Student("flili",19,180));

        System.out.println(set.size());
        System.out.println(set);

        //利用外部比较器,Student没有实现Comparable接口
        Comparator bj1 = new BiJiaoQi1();
        //传入外部比较器即可。
        TreeSet<Student> set = new TreeSet<>(bj1);
    }
}

输出结果:
在这里插入图片描述
– 观察输出结果可以明显发现,clili并没有存储两次,size为5(因为Student的内部比较器比较的是名字)

– 排序结果也为按名字升序排序

Map

  • Hashtable :jdk1.0开始

    • 效率低,线程安全,key不可以存入null值,会报空指针异常。
    • 方法与HashMap一致
  • HashMap:jdk1.2开始

    • 无序,唯一,按照Key对应
    • 集合数据对应的类型必须实现hashCode()和equals()方法
    • 效率高,线程不安全,key可以存入null值,null也遵循唯一

HashMap简要原理

HashMap源码:jdk1.8

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    //重要属性
    //默认容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认加载因子 | 装填因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //元素集合
    transient Set<Map.Entry<K,V>> entrySet;
    //元素个数
    transient int size;
    //修改次数
    transient int modCount;
    //临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
    int threshold;
    //装填因子
    final float loadFactor;
    
    //
    /// 构造
    //

    //空构造器
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    //带参构造
    public HashMap(int initialCapacity, float loadFactor) {
        //健壮性考虑代码
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //防止超过最大容量                                       
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //健壮性考虑代码    
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
        //为装填因子赋值
        this.loadFactor = loadFactor;
        初始化threshold大小
        this.threshold = tableSizeFor(initialCapacity);
    }
    /**
    >>> 无符号右移
    | 按位或
    下面这个简化得有点厉害,就是个语法糖。
    表达的意思是:
        找到第一个比入参大的2的高次幂。所以最后要n+1
        eg:传入65
        n = 65--->1000001
    **/
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1; //1000001 | 0100000 = 1100001
        n |= n >>> 2; //1100001 | 0011000 = 1111001
        n |= n >>> 4; //1111001 | 0000111 = 1111111
        n |= n >>> 8; //1111111 | 0000000 = 1111111
        n |= n >>> 16;//.... n = 1111----->不用再继续,会超出MAXMUM_CAPACITY
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
        //最后得出n = 128
    }

    /**
     * Implements Map.put and related methods.
     *
     * @param hash 得出hash值
     * @param key 存储的key值
     * @param value 存储的value值
     * @param onlyIfAbsent 如果设置为true,则不会替换,否则会替换
     * @param evict 如果设置为false,则table处于创建态
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab;//table
        Node<K,V> p;
        //n为map集合的容量大小
        int n, i;
        //如果数组为空,Resize();
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //判断table[i]是否为null,如果是,直接插入    
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //key是否存在,存在的话直接覆盖value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //hash值不相等,即key不相等;为红黑树结点
            //否则,tab[i](也就是p)是否为TreeNode
            //是的话,直接在红黑树中插入结点
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {//这个else一定会进入
                //否则,遍历链表
                for (int binCount = 0; ; ++binCount) {
                    //如果不存在,在尾部插入新结点
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //链表长度是否大于8,大于8转成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 判断链表中结点的key值与插入的元素的key值是否相等
                    // 相等,跳出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表下·
                    p = e;
                }
            }
            // 表示在桶中找到key值、hash值与插入元素相等的结点
            if (e != null) {
                V oldValue = e.value; //记录e.value
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//替换掉旧值
                // 访问后回调
                afterNodeAccess(e);
                // 返回旧值
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)//容量增加后是否超过阈值,若超过,则扩容
            resize();
        //插入后回调
        afterNodeInsertion(evict);
        return null;
    }
 }    

put方法

在这里插入图片描述

该图片为转载,点击查看原博客

JDK1.8中,HashMap采用数组+链表+红黑树实现
当链表长度超过阈值(8)时,将链表转换为红黑树

在这里插入图片描述

HashMap的数据存储实现原理(同样来自上篇博客,他写得很好)
流程:
1. 根据key计算得到key.hash = (h = k.hashCode()) ^ (h >>> 16);
2. 根据key.hash计算得到桶数组的索引index = key.hash & (table.length - 1),这样就找到该key的存放位置了:
    ① 如果该位置没有数据,用该数据新生成一个节点保存新数据,返回null;
    ② 如果该位置有数据是一个红黑树,那么执行相应的插入 / 更新操作;
    ③ 如果该位置有数据是一个链表,分两种情况一是该链表没有这个节点,另一个是该链表上有这个节点,注意这里判断的依据是key.hash是否一样:
        如果该链表没有这个节点,那么采用尾插法新增节点保存新数据,返回null;如果该链表已经有这个节点了,那么找到该节点并更新新数据,返回老数据。

经典面试题

(1)装填因子/加载因子为什么为0.75?
    如果装填因子设置为1:空间利用率得到了很大的满足,但很容易产生碰撞,产生链表,导致查询效率低
    如果装填因子设置太小,比如0.5:碰撞概率低,扩容,产生链表的几率低,查询效率高,但空间利用率太低
    所以取了一个折中的值:0.75
(2)主数组的长度为什么是2^n?
    原因1:使得 hash&(length)等效于 hash%length----->取余操作的效率没有位运算高
    原因2:降低hash冲突
  • TreeMap
    • 唯一,有序(按照一定顺序输出,不是指和输入顺序一致)
      -还是要有内部比较类或者外部比较类
    • 二叉搜索树
    • 红黑树

TreeMap底层原理简图:
在这里插入图片描述

TreeMap源码分析:

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
    //外部比较器
    private final Comparator<? super K> comparator;
    //根结点
    private transient Entry<K,V> root;
    //结点数量
    private transient int size = 0;
    //修改次数
    private transient int modCount = 0;

    //空构造器
    //将comparator初始化为null,这种时候,Tree的key类型的类必须实现内部比较器
    public TreeMap() {
        comparator = null;
    }

    //使用外部比较器
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

    /**
    * 查找。
    **/
    //根据键值获取value
    public V get(Object key) {
        Entry<K,V> p = getEntry(key);
        return (p==null ? null : p.value);
    }

    //查找方法的具体实现
    final Entry<K,V> getEntry(Object key) {
        
        //提供外部比较器的情况
        if (comparator != null)
            return getEntryUsingComparator(key);
        //以下为不提供外部比较器的情况
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        //从根开始
        Entry<K,V> p = root;
        while (p != null) {
            int cmp = k.compareTo(p.key); //与当前结点的键值比较大小
            //比他小,查找左子树
            if (cmp < 0)
                p = p.left;
            //比他大,查找右子树
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;//查找失败,返回null
    }
    //使用外部比较器进行查找,大同小异
    final Entry<K,V> getEntryUsingComparator(Object key) {
        @SuppressWarnings("unchecked")
            K k = (K) key;
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            Entry<K,V> p = root;
            while (p != null) {
                int cmp = cpr.compare(k, p.key);//使用外部比较器比较
                if (cmp < 0)
                    p = p.left;
                else if (cmp > 0)
                    p = p.right;
                else
                    return p;
            }
        }
        return null;
    }

    /**
    * 插入。
    **/
    public V put(K key, V value) {
        Entry<K,V> t = root;//根
        if (t == null) {//根为空,将新插入结点设置为根
            compare(key, key);
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;//返回null,因为之前该key位置没有元素
        }
        //根不为空的情况
        int cmp;
        Entry<K,V> parent;//父节点
        Comparator<? super K> cpr = comparator;
        //外部比较器的情况
        if (cpr != null) {
            do {
                parent = t;//当前key值与父节点比较,大于进入右子树,小于进入左子树
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);//找到key值一样的,覆盖
            } while (t != null);
        }
        //使用内部比较器的情况
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);//t.setValue的返回值是oldValue
            } while (t != null);
        }
        //如果没有key值一致的
        //新创建一个结点Entry,插入搜索到的父节点
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);//插入后进行红黑树的调整
        size++;
        modCount++;
        return null;//新结点,直接返回null
    }
}

Put流程

[1]判断根是否为空,为空将新结点置为根节点。
[2]不为空的情况下,分为使用内部比较器还是外部比较器,都从根节点出发进行比较
    - 外部比较器使用comparator
    - 内部比较器,key的类型所属类需要实现内部比较器
[3]如果现存结点的key值与新插入的key值一致,则覆盖,返回旧值。
[4]如果不存在,根据二叉搜索的结果,将新插入的值创建Entry,设置为叶节点。

红黑树的调整:

NOTE:叔叔为父节点的兄弟结点,我们将空域也看作一个结点,爷结点为父节点的父节点。
1、插入的为根节点,直接染黑
2、否则染为红色,插入
若破坏了红黑树的特性,需要染色
    - 若叔叔结点为红色
      - 叔结点,父节点,爷结点染色(都变为与原来相反的颜色)---->爷结点变为新结点(也就是把爷结点当作新插入的结点),重复上述步骤
    - 若叔叔结点为黑色
      - LL旋转,旋转完,将父和爷染色【以父结点为旋转中心】
      - RR旋转,------------------
      - LR旋转,旋转完,将新节点与爷结点染色【先以父结点为旋转中心,再以新结点为旋转中心】
      - RL旋转,-------------------------

调整过程图示:
在这里插入图片描述
红黑树源码解析见此博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值