Java学习笔记01

Java概述

Java是由Sun Microststems 公司于1995年5月推出的面向对象的程序设计语言,作者 詹姆斯•高斯林。

Java技术体系平台

  • Java SE 标准版
  • Java EE 企业版 主要学习这个
  • Java ME 小型版

Java重要特点

  • 语言简单
  • 面向对象 oop
  • 分布式
  • 语言健壮
  • 安全
  • 体系结构中立
  • 可移植
  • 跨平台
  • 解释型语言 需要解释器
  • 高性能
  • 动态

Java虚拟机 JVM

用于在不同的设备上运行.class字节码文件,屏蔽系统差异

JDK、JRE

JDK java开发工具包 Java Development Kit 开发者用

JDK=JRE+java开发工具

JRE java运行环境 Java Runtime Environment 普通用户用

JRE=JVM+Java核心类库

注意事项

  1. 文件名必须与public类名一致
  2. 一个源文件中最多只能有一个public类
  3. 编译之后每一个类对应一个.class文件
  4. Java语言严格区分大小写
  5. 每个语句以 “;” 结束
  6. java 命令之后跟类名
  7. main 方法就是 Java 程序的唯一合法入口
  8. 原则:一个文件只写一个类

Java基础语法

命名规范

标识符类型 命名范式 示例 备注
包 (Package) 全小写 com.apple.ios 建议使用反向域名
类 (Class) 大驼峰 StudentInfo 名词
方法 (Method) 小驼峰 getScore() 动词
变量 (Variable) 小驼峰 studentAge 描述性名词
常量 (Constant) 大写 + 下划线 MAX_AGE static final 修饰
抽象类 大驼峰 AbstractService 通常以 Abstract 开头
异常类 大驼峰 LoginException 必须以 Exception 结尾

变量

变量是程序的基本组成单位

三要素 :类型+名称+值

变量必须先声明再使用

同一个作用域之内变量不能重名

数据类型

  • 基本数据类型
    • 数值型
      • 整数类型,存放整数:byte[1]short[2]int[4]long[8]
      • 浮点 (小数) 类型:float[4]double[8]
    • 字符型:char[2],存放单个字符 'a'
    • 布尔型:boolean[1],存放 truefalse
  • 引用数据类型
    • 类 (class)
    • 接口 (interface)
    • 数组 ([])

