#免责声明#

本篇诞生初衷用于复习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 → double
  • char → 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 参数错误 srcPosdestPoslength 写错会导致错误复制或 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() 方法。
  • 真正的对象(DogCat)在 运行时 决定到底执行哪个实现,这就是 动态派发(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 高效的双端队列(推荐代替 StackLinkedList
需要不可变集合 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(); } 同时 向前/向后遍历、在遍历中插入、替换 支持 addsetremove
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(键值映射)
阻塞队列(生产者‑消费者) ArrayBlockingQueueLinkedBlockingQueueConcurrentLinkedQueue
线程安全的 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) 任意容器(ListSetMap …) 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()); 只要实现 SupplierAccumulatorCombinerFinisher 四个函数

常见坑 & 防御

解释 解决办法
忘记终端操作导致代码不执行 stream.filter(...); 只会创建一个惰性流,什么都不发生 必须加 forEachcollect 等终端操作
并行流对有序集合产生错误 对 ArrayList 的 parallelStream 仍然保持顺序,但自定义 Collector 可能不是线程安全的 使用 线程安全 的收集器(Collectors.toConcurrentMap)或保持顺序 stream().sequential()
Optional 忘记 orElse 抛异常 findFirst() 返回 Optional<T>,直接调用 .get() 当空会 NoSuchElementException 使用 orElseorElseGetifPresent 等安全方式
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::getField::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::getAnnotationMethod::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 倍) 在热点代码里缓存 MethodField 对象,或直接使用普通调用
破坏封装 通过 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 反射
目的 函数式处理 数据流(过滤、转换、聚合) 元编程:在运行时检查/操作类、对象结构
对数据的影响 只读(除 collectreduce 产生新对象) 可修改(字段、方法、构造器)
执行时机 惰性(只有终端操作才执行) 即时(调用 API 时马上执行)
常见使用场景 集合/数组的批量操作、并行计算、流式 API 框架自动装配、配置驱动实例化、动态代理、测试私有成员
性能 接近 原生循环;并行流可提升多核利用率 较慢(反射成本高),需缓存或限制使用
学习难度 对初学者友好(lambda、链式调用) 较抽象,需要理解 ClassFieldMethod 等底层概念

推荐阅读《Java核心技术卷》

官方文档:https://docs.oracle.com/javase/8/docs/api/java

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