Java动态数组(一)

Java动态数组(一)

前提概要

在《C 语言实现数组动态扩容》一文中,我们已经介绍了 C 语言中数组动态扩容的实现方式。实际上,在 Java 中同样可以实现类似的效果。

创建动态数组

在 Java 中,我们不需要像在 C 语言中那样手动分配内存。Java 已经为我们提供了现成的动态数组实现 ------ ArrayList,它是一个可以自动扩容的"可变长数组"。

它的底层原理其实并不复杂:

  • 本质上依然是一个普通数组;
  • 当元素装满后,会自动申请一个更大的数组,并将原数组中的元素复制过去;
  • 因此,我们只需关心"往里添加元素",而无需担心容量是否足够。

创建 ArrayList 的示例代码如下(第一行 import 是把 ArrayList 这个类引入当前文件,不要忘记加!):

java 复制代码
import java.util.ArrayList;

ArrayList<String> list1 = new ArrayList<>();

下面逐段拆解这行代码:

  1. ArrayList:类名,表示"动态数组"这个现成的类。
  2. <String> :泛型,表示该列表只能存储 String 类型的元素。
    • 指定 <String> 后,如果执行 list.add(100),编译器会直接报错。
    • 如果不写泛型(如 ArrayList list2 = new ArrayList<>()),则列表可以存储任意类型的元素。
  3. new ArrayList<>() :创建对象。后面的 <> 中无需再次指定类型,编译器会自动推断。() 里还可以传一个整数,比如 new ArrayList<>(20),用来设置初始容量,表示一开始先预留 20 个存储位置;如果已经知道大概要存多少个元素,提前设置可以减少自动扩容次数。不传也没关系,默认会自动处理,后续元素超出容量时仍然会自动扩容。

泛型

ArrayList<String> 中的 <String> 就叫"泛型",它用来限定这个动态数组只能存放哪一种类型的数据。

除了 String,你还可以根据实际需要指定其他类型,例如:

  • ArrayList<Integer>:只能存放整数;
  • ArrayList<Double>:只能存放小数;

注意:尖括号里不能写基本类型,比如 int,必须写成对应的包装类 Integer。

基本数据类型的封装类

Java 有 8 种基本数据类型,但它们都是关键字,不是一个真正的类。为了让基本类型也能像对象一样使用,比如存进集合、调用方法等,Java 针对每种基本数据类型都定义了对应的类,这些类统称为「封装类」。

常见对应关系如下:

  • int → Integer(整型 → 整数封装类)
  • byte → Byte(字节型 → 字节封装类)
  • long → Long(长整型 → 长整型封装类)
  • float → Float(单精度浮点型 → 单精度浮点封装类)
  • double → Double(双精度浮点型 → 双精度浮点封装类)

自动装箱与自动拆箱

一般情况下,使用 int 和 Integer 没有明显区别,这主要依赖自动装箱和自动拆箱:

java 复制代码
int a = 100;

// 自动装箱:把一个普通整数变量,存入 Integer 对象变量中
Integer a1 = a;

// 自动拆箱:把 Integer 对象变量转为基本类型整数变量
int a2 = a1;

封装类的独特优势

封装类最大的好处就是:它本质是一个真正的类,可以直接调用现成的方法。

基本类型只是一个数据,比如:

java 复制代码
int a = 100;
// 在这里敲 a. 不会弹出任何方法,因为 int 不是对象

而换成封装类之后:

java 复制代码
Integer b = 100;
// 在这里敲 b. 会弹出一堆现成的方法,直接用就行

下面按用途把常用的方法整理成几类。

1. 字符串和数字互相转换

  • Integer.parseInt("250"):把"字符串形式的数字"转成真正的数字,常用于处理用户输入、文件读取等场景。
  • b.toString():把 Integer 对象转回字符串,例如 b.toString() 会得到 "100"。
java 复制代码
int num = Integer.parseInt("250"); // 字符串 "250" 变成数字 250
String s = b.toString();           // Integer 对象变成字符串 "100"

2. 比较大小、取最大值/最小值

  • Integer.max(10, 20):返回两个数中的较大值,例如结果是 20。
  • Integer.min(10, 20):返回两个数中的较小值,例如结果是 10。
  • b.compareTo(200):比较 b 和 200 的大小,返回一个 int 类型的结果:结果为 -1 表示 b 比 200 小,等于 0 表示两者相等,结果为 1 表示 b 比 200 大。