注意事项

  1. 整型常量默认为 int 型,声明 long 型常量须后加‘l’或‘L’
  2. 浮点型常量默认为 double 型,声明 float 型常量,须后加‘f’或‘F’
  3. “小向大”是顺水推舟,自动完成;“大向小”是强人所难,必须强转。
  4. 浮点数=符号位+指数位+尾数位 尾数部分可能丢失,造成精度损失(小数都是近似值)
  5. 永远不要用 == 来比较两个浮点数。应该判断它们的差值是否小于一个极小的数值(如 1e-9
  6. 当我们对运算结果是小数的进行相等判断是,要小心应该是以两个数的差值的绝对值,在某个精度范围类判断
  7. char字符常量只能用单引号''引用,只能存单个字符或者是数字,本质是存储的是一个整数unicode对映的编码,可以进行计算

补充知识浮点数的计算机存储

计算机里的浮点数就像一个精密的二进制科学计数法,其存储结构固定为:

  • 符号位 (Sign):1位。01负。
  • 指数位 (Exponent)
    • 作用:决定数值范围(小数点移动多少位)。
    • 黑科技:使用偏移码(Bias),通过给真实指数加一个固定值(如127),强行把负指数变成正整数,方便硬件直接比较大小。
  • 尾数位 (Mantissa)
    • 作用:决定数值精度(长什么样)。
    • 黑科技隐藏整数位的 1。既然二进制规格化后第一位必然是1,干脆省掉不存,多赚1位精度。

基本数据类型转换

路径 A: byteshortintlongfloatdouble

路径 B: charintlongfloatdouble

为什么long是8个字节,float是4个字节,但是long会自动转为float?

它们底层的二进制存储方式完全不同。数值范围的大小并不取决于它占用了多少字节,而取决于它的结构设计

long(64 位定点数):1 位符号位 + 63 位数值位,纯精确整数。

float(32 位 IEEE 754 浮点数):1 位符号位 + 8 位指数位 + 23 位尾数位。

long(8 字节):最大值约 $9.22 \times 10^{18}$

float(4 字节):最大值约 $3.4 \times 10^{38}$(范围远大于 long

引入指数位后,表达范围被几何级放大。

注意看:

byte 可以自动转 shortshort 不能直接转 charbyte不能直接转 char。但是它们都可以统一自动转为 int再计算。原因是shortbyte有符号。

提升原则:表达式结果的类型自动提升为操作数中最大的类型,最小是int类型

注意事项:

byte short char 类型的变量进行计算都会转换成 int 类型

byte short char 类型的常量(字面常量)进行计算会进行优化机制

常量计算不溢出时,类型由左侧接收变量决定(直通不升格);一旦溢出,则被视为 int 类型(导致报错)。

只要公式中出现了一个变量,整个公式的“常量优化”特权就会立即消失,全面进入变量运算规则

强制类型转化

将容量大的数据类型转换为容量小的数据类型。使用时要加上强制转换符 ( ),但可能造成精度降低或溢出,格外要注意。

Java基本数据类型中,数值类型和boolean不能互相强制转换

基本数据类型和String类型的转换

基本数据类型转String类型:将基本类型的值+“”

String类型转基本数据类型:利用包装类 xxx.parsexxx(“”)

包装类的作用:让基本数据类型能够像对象一样工作,具备属性和方法

基本数据类型 对应的包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean
+号的作用
  1. 当左右两边都是数值类型时,则做加法运算
  2. 当左右两边有一方为字符串,则做拼接运算
  3. 运算顺序,从左到右

Java 转义字符

转义字符 含义
\' 单引号 '
\" 双引号 "
\\ 反斜杠 \
\n 换行
\r 回车
\t Tab 制表符(缩进)
\b 退格
\f 换页(很少用)

特殊转义字符\r

功能:将光标移动到当前行的开头

不同的操作系统对“换行”的定义各不相同:

  • Windows: 使用 \r\n(回车 + 换行)代表一行结束。
  • Unix / Linux / macOS: 统一使用 \n(换行)代表一行结束。

\r 有一个非常酷的用途:制作简单的命令行进度条。

for (int i = 0; i <= 100; i++) {
System.out.print("进度: " + i + "%\r"); // 利用 \r 回到行首不断覆盖
Thread.sleep(100); // 暂停一小会儿
}

注释

  • 单行注释 //
  • 多行注释 / /
  • 文档注释 /* /

被注释的文字,不会被JVM解释执行

代码规范

  1. 类、方法的注释,要以Javadoc的方式来写
  2. 非Java Doc 的注释,往往是给代码维护者看的
  3. 使用Tab进行右移,shift+tab 进行左移
  4. 运算符和=两边习惯性各加一个空格
  5. 源文件使用utf-8编码
  6. 行宽度不要超过80个字符
  7. 代码编写推荐行尾风格

修饰符

  1. public:公开的。无论是哪个类、哪个包、还是外部项目,都可以直接访问。
  2. protected:受保护的。同一个包内的类可以访问;不同包中的子类也可以访问。
  3. 默认(无修饰符):包级私有(package-private)。只有同一个包内的类可以访问,跨包了就不能访问。
  4. private:私有的。只有在定义它的类内部才能访问,体现了面向对象的封装性。通常属性会设为 private,然后通过 get/set 方法来间接访问。

运算符

算术运算符

示例

public class Test {

public static void main(String args[]) {
int a = 10;
int b = 20;
int c = 25;
int d = 25;
System.out.println("a + b = " + (a + b) );
System.out.println("a - b = " + (a - b) );
System.out.println("a * b = " + (a * b) );
System.out.println("b / a = " + (b / a) );
System.out.println("b % a = " + (b % a) );
System.out.println("c % a = " + (c % a) );
System.out.println("a++ = " + (a++) );
System.out.println("a-- = " + (a--) );
// 查看 d++ 与 ++d 的不同
System.out.println("d++ = " + (d++) );
System.out.println("d = " + d);
System.out.println("++d = " + (++d) );
}
}

结果

a + b = 30
a - b = -10
a * b = 200
b / a = 2
b % a = 0
c % a = 5
a++ = 10
a-- = 11
d++ = 25
d =26
++d = 27

注意事项

System.out.println(10 / 4); //从数学来看是 2.5, java 中 2
System.out.println(10.0 / 4); //java 是 2.5
int total = 4;
int score = 10;
double result = score / total; // 猜猜结果是多少? 2.0
System.out.println(result);

原因: score / total 的计算先发生,因为两个都是 int,所以先算出了 2

赋值: 算出 2 之后,才把这个整数赋值给 double result。这时 2 变成了 2.0

在 % 的本质 看一个公式a % b = a - a / b * b

由公式可以知道:在 Java 的取模运算中,结果的符号只取决于左操作数(被除数 $a$)的符号,而与右操作数(除数 $b$)无关。

面试题
int i = 1
i = i++;
System.out.println(i);

答案是 1

过程 temp = i i=i+1 i=temp

int i = 1
i = ++i;
System.out.println(i);

答案是 2

过程 i=i+1 temp=i i=temp

补充知识点

java和python在处理/的时候采用不同的处理方法

Java:向零取整 (Truncate)

Java 的整数除法采用的是 截断除法。简单来说,就是直接把小数点后面的部分“扔掉”。

  • 计算 -7 / 3 结果是 -2.333...,截断后变成 -2
  • 推导 % $-7 - (3 \times -2) = -7 + 6 = -1$。
Python:向下取整 (Floor)

Python 的整数除法(//)采用的是 地板除。它向数轴的左侧(负无穷方向)取整。

  • 计算 -7 // 3 结果是 -2.333...,向左取整变成 -3
  • 推导 % $-7 - (3 \times -3) = -7 + 9 = 2$。

关系运算符

示例

public class Test {

public static void main(String args[]) {
int a = 10;
int b = 20;
System.out.println("a == b = " + (a == b) );
System.out.println("a != b = " + (a != b) );
System.out.println("a > b = " + (a > b) );
System.out.println("a < b = " + (a < b) );
System.out.println("b >= a = " + (b >= a) );
System.out.println("b <= a = " + (b <= a) );
}
}

结果

a == b = false
a != b = true
a > b = false
a < b = true b >= a = true
b <= a = false

关系运算符的结果都是boolean类型,也是就是要么true要么false

instanceof 判断对象是不是某个类 / 子类 / 接口的实例,返回布尔值 true / false 判断运行类型

对象  instanceof  类型
Object obj = new String("hello");
// 判断obj是不是String的实例
boolean res = obj instanceof String;
System.out.println(res); // true

位运算符

示例

public class Test {
public static void main(String args[]) {
int a = 60; /* 60 = 0011 1100 */
int b = 13; /* 13 = 0000 1101 */
int c = 0;
c = a & b; /* 12 = 0000 1100 */
System.out.println("a & b = " + c );

c = a | b; /* 61 = 0011 1101 */
System.out.println("a | b = " + c );

c = a ^ b; /* 49 = 0011 0001 */
System.out.println("a ^ b = " + c );

c = ~a; /*-61 = 1100 0011 */
System.out.println("~a = " + c );

c = a << 2; /* 240 = 1111 0000 */
System.out.println("a << 2 = " + c );
c = a >> 2; /* 215 = 1111 */
System.out.println("a >> 2 = " + c );

c = a >>> 2; /* 215 = 0000 1111 */
System.out.println("a >>> 2 = " + c );
}
}

结果

a & b = 12
a | b = 61
a ^ b = 49
~a = -61
a << 2 = 240
a >> 2 = 15
a >>> 2 = 15

逻辑运算符

1) a&b : & 叫逻辑与:规则:当 a 和 b 同时为 true ,则结果为 true, 否则为 false
2) a&&b : && 叫短路与:规则:当 a 和 b 同时为 true ,则结果为 true,否则为 false
3) a|b : | 叫逻辑或,规则:当 a 和 b ,有一个为 true ,则结果为 true,否则为 false
4) a||b : || 叫短路或,规则:当 a 和 b ,有一个为 true ,则结果为 true,否则为 false
5) !a : 叫取反,或者非运算。当 a 为 true, 则结果为 false, 当 a 为 false 是,结果为 true
6) a^b: 叫逻辑异或,当 a 和 b 不同时,则结果为 true, 否则为 false

对于&&短路与而言,如果第一个条件为 false ,后面的条件不再判断,效率高

对于&逻辑与而言,如果第一个条件为 false ,后面的条件仍然会判断,效率低

||短路或:如果第一个条件为 true,则第二个条件不会判断,最终结果为 true,效率高

| 逻辑或:不管第一个条件是否为 true,第二个条件都要判断,效率低

赋值运算符

示例

public class Test {
public static void main(String args[]) {
int a = 10;
int b = 20;
int c = 0;
c = a + b;
System.out.println("c = a + b = " + c );
c += a ;
System.out.println("c += a = " + c );
c -= a ;
System.out.println("c -= a = " + c );
c *= a ;
System.out.println("c *= a = " + c );
a = 10;
c = 15;
c /= a ;
System.out.println("c /= a = " + c );
a = 10;
c = 15;
c %= a ;
System.out.println("c %= a = " + c );
c <<= 2 ;
System.out.println("c <<= 2 = " + c );
c >>= 2 ;
System.out.println("c >>= 2 = " + c );
c >>= 2 ;
System.out.println("c >>= a = " + c );
c &= a ;
System.out.println("c &= a = " + c );
c ^= a ;
System.out.println("c ^= a = " + c );
c |= a ;
System.out.println("c |= a = " + c );
}
}

