Java学废之路06——泛型与集合

本文详细介绍了Java中的泛型和集合。泛型帮助我们在编译时检查类型安全,消除运行时类型转换,提高代码可读性和安全性。文章通过实例解释了ArrayList、LinkedList、Vector和HashSet、LinkedHashSet、TreeSet的特性和使用场景,以及HashMap、HashTable、LinkedHashMap和TreeMap的存储结构、扩容机制与区别。此外,还涉及迭代器、Collections工具类和阻塞队列的概念与应用。
摘要由CSDN通过智能技术生成

六、泛型与集合

image-20210507103005073

6.1 泛型

为了能够更好的学习容器,我们首先要先来学习一个概念:泛型。

泛型是JDK1.5以后增加的,它可以帮助我们建立类型安全的集合。在使用了泛型的集合中,遍历时不必进行强制类型转换。JDK提供了支持泛型的编译器,将运行时的类型检查提前到了编译时执行,提高了代码可读性和安全性。

泛型的本质就是“数据类型的参数化”。泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用。使用泛型编写的代码要比那些杂乱的使用Object变量,然后再进行强制类型转换的代码具有更好的安全性,可读性。

6.1.1 概述

所谓泛型, 就是允许在定义类、 接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。 这个类型参数将在使用时(例如,继承或实现这个接口, 用这个类型声明变量、 创建对象时) 确定(即传入实际的类型参数, 也称为类型实参) 。

  • 从JDK1.5以后, Java引入了“参数化类型” 的概念,允许我们在创建集合时再指定集合元素的类型, 正如: List, 这表明该List只能保存字符串类型的对象
  • JDK1.5改写了集合框架中的全部接口和类, 为这些接口、 类增加了泛型支持,从而可以在声明集合变量、 创建集合对象时传入类型实参
  • 泛型必须是类,不能是基本数据类型
  • 如果实例化时,没有知名泛型的类型。默认的类型为Object类
6.1.2 作用

集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以引入泛型。

  • 解决元素存储的安全性问题,提高 Java 程序的类型安全
  • 解决获取数据元素时, 需要类型强制转换的问题。消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会
  • 使用泛型的主要优点是能够在编译时而不是在运行时检测错误

image-20201023153712282

image-20201023153831644

6.1.3 泛型应用举例

为解决运行时动态改变数组大小的问题,Java引入ArrayList类。——泛型数组列表

(1)概述

ArrayList是一个采用类型参数的泛型类。为指定数组列表保存的元素对象类型,使用一对尖括号将类名括起来加在后面。Java SE7中,右边的尖括号内的类型参数可以省略,此为“菱形”语法。

数组列表与数组的区别:

  • 数组列表

    new ArrayList<>(100),表示该数组列表有100个空位置可以使用,但当该100个空间满了的时候,数组列表会自动创建一个更大的数组,并且将所有的对象从较小的数组中拷贝到较大的数组中;在初始化完成之后,数组列表中不会含有任何元素;

    public static void main(String[] args) {
        List<String> list = new ArrayList<>(100);	//尽管已初始化100个单位
        int[] demo = new int[10];				//实例化时就已被分配空间
    
        System.out.println(demo.length);		//结果为10
    
        System.out.println(list.size());		//结果为0
        System.out.println(list.isEmpty());		//结果为true
    }
    
  • 数组

    new Children[100],表示为数组分配了100个元素的存储空间,而且就只有这100个空间可用。

(2)访问数组列表元素

数组列表可以通过add()方法来增加元素。数组列表自动扩展容量的便利增加了访问元素语法的复杂程度。原因是ArrayList并不是Java语言的一部分,它只是由某些人编写的被放在标准库中的一个实用类。

使用get和set方法实现访问或改变数组元素的操作。

使用ArrayList的好处:

  • 不必指出数组的大小
  • 使用add将任意多的元素添加至数组中
  • 使用size()替代length计算元素数目
  • 使用a.get(i)替代a[i]访问元素
(3)容器中的泛型

