1.3集合

1.3集合

1.集合架构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VtianVh4-1686210962052)(C:\Users\10059\AppData\Roaming\Typora\typora-user-images\image-20220114103642862.png)]

2.数组和集合的区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jGjtdQb6-1686210962054)(C:\Users\10059\AppData\Roaming\Typora\typora-user-images\image-20220113151254327.png)]

3.Iterable接口和Iterator接口

//Iterable接口中用到了iterator
public interface Iterator<E> {
    boolean hasNext();     //判断是否存在下一个元素,经常作为循环结构条件遍历元素
    E next();              //移动指针,返回下一个元素
    void remove();         //移除当前元素,切记使用迭代器遍历移除元素时务必使用该方法
}

简洁用法:

public void test(){
    for(Iterator<String> iterator = arrayList.iterator();iterator.hasNext();){
        String str = iterator.next();
        iterator.remove();
    }
}

注意事项:

在对集合进行遍历操作时,不能调用remove方法。如果需要在遍历中移除元素,需要使用迭代器。


4.Collection接口

(1)List

①ArrayList

底层为可变长度数组结构,按插入顺序存放各种引用类型的数据,查询效率高,线程不安全

https://blog.csdn.net/u010890358/article/details/80515284?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_default&utm_relevant_index=2

  • 动态扩容机制
    • new ArrayList()
      • 第一次扩容初始化长度为10,当数组的长度将要超过10就需要扩容。每一次需要扩容时在原先的基础上增加1.5倍向下取整
    • new ArrayList(int initialCapacity)和new ArrayList(Collection<? extends E> c)
      • 初始化长度为initialCapacity或c.size(),之后每一次需要扩容时在原先的基础上增加1.5倍
      • 注意:如果初始化长度为0的话,这边可以看到一个严重的问题,一旦我们执行了初始容量为0,那么根据扩容的算法前四次扩容每次都 +1,在第5次添加数据进行扩容的时候才是按照当前容量的1.5倍进行扩容。频繁的扩容会带来性能问题,因为扩容的时候涉及到Array.copyOf
  • 快速报错机制(fail-fast):Java集合容器有一种保护机制,能够防止多个进程同时修改同一个集合容器的内容。如果你在迭代遍历某个容器的过程中,另一个进程介入其中,并且插入,删除或修改此容器的某个对象,就会立刻抛出ConcurrentModificationException。
    • 每一个容器有自己的modCout,在判断是否要扩容时会modCout++。
    • 假设你往一个Integer类型的ArrayList插入了10条数据,那么每操作一次modCount(继承自父类AbstractList)就加一所以就变成10,而当你对这个集合进行遍历的时候就把modCount传到expectedModCount这个变量里,然后ArrayList在checkForComodification()方法中通过判断两个变量是否相等来确认当前集合是否是同步的,如果不同步就抛出ConcurrentModificationException。所谓的不同步指的就是,如果你在遍历的过程中对ArrayList集合本身进行add,remove等操作时候就会发生(因为操作后发生modCount的变化)。当然如果你用的是Iterator那么使用它的remove是允许的因为此时你直接操作的不是ArrayList集合而是它的Iterator对象
    • java.util包下面的所有的集合类都是 fail-fast 的,而java.util.concurrent包下面的所有的类都是 fail-safe 的。
List<String> list = new ArrayList<>();
list.add("abc");
list.add("def");
for(String element1 : list){
    list.add("abc");//报错,因为操作List本身的元素
    list.add(element1);//报错ConcurrentModificationException
}
②LinkedList

底层为双向链表结构,可以从头部或尾部插入(默认尾部插入),插入删除效率高,线程不安全

③Vector

底层为数组结构,按插入顺序存放各种引用类型的数据,线程安全

(2)Set

①HashSet

底层为哈希表结构,元素无序且唯一,线程不安全,可以插入null

②LinkedHashSet

底层为哈希表+双向链表结构,元素有序(FIFO(先进先出))且唯一,线程不安全,可以插入null

③TreeSet

底层为二叉树结构(红黑树),元素有序(实现Comparable接口或Comparator接口来决定)且唯一,线程不安全,不可以插入null


(3)总结

①ArrayList和LinkedList的区别
  • ArrayList是可变长度数组结构,数组需要连续的内存空间且一旦开辟不能回收,插入数据依靠移动数据来实现,因此适合查询操作,不适合插入删除操作
  • LinkedList是双向链表结构,链表只要有空闲内存就可插入不用连续内存,查询数据依靠移动指针,因此适合插入删除操作,不适合查询操作
②List和Set的区别
  • 有序性:
    • List按照插入元素的顺序取出元素,有序
    • Set按实现类分可以有序可以无序
  • 唯一性:
    • List存储的元素可以重复
    • Set存储的元素不可以重复
  • 获取元素:
    • List可以通过下标获取元素
    • Set没有下标,不能通过下标获取元素