结果

c = a + b = 30
c += a = 40
c -= a = 30
c *= a = 300
c /= a = 1
c %= a = 5
c <<= 2 = 20 c >>= 2 = 5
c >>= 2 = 1
c &= a = 0
c ^= a = 10
c |= a = 10

赋值运算符特点

1) 运算顺序从右往左

2) 赋值运算符的左边 只能是变量,右边 可以是变量、表达式、常量值

3) 复合赋值运算符等价于下面的效果比如:a+=3;等价于 a=a+3;
4) 复合赋值运算符会进行类型转换。

byte b = 3;
b += 2; // 等价 b = (byte)(b + 2);
b++; // b = (byte)(b+1);

三元运算符

条件表达式 ? 表达式 1: 表达式 2;

运算规则:

  1. 如果条件表达式为 true,运算后的结果是表达式 1;
  2. 如果条件表达式为 false,运算后的结果是表达式 2;

注意事项

1) 表达式 1 和表达式 2 要为可以赋给接收变量的类型(或可以自动转换)
2) 三元运算符可以转成 if—else 语句

运算优先级

优先级 运算符 类别 结合性
1(最高) expr++ expr-- 后置自增、后置自减 从左→右
2 ++expr --expr +expr(正) -expr(负) ~ ! (类型) 前置单目(自增自减、正负、取反、强制类型转换) 从右→左
3 * / % 乘、除、取模 从左→右
4 + - 加、减(含字符串拼接) 从左→右
5 << >> >>> 移位运算符 从左→右
6 < <= > >= instanceof 关系比较、类型判断 从左→右
7 == != 相等、不相等 从左→右
8 & 按位与 / 非短路逻辑与 从左→右
9 ^ 按位异或 从左→右
10 ` ` 按位或 / 非短路逻辑或 从左→右
11 && 短路逻辑与 从左→右
12 ` ` 短路逻辑或 从左→右
13 ? : 三元条件运算符 从右→左
14(最低) = += -= *= /= %= &= ^= ` =`<<= >>= >>>= 全部赋值复合赋值 从右→左

补充知识点,原码反码补码

原码 (Sign-Magnitude):最高位为符号位(0正1负),其余位是数值。

  • 直观但难算,0有两套写法。

反码 (Ones’ Complement):正数不变;负数符号位不变,数值位按位取反。

  • 进化的中间产物,解决了符号位参与运算的问题。

补码 (Two’s Complement):正数不变;负数在反码基础上 +1

  • 计算机真正采用的格式。

所有运算都在补码(Two’s Complement)基础上进行

程序控制结构

  1. 顺序控制

    程序从上到下逐行执行,中间没有任何判断和跳转

  2. 分支控制

    1. 单分支 if

    2. 双分支 if-else

    3. 多分支 if-else if- else

    4. switch分支

      表达式数据类型应和case后的变量类型一致,或者可以自动转成可以比较的类型

      switch(表达式)中的表达式的返回值必须是:byte short int char enum String

      case子句中的值必须是常量,而不能是变量

      default子句是可选的,当没有匹配的case时,执行default

      break语句用来在执行完成一个case分支后使程序跳switch语句块,如果没有break,程序会顺序执行,直到遇到break

  3. 循环控制

    1. for循环

      for循环四个要素 循环变量初始化 循环条件 循环操作 循环变量迭代

      注意 循环里面创建的变量都是临时的,用完就销毁

      循环初始值可以有多条初始化语句,但要求类型一样,并且中间用逗号隔开,循环变量迭代也可以有多条变量迭代语句,中间用逗号隔开。

    2. while循环

    3. do while循环

      先执行再判断,最少会执行一次

      最后 有一个;

  4. 多重循环控制

    1. break 终止循环
    2. continue 跳出本次循环
  5. 跳转控制语句 - return

    跳出所在的方法

数组、排序、查找

数组定义方式

  1. 动态分配

    数据类型 数组名[] = new 数组类型[大小];
  2. 先声明再分配

    数据类型 数组名[];
    数组名 = new 数组类型[大小];
  3. 数组名.length 表示数组的大小/长度

  4. 数据类型 数组名[]; 也可以 数据类型[] 数组名;

注意事项

  1. 数组是多个相同类型数据的组合,实现对这些数据的统一管理

  2. 数组中的元素可以是任何数据类型,包括基本类型和引用类型,但是不能混用。

  3. 数组创建后,如果没有赋值,有默认值

    | 类型 | 默认值 |
    | ———- | ——— |
    | byte | 0 |
    | short | 0 |
    | int | 0 |
    | long | 0 |
    | float | 0.0 |
    | double | 0.0 |
    | char | \u0000 |
    | boolean | false |
    | String | null |

  4. 使用数组的步骤

    1. 声明数组并开辟空间

    2. 给数组各个元素赋值

    3. 使用数组
  5. 数组的下标是从 0 开始的

  6. 数组下标必须在指定范围内使用,否则报:下标越界异常,

  7. 数组属引用类型,数组型数据是对象(object)

int a = 1
int b = a;
a和b是单独的空间,互不影响 基本数据类型
int[] a = [1,2];
int[] b = a;
a和b是引用同一个地址,相互影响 引用数据类型

Arrays类

一.概述

java.util.Arrays:数组工具类

  1. 包:java.util,使用需要导包
  2. 所有方法都是 static 静态方法,直接 Arrays.方法名() 调用
  3. 构造方法私有,不能 new Arrays 对象
  4. 作用:提供数组打印、排序、查找、复制、填充、比较等工具方法
import java.util.Arrays;

二、常用核心方法

  1. toString () 打印数组

普通数组直接打印输出地址,Arrays.toString 打印数组内容

int[] arr = {1,3,2};
System.out.println(Arrays.toString(arr));
//输出:[1, 3, 2]

注意:只支持一维数组

多维数组使用:deepToString()

int[][] arr2 = {{1,2},{3,4}};
System.out.println(Arrays.deepToString(arr2));
  1. sort () 数组排序

① 基本类型数组:升序排序(快速排序)

int[] arr = {5,2,9,1};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 5, 9]

② 对象数组 / 自定义排序(引用类型)

