Java 集合框架,泛型,包装类

文章目录

  • 集合框架
  • 泛型
    • Java 中的泛型
    • 裸类型(了解)
    • 原理
    • 泛型的上界
    • 泛型方法
    • 通配符
  • 包装类
  • ArrayList
    • 构造
    • 常见操作
  • LinkedList
  • Stack
  • Queue
  • PriorityQueue
  • Map
    • Map.Entry
    • Map 常用方法
  • Set
    • 常用方法

集合框架

Java 集合框架,泛型,包装类_第1张图片

  • Vector 一个古老的集合类,实现了一个动态数组,现在已经不常用
  • Stack
  • ArrayList 顺序表
  • LinkedList 链表+队列+双端队列
  • PriorityQueue 优先级队列
  • HashSet 集合(不重复)(哈希表实现)
  • HashMap 哈希表
  • TreeSet 有序集合(红黑树)
  • TreeMap 有序键值对(红黑树)

泛型

Java 中的泛型

Java 中的泛型是通过在类、接口或方法的声明中使用尖括号 < > 来实现的。

public class Box<T> { // 类型参数也可以指定多个,用 ',' 分隔
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

Java 中泛型的类型参数不能直接是基本数据类型, 而应该是引用类型

Box<int> intBox = new Box<>(42); // 错误的
Box<Integer> intBox = new Box<>(42); // 正确的
Box<Integer> intBox = new Box<Integer>(42); // 后面的Integer可以写也可以省略

规范:类型形参一般使用一个大写字母表示,常用的名称有:

  • E 表示 Element
  • K 表示 Key
  • V 表示 Value
  • N 表示 Number
  • T 表示 Type
  • S, U, V 等等 - 第二、第三、第四个类型

裸类型(了解)

裸类型就是不指定类型实参

Box box = new box();

注意:一般不用裸类型,裸类型是 Java 为了兼容老版本

原理

Java 泛型的原理是基于类型擦除(Type Erasure)的概念。在编译时,Java 编译器会擦除泛型类型的信息,将泛型代码转换为普通的非泛型代码。这样,泛型的类型信息只存在于编译期,而在运行时是不可见的。

  1. 类型参数擦除: 在编译时,泛型类型参数会被替换为它们的边界或者 Object 类型。例如,List 在运行时会被擦除为 List
  2. 桥方法(Bridge Methods): 泛型类和泛型接口的类型擦除可能导致擦除后的类或接口缺少某些方法。为了解决这个问题,编译器会生成桥方法,以确保子类或实现类仍然具有正确的类型。
  3. 泛型数组的限制: 由于数组在运行时需要知道元素的确切类型,Java 不允许创建泛型数组。因此,使用泛型数组可能会导致编译器警告或错误。
  4. 泛型的上界

    泛型的上界是指泛型类型参数的限制,用于指定该参数必须是某个特定类型或其子类型。在Java中,通过使用 extends 关键字来指定上界。上界限制了可以传递给泛型类型参数的类型范围。

    public class Box<T extends Number> {
        private T value;
    
        public Box(T value) {
            this.value = value;
        }
    
        public T getValue() {
            return value;
        }
    
        public static void main(String[] args) {
            // 使用泛型的上界,创建一个存储整数的盒子
            Box<Integer> intBox = new Box<>(42);
    
            // 使用泛型的上界,创建一个存储双精度浮点数的盒子
            Box<Double> doubleBox = new Box<>(3.14);
    
            // 下面的代码会导致编译错误,因为String不是Number的子类型
            // Box stringBox = new Box<>("Hello");
        }
    }
    

    一个泛型上界的典型运用

    // 求数组中的最大值
    class Alg<T extends Comparable<T>> { // 设置上界 Comparable
        public T findMax(T[] array) {
            T max = array[0];
            for (int i = 1; i < array.length; ++i) {
                if (max.compareTo(array[i]) < 0) { // 因为此处要对两个类型进行比较,所以必须是限定继承了Comparable接口
                    max = array[i];
                }
            }
            return max;
        }
    }
    

    泛型方法

    对上面的 Alg 类,我们发现 findMax() 方法的调用不需要依赖对象,所以可以设置为静态方法。

    但是静态方法因为不直接访问类的实例,也就无法获取类中的泛型信息,无法使用泛型类型参数 T