3. 类型转换

  • b.intValue():把 Integer 对象转成 int 基本类型。
  • b.doubleValue():把 Integer 对象转成 double 基本类型。

4. 获取常量或边界值

  • Integer.MAX_VALUE:直接获取 int 的最大值 2147483647。
  • 同理,Integer.MIN_VALUE 可以获取 int 的最小值。

5. 进制转换

  • Integer.toBinaryString(10):把十进制整数 10 转成二进制字符串,结果是 "1010"。
java 复制代码
int a = 10;
String bin = Integer.toBinaryString(a); // 得到 "1010"

使用注意事项

  • 定义 List 这类集合时,泛型必须写 Integer,不能写 int。
  • 比较两个 Integer 时,建议使用 equals() 而不是 ==。因为 == 比较的是对象地址,equals() 比较的才是具体内容。

例如:

java 复制代码
Integer b1 = 1828;
Integer b2 = 1828;

System.out.println(b1 == b2);      // false,超出缓存范围后比较的是对象地址
System.out.println(b1.equals(b2)); // true,比较的是具体内容

// 定义 List 时,泛型使用 Integer
ArrayList<Integer> list2 = new ArrayList<>();

常用操作方法

ArrayList 的常用方法可以按「增删改查」来记忆。下面先用一张表快速浏览,再配合一段示例代码。

操作 代码 说明
添加 list.add("abc") 添加到末尾
插入 list.add(0, "abc") 插入到指定位置,原有元素会向后移
获取 list.get(0) 取第 0 个元素,索引从 0 开始
修改 list.set(0, "xyz") 把第 0 个元素换成 "xyz"
删除 list.remove(0) 删除第 0 个元素
按内容删除 list.remove("abc") 删除第一个值为 "abc" 的元素
清空 list.clear() 删除全部元素,但集合对象还在
长度 list.size() 获取元素个数,注意不是数组的 length
是否为空 list.isEmpty() 没有元素时返回 true
是否包含 list.contains("abc") 是否包含该元素,返回 true 或 false
查找位置 list.indexOf("abc") 返回第一次出现的位置,不存在时返回 -1

与静态数组的方法对比

静态数组创建后长度固定,也没有 add()、remove() 这些方法;ArrayList 因为是真正的类,所以几乎所有操作都可以"点方法"完成。下面把同一个需求放到一起对比:

需求 静态数组写法 ArrayList 写法
创建容器 String[] arr = new String[3]; ArrayList<String> list = new ArrayList<>();
添加元素 arr[0] = "abc"; list.add("abc");
插入元素 需要手写移动逻辑,很麻烦 list.add(0, "top");
获取元素 arr[0] list.get(0)
修改元素 arr[0] = "xyz"; list.set(0, "xyz");
删除元素 arr[0] = null;(只是置空,不是真正删除) list.remove(0);
获取长度 arr.length(没有括号,是属性) list.size()(有括号,是方法)
是否包含 需要自己写循环判断 list.contains("abc");
是否为空 需要自己判断 arr.length == 0 list.isEmpty();

从表中可以看到两个关键区别:

  1. 静态数组的长度是固定的 ,创建时就要指定大小;ArrayList 会自动扩容,添加元素时不用提前考虑容量。
  2. 静态数组通过"下标 + 赋值"操作,ArrayList 通过方法调用操作 :数组有 length 属性,ArrayList 用 size() 方法;ArrayList 还能直接调用 add()、remove()、contains() 等现成方法。

关于「打印」和「排序」,两者都能做到,但写法不一样,而且不是都能直接 toString 打印出内容。

1. 打印内容:toString()

  • ArrayList 重写了 toString(),直接打印就能看到元素内容:
java 复制代码
ArrayList<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");