Arrays.sort(数组, Comparator) 支持自定义顺序(只能用于包装类型数组,不能用 int [],要用 Integer []

Integer[] arr = {5,2,9,1};
//降序
Arrays.sort(arr, (a,b)->b-a);

重点:基本类型 sort 不支持直接传入比较器!

拓展:可以指定区间排序 Arrays.sort(arr,from,to) 左闭右开

  1. binarySearch () 二分查找

前提条件:数组必须先升序排序!否则结果不可预测

  • 找到:返回元素索引
  • 找不到:返回 -(插入点)-1
int[] arr = {1,2,5,9};
int index = Arrays.binarySearch(arr,5); //返回2
int index2 = Arrays.binarySearch(arr,3); //找不到,返回负数
  1. copyOf () /copyOfRange () 数组复制
copyOf (数组,新长度)

从头复制,可扩容 / 缩容

int[] old = {1,2,3};
int[] newArr = Arrays.copyOf(old,5);
// [1,2,3,0,0]
copyOfRange (数组,起始索引,结束索引)

区间复制,左闭右开 [start,end)

int[] arr = {10,20,30,40};
int[] sub = Arrays.copyOfRange(arr,1,3);
// [20,30]

区别:数组自身 clone () 和 Arrays.copyOf 效果类似;copyOf 底层调用 System.arraycopy。

  1. fill () 数组填充

给数组全部 / 指定区间统一赋值

int[] arr = new int[5];
Arrays.fill(arr,8); //全部填充8
// [8,8,8,8,8]

Arrays.fill(arr,1,3,6); //下标1~2填充6
  1. equals () 判断两个数组内容是否相同
int[] a = {1,2,3};
int[] b = {1,2,3};
System.out.println(Arrays.equals(a,b)); //true

误区:

a==b 比较数组地址;

Arrays.equals() 比较数组中每个元素的值。

多维数组比较使用:deepEquals()

三、一维数组 vs 多维数组配套方法汇总

功能 一维数组 多维数组
打印内容 toString() deepToString()
比较内容 equals() deepEquals()

四、高频易错考点

  1. binarySearch 二分查找必须先排序,无序数组结果错误
  2. Arrays.sort 基本类型只能升序;想要降序必须使用包装数组 + Comparator
  3. copyOf 产生新数组,原数组不会改变
  4. Arrays.toString 不能打印多维数组,只会输出地址,要用 deepToString
  5. 数组是引用类型,equals() 默认比较地址,一定要用 Arrays.equals 比较元素
五、拓展:数组转集合(asList)
Integer[] nums = {1,2,3};
List<Integer> list = Arrays.asList(nums);

重要坑:

  1. asList 接收引用类型数组;不能直接传基本类型数组int[]
  2. 返回的 List 是 Arrays 内部静态类,固定大小!不能 add/remove,会抛出异常
  3. 集合和原数组互相引用,修改数组元素,集合同步变化

常用类

包装类

1) 针对八种基本数据类型相应的引用类型—包装类
2) 有了类的特点,就可以调用类中的方法

java 有 8 种基本数据类型,不是对象;

包装类:把基本类型包装成【对象】,位于java.lang

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

记忆:除了 int→Integerchar→Character,其余基本类型首字母大写就是包装类。

装箱 & 拆箱

  1. 手动装箱 / 拆箱(JDK5 之前写法)

装箱:基本类型 → 包装对象

int a = 10;
Integer num = Integer.valueOf(a); //装箱

拆箱:包装对象 → 基本类型

Integer num = new Integer(10);
int a = num.intValue(); //拆箱
  1. 自动装箱、自动拆箱(JDK5 + 新特性,编译器自动转换)
//自动装箱
Integer i = 10; //等价 Integer i = Integer.valueOf(10);

//自动拆箱
int n = i; //等价 int n = i.intValue();

底层本质:自动装箱底层调用valueOf();自动拆箱调用xxxValue()

Integer 缓存机制

Integer.valueOf(int i) 存在缓存数组 cache[]

  • 默认缓存范围:-128 ~ 127
  • 范围内数字:直接复用缓存对象,==结果 true
  • 超出范围:新建Integer对象,==结果 false
Integer a = 100;
Integer b = 100;
System.out.println(a == b); //true,在缓存区间

Integer c = 200;
Integer d = 200;
System.out.println(c == d); //false,超出缓存,两个不同对象

重要提醒:

== 比较:

两边如果是包装对象 → 比较地址

一边包装、一边基本类型 → 触发自动拆箱,比较数值

开发比较数值一律使用 .equals(),不要直接==

拓展缓存范围

只有 Byte、Short、Integer、Long 缓存区间固定[-128,127]

Character缓存 0~127

Float、Double没有缓存

字符串 和 基本类型互相转换

  1. 基本类型 → String

方式 1:变量 + ""

int num = 99;
String str = num + "";

方式 2:String.valueOf()(推荐)

String str = String.valueOf(num);
  1. String → 基本类型

使用包装类的静态方法 parseXxx()

String s = "123";
int n = Integer.parseInt(s);
double d = Double.parseDouble(s);

注意:字符串格式不对,抛出 NumberFormatException 数字格式异常。

包装类与基本类型区别汇总

  1. 默认值不同
  • 基本类型:有默认值(int=0,boolean=false)
  • 包装类:默认值 null

坑:包装类变量为 null 时,执行自动拆箱 → NullPointerException 空指针异常

Integer i = null;
int a = i; //运行报错!空指针
  1. 存储位置
  • 基本类型:局部变量存在
  • 包装类对象:对象本体在
  1. 使用场景
  • 集合 List/Map 不能存放基本类型,只能存包装类
List<Integer> list = new ArrayList<>(); //正确
List<int> list = new ArrayList<>(); //编译报错

String 类

一、String 基本介绍

  1. java.lang.String代表字符串
  2. 字符串本质:字符序列,底层:
    • JDK8:private final char[] value
    • JDK9+:private final byte[] value(优化存储)
  3. 重点:String 对象不可变(immutable)
    • value 数组被 final 修饰,数组引用不能指向新数组
    • 一旦字符串创建,内容无法修改

误区:

String s = "abc";
s = "def";

不是修改原来对象!只是让s引用指向新字符串对象,旧对象等待 GC 回收。

二、字符串创建两种方式

方式 1:直接赋值(字面量)

String str1 = "Java";

先去字符串常量池(堆内存)查找 “Java”

  • 找到:直接复用常量池中的对象
  • 没找到:在常量池中创建,引用指向常量池对象

方式 2:new String ()

String str2 = new String("Java");
  1. 先检查常量池有没有"Java",没有则放入常量池;
  2. 在堆中创建新 String 对象,引用指向堆对象。

经典面试对比

String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2); // true 都指向常量池

String s3 = new String("abc");
String s4 = new String("abc");
System.out.println(s3 == s4); // false 堆中两个不同对象

三、字符串拼接规则

规则 1:编译期常量优化

两个字面量直接相加,编译器自动合并