    要解决这个问题,可以把静态方法设置为泛型方法,将泛型参数放在方法的返回类型之前:

    class Alg {
        public static <T extends Comparable<T>> T findMax(T[] array) {
            T max = array[0];
            for (int i = 1; i < array.length; ++i) {
                if (max.compareTo(array[i]) < 0) {
                    max = array[i];
                }
            }
            return max;
        }
    }
    

    通配符

    ? 通配符: 表示未知类型,可以用在方法参数、方法返回类型、变量等地方。例如,在一个方法中接受一个未知类型的集合:

    public static void printList(List<?> list) {
        for (Object element : list) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
    

    通配符的上界

    通配符是为了处理泛型协变问题而引入的,特别是 ? extends T 这种形式

    比如我们有这样一个继承关系:

    class Fruit {
        // ...
    }
    
    class Apple extends Fruit {
        // ...
    }
    
    class Box<T> {
        private T value;
    
        public Box(T value) {
            this.value = value;
        }
    
        public T getValue() {
            return value;
        }
    }
    

    Apple 是 Fruit 的子类,那么我们可以认为 BoxBox 的子类,可以写出以下代码:

    public class Test {
        private static void fun(Box<Fruit> box) {
            System.out.println(box.getValue());
        }
        public static void main(String[] args) {
            Box<Apple> box = new Box<>(new Apple());
            fun(box); // 将 Box 传给 Box,报错
        }
    }
    

    解决方式:

    public class Test {
        private static void fun(Box<? extends Fruit> box) { // 加入通配符 ? extends Fruit ,正确
            System.out.println(box.getValue());
        }
        public static void main(String[] args) {
            Box<Apple> box = new Box<>(new Apple());
            fun(box);
        }
    }
    

    但是 fun 方法里是不能对 box 里的元素进行修改,因为 box 里面放的是 Fruit 及其子类,没有一个类型可以让 Fruit 及其子类 都能接收。

    通配符的下界

    使用 ? super T 通配符时,表示可以接受类型为 TT 的超类型的对象。这主要用于对泛型集合进行写入操作,允许向集合中添加 T 类型及其子类型的元素。

    <? super 下界>
    
    <? super Integer> // 表示可以传入的实参的类型是 Integer 或者 Integer 的父类
    

    例子:

    class Food {
    
    }
    
    class Fruit extends Food {
        // ...
    }
    
    class Apple extends Fruit {
        // ...
    }
    
    class Box<T> {
        private T value;
    
        public Box(T value) {
            this.value = value;
        }
    
        public void setValue(T newValue) {
            this.value = newValue;
        }
    
        public T getValue() {
            return value;
        }
    }
    
    public class Test {
        static void fun(Box<? super Fruit> box) { 
            // box 内可以存放 Fruit 及其子类的对象
            box.setValue(new Fruit()); // 行
    //        box.setValue(new Food()); // 不行
            box.setValue(new Apple()); // 行
        }
        public static void main(String[] args) {
            Box<Food> box = new Box<>(new Food());
            // 类型参数 Food 是 Fruit 的超类,可以传参
            fun(box);
        }
    }
    

    因为 Box 里的元素是 Fruit 类型及其父类类型,那么这些类型一定可以接收 Fruit 的子类对象。而如果你读数据,读出来的是 Fruit 类型或其父类类型,你没有一个很好的类型去接收,如果使用向下转型又不安全。所以通配符的下界适合写数据,不适合读数据。

    包装类

    装箱:基本数据类型->包装类类型

    int a = 10;
    Integer b = a; // 自动装箱
    Integer c = Integer.valueOf(a); // 手动装箱
    

    拆箱:包装类类型->基本数据类型

    Integer a = 20; // 自动装箱
    int b = a;  // 自动拆箱
    double d1 = a; // 自动拆箱
    
    double d2 = a.doubleValue(); // 手动拆箱
    

    特殊案例:

    public static void main(String[] args) {
        Integer a = 100;
        Integer b = 100;
        Integer c = 200;
        Integer d = 200;
        System.out.println(a == b);
        System.out.println(c == d);
    }
    /* 输出:
    true
    false
    */
    

    为什么值都是 100 就相等,值都是 200 的却不相等了呢?

    装箱底层其实就是调用的 valueOf(),查看 valueOf() 源码:

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high) // low 为 -128,high 为 127
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    

