【Java基础】爆肝2万字速通Java基础
#免责声明#
本篇诞生初衷用于复习Java基础,并不适合纯新人学习
注(更新的注解会添加到这里)
更正了collation和map的关系
API:接口和方法
熟悉基本数据类型
| 类型 | 位数 | 包装类 | 取值范围 | 所属种类 |
|---|---|---|---|---|
| byte | 8 | Byte | -128~127 | 整数型 |
| short | 16 | Short | -32768~32767 | 整数型 |
| int | 32 | Integer | -2³¹ ~ 2³¹‑1 | 整数型 |
| long | 64 | Long | -2⁶³ ~2⁶³‑1 | 整数型 |
| float | 32 | Float | ±3.40282347E+38 (≈7 位十进制有效位) | 浮点型 |
| double | 64 | Double | ±1.7976931348623157E+308 (≈15 位十进制有效位) | 浮点型 |
| char | 16 | Character | '\u0000' .. '\uFFFF' (Unicode 基本多语言平面) |
字符型 |
| boolean | 1 | Boolean | true / false |
布尔型 |
| void | - | - | 仅用于方法返回类型 | 空返回型 |
强制转换:(数据类型)
float a = 10.0f;
int b = (int) a;
System.out.println(b);
自动转换也称隐式转换,小范围会在特定条件下自动像大范围转换,但是:
- 数据类型兼容性:两种数据类型必须兼容,例如整型和浮点型之间可以自动转换,但布尔类型不能与其他类型自动转换。
- 目标类型范围更大:目标数据类型的范围必须大于源数据类型。例如,
int可以自动转换为long,因为long的范围更大。
数值取值范围(自动类型转换规则):
byte→short→int→long→float→doublechar→int→long→float→double
数组:
什么是数组?
| 特点 | 解释 |
|---|---|
| 固定长度 | 创建后长度就不能改变(除非新建一个更大的数组再拷贝)。 |
| 元素同质 | 同一个数组里只能放 同一种类型(基本类型或同一类对象)。 |
| 连续内存 | 在堆(或栈)里占用 连续的内存块,读取时 CPU 缓存友好,速度极快。 |
| 本质是对象 | int[]、String[] 等本身是 引用类型,可以赋值给 Object,可以 null。 |
类比:数组就像一排排好编号的格子(下标),每个格子只能放一种东西,而且格子数在建造时就已经确定。
声明、创建 & 初始化
基本语法(最常见的几种写法)
| 写法 | 说明 | 示例 |
|---|---|---|
| 声明 + 创建 | 同时声明变量并分配内存 | int[] arr = new int[5]; |
| 声明 + 初始化列表 | 在代码里直接写出每个元素的值 | int[] arr = {1, 2, 3, 4, 5}; |
| 只声明,稍后创建 | 适用于以后再决定长度 | String[] names;names = new String[3]; |
使用 var(JDK 10+) |
让编译器自行推断类型 | var numbers = new int[4]; |
注意:方括号
[]可以写在 类型 或 变量名 的后面,两者等价。
int[] a; // 推荐写法 int a[]; // 老式写法,仍然可以编译
### 2.2 默认值(未手动赋值时)
| 类型 | 默认值 |
|------|--------|
| `byte`/`short`/`int`/`long` | `0` |
| `float`/`double` | `0.0` |
| `char` | `'\u0000'`(空字符) |
| `boolean` | `false` |
| **引用类型**(如 `String`、自定义对象) | `null` |
> **示例**:
> ```java
int[] a = new int[3]; // a[0]、a[1]、a[2] 都是 0
String[] s = new String[2]; // s[0]、s[1] 为 null
手动赋值
int[] nums = new int[4];
nums[0] = 10;
nums[1] = 20;
nums[2] = 30; // 可以直接通过下标赋值
nums[3] = 40;
访问元素 & 长度
| 操作 | 示例 | 说明 |
|---|---|---|
| 读取 | int x = nums[2]; |
读取下标为 2 的元素(下标从 0 开始)。 |
| 写入 | nums[2] = 99; |
替换下标 2 的值。 |
| 数组长度 | int len = nums.length; |
length 是 属性(不是方法),返回该数组的固定大小。 |
常见错误:下标越界会抛
ArrayIndexOutOfBoundsException,典型错误是 使用<=而不是<:
for (int i = 0; i < nums.length; i++) { … } // 正确
for (int i = 0; i <= nums.length; i++) { … } // 错误,最后一次 i=length 越界
遍历数组的几种写法
| 方法 | 代码示例 | 适用场景 |
|---|---|---|
普通 for 循环(可获取下标) |
java\nfor (int i = 0; i < arr.length; i++) { System.out.println(arr[i]); }\n |
需要下标或要在遍历中修改特定位置 |
增强 for‑each 循环 |
java\nfor (int v : arr) { System.out.println(v); }\n |
只需要读元素,代码最简洁 |
while 循环 |
java\nint i = 0;\nwhile (i < arr.length) { System.out.println(arr[i++]); }\n |
老式写法,功能等同于 for |
Arrays.stream(Java 8+) |
java\nArrays.stream(arr).forEach(System.out::println);\n |
想使用 Stream 的函数式操作(filter、map、reduce) |
多维数组(二维、三维 …)
基本概念:数组的数组
int[][] matrix = new int[3][4]; // 3 行、4 列的二维整型数组
int[][] board = { // 直接初始化(不规则长度也行)
{1, 2, 3},
{4, 5},
{6, 7, 8, 9}
};
- 每一行 实际上是一个
int[],所以 二维数组可以是“锯齿形”(行长度不一致)。 - 访问方式:
matrix[row][col],同理三维arr[i][j][k]。
常见遍历
for (int i = 0; i < matrix.length; i++) { // 行
for (int j = 0; j < matrix[i].length; j++) { // 列
System.out.print(matrix[i][j] + " ");
}
System.out.println();
}
java.util.Arrays 常用工具(让数组更好用)
| 方法 | 功能 | 示例 |
|---|---|---|
Arrays.toString(arr) |
把一维数组转成 可打印的字符串 | System.out.println(Arrays.toString(nums)); |
Arrays.deepToString(arr) |
递归打印 多维数组 | System.out.println(Arrays.deepToString(matrix)); |
Arrays.sort(arr) |
原地升序 排序(基本类型或实现了 Comparable 的对象) |
Arrays.sort(nums); |
Arrays.binarySearch(arr, key) |
对已 排好序 的数组进行 二分查找,返回下标或负数(未找到) | int idx = Arrays.binarySearch(sorted, 42); |
Arrays.copyOf(arr, newLen) |
复制数组(如果 newLen > 原长度会在末尾补 0/null) |
int[] copy = Arrays.copyOf(nums, 10); |
System.arraycopy(src, srcPos, dest, destPos, length) |
高效 的 数组拷贝(底层使用 System.arraycopy) |
System.arraycopy(nums, 0, copy, 2, nums.length); |
Arrays.fill(arr, value) |
把整个数组全部填充为同一个值 | Arrays.fill(arr, -1); |
Arrays.equals(a, b) |
比较两个一维数组内容是否相等(元素逐个比较) | boolean same = Arrays.equals(arr1, arr2); |
Arrays.deepEquals(a, b) |
用于 多维数组 的深度比较 | boolean same2 = Arrays.deepEquals(matrix1, matrix2); |
小贴士:
Arrays.sort对基本类型使用 原生快速排序(Dual-Pivot Quicksort),对对象使用TimSort(已优化的归并/插入混合)。如果想自定义排序规则,对象数组可以使用Arrays.sort(arr, Comparator)。
数组 vs. ArrayList
| 条件 | 推荐使用 | 说明 |
|---|---|---|
| 长度固定 / 已知 | 数组 (int[]) |
内存占用最小、访问最快 |
| 需要经常增删、长度不确定 | ArrayList<E> |
自动扩容、提供 add/remove,但内部仍是数组(复制成本) |
| 存放基本类型,且对性能极度敏感 | 基本类型数组 (int[], double[]) |
ArrayList<Integer> 会产生包装对象,额外 GC 开销 |
想使用 Collections 工具类(sort、binarySearch、shuffle) |
数组(配合 Arrays)或 ArrayList(配合 Collections) |
两者都有对应工具,任选其一即可 |
需要 null 标记“未赋值” |
数组(引用类型)或 ArrayList(都支持 null) |
基本类型数组不支持 null(只能用包装类) |
面试技巧:若面试官问 “为什么用数组而不是
ArrayList?”
答:① 固定长度、占用最少内存;② 访问 O(1) 并且没有装箱;③ 在高性能计算或底层 API(如java.nio.ByteBuffer)必须使用数组。
常见错误 & 防御技巧
| 错误情形 | 解释 | 正确做法 |
|---|---|---|
| 数组越界 | 使用了 arr[> length-1] 或负数下标 |
永远使用 i < arr.length 作为循环条件 |
误把 length 当作方法 |
arr.length(); 编译错误 |
正确写法 arr.length(没有括号) |
把数组当作 List 使用(想 add) |
int[] a = new int[3]; a.add(5); 报错 |
用 ArrayList<Integer>,或手动 扩容 int[] b = Arrays.copyOf(a, a.length+1); b[a.length]=5; |
忘记数组默认值导致 NullPointerException |
String[] s = new String[3]; System.out.println(s[0].length()); |
先判空或在创建后手动填充 s[i] = "" |
使用 System.arraycopy 参数错误 |
srcPos、destPos、length 写错会导致错误复制或 ArrayIndexOutOfBoundsException |
参考官方文档,确保起始位置 + 复制长度不超过任何数组长度 |
| 多维数组不规则导致空指针 | int[][] a = new int[3][]; a[0] = new int[2]; // a[1]、a[2] 仍为 null |
在遍历前先判断 a[i] != null,或者一次性完整初始化 |
| 数组与集合混用时的包装装箱 | List<Integer> list = new ArrayList<>(Arrays.asList(arr));(arr 为 int[])会产生 装箱 |
使用 IntStream.of(arr).boxed().collect(Collectors.toList()) 或手写循环装箱,注意 GC 开销 |
进阶话题
| 主题 | 简要说明 |
|---|---|
| 可变长度参数(Varargs) | 方法签名 void foo(int... nums) 实际在内部会生成一个 int[],调用时可以传多个或直接传数组。 |
数组的克隆(clone()) |
int[] copy = original.clone(); 产生 浅拷贝(基本类型数组完整复制,引用类型数组只复制引用)。 |
| 数组的序列化 | ObjectOutputStream 可以直接写入 int[],Arrays 没有直接提供序列化方法。 |
| 内存布局 | Java 中数组对象头 12~16 字节 + 实际元素数据(基本类型占原始字节数,引用占 4 / 8 字节),一次性分配,适合大量数据的连续读取。 |
数组与 java.nio(ByteBuffer) |
ByteBuffer.wrap(byteArray) 可把 byte[] 包装成 NIO 缓冲区,进行高效 I/O(网络、文件)。 |
| 数组的并行流 | IntStream.of(arr).parallel().forEach(...); 在多核 CPU 上并行处理。 |
String类
String是对象(不是基本类型),用来保存文本(一串字符)
String类型用于字符串的使用,但是String类型是不可变类型(定义后无法更改)
String s = "hello";
常见创建方法:
| 方式 | 示例 | 说明 |
|---|---|---|
| 字面量(最常见) | String s = "Java"; |
编译器直接生成对象,放进 字符串常量池(后面会讲) |
**new**关键字 |
String s = new String("Java"); |
每次都会新建对象,除非有特殊需求不要这样写 |
| 拼接 | String s = "Hello, " + name; |
+ 在编译时会转成 StringBuilder,适合少量拼接 |
String.valueOf |
String s = String.valueOf(123); |
把任意类型(int、double、Object)转换成字符串 |
toString() |
String s = obj.toString(); |
只要对象重写了 toString,就可以得到它的字符串表示 |
String 常用方法
| 方法 | 用途 | 示例 |
|---|---|---|
length() |
返回字符个数 | s.length(); // "Java".length() = 4 |
charAt(int i) |
取第 i(从 0 开始)个字符 |
"abc".charAt(1) // 'b' |
substring(int begin) / substring(int begin, int end) |
截取子串 | "hello".substring(1,4) // "ell" |
indexOf(String sub) |
查找子串第一次出现的位置,找不到返回 -1 |
"hello".indexOf("l") // 2 |
contains(CharSequence s) |
判断是否包含某子串 | "hello".contains("ll") // true |
replace(char old, char new) |
替换字符 | "hello".replace('l','x') // "hexxo" |
replaceAll(String regex, String repl) |
正则替换(高级) | "123-456".replaceAll("\\d", "*") // "***-***" |
split(String regex) |
按正则分割成数组 | "a,b,c".split(",") // ["a","b","c"] |
toUpperCase() / toLowerCase() |
大小写转换 | "Java".toUpperCase() // "JAVA" |
trim() |
去掉首尾空格(ASCII 空格) | " abc ".trim() // "abc" |
equals(Object o) |
内容相等比较(最常用) | "abc".equals(s2) |
compareTo(String other) |
字典序比较,返回负/0/正 | "a".compareTo("b") // -1 |
valueOf(...)(静态) |
任意类型转字符串 | String s = String.valueOf(3.14); |
format(String fmt, Object... args) |
按格式拼接(类似 printf) | String s = String.format("Hi %s, age %d", name, age); |
记住:**判断两个字符串内容是否相同,一定要用
equals,不要用==(==只比较引用是否相同)。
String 与 StringBuilder / StringBuffer 的区别
| 类 | 是否可变 | 是否线程安全 | 适用场景 |
|---|---|---|---|
String |
不可变 | (因为不可变) | 用来保存最终的、不会再改的文本 |
StringBuilder |
可变 | 仅限单线程 | 大量拼接(如循环中)推荐使用 |
StringBuffer |
可变 | 同步,线程安全 | 必须在多线程共享同一个可变字符串时使用(极少见) |
常见错误
| 错误 | 说明 | 正确写法 |
|---|---|---|
用 == 比较字符串内容 |
== 比的是 引用,不是内容 |
if (s1.equals(s2)) |
在循环里大量使用 + 拼接 |
每次循环都会产生新对象,性能差 | 使用 StringBuilder |
忘记 trim 只去掉 ASCII 空格 |
中文全角空格或其他 Unicode 空白不会被去掉 | JDK 11+ 用 s.strip() |
直接把 byte[] 当 String 用 |
需要解码成字符 | new String(bytes, StandardCharsets.UTF_8) |
substring 用旧版 JDK 仍共享底层数组 |
子串会持有大字符串的全部字符数组(内存泄漏) | JDK 7u6 以后已解决;如果你仍在老版,手动 new String(sub) |
| 把数字直接加到字符串上误当作算术 | "10" + 2 结果是 "102"(字符串拼接) |
如果要做加法先把字符串转成数值:int sum = Integer.parseInt(str) + 2; |
把其他类型和 String 互相转换
| 场景 | 代码示例 |
|---|---|
| 数值 → 字符串 | String s = String.valueOf(123);String s2 = Integer.toString(123); |
| 字符串 → 整数 | int n = Integer.parseInt("456");int n2 = Integer.valueOf("456"); // 自动装箱成 Integer |
| 字符串 → 浮点数 | double d = Double.parseDouble("3.14"); |
| 对象 → 字符串 | String s = obj.toString(); // 前提是对象已经实现了 toString |
| 字符串 → 字节数组(编码) | byte[] bytes = s.getBytes(StandardCharsets.UTF_8); |
| 字节数组 → 字符串(解码) | String s = new String(bytes, StandardCharsets.UTF_8); |
String= 不可变的文本对象,内容相同就用equals,大量拼接用StringBuilder,碰到字符编码要显式指定,需要共享同一文本时才使用intern()。
面向对象三大特性:封装,继承,多态。
在面向对象中,门打开这个动作是门自己打开,然后程序调用,而不是程序主动打开门!!!
同理可得:上楼梯是楼梯自己上然后程序调用
封装(Encapsulation)——把内部细节藏起来,只暴露安全的入口
什么是封装?
把 属性(字段) 和 行为(方法) 打包进同一个类里,并 限制外部直接访问。
只有类内部自己可以直接改字段,外部只能通过 受控的 getter / setter(或只读的 getter)来获取或修改。
为什么要封装?
| 目的 | 好处 |
|---|---|
| 防止外部随意改数据 | 避免产生非法状态(比如年龄负数) |
| 隐藏实现细节 | 改动内部实现时,外部代码不需要修改 |
| 提供统一的校验/业务规则 | 所有修改都经过同一段代码,易维护 |
| 提升可读性 & 可维护性 | 代码使用者只需要记住 “我能干什么”,不必关心 “内部怎么实现”。 |
常用关键字
| 关键字 | 作用 |
|---|---|
private |
成员只能在本类内部访问 → 最常用 |
protected |
本类、同包、子类可见 |
public |
任意位置都能访问(慎用) |
default(不写修饰符) |
同包可见 |
public class Person {
// ① 成员属性设为 private,外部看不到
private String name;
private int age; // 年龄应该是非负数
// ② 构造器:创建对象时必须提供合法值
public Person(String name, int age) {
this.name = name;
setAge(age); // 用 setter 统一校验
}
// ③ 只读的 getter
public String getName() {
return name;
}
// ④ 受控的 setter(可以加入校验逻辑)
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负");
}
this.age = age;
}
// ⑤ 行为:提供对外的“操作”
public void introduce() {
System.out.println("我叫 " + name + ",今年 " + age + " 岁。");
}
}
注:以上代码还有一个名字,叫做JavaBeen类
调用:
Person p = new Person("小明", 20); //在main方法中调用
p.introduce(); //正常
p.setAge(25); //合法更新
// p.age = -5; //编译错误,外部不能直接改
继承(Inheritance)——建立 “是 … 的” 关系,代码复用
什么是继承?
子类 继承 父类的属性和方法,并可以 扩展 自己的特有功能。
用一句话概括:子类是父类的 “特例”。(“Student 是 Person 的一种”。)
关键语法
class 子类名 extends 父类名 { … }
extends表示 单继承(Java 每个类只能有一个直接父类)。- 子类可以 重写(override)父类的非
final方法,也可以 新增属性/方法。
为什么要继承?
| 好处 | 说明 |
|---|---|
| 代码复用 | 父类的通用属性/方法不必在每个子类里重复写 |
| 统一抽象 | 把所有子类的共性抽象到父类,便于管理 |
| 多态的前置 | 只有建立了父子关系,才能在运行时把子类对象当成父类使用(后面讲) |
// Person.java (前面的代码保持不变)
// Student.java
public class Student extends Person { // 继承 Person
private String school; // 新增属性
public Student(String name, int age, String school) {
super(name, age); // 调用父类构造器,完成 name、age 初始化
this.school = school;
}
public String getSchool() {
return school;
}
// 给 Student 加一个新行为
public void study(String subject) {
System.out.println(getName() + " 在 " + school + " 学习 " + subject);
}
// 重写父类的方法,让表现更具体
@Override
public void introduce() {
// super.introduce() 调用父类的实现,后面加自己的信息
super.introduce(); // “我叫 小明,今年 20 岁。”
System.out.println("我在 " + school + " 学习。");
}
}
调用:
Student s = new Student("小红", 19, "清华大学");
s.introduce(); // 父类+子类的自我介绍
s.study("数学"); // 子类特有行为
extends→ 建立 “is‑a” 关系。super关键字用于 调用父类 的构造器或成员方法。- 可以 覆盖(
@Override)父类方法,实现子类自己的表现。
多态(Polymorphism)——同一个引用,不同的行为
多态让 同一个变量(引用)在 不同情境 下 执行不同的代码。
在 Java 中有两类多态:
| 类型 | 发生阶段 | 典型写法 | 说明 |
|---|---|---|---|
| 编译时多态(静态) | 编译阶段 | 方法重载(overload) | 同名方法参数不同,编译器根据参数列表挑选 |
| 运行时多态(动态) | 程序运行时 | 方法重写(override) + 向上转型(up‑casting) | 父类引用指向子类对象,实际执行子类的实现 |
下面分别演示。
编译时多态 —— 方法重载(Overload)
public class Calculator {
// 同名的三个方法,参数类型/个数不同
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; }
public String add(String a, String b) { return a + b; }
}
使用时:
Calculator calc = new Calculator();
System.out.println(calc.add(2, 3)); // 调用 int 版 → 5
System.out.println(calc.add(2.5, 3.1)); // 调用 double 版 → 5.6
System.out.println(calc.add("A", "B")); // 调用 String 版 → AB
编译器在 看参数的类型 后决定调用哪一个,运行时不会再变化。
运行时多态 —— 方法重写 + 向上转型
定义父类(Animal)和子类(Dog、Cat)
// Animal.java
public class Animal {
public void sound() {
System.out.println("动物会发声");
}
}
// Dog.java
public class Dog extends Animal {
@Override
public void sound() {
System.out.println("汪汪!");
}
}
// Cat.java
public class Cat extends Animal {
@Override
public void sound() {
System.out.println("喵喵!");
}
}
向上转型(Up‑casting)—— 父类引用指向子类对象
Animal a1 = new Dog(); // a1 编译时看作 Animal,实际对象是 Dog
Animal a2 = new Cat(); // 同理
a1.sound(); // 运行时调用 Dog 的 sound → "汪汪!"
a2.sound(); // 运行时调用 Cat 的 sound → "喵喵!"
关键点
- 父类引用 (
Animal) 只知道父类里声明了sound()方法。- 真正的对象(
Dog、Cat)在 运行时 决定到底执行哪个实现,这就是 动态派发(runtime dispatch)。
③ 多态的好处
| 好处 | 举例 |
|---|---|
| 统一接口 | 只要有 Animal 类型的集合,就可以统一遍历 sound(),不需要区分是狗还是猫。 |
| 灵活扩展 | 增加新动物(Bird)只需要 extends Animal 并 override sound(),原有代码不需改动。 |
| 解耦 | 调用方只依赖父类/接口,降低代码耦合度。 |
接口实现多态(Interface Polymorphism)
Java 里 接口 也是实现运行时多态的常用方式:
public interface Drawable {
void draw();
}
public class Circle implements Drawable {
@Override
public void draw() { System.out.println("画圆"); }
}
public class Rectangle implements Drawable {
@Override
public void draw() { System.out.println("画矩形"); }
}
调用:
Drawable d1 = new Circle(); // 父接口引用指向子类对象
Drawable d2 = new Rectangle();
d1.draw(); // 画圆
d2.draw(); // 画矩形
常用集合框架:
为什么要有集合?
- 数组只能固定长度、只能存同一种基本类型,不能直接增删、排序、搜索。
- 集合框架提供 统一的 API,把 增删改查、遍历、排序、同步 等功能封装起来,帮助你写 更简洁、可维护、可扩展 的代码。
一句话:集合 = “可伸缩的容器”,比数组更灵活、更强大。
在java中集合(Collation)有两种模式,一种是List(有序,有索引,可重复),另一种是set(无序,无索引,不可重复),有序集合 (List) 和 唯一性集合 (Set)。
Collection:这是所有集合类的根接口。
List接口:代表“有序序列”。(特点:允许重复元素,顺序重要)Set接口:代表“唯一集合”。(特点:不允许重复元素,元素唯一性重要)
Map 接口:代表“键值对映射”。(特点:用 Key 去查找 Value,键必须唯一)
核心 接口(interface)——你写代码时看到的大多数类型
| 接口 | 继承关系 | 说明(最常见的操作) | 适用场景 |
|---|---|---|---|
Collection<E> |
- | 添加、删除、遍历、大小、判断是否为空 | 所有 单值容器(List、Set、Queue)都实现它 |
List<E> |
Collection<E> |
有序、根据下标访问、允许重复 | 需要顺序、频繁随机访问(如数组列表) |
Set<E> |
Collection<E> |
唯一(不允许重复),不保证顺序 | 去重、快速判断元素是否出现 |
Queue<E> |
Collection<E> |
FIFO(先入先出)或优先级队列 | 任务调度、生产者‑消费者 |
Deque<E> |
Queue<E> |
双端队列,两端都能插入/弹出 | 栈、双端队列、滑动窗口 |
Map<K,V> |
- (不是 Collection) |
键‑值映射,键唯一,提供 get(key)、put(key,value) |
字典、缓存、关联数据结构 |
常用 实现类(Class)——每个接口最常见的几种实现
下表只列出 最常用、在面试/项目中出现频率最高 的类。
| 接口 | 实现类 | 关键特性 | 适用场景 |
|---|---|---|---|
| List | ArrayList<E> |
基于 动态数组,支持快速随机访问 O(1),尾部添加 O(1),中间插入/删除 O(n) |
大多数“普通列表”,如查询结果、集合过滤后保存 |
LinkedList<E> |
基于 双向链表,头/尾插入/删除 O(1),随机访问 O(n) |
需要频繁在头部/中间插入或删除的场景(如实现队列、栈) | |
Vector<E> |
线程安全的 ArrayList(每个方法加 synchronized) |
老代码、极少使用 | |
Stack<E> |
继承自 Vector,实现 栈(push/pop) |
已被 Deque 替代,基本不推荐 |
|
| Set | HashSet<E> |
基于 哈希表,查询、插入、删除 O(1)(平均) |
需要 快速去重、判断是否出现 |
LinkedHashSet<E> |
同 HashSet,但保留插入顺序 |
需要唯一且遍历顺序与插入顺序保持一致 | |
TreeSet<E> |
基于 红黑树,元素自动 排序(自然顺序或自定义 Comparator),操作 O(log n) |
需要 有序、范围查询(如排行榜) | |
| Queue / Deque | ArrayDeque<E> |
基于数组的双端队列,无容量限制,头尾操作 O(1) |
高效栈或队列(替代 LinkedList) |
LinkedList<E> |
同时实现 List 与 Deque,适合 中小规模 的双端操作 |
简单的任务队列、双端缓存 | |
PriorityQueue<E> |
基于 堆,按自然顺序或 Comparator 取最小(或最大)元素,poll O(log n) |
优先级调度、最小/最大堆 | |
| Map | HashMap<K,V> |
基于 哈希表,基本操作 O(1)(平均) |
最常用的键值映射 |
LinkedHashMap<K,V> |
同 HashMap,保持插入顺序(或访问顺序) |
实现 LRU 缓存、保持遍历顺序 | |
TreeMap<K,V> |
基于 红黑树,键自动排序,操作 O(log n) |
需要按键的自然顺序遍历 | |
Hashtable<K,V> |
线程安全(每个方法 synchronized),已被 ConcurrentHashMap 替代 |
老项目中遗留的代码 | |
ConcurrentHashMap<K,V> |
高并发的 无锁/分段锁 实现,读写性能极佳 | 多线程共享缓存、统计计数器 |
简单代码示例
注意:所有示例均使用 泛型(
<String>、<Integer>),保证类型安全。
4.1 List – ArrayList
List<String> list = new ArrayList<>(); // 只保留接口类型
list.add("Apple");
list.add("Banana");
list.add("Cherry");
// 随机访问
System.out.println(list.get(1)); // Banana
// 遍历(推荐 for‑each)
for (String fruit : list) {
System.out.println(fruit);
}
4.2 List – LinkedList(当作队列)
Deque<Integer> deque = new LinkedList<>(); // 也可写成 Queue<Integer>
deque.offerFirst(10); // 头部入队
deque.offerLast(20); // 尾部入队
System.out.println(deque.pollFirst()); // 10
System.out.println(deque.pollLast()); // 20
4.3 Set – HashSet
Set<Integer> set = new HashSet<>();
set.add(5);
set.add(3);
set.add(5); // 重复元素会被忽略
System.out.println(set.contains(3)); // true
4.4 Set – TreeSet(自动排序)
Set<String> sorted = new TreeSet<>();
sorted.add("banana");
sorted.add("apple");
sorted.add("cherry");
System.out.println(sorted); // [apple, banana, cherry]
4.5 Map – HashMap
Map<String, Integer> map = new HashMap<>();
map.put("Alice", 90);
map.put("Bob", 78);
System.out.println(map.get("Alice")); // 90
// 迭代 Entry(键值对)—最常用方式
for (Map.Entry<String, Integer> e : map.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}
4.6 Map – LinkedHashMap(保持插入顺序)
Map<String, String> linked = new LinkedHashMap<>();
linked.put("first", "1");
linked.put("second", "2");
linked.put("third", "3");
// 直接遍历保持顺序
linked.forEach((k, v) -> System.out.println(k + "=" + v));
4.7 Queue – PriorityQueue(最小堆)-->好像不怎么用
Queue<Integer> pq = new PriorityQueue<>();
pq.offer(30);
pq.offer(10);
pq.offer(20);
System.out.println(pq.poll()); // 10(最小值先弹出)
4.8 线程安全集合
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> concurrent = new ConcurrentHashMap<>();
Collections.synchronizedXxx通过 同步锁 把普通集合包装成线程安全(适合 读少写多 的场景);ConcurrentHashMap则在 高并发读写 场景下更高效。
如何挑选合适的集合?
下面提供 “选型判断表”,只要回答几个问题,就能快速定位最合适的实现。
| 需要什么特性? | 推荐集合 | 说明 |
|---|---|---|
| 顺序、按下标访问、随机读多 | ArrayList |
O(1) 随机读,动态扩容 |
| 顺序、经常在头部/中间插入/删除 | LinkedList / ArrayDeque(双端) |
头尾 O(1),但随机读慢 |
| 只需要唯一元素、快速判断是否存在 | HashSet |
O(1) 查找 |
| 唯一且遍历顺序要保持 | LinkedHashSet |
记住插入顺序 |
| 唯一且要自动排序 | TreeSet |
红黑树,O(log n) |
| 键值映射,查询快 | HashMap |
最常用 |
| 键值映射,需要遍历顺序 | LinkedHashMap |
维护插入顺序(或访问顺序) |
| 键值映射,需要键自动排序 | TreeMap |
红黑树,按键有序 |
| 多线程共享的映射 | ConcurrentHashMap |
高并发读写 |
| 需要优先级(最小/最大)取出 | PriorityQueue |
堆实现 |
| 需要双端操作(栈+队列) | ArrayDeque |
高效的双端队列(推荐代替 Stack、LinkedList) |
| 需要不可变集合 | Collections.unmodifiableList/Set/Map 或 List.of()(JDK 9+) |
创建后不允许修改,安全传参 |
小技巧:如果你不确定,用
ArrayList(List)和HashMap(Map)是最安全的默认选择,后面再根据需求换实现。
遍历集合的几种写法
| 方式 | 代码示例 | 适用范围 | 注意 |
|---|---|---|---|
增强 for 循环(for‑each) |
for (String s : list) { … } |
所有实现了 Iterable 的集合 |
简洁、只读遍历 |
| Iterator | Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); … } |
需要在遍历时 安全删除 元素 | it.remove() 只能在 Iterator 中删 |
ListIterator(专用于 List) |
ListIterator<String> li = list.listIterator(); while (li.hasNext()) { … li.remove(); } |
同时 向前/向后遍历、在遍历中插入、替换 | 支持 add、set、remove |
| forEach(Java 8+) | list.forEach(s -> System.out.println(s)); |
所有集合的 函数式 写法 | 用 Lambda 更简洁,内部仍是遍历 |
| Stream API | list.stream().filter(s -> s.startsWith("A")).forEach(System.out::println); |
需要 过滤、映射、聚合等链式操作 | 想要惰性求值、并行流(parallelStream()) |
最佳实践:
- 只读遍历时首选 增强 for 或 forEach(最简洁)。
- 需要删除时使用 Iterator(
it.remove()),不要在增强 for 中直接list.remove,会抛ConcurrentModificationException。
泛型(Generics)——让集合“知道”存放的是什么
List<String> list = new ArrayList<>(); // 只能放 String
list.add("hello"); // OK
list.add(123); // 编译错误!类型安全
- 没有写泛型(原始类型)会产生 unchecked 警告,容易在运行时出现
ClassCastException。 - **JDK 7+**可以使用 钻石操作符
< >让编译器自动推断类型(如new ArrayList<>()),不必在构造函数里再次写<String>。
线程安全与同步集合
| 场景 | 推荐方案 |
|---|---|
| 读多写少、简单同步 | Collections.synchronizedList(new ArrayList<>())(把普通集合包装成同步的) |
| 高并发读写 | CopyOnWriteArrayList(写少读多)或 ConcurrentHashMap(键值映射) |
| 阻塞队列(生产者‑消费者) | ArrayBlockingQueue, LinkedBlockingQueue, ConcurrentLinkedQueue |
| 线程安全的 Set | Collections.synchronizedSet(new HashSet<>()) 或 ConcurrentSkipListSet(有序并发) |
练手:使用
ConcurrentHashMap<Integer, Integer> counter = new ConcurrentHashMap<>();并发counter.merge(key, 1, Integer::sum);来实现安全计数。
常用工具类 java.util.Collections
| 方法 | 功能 | 示例 |
|---|---|---|
sort(List<T>) |
对 List 进行 自然顺序 排序 | Collections.sort(list); |
binarySearch(List<T>, T key) |
二分查找(列表必须先排好序) | int idx = Collections.binarySearch(sortedList, "Bob"); |
reverse(List<T>) |
反转列表顺序 | Collections.reverse(list); |
shuffle(List<T>) |
随机打乱顺序 | Collections.shuffle(list); |
unmodifiableList(List<T>) |
返回 只读 视图 | List<String> readOnly = Collections.unmodifiableList(list); |
emptyList() / emptySet() / emptyMap() |
返回 不可修改的空集合(常用于返回值避免 null) |
return Collections.emptyList(); |
Java Stream(流)——对集合/数组进行 函数式 操作的管道
什么是 Stream?
- Stream ≠ 集合,它是 对数据源(Collection、数组、
java.util.stream.Stream本身、IntStream…)的一个 管道(pipeline)。 - 只负责 读取、转换、聚合 数据,不存储 数据。
- 懒惰执行:只有终端操作触发时,所有中间操作才会真正执行。
- 可以并行(
parallelStream())——底层会把数据分块并交给多线程处理。
类比:把原材料(集合)送进流水线(Stream),每道工序(filter、map …)只负责处理,不改动原材料本身。最后一个工序(forEach、collect …)把成品拿出来。
创建 Stream 的几种方式
| 场景 | 代码 |
|---|---|
集合 → Stream |
List<String> list = …; Stream<String> s = list.stream(); |
| 集合并行 | list.parallelStream(); |
数组 → Stream |
int[] arr = {1,2,3}; IntStream s = Arrays.stream(arr); |
| 单个值 | Stream.of("a","b","c"); |
| 无限流(生成器) | Stream.iterate(0, n -> n + 1).limit(5).forEach(System.out::println); |
中间操作(不会触发计算)
| 操作 | 作用 | 示例 |
|---|---|---|
filter(Predicate) |
按条件保留 | s.filter(x -> x > 10) |
map(Function) |
把元素转换为另一种 | s.map(String::length) |
flatMap(Function) |
把每个元素映射为 流 再扁平化 | list.stream().flatMap(s -> Arrays.stream(s.split(","))) |
distinct() |
去重 | s.distinct() |
sorted() / sorted(Comparator) |
排序 | s.sorted() |
limit(n) / skip(n) |
前 n 条 / 跳过 n 条 | s.limit(5) |
peek(Consumer) |
查看每个元素(调试) | s.peek(System.out::println) |
parallel() |
把当前流转为并行流 | s.parallel() |
注意:这些操作返回的仍然是 Stream,可以继续链式调用。
终端操作(真正触发计算)
| 操作 | 返回类型 | 示例 |
|---|---|---|
forEach(Consumer) |
void |
s.forEach(System.out::println); |
collect(Collector) |
任意容器(List、Set、Map …) |
List<String> result = s.collect(Collectors.toList()); |
reduce(identity, BinaryOperator) |
聚合成单值 | int sum = s.reduce(0, Integer::sum); |
count() |
long |
long n = s.count(); |
anyMatch(Predicate) / allMatch / noneMatch |
boolean |
boolean hasBig = s.anyMatch(x -> x > 100); |
findFirst() / findAny() |
Optional<T> |
Optional<String> first = s.findFirst(); |
toArray() |
Object[] / int[] … |
String[] arr = s.toArray(String[]::new); |
示例:
import java.util.*;
import java.util.stream.*;
public class StreamDemo {
public static void main(String[] args) {
List<String> words = Arrays.asList("Apple", "banana", "Cherry", "date", "Elderberry");
// 过滤出首字母是大写的单词
List<String> caps = words.stream()
.filter(w -> Character.isUpperCase(w.charAt(0)))
.collect(Collectors.toList());
System.out.println("首字母大写: " + caps); // [Apple, Cherry, Elderberry]
// 把单词转成长度并求和
int totalLen = words.stream()
.mapToInt(String::length) // 转成 IntStream(更高效)
.sum();
System.out.println("总字符数: " + totalLen); // 33
// 并行统计所有单词的最长长度
int maxLen = words.parallelStream()
.mapToInt(String::length)
.max()
.orElse(0);
System.out.println("最长单词长度: " + maxLen); // 10
//生成 0~9 的平方数,取前 5 个
List<Integer> squares = IntStream.range(0, 10)
.map(i -> i * i)
.limit(5)
.boxed() // int → Integer
.collect(Collectors.toList());
System.out.println("前 5 个平方: " + squares); // [0, 1, 4, 9, 16]
}
}
使用小技巧:
| 场景 | 推荐写法 | 说明 |
|---|---|---|
| 只想遍历 | list.forEach(System.out::println); |
简洁,内部仍是顺序遍历 |
| 把集合转成 Map | list.stream().collect(Collectors.toMap(Person::getId, Function.identity())); |
key → value 一步到位 |
| 分组 | list.stream().collect(Collectors.groupingBy(Person::getDept)); |
按属性分组返回 Map<Dept, List<Person>> |
| 统计 | list.stream().collect(Collectors.summarizingInt(Person::getAge)); |
IntSummaryStatistics 包含 count, sum, min, max, average |
| 自定义收集器 | Collector.of(() -> new StringBuilder(), (sb, s) -> sb.append(s).append(","), StringBuilder::append, sb -> sb.substring(0, sb.length() - 1).toString()); |
只要实现 Supplier, Accumulator, Combiner, Finisher 四个函数 |
常见坑 & 防御
| 坑 | 解释 | 解决办法 |
|---|---|---|
| 忘记终端操作导致代码不执行 | stream.filter(...); 只会创建一个惰性流,什么都不发生 |
必须加 forEach、collect 等终端操作 |
| 并行流对有序集合产生错误 | 对 ArrayList 的 parallelStream 仍然保持顺序,但自定义 Collector 可能不是线程安全的 |
使用 线程安全 的收集器(Collectors.toConcurrentMap)或保持顺序 stream().sequential() |
Optional 忘记 orElse 抛异常 |
findFirst() 返回 Optional<T>,直接调用 .get() 当空会 NoSuchElementException |
使用 orElse, orElseGet, ifPresent 等安全方式 |
mapToInt vs map |
map 仍返回 对象流,mapToInt 返回 原始 IntStream(更高效) |
需要数值运算时首选 mapToInt / mapToLong / mapToDouble |
| 流只能使用一次 | 生成的 Stream 消费掉后 不能再次使用 |
若需要复用,重新创建新的流或使用 Supplier<Stream<T>> 包装一次性流 |
| 忘记关闭 IO 流 | Files.lines(Path) 返回 Stream<String>,内部持有打开的文件句柄 |
用 try‑with‑resources 包装:try (Stream<String> lines = Files.lines(path)) { … } |
Java 反射(Reflection)——在运行时“看见”或“操作”类、方法、字段
为什么需要反射?
- 在代码运行时 动态获取 类的结构(属性、方法、构造器),甚至 创建实例、调用方法。
- 常见的 框架(Spring、Hibernate、JUnit)都依赖反射完成 自动装配、持久化、测试注入。
- 可以把硬编码的类名、字段名、方法名写在配置文件或注解里,运行时读取并执行。
注意:反射 破坏了 编译时的 类型安全,并且有 性能开销,所以 仅在必要时 使用;业务代码优先使用普通调用。
获取 Class 对象(类的“镜子”)
| 方法 | 示例 | 适用场景 |
|---|---|---|
ClassName.class(类字面量) |
Class<Person> c = Person.class; |
编译时已知类 |
obj.getClass() |
Class<? extends Person> c = p.getClass(); |
已有对象实例 |
Class.forName("com.xxx.Person") |
Class<?> c = Class.forName("com.demo.Person"); |
类名是 运行时 从配置/注解/字符串获取 |
Class里保存 所有成员元数据(字段、方法、构造器、注解、父类、实现的接口等)。
常用反射 API
| 操作 | 关键方法 | 示例代码 |
|---|---|---|
| 获取构造器 | getConstructor(..), getDeclaredConstructor(..) |
Constructor<Person> ctor = Person.class.getConstructor(String.class, int.class); |
| 实例化对象 | newInstance()(不推荐)或 Constructor::newInstance |
Person p = ctor.newInstance("Tom", 25); |
| 获取/修改字段 | getDeclaredField(..), setAccessible(true), Field::get, Field::set |
java\nField f = Person.class.getDeclaredField("age");\nf.setAccessible(true); // 破坏 private\nint age = (int) f.get(p);\nf.set(p, 30);\n |
| 获取/调用方法 | getDeclaredMethod(..), Method::invoke |
java\nMethod m = Person.class.getMethod("introduce");\nm.invoke(p); // 调用实例方法\n |
| 获取注解 | Class::getAnnotation, Method::getAnnotations |
if (clazz.isAnnotationPresent(Entity.class)) { … } |
| 获取父类/接口 | getSuperclass(), getInterfaces() |
Class<?> superCls = Person.class.getSuperclass(); |
getDeclared*能获取 private 成员(配合setAccessible(true)),而get*只能获取 public 成员。
完整示例:从类名创建对象、设置私有字段、调用私有方法
package demo;
import java.lang.reflect.*;
public class ReflectDemo {
public static void main(String[] args) throws Exception {
// —— 通过类名获取 Class 对象(假设配置文件里写了类全限定名) ——
Class<?> clazz = Class.forName("demo.Person");
// —— 找到对应的构造器并实例化 ——
// 这里 Person 有一个无参构造器
Constructor<?> ctor = clazz.getDeclaredConstructor(); // private 也行
ctor.setAccessible(true); // 打破访问限制(如果构造器是 private)
Object personObj = ctor.newInstance(); // 创建实例
// —— 给私有字段 name、age 赋值 ——
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(personObj, "小张"); // 把 name 改成“小张”
Field ageField = clazz.getDeclaredField("age");
ageField.setAccessible(true);
ageField.setInt(personObj, 28); // 设置年龄为 28
// —— 调用一个私有方法(introduce) ——
Method introduce = clazz.getDeclaredMethod("introduce");
introduce.setAccessible(true);
introduce.invoke(personObj); // 输出:我叫 小张,今年 28 岁。
}
}
//--------------------- Person.java ---------------------
package demo;
public class Person {
private String name;
private int age;
private Person() {} // 私有构造器
private void introduce() { // 私有方法
System.out.println("我叫 " + name + ",今年 " + age + " 岁。");
}
}
运行结果:
我叫 小张,今年 28 岁。
这段代码展示了反射可以 突破
private,在运行时动态创建对象并操作它们——这正是许多框架(Spring 的依赖注入、ORM 的实体映射)做的事。
实际业务中常见的反射用例
| 场景 | 典型代码/思路 |
|---|---|
| 类路径配置 → 动态创建对象(插件系统) | Class<?> c = Class.forName(className); Object o = c.getDeclaredConstructor().newInstance(); |
| 读取注解 → 自动装配依赖 | if (field.isAnnotationPresent(Autowired.class)) { … } |
对象属性复制(如框架的 BeanUtils.copyProperties) |
for (Field f : src.getClass().getDeclaredFields()) { f.setAccessible(true); Object v = f.get(src); targetField.set(target, v); } |
| 序列化/反序列化(把对象写入磁盘或网络) | ObjectOutputStream/ObjectInputStream 在内部使用反射读取 serialVersionUID、字段信息 |
| 单元测试(调用私有方法) | JUnit + setAccessible(true) 调用被测试类的内部实现细节(不推荐生产代码) |
反射的坑 & 性能、权限注意
| 坏处 | 解释 | 规避办法 |
|---|---|---|
| 性能开销 | 反射涉及 动态解析,比普通调用慢(约 10‑100 倍) | 在热点代码里缓存 Method、Field 对象,或直接使用普通调用 |
| 破坏封装 | 通过 setAccessible(true) 能改动 private 成员 |
只在框架/工具 层使用,业务代码尽量不暴露 |
| 编译期失去检查 | 方法名、字段名写错在编译时不报错,运行时会抛 NoSuchMethodException / NoSuchFieldException |
使用 常量或注解 保存元数据;或者在启动时提前校验 |
| 安全管理器(SecurityManager) | 在受限环境(如 Applet、部分服务器)会阻止 setAccessible |
需要相应的 权限,或不用反射 |
| 类加载问题 | Class.forName 会触发类的 静态初始化块,可能产生副作用 |
只在需要时加载;或使用 ClassLoader.loadClass 细粒度控制 |
最佳实践:业务代码中尽量 不直接使用 反射;若必须,用 工具类(如
ReflectionUtils)封装,缓存Method/Field并捕获异常统一处理。
小结:常用 API 快速记忆表
| 功能 | 关键代码 | 备注 |
|---|---|---|
| 获取 Class | Class<?> c = Class.forName(name);MyClass.class |
三种方式任选其一 |
| 实例化 | c.getDeclaredConstructor().newInstance(); |
推荐使用 Constructor::newInstance(可抛具体异常) |
| 读取字段 | Field f = c.getDeclaredField("age"); f.setAccessible(true); int age = (int) f.get(obj); |
读取 private 必须 setAccessible |
| 写入字段 | f.set(obj, 30); |
同上 |
| 调用方法 | Method m = c.getMethod("sayHello", String.class); m.invoke(obj, "World"); |
参数类型、返回值需匹配 |
| 获取注解 | MyAnno ann = c.getAnnotation(MyAnno.class); |
常用于框架读取元信息 |
| 获取父类/接口 | c.getSuperclass(); c.getInterfaces(); |
用于层次结构分析 |
对比:Stream vs 反射
| 维度 | Stream | 反射 |
|---|---|---|
| 目的 | 函数式处理 数据流(过滤、转换、聚合) | 元编程:在运行时检查/操作类、对象结构 |
| 对数据的影响 | 只读(除 collect、reduce 产生新对象) |
可修改(字段、方法、构造器) |
| 执行时机 | 惰性(只有终端操作才执行) | 即时(调用 API 时马上执行) |
| 常见使用场景 | 集合/数组的批量操作、并行计算、流式 API | 框架自动装配、配置驱动实例化、动态代理、测试私有成员 |
| 性能 | 接近 原生循环;并行流可提升多核利用率 | 较慢(反射成本高),需缓存或限制使用 |
| 学习难度 | 对初学者友好(lambda、链式调用) | 较抽象,需要理解 Class、Field、Method 等底层概念 |
推荐阅读《Java核心技术卷》
更多推荐




所有评论(0)