String s1 = "a"+"b"+"c";
// 编译器优化等价于 String s1 = "abc";

规则 2:存在变量参与拼接 → 在堆创建新对象

String a = "a";
String s = a + "b";
//底层:new StringBuilder().append(a).append("b").toString()
//toString()底层 new String(),对象在堆,不进入常量池

intern () 方法

str.intern():主动把字符串放入常量池

如果常量池已经存在相同内容,返回常量池引用;不存在则将对象放入常量池并返回池内引用。

四、== 和 equals ()

  1. ==

    • 基本类型:比较数值
    • String 引用:比较对象地址
  2. equals()

    • String 重写了 Object 的 equals,比较字符串内容

      开发规范:判断字符串内容相等一律使用 equals()

String s1 = new String("hello");
String s2 = "hello";
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true

五、常用 API 方法

String str = "HelloJava";
str.length(); // 获取字符长度
str.charAt(索引); // 获取指定位置字符
str.indexOf("Java"); // 查找子串起始索引,找不到返回-1
str.substring(begin,end); // 截取字符串 [左闭右开)
str.trim(); // 去除首尾空格(JDK11:strip支持全角空格)
str.toUpperCase(); // 转大写
str.toLowerCase(); // 转小写
str.contains("He"); // 是否包含子串
str.startsWith("He"); // 是否以指定开头
str.endsWith("va"); // 是否以指定结尾
str.split(","); // 按照分隔符切割 → String数组
str.replace("a","b"); // 字符/字符串替换

六、不可变性带来的优缺点

优点:

  1. 字符串常量池实现复用,节省内存
  2. 哈希值只需要计算一次,适合作为 HashMap 的 key
  3. 线程安全,多线程并发访问无问题

缺点:

频繁字符串拼接,不断产生新对象,产生大量垃圾。

大量拼接解决方案:StringBuilder / StringBuffer

Stringbuffer类

一、基础概念

java.lang.StringBuffer可变字符序列

底层:char[](JDK9 之后byte[]),数组没有被final修饰,可以直接扩容修改内容,不会频繁创建新对象

对比:

  • String:不可变,每次拼接产生新对象,大量拼接效率极低
  • StringBuffer:可变容器,原地修改字符数组

二、构造方法

// 空参,默认容量 16
StringBuffer sb1 = new StringBuffer();

// 指定初始容量
StringBuffer sb2 = new StringBuffer(50);

// 根据字符串创建,容量 = 字符串长度 + 16
StringBuffer sb3 = new StringBuffer("hello");

默认容量 16:预留空间,减少频繁扩容。

