初识集合框架

在阅读本文之前,建议读者优先阅读作者的Java专栏。


目录

前言

一、集合框架简介

二、集合框架的数据结构与算法知识

三、初识包装类及泛型

总结


前言

本文主要介绍Java中的集合框架部分的相关知识。


一、集合框架简介

Java集合框架又被称为容器,它是定义在java.util包下的一组接口和其实现类,其主要表现为将多个元素置于一个单元中,用于对这些元素快速便捷的存储、检索和管理,也即我们平时俗称的增删改查。这些接口和类大致如下图所示。

二、集合框架的数据结构与算法知识

数据结构是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的

集合。该阶段,我们主要学习以下容器,每个容器其实都是对某种特定数据结构的封装,大概了解一下,后序会给大家详细讲解并模拟实现。Collection,是一个接口,包含了大部分容器常用的一些方法;List,是一个接口,规范了ArrayList和LinkedList中要实现的方法;ArrayList,实现了List接口,底层为动态类型顺序表;LinkedList,实现了List接口,底层为双向链表;Stack,底层是栈,栈是一种特殊的顺序表;Queue,底层是队列,队列是一种特殊的顺序表;Deque,是一个接口;Set,集合,是一个接口,里面放置的是K模型;HashSet,底层为哈希桶,查询的时间复杂度为O(1);TreeSet,底层为红黑树,查询的时间复杂度为O(logN),关于key有序的;Map,映射,里面存储的是K-V模型的键值对;HashMap,底层为哈希桶,查询时间复杂度为O(1),TreeMap,底层为红黑树,查询的时间复杂度为O(logN),关于key有序。

关于时间复杂度和空间复杂度的相关知识,请参考我下面的文章:

零基础入门C语言之复杂度讲解-CSDN博客

三、初识包装类及泛型

在Java中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型,它们基本有着如下的对应:

基本数据类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

那么在了解上述部分之后,我们需要来介绍一下装箱和拆箱这两种操作。装箱就是把基本数据类型变为包装类类型的过程,比如我们如下的代码:

java 复制代码
public class Main {
    public static void main(String[] args) {
        int a = 10;
        Integer i1 = Integer.valueOf(a);
        Integer i2 = 10;
        System.out.println(i1);
        System.out.println(i2);
    }
}

其运行结果如下:

我们此时可以用javap命令来观看一下它底层的过程:

可以看到我用红框圈出的部分其实就是我们两个Integer所在的语句,同时虽然第二句并没有调用其中的valueOf方法,编译器也自动帮我们进行了这个操作。我们就把第一句就叫做显式装箱,第二句为隐式装箱。然后我们再键入如下的代码:

java 复制代码
public class Main {
    public static void main(String[] args) {
        Integer i1 = 10;
        int i2 = i1;
        int i3 = i1.intValue();
        double i4 = i1.doubleValue();
        System.out.println(i1);
        System.out.println(i2);
        System.out.println(i3);
        System.out.println(i4);
    }
}

其运行结果如下:

同样我们用javap命令来看一下底层运行过程:

可以看到上面圈出的这三部分,其实就是拆箱,所以拆箱就是把包装类类型变为基本数据类型的过程。我们接下来看看下面这部分代码,读者可以先行猜测一下这段代码最终运行的结果:

java 复制代码
public class Main {
    public static void main(String[] args) {
        Integer a = 100;
        Integer b = 100;
        System.out.println(a == b);
        Integer c = 200;
        Integer d = 200;
        System.out.println(c == d);
    }
}

其运行结果如下:

为什么会产生这种结果呢?我们可以去看看Integer的源码:

java 复制代码
@IntrinsicCandidate
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

根据这部分源码,我们就可以初步猜测源码中的i应该是在一个范围的时候,就直接去数组拿值,当不在这个范围的时候,它就会返回新的对象,而我们如果用等号去比较新的对象的话,那必然是不一样的。那么这个low和high都分别是多少呢?我们可以去看看:

java 复制代码
            int h = 127;
            String integerCacheHighPropValue =
                VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    h = Math.max(parseInt(integerCacheHighPropValue), 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

所以我们这-128~127这256个数字就被存放在cache这个数组之中,然后从-128开始按顺序存放在整个cache数组之中。然后我们再来看一下什么是泛型?一般的类和方法,只能使用具体的类型,要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。泛型是在JDK1.5引入的新的语法,通俗讲泛型就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化。比如现在,我们需要实现一个类,这个类中包含一个数组成员,使得数组之中可以存放任何类型的数据,也可以根据成员方法返回数组之中某个下标的值。由于Object是所有类的父类,我们就借用一下它,给出如下的代码:

java 复制代码
class myArray{
    public Object[] array = new Object[10];

    public void setValue(int pos, Object val){
        array[pos] = val;
    }

    public Object getValue(int pos){
        return array[pos];
    }
}
public class Main {
    public static void main(String[] args) {
        myArray obj = new myArray();
        obj.setValue(0, 10);
        obj.setValue(1, "hello");

        System.out.println(obj.getValue(0));
        System.out.println(obj.getValue(1));
    }
}

其运行结果如下:

但是在我们写完这部分代码之后,我们就会有两个比较明显的问题。第一个就是这种方式存放数据过于杂乱了,我们什么类的数据都可以放进去;第二个就是如果说我们不是直接输出的话,而是获取这个数据并存放到一个变量之中,我们就必须去进行强转,而每次都要强转就十分麻烦。这个时候我们就可以借用泛型来解决这些问题。泛型的主要目的就是指定当前的容器,要持有什么类型的对象。让编译器去做检查。此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。它的基本语法就是如下形式:

java 复制代码
class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}