System.out.println(list); // [张三, 李四]
  • 静态数组不能直接打印出内容,直接打印会得到类似 [Ljava.lang.String;@1b6d3586 这样的"类型@地址"。要查看元素内容,需要使用 Arrays.toString(),并且记得在文件开头导入 java.util.Arrays:
java 复制代码
import java.util.Arrays;

String[] arr = {"张三", "李四"};

System.out.println(arr);                  // [Ljava.lang.String;@1b6d3586,看不到内容
System.out.println(Arrays.toString(arr)); // [张三, 李四]

2. 排序:sort()

排序,就是把容器里的元素按从小到大的顺序重新排列。静态数组和 ArrayList 都支持排序,调用排序方法后,原容器里的内容会被直接改变,方法本身不需要接收返回值。

静态数组:使用 Arrays.sort()

Arrays 是专门为数组提供辅助功能的工具类,排序前需要先在文件开头导入:

java 复制代码
import java.util.Arrays;

排序时直接把数组传进去即可,默认按从小到大(升序)排列:

java 复制代码
int[] arr = {3, 1, 2};

Arrays.sort(arr);                          // 默认从小到大
System.out.println(Arrays.toString(arr));  // [1, 2, 3]
  • Arrays.sort(arr) 没有返回值 ,调用结束后 arr 本身已经被重新排列;
  • 打印时仍然要使用 Arrays.toString(),直接打印 arr 依然看不到元素内容。

ArrayList:使用 Collections.sort() 或 list.sort()

ArrayList 是集合,排序有两种常见写法,效果完全一样,默认都是升序。

第一种,使用集合工具类 Collections,同样需要先导入:

java 复制代码
import java.util.Collections;

Collections.sort(list);   // 默认从小到大

第二种,直接调用 ArrayList 自带的 sort() 方法(Java 8 及以上可用),括号里传 null 表示按照"自然顺序"排序,也就是默认升序:

java 复制代码
list.sort(null);          // 与 Collections.sort(list) 等价,默认升序

完整示例如下:

java 复制代码
import java.util.ArrayList;
import java.util.Collections;

ArrayList<Integer> list = new ArrayList<>();
list.add(3);
list.add(1);
list.add(2);

Collections.sort(list);   // 也可以写成 list.sort(null);
System.out.println(list); // [1, 2, 3]

注意:ArrayList 重写了 toString(),所以排序完成后直接 System.out.println(list) 就能看到 [1, 2, 3] 这样的内容,不需要再做额外转换。

下期预告

虽然 ArrayList 已经帮我们实现了自动扩容,日常使用非常方便,但在一些比赛场景(例如蓝桥杯)中,还是要留意 JDK 版本带来的限制。

这里先纠正一个常见误解:并不是低版本 JDK 用不了 ArrayList 。ArrayList 从很早期的 JDK 1.2 就已经存在,它本身不是新特性。

真正的差别在于:不同 JDK 版本下,能用的方法不一样。以排序为例:

java 复制代码
list.sort(null);          // JDK 8 及以上才能使用
Collections.sort(list);   // 老版本 JDK 也能使用

List 接口自带的 sort() 方法是 Java 8 才加入的。如果比赛环境使用的是 JDK 7 或更早版本,写 list.sort(null) 会编译不通过,只能改用 Collections.sort(list)。

蓝桥杯等比赛的历史环境普遍比较保守,很长一段时间使用的是 JDK 1.6、1.7、1.8 这样的老版本,而不是最新 JDK。因此,一些新语法、新方法在比赛中可能会"水土不服"。

正因如此,下一期我们会回到底层,用最基础的语法手写一个"自动扩容的数组" ,不依赖某个新版本才有的特性。这样即使换到低版本比赛环境,代码也能稳定运行,同时还能真正搞懂 ArrayList 自动扩容背后的原理。

相关推荐
\光辉岁月/1 小时前
3.包装类
java·开发语言·包装类
我命由我123451 小时前
软件工程概念 - 可插拔性
java·后端·学习·java-ee·系统架构·软件工程·学习方法
木易 士心2 小时前
Java 后端驱动远程手机自动化:ADB + Python + 内网穿透实战全解
java·后端·智能手机·自动化
十年Java程序媛2 小时前
JVM 内存区域与对象内存布局|new 对象底层流程,对象头 MarkWord 详解
java·jvm·spring boot
用户094248568032 小时前
第2章:Windows、WSL2 与 Docker 环境搭建Redis
java·jvm
小物学长2 小时前
HJ212-2017 对接与环境在线监测平台设计:报文、CRC、应答、预警和传输有效率
java
171320330计算机毕设编程2 小时前
计算机毕业设计 基于springboot+vue的在线观鸟视频播放网站 LW PPT
java·大数据·vue.js·spring boot·算法·课程设计
IT枫斗者枫哥2 小时前
JDBC事务实测:操作日志写入失败时,让客户修改一起回滚
java
OxYGC2 小时前
Spring 面经系列 · 第 1 篇:Bean 生命周期、单例语义、依赖注入与线程安全
java·安全·spring