    可以看到,当 i 在 -128 ~ 127 的时候就直接返回 cache 里面的对象了,如果超过了这个范围就会 new 一个新的对象。这就是导致上述问题的原因。

    所以涉及引用类型比较大小,都应该使用对应的比较方法,而不是 == 号,包装类也不例外。

    ArrayList

    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    
    • ArrayList 实现了 RandomAccess, Cloneable, java.io.Serializable 接口,表明它是可随机访问的,可以 clone 的,支持序列化的。
    • ArrayList 不是线程安全的
    • ArrayList 底层是一段连续的空间,并且可以动态扩容, 是一个动态类型的顺序表

    构造

    方法 解释
    ArrayList() 构造一个初始容量为10的空列表
    ArrayList(int initialCapacity) 构造一个初始容量为 initialCapacity 的空列表
    ArrayList(Collection c) 利用其他 Collection 构造 ArrayList

    问题1:调用无参构造后,ArrayList 的容量是多少?

    答案:0

    Java 集合框架,泛型,包装类_第2张图片

    elementDataArrayList 底层维护的用来存储元素的数组,在调用无参构造后,只是把 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 这个空数组的引用赋给了它,所以容量还是 0

    在第一次调用 add 时:

    Java 集合框架,泛型,包装类_第3张图片

    结论:第一次调用 add 的时候,底层数组 elementData 的容量才变成了10,如果只是调用无参构造方法,容量是0.


    问题2:扩容是几倍扩

    Java 集合框架,泛型,包装类_第4张图片

    查看扩容方法 grow ,其中 int newCapacity = oldCapacity + (oldCapacity >> 1); 可以得知的 1.5 倍扩容


    使用指定容量构造

    Java 集合框架,泛型,包装类_第5张图片

    如果指定的容量 > 0,就直接给你开对应容量的数组;如果 = 0,就把空数组传过来;如果 < 0 ,则抛非法参数异常

    常见操作

    方法 解释
    boolean add(E e) 尾插,返回true
    void add(int index, E element) 在指定位置插入,如果index越界会抛异常
    boolean addAll(Collection c) 尾插一个集合的元素,可能抛空指针异常
    boolean addAll(int index, Collection c) 指定位置的版本,可能抛越界或空指针异常
    E remove(int index) 删除指定位置的元素,返回被删除的元素
    boolean remove(Object o) 删除第一个出现的 o
    E get(int index) 获取 index 位置的元素
    E set(int index, E element) 设置 index 下标的元素为 element
    void clear() 清空
    boolean contains(Object o) 查看 o 是否在 ArrayList 中
    int indexOf(Object o) 返回第一个 o 所在位置下标
    int lastIndexOf(Object o) 返回最后一个 o 所在位置下标
    List subList(int fromIndex, int toIndex) 截取部分,返回的引用仍指向原顺序表

    遍历

    for循环,println(arrayList),foreach 都和普通数组一样。

    迭代器

    Iterator<Integer> it = arrayList.iterator(); // 获取迭代器对象, 也可以用listIterator()
    while (it.hasNext()) { // 判断是否有下一个
        System.out.println(it.next()); // 访问,并向后走一步
    }
    
    • iterator()Iterable接口定义的方法,返回的迭代器只能向前遍历,并且不支持在遍历过程中修改集合。
    • listIterator()List接口定义的方法,返回的迭代器可以向前和向后遍历,同时支持在遍历过程中对集合进行修改。
    • 迭代器获取的时候也可以传入下标,来获取指定位置的迭代器

    LinkedList

    public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable
    

    底层由链表实现

    • 支持无参构造和集合元素构造
    • 提供的常用操作方法和 ArrayList 的大同小异

    Stack

    方法 解释
    Stack() 构造一个空的栈
    E push(E item) 入栈,并返回 item
    E pop() 出栈,并返回出栈的元素
    E peek() 获取栈顶元素
    int size() 获取栈中有效元素的个数
    boolean empty() 检测栈是否为空

    Queue

    Queue 是个接口,所以你并不能 new 它,而是 new 它的子类 LinkedList

    Queue<Integer> queue = new LinkedList<>();
    
    方法 解释
    boolean offer(E e) 入队
    E poll() 出队
    E peek() 获取队头元素

    PriorityQueue

    默认创建小堆,容量为11