那我们就可以对我们上面的代码进行相关的改造:

java 复制代码
class myArray<E>{
    public Object[] array = new Object[10];

    public void setValue(int pos, Object val){
        array[pos] = val;
    }

    public E getValue(int pos){
        return (E)array[pos];
    }
}
public class Main {
    public static void main(String[] args) {
        myArray<Integer> myArray = new myArray<>();
        myArray.setValue(0, 10);
        myArray.setValue(1, 20);
        myArray.setValue(2, 30);

        Integer a = myArray.getValue(0);
        Integer b = myArray.getValue(1);
        Integer c = myArray.getValue(2);

        System.out.println(a);
        System.out.println(b);
        System.out.println(c);

        myArray<String> arr = new myArray<>();
        arr.setValue(0,"hello");
        arr.setValue(1," world");

        String s1 =  arr.getValue(0);
        String s2 =  arr.getValue(1);
        System.out.println(s1 + s2);
    }
}

其运行结果如下:

需要注意的是,首先我们类名后的<E>代表占位符,表示当前类是一个泛型类,它其中的形参一般都是用一个大写字母来代替的,常用的有如下几种,E表示Element,K表示Key,V表示Value,N表示Number,T表示Type,S, U, V 等等表示第二、第三、第四个类型;其次我们不能够new泛型类型的数据,应该在类型后面加入<>来指定当前的类型;然后我们如果说在指定好类型之后传输了一个错误的类型就会报错,这是因为在我们创建对象的时候就已经指定类当前的类型了,到我们传输数据的时候,编译器就会替我们去做检查;最后我们的泛型只能接受包装类,不允许直接传入基本数据类型,而如果说编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写,也就是下面这种意思:

java 复制代码
MyArray<Integer> list = new MyArray<>();

此时如果说我们修改一下上面代码部分变为下面这样:

java 复制代码
myArray myArray = new myArray();
myArray.setValue(0, 10);
myArray.setValue(1, 20);
myArray.setValue(2, "abcdef");
Integer a = myArray.getValue(0);
Integer b = myArray.getValue(1);
Integer c = (Integer)myArray.getValue(2);       

此时我们并没有写<>但是却没有报错,这就是一种裸类型,它是一个泛型类却没有带类型实参,我们不要自己去使用裸类型,它是为了兼容老版本的API保留的机制。在了解完上面关于泛型使用的部分之后,我们再来聊聊泛型是如何编译的呢?实际上,泛型是编译时期的一种机制,在运行的时候我们没有泛型的概念。我们可以利用javap命令来看一下:

我们发现实际上这个E其实被替换为了Object类,我们就称这种机制为擦除机制。Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。具体有关泛型擦除机制的文章可以参考下面:

Java泛型擦除机制之答疑解惑 - 知乎

在我们定义泛型类的时候,有时需要对传入的类型变量做一定约束,这时我们可以通过类型边界来约束。它遵循如下的语法形式:

java 复制代码
class 泛型类名称<类型形参 extends 类型边界> {
...
}

同样我们也可以将泛型和方法结合:

java 复制代码
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }

总结

本文介绍了Java集合框架的基础知识,包括集合框架的概念、常用数据结构与算法,以及包装类和泛型的使用。集合框架是Java中用于存储和管理数据的核心组件,包含List、Set、Map等多种容器。文章详细讲解了包装类的装箱拆箱机制,以及泛型的定义和使用方法,包括泛型类的创建、类型擦除机制等。通过代码示例展示了如何利用泛型提高代码的安全性和灵活性,同时解释了泛型在编译期的处理机制。

相关推荐
Zzxy2 小时前
Spring Boot 参数校验
java·spring boot
黑眼圈子2 小时前
总结一下用Java做算法的常用类和方法
java·开发语言·算法
Magic--2 小时前
深入解析管道:最基础的进程间通信(IPC)实现
java·服务器·unix
如何原谅奋力过但无声3 小时前
【chap11-动态规划(上 - 基础题目&背包问题)】用Python3刷《代码随想录》
数据结构·python·算法·动态规划
架构师沉默3 小时前
为什么国外程序员都写独立博客,而国内都在公众号?
java·后端·架构
带刺的坐椅3 小时前
SolonCode v2026.4.1 发布(比 ClaudeCode 简约的编程智能体)
java·ai·llm·agent·solon-ai·claudecode·soloncode
殷紫川3 小时前
从单体到亿级流量:登录功能全场景设计指南,踩过的坑全给你填平了
java
Filwaod3 小时前
Cursor+IDEA开发问题
java·idea·cursor
爱丽_3 小时前
Spring 事务:传播行为、失效场景、回滚规则与最佳实践
java·后端·spring