三、核心常用方法(全部支持链式调用

StringBuffer sb = new StringBuffer();

sb.append("java"); // 末尾追加(最常用)
sb.insert(1,"A"); // 指定索引位置插入
sb.delete(0,2); // 删除 [左闭右开区间]
sb.replace(0,1,"Hi"); // 区间替换
sb.reverse(); // 字符串反转
sb.setCharAt(0,'X'); // 修改指定位置字符

sb.length(); // 当前实际字符长度
sb.capacity(); // 获取底层数组总容量

// 转换成String
String str = sb.toString();

四、扩容机制(重点)

  1. append 后字符超出容量,触发扩容
  2. 扩容公式:新容量 = 原有容量 * 2 + 2
  3. 如果扩容后依然不够,则直接使用所需最小容量
  4. 底层:新建更大数组,复制旧数组内容

示例:初始容量 16 → 扩容后 16*2+2=34

五、StringBuffer 和 String 的互相转换

// String → StringBuffer
String str = "hello";
StringBuffer sb = new StringBuffer(str);

// StringBuffer → String
String result = sb.toString();

六、高频易错知识点

  1. append 返回的是自身 this 对象,所以支持链式调用
sb.append("a").append("b").append("c");
  1. toString() 会新建 String 对象,不会复用底层数组
  2. 不要频繁 new StringBuffer,预估长度尽量指定初始容量,避免多次扩容
  3. 线程不安全场景误用 StringBuffer 不能带来收益,反而降低速度;单线程优先StringBuilder

七、经典误区

错误理解:

“StringBuffer 比 String 快”

准确说法:

少量拼接差距不大;大量循环拼接时,String 不会产生大量临时对象,性能差距巨大

StringBuilder 类

一、基础介绍

java.lang.StringBuilder

可变字符序列,用于字符串拼接,JDK1.5 推出

底层:字符数组(JDK8 char [] / JDK9 byte []),数组无 final 修饰,可以直接修改,不会频繁创建新字符串对象。

和 StringBuffer API 几乎完全一致,唯一区别:没有 synchronized 同步锁

二、构造方法

// 1.无参构造:初始容量 16
StringBuilder sb1 = new StringBuilder();

// 2.指定初始容量
StringBuilder sb2 = new StringBuilder(30);

// 3.传入字符串,容量 = 字符串长度 + 16
StringBuilder sb3 = new StringBuilder("hello");

三、核心常用方法(支持链式调用)

StringBuilder sb = new StringBuilder();

sb.append("Java"); // 末尾追加(最常用)
sb.insert(1,"_"); // 指定位置插入
sb.delete(0,2); // 删除 [start,end) 左闭右开
sb.replace(0,1,"Hi"); // 区间替换
sb.reverse(); // 字符反转
sb.setCharAt(0,'A'); // 修改指定下标字符

sb.length(); // 实际字符个数
sb.capacity(); // 底层数组总容量

// 转为String(生成新字符串对象)
String str = sb.toString();

原理:所有修改操作直接操作底层数组,不产生新对象。

四、扩容机制

触发时机:追加内容后,字符总数超过当前容量

默认扩容公式:

新容量 = 原有容量 * 2 + 2

如果扩容后的容量依旧不够存放,直接采用所需最小容量

示例:

初始容量 16 → 扩容:16×2+2 = 34

优化建议:如果你提前预估字符串长度,创建时指定容量,避免多次扩容、数组复制,提升性能。

五、String / StringBuilder / StringBuffer 三者终极对比

可变性 线程安全 性能 适用场景
String 不可变 线程安全 少量拼接、字符串常量
StringBuffer 可变 线程安全 方法加synchronized 较低 多线程并发拼接
StringBuilder 可变 线程不安全 无锁 最高 单线程拼接(开发首选)

开发准则:

绝大多数业务代码(单线程) → 优先 StringBuilder

六、相互转换

// String → StringBuilder
String s = "demo";
StringBuilder sb = new StringBuilder(s);

// StringBuilder → String
String res = sb.toString();

七、高频考点 & 易错点

  1. 线程不安全典型场景

    多个线程同时操作同一个 StringBuilder 对象,会出现数据错乱、数组越界异常。

解决方案:多线程改用 StringBuffer,或者每个线程新建独立 StringBuilder。

  1. 循环拼接大坑
// 性能极差!每次循环产生大量中间String对象
String str = "";
for(int i=0;i<10000;i++){
str += i;
}

// 推荐写法
StringBuilder sb = new StringBuilder();
for(int i=0;i<10000;i++){
sb.append(i);
}
String str = sb.toString();
  1. 编译器优化误区
String s = "a"+"b"+"c";
// 编译期优化为 String s = "abc";

String a = "a";
String s = a + "b";
// 底层自动 new StringBuilder 拼接
// 但是!只限于**单行简单拼接,循环内不会优化!**

Math 类

一、基础介绍

java.lang.Math
  1. 提供大量数学计算静态方法,全部是 static不需要 new 对象,直接类名。方法调用
  2. 构造方法被 private 修饰,无法实例化
  3. 包含两个常用静态常量
Math.PI    //圆周率 π ≈ 3.1415926535...
Math.E //自然常数 e ≈ 2.71828...

调用方式示例:

double num = Math.abs(-10);

二、常用方法分类

  1. 绝对值 abs ()
Math.abs(-5);        //int 绝对值 → 5
Math.abs(-3.14); //double 绝对值 →3.14
  1. 取最值 max /min
Math.max(10,20);     //返回较大值 20
Math.min(10,20); //返回较小值 10
//支持int long float double
  1. 四舍五入、向上取整、向下取整
//1.四舍五入 → long
Math.round(3.4); //3
Math.round(3.5); //4

//2.向上取整(天花板),返回double
Math.ceil(3.1); //4.0
Math.ceil(-3.1); //-3.0

//3.向下取整(地板),返回double
Math.floor(3.9); //3.0
Math.floor(-3.1); //-4.0

极易踩坑:负数的 ceil、floor 不要凭直觉判断!

  1. 幂运算、开平方
Math.pow(2,3);    //2^3 = 8.0  参数、返回值都是double
Math.sqrt(16); //平方根 √16 =4.0
  1. 随机数 random ()
Math.random(); 
// 返回 [0.0 , 1.0) 之间的随机小数,包含0.0,不包含1.0

业务常用公式:获取 [a,b] 的随机整数

// 范围 [min,max]
int num = (int)(Math.random() * (max - min + 1)) + min;

//示例:1~10随机整数
int r = (int)(Math.random() * 10) + 1;

拓展:JDK 推荐也可以使用 java.util.Random 类,功能更强。

  1. 三角函数(参数是弧度,不是角度)
Math.sin(弧度);
Math.cos(弧度);
Math.tan(弧度);
Math.toRadians(角度); //角度 → 弧度
Math.toDegrees(弧度); //弧度 → 角度

三、Math vs StrictMath(了解)

  • Math:部分方法平台相关,底层可以使用 CPU 优化,计算结果微小误差

  • StrictMath:严格遵循浮点运算标准,所有平台计算结果完全一致;速度略慢。

    日常开发几乎只用 Math。

四、高频易错点

  1. round() 返回 long;ceil() / floor() 返回 double
double d = Math.ceil(2.2); //d=3.0 是小数类型,不是int
  1. Math.random () 左闭右开 [0.0,1.0)
  2. pow 返回 double,即使整数次方,不能直接当 int 使用,需要强转
  3. 三角函数传入的是弧度,直接传入角度结果错误

Arrays 类

一、概述

java.util.Arrays:数组工具类

  1. 包:java.util,使用需要导包
  2. 所有方法都是 static 静态方法,直接 Arrays.方法名() 调用
  3. 构造方法私有,不能 new Arrays 对象
  4. 作用:提供数组打印、排序、查找、复制、填充、比较等工具方法
import java.util.Arrays;

二、常用核心方法

  1. toString () 打印数组

普通数组直接打印输出地址,Arrays.toString 打印数组内容

int[] arr = {1,3,2};
System.out.println(Arrays.toString(arr));
//输出:[1, 3, 2]

注意:只支持一维数组

多维数组使用:deepToString()

int[][] arr2 = {{1,2},{3,4}};
System.out.println(Arrays.deepToString(arr2));
  1. sort () 数组排序

① 基本类型数组:升序排序(快速排序)

int[] arr = {5,2,9,1};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 5, 9]

② 对象数组 / 自定义排序(引用类型)