容器相关类都定义了泛型,我们在开发和工作中,在使用容器类时都要使用泛型。这样,在容器的存储数据、读取数据时都避免了大量的类型判断,非常便捷。

public class Test {
   
    public static void main(String[] args) {
   
        // 以下代码中List、Set、Map、Iterator都是与容器相关的接口;
        List<String> list = new ArrayList<String>();
        Set<Man> mans = new HashSet<Man>();
        Map<Integer, Man> maps = new HashMap<Integer, Man>();
        Iterator<Man> iterator = mans.iterator();
    }
}
6.1.4 自定义泛型
  • 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型

    // static的方法中不能声明泛型
    // public static void show(T t) {}
    
  • 异常类不能是泛型的

    // 异常类不能声明为泛型类
    // public class MyException<T> extends Exception{}
    
  • 父类有泛型 class Father<T1, T2> {} ,子类可以选择保留泛型也可以选择指定泛型类型:

    • 子类不保留父类的泛型:按需实现
      • 没有类型:擦除。class Son1 extends Father {}
      • 具体类型:全覆盖。class Son2 extends Father<Integer, String>
    • 子类保留父类的泛型:泛型子类
      • 全部保留:class Son3<T1, T2> extends Father<T1, T2> {}
      • 部分保留:class Son4 extends Father<Integer, T2> {}

6.2 集合概述

开发和学习中需要时刻和数据打交道,如何组织这些数据是我们编程中重要的内容。 我们一般通过“容器”来容纳和管理数据。数组就是一种容器,可以在其中放置对象或基本类型数据。

  • 数组的优势:是一种简单的线性序列,可以快速地访问数组元素,效率高。如果从效率和类型检查的角度讲,数组是最好的。

  • 数组的劣势:不灵活。容量需要事先定义好,不能随着需求的变化而扩容。

基于数组并不能满足我们对于“管理和组织数据的需求”,所以我们需要一种更强大、更灵活、容量随时可扩的容器来装载我们的对象。 这就是容器,也叫集合(Collection)。Java 集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。

Java 集合可分为 Collection 和 Map 两种体系:

  • Collection接口: 单列数据, 定义了存取一组对象的方法的集合
    • List: 元素有序、可重复的集合
    • Set: 元素无序、不可重复的集合
  • Map接口: 双列数据,保存具有映射关系“key-value对”的集合

以下是容器的接口层次结构图:

image-20201007153626871

6.3 Collection接口

集合层次结构中的根界面 。 集合表示一组被称为其元素的对象。Collection 表示一组对象,它是集中、收集的意思。Collection接口的两个子接口是List、Set接口。

image-20200805152416106

由于List、Set是Collection的子接口,意味着所有List、Set的实现类都有上面的方法。

6.3.1 List
public interface List<E> extends Collection<E>

List是有序、可重复的容器,集合中的每个元素都有其对应的顺序索引。其可以精确控制列表中每个元素的插入位置。 用户可以通过整数索引(列表中的位置)访问元素,并搜索列表中的元素。与集合不同,列表通常允许重复的元素。常用实现类有ArrayList \ LinkedList \ Vector.

  • 有序:List中每个元素都有索引标记。可以根据元素的索引标记(在List中的位置)访问元素,从而精确控制这些元素。
  • 可重复:List允许加入重复的元素。更确切地讲,List通常允许满足 e1.equals(e2) 的元素重复加入容器。
image-20200805152454540
(1)ArrayList

ArrayList是可以存放任意数量的对象,长度不受限制,可以通过定义新的更大的数组,将旧数组中的内容拷贝到新数组,来实现扩容。

public class ArrayList<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable

ArrayList底层是用数组实现的存储。其与数组的区别是ArrayList可以扩容。ArrayList 的特点:查询效率高,增删效率低,线程不安全。

jdk7 中的 ArrayList 的对象的创建类似于单例的饿汉式,而 jdk8 中的 ArrayList 的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。


jdk1.7

ArrayList像饿汉式,直接创建一个默认初始容量为10的数组。

ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData
list.add(123);//elementData[0] = new Integer(123);
...
list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。