    创建大堆,需要传比较器:

    PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    });
    

    如果是自定义类,也可以通过实现 Comparable 接口来指定比较规则

    也可以用 lambda 表达式:

    PriorityQueue<Integer> priorityQueue = new PriorityQueue<>((o1, o2) -> o2 - o1);
    

    优先级队列的扩容说明:

    • 容量小于 64 时,2倍扩容
    • 容量大于等于64,1.5倍扩容
    • 容量超过 MAX_ARRAY_SIZE,按照 MAX_ARRAY_SIZE 扩容

    Map

    Map 是接口,K-V模型,使用时需要实例化它的子类 TreeMap 或 HashMap

    Map.Entry

    Map.Entry 是 Map 内部实现的用来存放 键值对映射关系的内部类,该内部类中主要提供了 的获取,value 的设置以及 key 的比较方式

    方法 解释
    K getKey() 返回 entry 中的 key
    V getValue() 返回 entry 中的 value
    V setValue(V value) 将 value 替换为指定的 value

    注意:Key 不能设置

    Map 常用方法

    方法 解释
    V get(Object key) 返回 key 对应的 value
    V getOrDefault(Object key, V defaultValue) 返回 key 对应的 value,如 key 不存在,返回默认值
    V put(K key, V value) 设置 key 对应的 value
    V remove(Object key) 删除 key 对应的映射关系
    Set keySet() 返回所有 key 的不重复集合
    Collection values() 返回所有 value 的可重复集合
    Set> entrySet() 返回所有的 key-value 映射关系
    boolean containsKey(Object key) 判断是否包含 key
    boolean containsValue(Object value) 判断是否包含 value

    Set

    Set 是接口,K 模型,使用时需要实例化它的子类 TreeSet 或 HashSet

    常用方法

    方法 解释
    boolean add(E e) 添加元素,重复元素不会被添加成功
    void clear() 清空
    boolean contains(Object o) 判断 o 是否在集合中
    Iterator iterator() 返回迭代器
    boolean remove(Object o) 删除
    int size() 返回元素个数
    boolean isEmpty() 判断是否为空
    Object[] toArray() 将Set转换为数组
    boolean containsAll(Collection c) 判断集合 c 中的元素是否在Set中全部存在
    boolean addAll(Collection c) 将集合 c 中的元素添加到 Set 中,可以达到去重的效果

    实现 Set 接口的常用类还有 LinkedHashSet,是在 HashSet 的基础上维护了一个双向链表来记录元素的插入次序

    TreeSet 底层是 TreeMap 实现,查看源码:

    public TreeSet() {
        this(new TreeMap<E,Object>()); // 因为是 K 模型,第二个类型参数给的是Object,是用来占位的
    }
    

    下面简单了解一下源码:

    默认负载因子是0.75:

    Java 集合框架,泛型,包装类_第6张图片

    默认桶的初始大小为16:

    img

    只是初始化,桶的容量是 0,在第一次 put 的时候才会给哈希桶分配 16 的容量

    桶的最大容量:

    img

    树化的条件1:链表的长度>=8

    Java 集合框架,泛型,包装类_第7张图片

    树化的条件2:桶的容量>=64

    Java 集合框架,泛型,包装类_第8张图片

    树退化的条件

    img

    哈希桶

    Java 集合框架,泛型,包装类_第9张图片

    计算哈希值的方式:

    Java 集合框架,泛型,包装类_第10张图片

    h >>> 16 位将高位移到低位,然后与原来的 h 异或,这样得到的 h ,低 16 位既有高位的信息又有低位的信息。

    因为在 Java 的哈希表实现中,会使用二的幂次作为哈希表的大小,并使用位掩码进行索引计算。由于使用了二的幂次,高位的哈希值可能会在索引计算中失去一些信息,导致一些哈希冲突。为了减少这种冲突,采用了一种位传播的策略。

    在 Java 中,哈希表的数组长度为2的幂。这是因为在使用二进制表示时,取模运算(%)可以被优化为位运算,即使用掩码进行操作,而不是昂贵的除法运算。这种优化可以提高性能。

    例如,如果哈希表的长度为2^n(其中n是非负整数),那么对于任意正整数k,k % (2^n) 可以等效为 k & (2^n - 1)。

    你可能感兴趣的:(Java,java,开发语言)