Arrays.sort(数组, Comparator) 支持自定义顺序(只能用于包装类型数组,不能用 int [],要用 Integer []

Integer[] arr = {5,2,9,1};
//降序
Arrays.sort(arr, (a,b)->b-a);

重点:基本类型 sort 不支持直接传入比较器!

拓展:可以指定区间排序 Arrays.sort(arr,from,to) 左闭右开

  1. binarySearch () 二分查找

前提条件:数组必须先升序排序!否则结果不可预测

  • 找到:返回元素索引
  • 找不到:返回 -(插入点)-1
int[] arr = {1,2,5,9};
int index = Arrays.binarySearch(arr,5); //返回2
int index2 = Arrays.binarySearch(arr,3); //找不到,返回负数
  1. copyOf () /copyOfRange () 数组复制
copyOf (数组,新长度)

从头复制,可扩容 / 缩容

int[] old = {1,2,3};
int[] newArr = Arrays.copyOf(old,5);
// [1,2,3,0,0]

copyOfRange (数组,起始索引,结束索引)

区间复制,左闭右开 [start,end)

int[] arr = {10,20,30,40};
int[] sub = Arrays.copyOfRange(arr,1,3);
// [20,30]

区别:

数组自身 clone () 和 Arrays.copyOf 效果类似;copyOf 底层调用 System.arraycopy。

  1. fill () 数组填充

给数组全部 / 指定区间统一赋值

int[] arr = new int[5];
Arrays.fill(arr,8); //全部填充8
// [8,8,8,8,8]

Arrays.fill(arr,1,3,6); //下标1~2填充6
  1. equals () 判断两个数组内容是否相同
int[] a = {1,2,3};
int[] b = {1,2,3};
System.out.println(Arrays.equals(a,b)); //true

误区:

a==b 比较数组地址;

Arrays.equals() 比较数组中每个元素的值。

多维数组比较使用:deepEquals()

三、一维数组 vs 多维数组配套方法汇总

功能 一维数组 多维数组
打印内容 toString() deepToString()
比较内容 equals() deepEquals()

四、高频易错考点

  1. binarySearch 二分查找必须先排序,无序数组结果错误
  2. Arrays.sort 基本类型只能升序;想要降序必须使用包装数组 + Comparator
  3. copyOf 产生新数组,原数组不会改变
  4. Arrays.toString 不能打印多维数组,只会输出地址,要用 deepToString
  5. 数组是引用类型,equals() 默认比较地址,一定要用 Arrays.equals 比较元素

五、拓展:数组转集合(asList)大坑

Integer[] nums = {1,2,3};
List<Integer> list = Arrays.asList(nums);

重要坑:

  1. asList 接收引用类型数组;不能直接传基本类型数组int[]
  2. 返回的 List 是 Arrays 内部静态类,固定大小!不能 add/remove,会抛出异常
  3. 集合和原数组互相引用,修改数组元素,集合同步变化

System 类

一、概述

java.lang.System
  1. 全部成员都是静态,直接 System.xxx 调用,无需实例化
  2. 构造方法 private,禁止 new System()
  3. 代表程序运行平台系统资源,提供标准输入输出、时间、数组复制、退出程序、环境变量等操作

二、三大静态常量(流对象)

// 标准输入流:控制台键盘输入
public static final InputStream in;
// 标准输出流:控制台打印(常用 System.out.println())
public static final PrintStream out;
// 标准错误输出流:红色字体输出,优先打印,不受输出缓冲区控制
public static final PrintStream err;

区分 out 和 err:

  • out:普通信息输出,缓冲区缓存,顺序不一定立刻打印
  • err:错误信息,立即输出,控制台一般红色

三、核心常用方法

  1. arraycopy () 数组复制,底层非常重要
public static void arraycopy(
Object src, //源数组
int srcPos, //源数组起始索引
Object dest, //目标数组
int destPos, //目标数组起始索引
int length //复制元素个数
)

示例:

int[] a = {10,20,30,40,50};
int[] b = new int[5];
System.arraycopy(a,1,b,0,3);
// b = [20,30,40,0,0]

特点:

  • native 本地方法,由 C 实现,速度极快

  • Arrays.copyOf/copyOfRange 底层本质调用 System.arraycopy

    注意:自己复制自己、索引越界会抛出 ArrayIndexOutOfBoundsException

  1. 获取时间
// 当前时间毫秒数:从【1970-01-01 00:00:00 UTC】到现在的毫秒
long time1 = System.currentTimeMillis();

// JDK1.8新增:纳秒!用于计算代码执行耗时,不代表系统日期
long time2 = System.nanoTime();

使用场景:计算程序运行耗时

long start = System.currentTimeMillis();
//执行代码...
long end = System.currentTimeMillis();
System.out.println("耗时:"+(end-start)+"ms");

区别:

currentTimeMillis:基于系统时钟,可以手动修改系统时间,数值会跳变

nanoTime:基于虚拟机计时器,适合计算时间间隔,不适合获取日期

  1. exit (状态码) 终止 JVM
System.exit(0);  //正常退出
System.exit(1); //异常退出,非0代表程序异常终止

重点考点:

执行 System.exit() 直接关闭虚拟机,finally 代码块不会执行!

  1. gc () 建议垃圾回收
System.gc();

作用:向 JVM建议执行垃圾回收;

只是建议,不保证立刻执行 GC,什么时候回收由虚拟机决定。

  1. 获取环境变量、系统属性
// 获取单个系统属性
String path = System.getProperty("user.dir"); //当前项目工作目录
String os = System.getProperty("os.name"); //操作系统名称

//获取环境变量
String javaHome = System.getenv("JAVA_HOME");

常用属性:

user.name 当前用户名

java.version Java 版本

四、输入输出重定向(了解)

//把输出写入文件,不再打印控制台
System.setOut(new PrintStream("log.txt"));

五、易错知识点

  1. System.arraycopy 是 native 方法,效率高于手动 for 循环复制数组
  2. System.exit(0) 停止 JVM,会跳过 finally 执行
  3. System.gc() 只是通知虚拟机,不能强制垃圾回收
  4. currentTimeMillis() 毫秒时间戳;nanoTime() 仅用于计算间隔
  5. System.outSystem.err 是两个不同流,打印顺序无法保证

六、拓展对比

Arrays.copyOf → 底层调用 System.arraycopy

System.arraycopy 是底层原生方法,开发中很少直接手写,框架大量使用。

BigInteger 和 BigDecimal类

一、背景引入

  1. 基本数据类型局限longint有取值上限;float/double 浮点运算存在精度丢失(二进制存储小数缺陷)。
System.out.println(0.1 + 0.2);
//输出 0.30000000000000004,不是0.3
  1. 解决方案
  • BigInteger:超大整数(无上限)
  • BigDecimal:高精度小数,金融、金额计算必须使用包:java.math

二、BigInteger(超大整数)

  1. 创建对象

不能直接使用 + - * / 运算符!只能调用方法

//方式1:字符串构造【推荐】
BigInteger num1 = new BigInteger("12345678901234567890");

//方式2:long转(数值不能超过long范围)
BigInteger num2 = BigInteger.valueOf(100);
  1. 常用运算方法
BigInteger a = new BigInteger("20");
BigInteger b = new BigInteger("7");

a.add(b); //加法 a+b
a.subtract(b); //减法 a-b
a.multiply(b); //乘法 a*b
a.divide(b); //除法(整除,不能有余数,否则报错)
a.remainder(b);//取余数
a.mod(b); //模运算

a.abs(); //绝对值
a.max(b); //最大值
a.min(b); //最小值

重点:所有运算返回新 BigInteger 对象,原对象不可变!

  1. 带余数除法(divideAndRemainder)
BigInteger[] res = a.divideAndRemainder(b);
res[0] 商,res[1]余数

三、BigDecimal(高精度小数 )

  1. 对象创建
// 不推荐!double构造依然存在精度损失
BigDecimal num = new BigDecimal(0.1);

// 推荐两种写法
BigDecimal num1 = new BigDecimal("0.1"); //字符串构造
BigDecimal num2 = BigDecimal.valueOf(0.1); //valueOf底层自动转字符串
  1. 四则运算

同样不可变,运算返回新对象

BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");

a.add(b); //加
a.subtract(b); //减
a.multiply(b); //乘

// 除法极易报错!除不尽会抛出 ArithmeticException
a.divide(b);
  1. 除法 & 保留小数、舍入模式(核心)

除法必须指定保留位数 + 舍入模式

BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
//保留2位小数,四舍五入
a.divide(b,2,BigDecimal.ROUND_HALF_UP);

常用舍入模式:

  1. ROUND_HALF_UP:四舍五入(日常最常用)
  2. ROUND_UP:向上取整
  3. ROUND_DOWN:直接截断,舍弃后面小数
  4. ROUND_HALF_EVEN:银行家舍入(JDK 默认)
  1. 比较大小

禁止使用 ==(比较地址)

尽量不要用 equals ()!equals 会比较小数位数

BigDecimal d1 = new BigDecimal("2.0");
BigDecimal d2 = new BigDecimal("2.00");
System.out.println(d1.equals(d2)); //false!位数不同

正确方式:compareTo()

// 返回值:
// 0 相等 >0 当前数大 <0 当前数小
if(d1.compareTo(d2) == 0){
System.out.println("数值相等");
}
  1. 小数点操作
bd.setScale(2,BigDecimal.ROUND_HALF_UP); //保留2位小数

四、BigInteger vs BigDecimal 对比

适用场景 存储内容
BigInteger 超大整数运算 整数,无小数
BigDecimal 金融金额、高精度小数 支持小数,解决浮点精度丢失

共性:

  1. 不可变类,每次计算产生新对象
  2. 不支持运算符重载,必须调用方法
  3. java.math

五、高频易错知识点

double 直接传入 BigDecimal 构造器会精度丢失,优先字符串构造

  1. BigDecimal 除法如果无法整除,必须指定精度和舍入模式,否则抛异常
  2. equals 会比较小数位数;数值比较用compareTo
  3. 对象不可变,运算结果需要接收返回值
//错误写法
bd.add(xxx); //结果丢失,原对象不变
//正确
bd = bd.add(xxx);
  1. double/float 不要用于金额计算,强制使用 BigDecimal

六、开发规范(金融项目)

  1. 数据库金额:decimal 类型
  2. Java 代码:使用 BigDecimal 存储金额
  3. 运算统一指定保留小数位数与四舍五入

日期时间类

分为两大体系:

  1. 旧版(JDK1.0 ~ JDK7)DateSimpleDateFormatCalendar

    缺点:线程不安全、API 设计差、可变性

  2. 新版(JDK8+ 推荐)java.time 包LocalDate、LocalTime、LocalDateTime、DateTimeFormatter、Instant、Duration、Period

    优点:不可变、线程安全、设计优雅,项目优先使用

一、旧 API(了解,尽量不推荐新项目使用)

  1. java.util.Date

代表瞬时时间点(毫秒时间戳)

// 当前系统时间
Date date = new Date();
// 指定毫秒时间戳构造
Date date2 = new Date(1750000000000L);

// 获取时间戳毫秒
long time = date.getTime();
// 设置时间戳
date.setTime(1750000000000L);

缺点:

  1. Date对象可变;setTime 修改原有对象
  2. 无法直接获取年、月、日;月份从 0 开始(0=1 月,大坑)

  3. SimpleDateFormat 日期格式化

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// Date → 字符串
String str = sdf.format(new Date());
// 字符串 → Date
Date date = sdf.parse("2026-07-28 12:30:00");

致命坑:SimpleDateFormat 线程不安全!多线程共用会异常。

常用格式符号:

  • yyyy 4 位年份
  • MM 月份 (01~12)
  • dd 日期
  • HH 24 小时制;hh12 小时制
  • mm分钟;ss秒;SSS毫秒
  1. Calendar 日历类(JDK1.1 替代 Date)

抽象类,常用实现 GregorianCalendar

Calendar cal = Calendar.getInstance();
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH); // 依旧 0代表一月!
int day = cal.get(Calendar.DAY_OF_MONTH);