5.Map接口

HashMap:JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间

LinkedHashMapLinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:《LinkedHashMap 源码详细分析(JDK1.8)》open in new window

Hashtable:数组+链表组成的,数组是 Hashtable 的主体,链表则是主要为了解决哈希冲突而存在的

TreeMap:红黑树(自平衡的排序二叉树)

(1)HashMap

①核心概念
  • 原理:底层数据结构是数组 + 链表(也就是散列链表 = 哈希表)。数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。当某一个链表长度大于阈值(默认为 8)并且数组长度小于64时会选择先进行数组扩容,而不是转换为红黑树。否则将这个链表转化为红黑树,以减少搜索时间

  • capacity 数组容量:默认的数组初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, HashMap 总是使用 2 的幂作为哈希表的大小。

  • loadFactor 负载因子

    • loadFactor 负载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。
    • loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值
    • 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量超过了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能
  • threshold 扩容阈值

    • threshold = capacity * loadFactor当 map.size()>threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。在开发中扩容是很耗费性能的,所以要尽量避开

    jdk1.8之后的内部结构-HashMap

  • 多线程操作导致死循环(已解决)和数据覆盖

    • 在jdk1.8之前,hashmap的链表采用头插法,在并发环境下会导致指针指向错误,最终形成环形链表造成死循环。在jdk1.8时,改用尾插法,解决了死循环,但是在并发环境下仍然会存在数据覆盖问题,所以在并发环境下使用ConcurrentHashMap
②和HashTable的区别

线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);

效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它;

对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException

初始容量大小和每次扩充容量大小的不同: ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。

底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。Hashtable 没有这样的机制。

**无序:**二者都是无序的

**键:**需要重写equals和hashCode方法,最好是用不可变对象(例如String)

③和HashSet的区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nVfhjSNa-1686210962057)(C:\Users\10059\AppData\Roaming\Typora\typora-user-images\image-20230606092222561.png)]

(2)ConcurrentHashMap

  • 底层数据结构:数组+链表(红黑树)
  • 如何保证线程安全:synchronized + CAS。给Node数组加锁
  • Java8 ConcurrentHashMap 存储结构

(3)TreeMap

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vNfPb4AN-1686210962060)(C:\Users\10059\AppData\Roaming\Typora\typora-user-images\image-20220114105414444.png)]

(4)Map的遍历

①在for-each循环中使用entrySet()遍历entries
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
 
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
 
    System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
 
}
②在for-each循环中使用keySet()或values()遍历keys或values
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
 
//遍历map中的键
 
for (Integer key : map.keySet()) {
 
    System.out.println("Key = " + key);
 
}
 
//遍历map中的值
 
for (Integer value : map.values()) {
 
    System.out.println("Value = " + value);
 
}
③使用Iterator遍历,适合用于在遍历时需要remove元素
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
 
Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();
 
while (entries.hasNext()) {
 
    Map.Entry<Integer, Integer> entry = entries.next();
 
    System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
 
}

6.集合元素排序Comparable 和 Comparator 有什么区别?

二者都是顶级的接口。我们先从二者的字面含义来理解它,Comparable 翻译为中文是“比较”的意思,而 Comparator 是“比较 器”的意思。Comparable 是以 -able 结尾的,表示它自身具备着某种能力

(1)Comparable接口

public interface Comparable<T> {
   //return -1; //-1表示放在红黑树的左边,即逆序输出
   //return 1;  //1表示放在红黑树的右边,即顺序输出
   //return o;  //表示元素相同,仅存放第一个元素 
   public int compareTo(T o);
}

Comparable接口定义了compareTo(T o)方法。方法接收的参数 o 是要对比的对象,排序规则是用当前对象和要对比的对象进行比较,然后返回一个 int 类型的值。正序从小到大的排序规则是:使用当前的对象值减去要对比对象的值;而倒序从大到小的排序规则刚好相反:是用对比对象的值减去当前对象的值。Collections.sort 和 Arrays.sort 想要支持某个实现类的排序,这个实现类需要实现 Comparable 接口并重写 compareTo 方法就可以实现某个类的排序了。

示例:

public class ComparableExample {
    public static void main(String[] args) {
        Person p1 = new Person(1, 18, "Java");
        Person p2 = new Person(2, 22, "MySQL");
        Person p3 = new Person(3, 6, "Redis");
        List<Person> list = new ArrayList<>();
        list.add(p1);
        list.add(p2);
        list.add(p3);
        // 进行排序操作(根据 Person 类中 compareTo 中定义的排序规则)
        Collections.sort(list);
        list.forEach(p -> System.out.println(p.getName() +
        ":" + p.getAge()));
    }
}

@Getter
@Setter
@ToString
static class Person implements Comparable<Person> {
    private int id;
    private int age;
    private String name;
    public Person(int id, int age, String name) {
        this.id = id;	
        this.age = age;
        this.name = name;
    }
    