默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。


jdk1.8

ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个始容量为10的数组。

//底层Object[] elementData初始化为{},并没有创建长度为10的数组
ArrayList list = new ArrayList();

//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]
list.add(123);

后续的添加和扩容操作与 jdk 7 相同。


扩容源码

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
   
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 将新容量更新为旧容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = 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);
}
(2)LinkedList
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, Serializable

LinkedList底层用双向链表实现的存储。

特点:查询效率低,增删效率高,线程不安全。LinkedList是不同步的。如果多个线程同时访问链接列表,并且至少有一个线程在结构上修改列表,则必须在外部进行同步。


LinkedList源码分析:

LinkedList 内部没有声明数组,而是定义了 Node 类型的 first 和 last,用于记录首末元素。同时,定义内部类 Node,作为 LinkedList 中保存数据的基本结构。 Node 除了保存数据,还定义了两个变量:

  • prev变量记录前一个元素的位置
  • next变量记录下一个元素的位置
image-20201021170927569

内部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;
    }
}
(3)Vector
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable

Vector底层是用数组实现的List,相关的方法都加了同步检查,因此“线程安全,效率低”。Vector类实现了可扩展的对象数组。 像数组一样,它包含可以使用整数索引访问的组件。 但是, Vector的大小可以根据需要增长或缩小,以适应在创建Vector之后添加和删除项目。

(4)总结

image-20201023150443387

  • 线程安全:Vector是线程安全的,ArrayList 与 LinkedList 是线程不安全的。在jdk1.8中,可以使用Collections工具类,返回一个SynchronizedList线程安全的List
  • 随机访问速度:由于Vector中同步锁的存在,ArrayList > Vector;由于 LinkedList 是基于双链表结构,所以LinkedList 的访问速度更慢
  • 随机移动速度:LinkedList > ArrayList

image-20201021155742812

6.3.2 Set
public interface Set<E> extends Collection<E>

Set接口是Collection的子接口, Set接口没有提供额外的方法,方法和Collection保持完全一致。

  • Set 内元素是不重复的。集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set 集合中,则添加操作失败。即保证添加的元素按照equals()方法对比不能为 true,否则就不能加入;并且 Set 中只允许放入一个 null 元素,不能多个。

  • Set 内元素是无序的。无序指的是存放时的位置不会像List一样挨着依次存放,而是根据hash值确定。存储的数据在底层数组中并非按照数组(jdk1.7中Set底层是数组,jdk1.8中为HashMap)索引的顺序添加,而是根据数据的哈希值进行位置确定的,因此会无序。

Set常用的实现类有:HashSet、LinkedHashSet、TreeSet等。

(1)HashSet

HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。

  • 不能保证元素的排列顺序
  • HashSet 不是线程安全的
  • 集合元素可以是 null

HashSet 集合判断两个元素相等的标准: 两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。 【在同一个加载器下才有效】


HashSet 元素的添加元素 a 的过程:

元素的添加过程中为保证 Set 的不重复性,必定要进行比较操作。为了提高比较的效率,采用 hashCode()+equals() 方法相结合的方式进行:

通过先使用 hashCode() 方法来确定元素的索引位置,减少了散列表中添加元素时为保证数据的不重复性而做的 equals 操作次数,以此来提高散列表的查找与存取效率。

  • 哈希值:是一个十进制的整数,由系统随机给出【对象的地址值,是一个逻辑地址,是模拟出来的地址,不是数据实际存储的物理地址】int hashCode():返回对象的哈希码值

  • 索引值:HashSet底层结构中的存放位置【物理地址】,是某对象的哈希值经过特定算法计算求得的

image-20201021222718606

  • 首先调用元素a所在类的hashCode()方法,计算元素a的哈希值
  • 此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),以保证无序性
  • 判断数组此索引位置上是否已经有元素:
    • 如果此位置上没有其他元素,则元素a添加成功。 —>情况1
    • 如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
      • 如果hash值不相同,则元素a添加成功。—>情况2
      • 如果hash值相同,进而需要调用元素a所在类的equals()方法,以保证不重复性
        • equals()返回true,元素a添加失败
        • equals()返回false,则元素a添加成功。—>情况3