// 设置日期
cal.set(2026,6,28);
Date date = cal.getTime(); //Calendar转Date

缺点:依旧可变,API 别扭。


二、JDK8 全新时间包 java.time

所有对象不可变,修改操作返回新对象;线程安全!

  1. 核心类区分
作用
Instant 时间戳(UTC 标准时间,毫秒 / 纳秒),对应旧 Date
LocalDate 只有年月日 2026-07-28(无时分秒、无时区)
LocalTime 只有时分秒 14:30:20
LocalDateTime 年月日 + 时分秒,无时区
ZonedDateTime 带时区的完整时间(需要时区运算使用)
DateTimeFormatter 格式化类(替代 SimpleDateFormat,线程安全)
Duration 时间间隔:时分秒(秒、毫秒级别,两个时间点)
Period 日期间隔:年、月、日(两个 LocalDate)
  1. 创建对象
// 获取当前时间
LocalDate nowDate = LocalDate.now();
LocalTime nowTime = LocalTime.now();
LocalDateTime nowDT = LocalDateTime.now();

// 指定时间创建
LocalDate date = LocalDate.of(2026,7,28);
LocalDateTime dt = LocalDateTime.of(2026,7,28,14,30);

// Instant 时间戳
Instant instant = Instant.now();
// 时间戳转Instant
Instant instant2 = Instant.ofEpochMilli(1750000000000L);
  1. 常用运算(不可变!操作返回新对象)
LocalDateTime time = LocalDateTime.now();

time.plusDays(3); //加3天
time.minusHours(2); //减2小时
time.plusMonths(1); //加1月

// 获取单独字段
int month = time.getMonthValue(); //月份1~12(正常!不再从0开始)
int day = time.getDayOfMonth();

// 修改指定字段
time.withYear(2027);
  1. 格式化与解析 DateTimeFormatter 线程安全
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime now = LocalDateTime.now();

//时间 → 字符串
String str = now.format(formatter);
//字符串 → 时间
LocalDateTime dt = LocalDateTime.parse("2026-07-28 15:20:00",formatter);
  1. 时间间隔计算 Duration / Period
// 时分秒间隔(LocalDateTime / Instant)
LocalDateTime t1 = LocalDateTime.of(2026,7,28,10,0);
LocalDateTime t2 = LocalDateTime.of(2026,7,28,12,30);
Duration duration = Duration.between(t1,t2);
System.out.println(duration.toHours()); //相差小时

// 年月日间隔(仅LocalDate)
LocalDate d1 = LocalDate.of(2026,1,1);
LocalDate d2 = LocalDate.of(2026,7,28);
Period period = Period.between(d1,d2);
period.getMonths();
  1. 新旧 API 互相转换(项目兼容必备)
// LocalDateTime ↔ Date
//1. LocalDateTime -> Date
Instant instant = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant();
Date date = Date.from(instant);

//2. Date -> LocalDateTime
LocalDateTime ldt = new Date().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
  1. 时区简单说明

LocalDateTime不带时区,跨时区业务(海外系统)必须使用 ZonedDateTime

服务器一般统一使用 UTC 时间存储。

三、新旧 API 核心对比

特性 Date/SimpleDateFormat java.time(JDK8)
可变性 可变 不可变
线程安全 SimpleDateFormat 不安全 全部安全
月份 0 开始 1 开始,符合直觉
API 难用,功能弱 丰富清晰

四、高频易错知识点

  1. 旧 Date、Calendar 月份从 0 开始;LocalDateTime 月份 1~12
  2. SimpleDateFormat 禁止多线程共享;新项目一律用 DateTimeFormatter
  3. LocalDateTime 无时区;不要直接用于跨时区业务
  4. Duration 处理时分秒;Period 只处理年月日,不能混用
  5. 所有 JDK8 时间对象修改后返回新实例,原对象不变