    @Override
    public int compareTo(Person p) {
        return p.getAge() - this.getAge();
    }
}


注意事项:如果自定义对象没有实现 Comparable 接口,那么它是不能使用 Collections.sort 方法进行 排序的,编译器会提示如下错误:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kZXuXqVA-1686210962061)(C:\Users\10059\AppData\Roaming\Typora\typora-user-images\image-20230605111728614.png)]

(2)Comparator接口

@FunctionalInterface
public interface Comparator<T> {
    //return -1; //-1表示放在红黑树的左边,即逆序输出
    //return 1;  //1表示放在红黑树的右边,即顺序输出
    //return o;  //表示元素相同,仅存放第一个元素 
    int compare(T o1, T o2);
    
    //。。。其它已实现方法
}

Comparator接口是一个函数式接口,定义了一个未实现的排序方法compare(T o1,T o2)。Collections.sort 和 Arrays.sort 想要支持某个实现类的排序,这个实现类除了实现Comparable接口这一个办法之外,还可以通过传入一个实现了Comparator<实现类>接口的比较器类。

示例:

public class ComparatorExample {
    public static void main(String[] args) {
        Person p1 = new Person(1, 18, "Java");
        Person p2 = new Person(2, 22, "MySQL");
        Person p3 = new Person(3, 6, "Redis");
        List<Person> list = new ArrayList<>();
        list.add(p1);
        list.add(p2);
        list.add(p3);
        // 进行排序操作(根据 PersonComparator 中定义的排序规则)
        Collections.sort(list, new PersonComparator());
        list.forEach(p -> System.out.println(p.getName() +
        ":" + p.getAge()));
        
        
        // 这边可以通过匿名内部类的方式
        // 使用 Comparator 匿名类的方式进行排序
        list.sort(new Comparator<Person>() {
            @Override
            public int compare(Person p1, Person p2) {
                return p2.getAge() - p1.getAge();
            }
        });

    }
    
    class PersonComparator implements Comparator<Person> {
        @Override
        public int compare(Person p1, Person p2) {
            return p2.getAge() - p1.getAge();
        }
	}
    
    @Getter
    @Setter
    class Person {
        private int id;
        private int age;
        private String name;
        public Person(int id, int age, String name) {
        this.id = id;
        this.age = age;
        }
    }

}


7.集合元素唯一性如何保证:hashCode() 和 equals()

元素唯一性靠重写hashCode()和equals()方法来决定,没有重写则无法保证唯一性。

具体实现唯一性的比较过程:存储元素首先会使用hash()算法函数生成一个int类型hashCode散列值,然后已经的所存储的元素的hashCode值比较,如果hashCode不相等,则所存储的两个对象一定不相等,此时存储当前的新的hashCode值处的元素对象;如果hashCode相等,存储元素的对象还是不一定相等,此时会调用equals()方法判断两个对象的内容是否相等,如果内容相等,那么就是同一个对象,无需存储;如果比较的内容不相等,那么就是不同的对象,就该存储了

package learn01;

import java.util.HashSet;

/**
 * @author zhangshiqin
 * @date 2022/1/13 - 16:55
 */
public class Student {

    public static void main(String[] args) {
        Student s1 = new Student("1234567","zhangsan");
        Student s2 = new Student("1234567","linlin");

        HashSet<Student> hashSet = new HashSet<>();
        hashSet.add(s1);
        hashSet.add(s2);

        System.out.println(hashSet.toString());
    }

    //唯一约束
    private String id;

    private String name;

    public Student(){}

    public Student(String id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public int hashCode() {
        return id.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if(obj == this){
            return true;
        }
        if(obj == null) {
            return false;
        }
        Student s = (Student) obj;
        if(s.id == null){
            return this.id == null;
        }

        return s.id.equals(this.id);
    }

    @Override
    public String toString() {
        return "Student{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                '}';
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

8.红黑树

二叉查找树,通过旋转(树的左旋右旋)和颜色来保证树的平衡。需要满足以下几点性质:

  • 每个节点不是黑的就是红的

  • 根节点必须要是黑色,所有叶子接口必须要是黑色

  • 红色节点的子节点一定都必须是黑色
    “, name='” + name + ‘’’ +
    ‘}’;
    }

    public String getId() {
    return id;
    }

    public void setId(String id) {
    this.id = id;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }
    }




## 8.红黑树

二叉查找树,通过旋转(树的左旋右旋)和颜色来保证树的平衡。需要满足以下几点性质:

* 每个节点不是黑的就是红的

* 根节点必须要是黑色,所有叶子接口必须要是黑色
* 红色节点的子节点一定都必须是黑色
* 从一个节点到它的子孙节点的所有路径,黑色节点的个数必须相同
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值