对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上数据以链表的方式存储。

image-20201021215036261

hashCode()的作用是为了提高在散列结构(Hash)存储中查找的效率,在线性表中没有作用;只有每个对象的 hash 码尽可能不同才能保证散列的存取性能,事实上 Object 类提供的默认实现确实保证每个对象的 hash 码不同(在对象的内存地址基础上经过特定算法返回一个 hash 码)。在 Java 有些集合类(HashSet)中要想保证元素不重复可以在每增加一个元素就通过对象的 equals 方法比较一次,那么当元素很多时后添加到集合中的元素比较的次数就非常多了,也就是说如果集合中现在已经有 3000 个元素则第 3001 个元素加入集合时就要调用 3000 次 equals 方法,这显然会大大降低效率,于是 Java 采用了哈希表的原理,这样当集合要添加新的元素时会先调用这个元素的 hashCode 方法计算出它的Hash值,然后就可以根据算法定位到它要存放的物理位置上,如果这个位置上没有元素则它就可以直接存储在这个位置上而不用再进行任何比较了,如果这个位置上已经有元素了则就调用它的 equals 方法与新元素进行比较,相同的话就不存,不相同就散列其它的地址,这样一来实际调用 equals 方法的次数就大大降低了,几乎只需要一两次,而 hashCode 的值对于每个对象实例来说是一个固定值(索引)

当在索引位置有元素时的比较过程是根据JAVA规定来实现的:

  • 因为hashCode()方法返回的是一个hash值,这对元素来说是一个固定值,其运算速度更快,所以优先调用hashCode()方法,判断返回值的异同;
    • 不同时,返回false,说明此索引位置的元素与新加的元素不同,则进行添加【减少equals()方法的调用】
    • 相同时,返回true,此时不能确定两元素是否相同,还需要进一步调用equals()方法来判断
      • equals返回true,说明两元素相同,不进行添加操作;
      • equals返回false,说明两元素不同,进行添加。
(2)LinkedHashSet
  • LinkedHashSet 是 HashSet 的子类
  • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
  • LinkedHashSet 插入性能略低于 HashSet, 但在遍历访问 Set 里的全部元素时有很好的性能。
  • LinkedHashSet 不允许集合元素重复。

image-20201022104322345

此时结果显示的“无序”与“有序”并非是Set集合中的无序性。Set中定义的无序性是指在存储位置中的无序性,而非是输出时表面显示的“无序性”。

(3)TreeSet
  • TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态
  • TreeSet 中存储数据的类型必须是具有可比性的,即数据的类型是一致
  • TreeSet 底层使用红黑树结构存储数据,因此也保证了数据的不重复性
  • TreeSet 两种排序方法:自然排序定制排序。默认情况下,TreeSet 采用自然排序

自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列。

  • 向 TreeSet 中添加元素时,只有第一个元素无须调用compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较
  • 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类型的对象
  • 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 方法比较的返回值是否为0,而不是根据equals()方法
  1. TreeSet 中存储基本数据类型的数据时,将自动将其转化为它们的包装类,然后调用各个包装类中已经重写好的compareTo方法来进行比较。前提是数据类型保持一致
  2. TreeSet 中存储某些特定类对象时,如String、Date等,由于这些类都实现了Comparable 接口并重写了compareTo方法,所以在 TreeSet 遍历时不会报错:
    • String:按字符串中字符的 Unicode 值进行比较
    • Date、 Time:后边的时间、日期比前面的时间、日期大
  3. TreeSet 中存储自定义类对象时,则该对象的类必须实现 Comparable 接口,否则会报如下错误。实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小
image-20201022153712224

TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。

定制排序:通过 Comparator接口 来实现。 需要重写 compare(T o1,T o2) 方法。

  • 要实现定制排序&#
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我姓弓长那个张

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值