第二章 Java 基础语法

89 阅读16分钟

一、注释(comment)

  • 什么是注释? 源文件中用于解释、说明程序的文字就是注释。

  • 注释的意义

    • 注释是一个程序员必须具有的良好编程习惯。实际开发中,程序员可以先将自己的 思想 通过注释整理出来,再用 代码 去体现。
    • 增强代码的可读性,方便自己和他人后期查阅和调试。
  • Java 中的注释类型

    • 单行注释、多行注释、文档注释

注意:编译以后生成的.class 结尾的字节码文件中不包含注释的信息,不会被 JVM 解释执行

1、单行注释

指注释内容仅限于本行,一旦回车换行就表示注释结束了。

  • **格式:**​//注释文字
  • 说明:双斜线放在注释文字之前,一般放在需要注释的代码的上方或行尾

2、多行注释

指注释内容跨越了多行。

  • **格式:**​/*注释文字*/
  • 说明:多行注释必须使用 /* */将注释文字包含起来,注释信息可以是一行或多行文字。一般放在需要注释的代码的上方

注意:多行注释里面不允许有多行注释嵌套。(因为从 /*开始,当遇到的第一个 */时系统就会认为多行注释结束了)

3、文档注释

Java 特有的一种注释形式,其注释内容可以被 JDK 提供的 javadoc.exe 文档工具解析,生成一套以网页文件形式体现的应用程序编程接口(Application Programming Interface, API)说明文档,这样其他人可以在不看源代码的情况下,通过 API 说明文档快速地了解程序。

  • 格式:
/** 
*注释文字
*@author xxx 
*@version v1.0
*/
  • 说明:

    • 文档注释需要使用 /***/ 将注释内容包含起来,注释内容可以是一行或多行文字。
    • 文档注释一般放在接口属性方法上方
    • 文档注释中通常会有一些 @author@version 等注释标签,用以表示作者和版本等信息。

二、关键字(keyword)

官方地址: docs.oracle.com/javase/tuto…

高级编程语言由一系列单词和符号组成,并且能与计算机进行交互,实现逻辑功能。为了让程序员与计算机能够进行更好的交互,会提前给一些单词赋予特殊的含义,这些在程序语言中具有特殊含义的单词叫作关键字。其中有一部分关键字在 Java 中并没有使用,暂时没有赋予特殊含义,这部分称为保留字

注意:关键字比较多,不需要死记硬背,学到哪里记到哪里即可。

1、 关键字

被 Java 语言赋予了特殊含义,用做专门用途的字符串(或单词)

特点:关键字都是小写字母

关键字

2、 保留字

保留关键字 const​ 和 goto​ ,它们当前未被使用。

goto​:goto 语句在其他语言中叫作"无限跳转"语句。Java 语言不再使用 goto 语句,这是因为 goto 语句会破坏程序结构。在 Java 语言中可以通过 break、continue 和 return 实现"有限跳转"。

const​:const 在其他语言中是声明常量的关键字,在 Java 语言中使用 final​ 方式声明常量。

3、 特殊值

truefalsenull

这三个特殊值看起来像是关键字,但实际上是字面量值,表示特殊的布尔值和空值。后面在给标识符命名时,同样要避开使用特殊值。

三、标识符( identifier)

  1. 定义: 标识符是 Java 中对各种变量、方法、类等要素命名时使用的字符序列,凡是需要命名的要素名统称为标识符。简单理解就是名字。

  2. 标识符的命名规则(必须遵守的硬性规定):

    • 标识符由英文字母大小写、数字 0-9 、下划线_、美元符号 $ 组成
    • 虽然官方说只要是 Unicode 字符集中的字符都可以,但是不推荐使用上述以外的,如中文等
    • 数字不可以开头。
    • 不能包含空格。
    • 不可以使用关键字和保留字,但能包含关键字和保留字。
    • Java 中严格区分大小写,长度无限制。
  3. 标识符的命名规范(建议遵守的软性要求):

命名规范

​注意:

  • 凡是自己可以起名字的地方都叫标识符。
  • 下划线问题:JDK9 版本开始,禁止独立的 _ 作为标识符
  • 在起名字时,为了提高阅读性,要尽量有意义,见名知意。

四、常量(Constant)

1、什么是常量?

常量是一个广义的概念,指在程序运行期间其值不会改变的量。它们的值在代码编写时就确定了,不能更改。

2、分类

  • 字面量:又叫"字面值常量",表示值的固定数据,本质上是一个值。直接使用,不用声明。

    • 整数字面量:intlong 类型(如 42, 100L
    • 浮点字面量:doublefloat 类型(如 3.14, 5.67F
    • 字符字面量:char 类型(单引号 '' 包围单个字符,如 'A'
    • 字符串字面量:String 类型(双引号 "" 包围,如 "Hello"
    • 布尔字面量:truefalse
    • 空字面量:null(表示引用类型变量未指向任何对象)
  • 最终变量:由 final 修饰的变量,又叫"符号常量",具有明确的标识符和数据类型。本质上是一个变量。必须声明:final 数据类型 最终变量名 = 值;

3、规则

  • 字符串字面量必须用双引号 "" 包裹。
  • 字符字面量必须用单引号 '' 包裹,且仅能包含单个字符。
  • 布尔字面量仅限 truefalse
  • 浮点字面量默认是 double 类型,需添加 Ff 后缀表示 float(如 3.14F)。
  • 整数字面量默认是 int 类型,需添加 Ll 后缀表示 long(推荐大写 L,如 100L)。
  • 最终变量命名应使用全大写字母和下划线(如 MAX_VALUE)。

4、字面量和最终变量使用区别

  • 字面量直接嵌入代码,但重复使用时代码可维护性差(需多处修改)。
  • 最终变量通过命名复用值,增强代码可读性和可维护性(仅需修改一处)。

5、举例

class Demo {
    public static void main(String[] args) {
        // 字面量示例
        System.out.println("姓名");        // String类型
        System.out.println(18);           // int类型
        System.out.println('女');         // char类型
        System.out.println(84.5);         // double类型
        System.out.println(true);         // boolean类型
        System.out.println(57814714855L); // long类型(L后缀)
        System.out.println("33088119961202157X"); // String类型(含字母X)

        // 最终变量(符号常量)示例
        final double PI = 3.14;          // 声明
        System.out.println(PI);           // 输出 3.14
    }
}

五、变量(Variable)

1、 概述

变量实质:内存中的一块数据存储区域,该区域有自己的名称(变量名)和类型(数据类型),其值可以在程序运行期间改变。

变量三要素:数据类型、变量名、变量值

  • 数据类型 决定了这个变量中要存储的数据值的 类型 及这块内存的 宽度
  • 变量名 就是一个标识符,方便在程序中使用
  • 变量值 就是这个变量具体存储的值

2、 变量的使用

所谓使用,是指在变量的作用域内将变量中的值拿出来进行 打印、运算、比较 等。

  • **声明:**​数据类型 变量名;

    变量的声明相当于向 JVM 申请一部分指定数据类型大小的内存。 不同的数据类型,需要占用的内存大小是不同的。另外,JVM 中每字节的内存都有自己的编号,称为 内存地址​,但是在程序中直接使用内存地址是极其不方便的,因此需要给这部分内存命名,方便在程序中对这部分内存进行访问和使用。

变量声明内存图

  • **赋值:**​变量名 = 值;

    变量的赋值相当于将符号"="右边的值放到对应的内存中。

变量赋值内存图

  • **变量的初始化:**​数据类型 变量名 = 值;

    第一次给变量赋值称为变量的初始化,即同时声明和赋值

3、 变量的使用要求

  • 变量必须先声明再使用
  • 变量必须在初始化后才能使用
  • 变量有作用域(即一个 {} 内),并且在同一个作用域中不可以重复命名,但是可以重复赋值
  • 变量的值可以变化,但必须在变量声明的数据类型范围内

六、基本数据类型

Java 是一门强类型语言,根据存储元素的需求不同,我们将数据类型划分为基本数据类型和引用数据类型。

数据类型图

基本数据类型关键字内存占用取值范围
字节型byte1 个字节-128 至 127
短整型short2 个字节-32768 至 32767
整型int​(默认)4 个字节-231 至 231-1 (-2147483648 至 2147483647)
长整型long8 个字节-263 至 263-1 (约 -9 x 1018 至 9 x 1018)
单精度浮点数float4 个字节1.4013E-45 至 3.4028E+38
双精度浮点数double​(默认)8 个字节4.9E-324 至 1.7977E+308
字符型char2 个字节0 至 65535
布尔类型boolean至少 1 个字节true​, false​ (可以作为判断条件使用)

注意:

  • 所有整数类型(byte, short, int, long)的表数范围和所占用的内存大小都是固定的。这一特性不受具体操作系统或硬件的影响,是保证 Java 程序可移植性的基石。
  • String类型属于引用数据类型,并且它本身就是一个类 class

1、 计算机存储单位

  • 位(bit):是数据存储的 最小​ 单位。二进制数系统中,每个 0 或 1 就是一个位,叫做 bit(比特),其中 8 bit 就称为一个字节(Byte)。
  • 字节(Byte):是计算机用于 计量存储容量​ 的 基本​ 单位,一个字节等于 8 bit。
换算单位等价于
8 bit1 Byte
1024 Byte1 KB
1024 KB1 MB
1024 MB1 GB
1024 GB1 TB

计算机底层如何存储数据见本章:【拓展】进制

2、 整型

整形包括字节型 byte、短整型 short、整形 int、长整型 long

整型

【注意】

  1. 默认类型与声明规则

    • Java 中的整数常量默认为 int类型

    • 当定义的数值超出 int​ 范围并赋给 long​ 类型时,必须在数值后添加后缀 “L” 或 “l 。强烈推荐使用大写的 “L” 以避免与数字 “1” 混淆。

      示例: long value = 3000000000L;​ // 不加 L 会导致编译错误,因为 30 亿超出了 int 的范围

  2. 类型选择常规实践

    • 在开发中,int​ 是最常用、首选的整数类型。
    • 仅当确认数值可能超出 int​ 的存储范围时(约 ±21 亿),才需要使用 long​。

3、 浮点型

浮点型包括单精度 float、双精度 double

浮点型

【注意】

  1. 声明规则与默认类型

    • Java 中的小数常量​默认为 double类型​。
    • 因此,在定义 float​ 类型变量时,必须在数值后添加后缀 f​ 或 F​。示例: double d = 3.14;float f = 3.14f;
  2. 精度对比与选择

    • float(单精度) ​:有效数字约 7 位,精度较低,可能无法满足需求。
    • double(双精度) ​:精度是 float​ 的两倍,是开发中表示小数的​常规和推荐选择​。
  3. 两种表示形式

    • 十进制​:必须包含小数点。如 5.12​、512.0f​、.512​。
    • 科学计数法​:使用 e​ 或 E​ 表示 10 的幂。如 5.12e2​ (即 5.12 × 10²)。

4、 字符型

(1)基本定义与存储原理

  • 定义char​ 用于存储单个字符,占用 2 个字节的内存空间。
  • 存储原理:Java 采用 Unicode 字符集。在底层,char​ 并非直接存储字符图形,而是存储该字符在 Unicode 编码表中对应的整数值。这使得一个 char​ 变量既可以表示一个英文字母,也可以表示一个汉字或其他语言的字符。

字符型

(2)声明与赋值的三种主要形式

  1. 直接量形式:最常见的方式,使用单引号 ' '​ 将单个字符括起来。

    • 示例: char c1 = 'A'; char c2 = '中'; char c3 = '5';
  2. Unicode 值形式:通过指定字符的 Unicode 编码值来赋值,格式为 '\uXXXX'​,其中 XXXX​ 是一个十六进制整数。

    • 示例: char c = '\u0041';​ // 结果是字符 'A'
  3. 转义字符形式:对于一些无法直接输入或具有特殊含义的字符,使用反斜杠 \​ 进行转义。

    • 示例: char newLine = '\n';​ // 代表换行符

(3)常见的转义字符

转义字符Unicode 值说明
\n\u000a换行符
\t\u0009制表符 (Tab)
\r\u000d回车符
\'\u0027单引号
\"\u0022双引号
\\\u005c反斜杠
\b\u0008退格符

(4)运算特性

由于 char​ 类型在底层存储为数值,因此它可以参与数学运算。运算时,使用的是其在 Unicode 表中对应的整数值。

  • 'a' 对应 97 :a-z 是连续的,所以'b'对应的数值是 98,'c'是 99,依次递加
  • 'A' 对应 65 :A-Z 是连续的,所以'B'对应的数值是 66,'C'是 67,依次递加
  • '0' 对应 48 :0-9 是连续的,所以'1'对应的数值是 49,'2'是 50,依次递加
char c = 'a';
int code = c + 1; // 结果是 98,因为 'a' 的 Unicode 值是 97
System.out.println((char)code); // 输出 'b'

5、 布尔类型

(1)定义与核心规则

  • 定义boolean​ 是 Java 中用于表示逻辑值的基本数据类型。
  • 取值:它只有两个固定的值:true​ (真) 和 false​ (假)。不存在 null​ 或其他任何值。
  • 重要规则:Java 语言强制要求 boolean​ 类型的纯粹性。不可以使用 0 或非 0 的整数来代替 false true​。这一点与 C/C++ 等语言有本质区别。

(2)主要应用场景

boolean​ 类型是所有逻辑判断的基石,主要用于程序中的流程控制:

  • 条件语句if (...)​, if-else ...
  • 循环控制while (...)​, do-while (...)​, for (...; condition; ...)
  • 三元运算符condition ? expressionIfTrue : expressionIfFalse

(3)底层实现说明(JVM 层面)

  • 编译时转换:根据《Java 虚拟机规范》,JVM 内部并没有为 boolean​ 类型设置专用的字节码指令。
  • 内部表示:在编译后,Java 源码中的 boolean​ 值通常会被转换为 int​ 类型来处理:true​ 使用整数 1​ 表示,false​ 使用整数 0​ 表示。
  • 开发者须知:这纯粹是 JVM 的内部实现机制,开发者在编写 Java 代码时必须严格遵守第一条规则,不能将 int​ 和 boolean​ 混用。

七、数据类型的转换

Java 语言只支持布尔型(boolean​)之外的七大基本数据类型间的转换,它们之间的转换类型分为自动类型转换强制类型转换String​ 与基本类型间的转换需通过特定方法。

1、 自动类型转换

自动类型转换,也称为“拓宽转换”,发生在将一个表示范围较小的数据类型赋值给一个表示范围较大的数据类型时。这种转换是编译器自动完成的,无需人工干预。

(1)转换规则

转换的核心原则是:目标类型的表示范围必须大于或等于源类型的表示范围

这个过程可以理解为将小容量的容器中的水倒入大容量的容器,是安全的。

(2)转换关系

基本数据类型的自动转换关系如图所示,根据箭头指向自动转换,反之,则不可以。

实线箭头表示无信息丢失的转换

虚线箭头表示可能有精度损失的转换

(3)表达式中的类型提升

在涉及多种数据类型的混合运算中,系统会遵循“类型提升”(Type Promotion)规则,以确保计算的精确性。

  • 规则一:自动提升至最大类型 当多种类型的数据进行混合运算时,所有操作数会首先被自动转换为表达式中表示范围最大的那个类型,然后再进行计算。

    int i = 10;
    byte b = 2;
    double d = 2.5;
    
    // 在计算 i + b + d 时,i 和 b 会被提升为 double 类型
    // 最终结果必须用 double 或范围更大的类型来接收
    double sum = i + b + d; // sum 的结果是 14.5
    
  • 规则二:byte, short, char的特殊处理byte​、short​、char​ 类型的变量参与运算时,它们会首先被自动提升为 int类型,然后再进行计算。这是 Java 为保证运算效率和精度而设计的。

    byte b1 = 10;
    byte b2 = 20;
    
    // 编译错误! b1 + b2 的结果在运算时已被提升为 int 类型
    // 不能将一个 int 类型的值赋给 byte 类型的变量
    // byte b3 = b1 + b2;
    
    // 正确做法:使用 int 接收或进行强制类型转换
    int result = b1 + b2;
    byte b3 = (byte) (b1 + b2); // 强制转换,但需注意溢出风险
    
    char c1 = 'A'; // ASCII 值为 65
    char c2 = 'B'; // ASCII 值为 66
    int charSum = c1 + c2; // 正确。c1 和 c2 被提升为 int,结果为 131
    

2、 强制类型转换

强制类型转换,也称为“缩窄转换”,发生在需要将一个表示范围较大的数据类型转换为一个表示范围较小的数据类型时。这种转换可能导致数据溢出精度损失,因此必须由开发者显式声明。

(1)语法格式

通过在变量前使用 (目标数据类型)​ 的格式来实现强制转换。

目标数据类型 变量名 = (目标数据类型) 值或变量;

(2)转换规则与风险

强制转换类似于将大容器中的水倒入小容器,可能会有部分水溢出(数据丢失)。

  • 整数类型转换(可能导致数据溢出) 将一个大范围的整数类型(如 int​)转换为小范围的整数类型(如 byte​)时,系统会直接截断高位字节,只保留低位字节。

    int i = 200; // 200 的二进制表示为: 0000 0000 0000 0000 0000 0000 1100 1000
    
    // 强制转换为 byte (8位) 时,会截断前 24 位,只保留后 8 位:1100 1000
    // 在 byte 中,1100 1000 是一个负数的补码,其值为 -56
    byte b = (byte) i; 
    System.out.println(b); // 输出: -56,发生了数据溢出
    
  • 浮点数转整数(导致精度损失) 将一个浮点数类型(float​ 或 double​)强制转换为整数类型时,系统会直接截断小数部分,只保留整数部分,而不是进行四舍五入。

    double pi = 3.14159;
    int intPi = (int) pi;
    System.out.println(intPi); // 输出: 3,小数部分被完全舍弃
    

3、基本类型与 String 类型的转换

String​ 是一个引用数据类型,而非基本数据类型。它与基本数据类型之间的转换规则是独特的。

(1)基本类型转换为 String

有两种常用方式:

  • 使用 String.valueOf()方法:这是最推荐的方式,代码意图清晰。
  • 使用空字符串连接:利用 +​ 运算符的特性,任何类型与字符串相加都会生成一个新的字符串。
int num = 100;
String str1 = String.valueOf(num); // 推荐

String str2 = num + ""; // 简洁,但可读性稍差

(2)String 转换为基本类型

要将字符串转换为基本数据类型,必须借助基本类型对应的包装类(Wrapper Class)parse​ 方法。

注意:被转换的字符串必须是该数据类型能合法表示的字面值,否则会抛出 NumberFormatException​ 异常。

String s1 = "123";
int num1 = Integer.parseInt(s1); // "123" -> 123

String s2 = "3.14";
double num2 = Double.parseDouble(s2); // "3.14" -> 3.14

// 运行时异常:字符串 "hello" 无法被解析为数字
// String s3 = "hello";
// int num3 = Integer.parseInt(s3); // java.lang.NumberFormatException

重要String​ 类型不能通过 (int)​、(double)​ 等强制转换语法转换为其他类型。

4 、 要点总结

  1. 转换前提:除 boolean​ 外,七种基本类型可相互转换。String​ 与基本类型间的转换需通过特定方法。

  2. 自动转换:从小范围类型到大范围类型的安全转换 (byte​ -> short​ -> int​ -> long​ -> float​ -> double​)。

  3. 表达式提升

    • 混合运算时,所有操作数提升到表达式中的最高类型。
    • byte​、short​、char​ 在运算时,总是先提升为 int​。
  4. 强制转换

    • 从大范围类型到小范围类型的显式转换,语法为 (目标类型)值​。
    • 两大风险:整数转换可能因截断导致数据溢出;浮点数转整数会截断小数导致精度损失
  5. String 转换

    • 基本类型 -> String :使用 String.valueOf() 方法或使用 + ​空字符串连接
    • String -> 基本类型:需使用包装类的 parseXXX()​ 方法,且原字符串内容必须合法。

八、运算符(Operator)

1、定义

运算符是用于表示数据运算赋值比较等操作的特殊符号。

2、分类

  • 按功能分类 (最常用的分类方式)
分类运算符说明
算术运算符+​, -​, *​, /​, %​, ++​, --用于执行基本的数学运算。
赋值运算符=​, +=​, -=​, *=​, /=​, %=​, &=​, ^=​, |=​, <<=​, >>=​, >>>=用于将一个值赋给变量。
比较 (关系) 运算符==​, !=​, >​, <​, >=​, <=用于比较两个值,其运算结果永远是 boolean​ 类型。
逻辑运算符&&​ (短路与), ||​ (短路或), !​ (逻辑非)专门用于操作 boolean​ 类型的值。
位运算符&​, |​, ^​, ~​, <<​, >>​, >>>用于对数据的二进制位进行直接操作。
条件运算符? :Java 中唯一的三元运算符,格式为 (条件) ? 结果1 : 结果2​。
Lambda 运算符->Java 8 引入,用于简化匿名函数的书写。
  • 按操作数个数分类
分类定义示例运算符
一元运算符只需要一个操作数。++​ (自增), --​ (自减), !​ (逻辑非)
二元运算符需要两个操作数。+​ (加法), =​ (赋值), >​ (大于)
三元运算符需要三个操作数。? :​ (条件运算符)

3、 算术运算符

运算符名称示例
+加法 / 字符串连接5 + 3
-减法5 - 3
*乘法5 * 3
/除法5 / 3
%取模 (求余)5 % 3
++自增i++​, ++i
--自减i--​, --i

(1)除法 /​ 与取模 %

场景一:整数运算

当所有操作数都是整数时(byte​, short​, char​, int​, long​):

  • 除法 / ​:结果仍然是整数,小数部分会被直接截断(不是四舍五入)。
  • 取模 % ​:结果是整数除法的余数。其符号永远与被模数(左边的操作数)保持一致
// --- 整数运算示例 ---

// 除法 (截断)
System.out.println(10 / 3);      // 输出: 3
System.out.println(-10 / 3);     // 输出: -3

// 取模 (符号看左边)
System.out.println(10 % 3);      // 输出: 1
System.out.println(-10 % 3);     // 输出: -1 (符号与-10一致)
System.out.println(10 % -3);     // 输出: 1  (符号与10一致)

场景二:浮点数运算

当任意一个操作数是浮点数时(float​, double​):

  • 除法 / ​:执行标准的数学除法,结果是 double​ 或 float​。
  • 取模 % ​:执行浮点数求余,结果也可能是浮点数。
// --- 浮点数运算示例 ---

System.out.println(10.0 / 3);    // 输出: 3.3333333333333335
System.out.println(-10.5 % 3);   // 输出: -1.5 (符号与-10.5一致)

特殊情况:操作数为 0

当除数或模数为 0 时,整数和浮点数的行为截然不同,这也是一个常见的面试考点。

操作结果说明
整数 / 0java.lang.ArithmeticException程序会因异常而崩溃。
浮点数 / 0Infinity程序正常运行,得到一个表示“无穷大”的特殊值。
0.0 / 0.0NaN“Not a Number”,表示一个不确定的数值。
浮点数 % 0NaN程序正常运行,结果同样为 NaN​。

(2)自增 ++​ 与自减 --

++​ 和 --​ 分为前置和后置两种形式,它们的区别仅在表达式中才会体现。

  • **前置 (Prefix):**​ ++i, --i

    • 规则:先让变量的值自增/减 1,然后使用变化后的值参与整个表达式的运算。可以记为“先变后用”。
  • 后置 (Postfix):i++, i--

    • 规则:先使用变量的原始值参与整个表达式的运算,然后再让变量的值自增/减 1。可以记为“先用后变”。
// 前置示例
int a = 5;
int b = ++a; // a 先变成 6, 然后 b 被赋值为 6
// 结果: a=6, b=6

// 后置示例
int x = 5;
int y = x++; // y 先被赋值为 x 的原始值 5, 然后 x 再变成 6
// 结果: x=6, y=5

注意 :避免在单个表达式中对同一变量进行多次自增/自减

int j = i++ + ++i * i++; ​这样的代码虽然在 Java 中有明确的运算顺序,但极大地降低了代码的可读性,是 bug 的主要来源,属于严禁使用的编码风格。在面试中可能会遇到此类题目以考察你对运算顺序的理解,但在实际项目中,应将其拆分为多个清晰的语句。

(3)特殊情况:+​ 作为字符串连接符

+​ 运算符的任一侧出现 String​ 类型的操作数时,它将不再执行算术加法,而是执行字符串连接操作。

String​ 字符串可以和八种基本数据类型变量做运算,且运算只能是连接运算 +​,运算的结果仍然是 String​ 类型。

System.out.println("Hello" + 2024); // 输出: "Hello2024"

// 注意运算顺序
System.out.println(10 + 20 + "Hi");   // 输出: "30Hi" (先计算 10+20)
System.out.println("Hi" + 10 + 20);   // 输出: "Hi1020" (从左到右依次连接)

4、 赋值运算符

赋值运算符的核心功能是将一个值(或表达式的结果)赋给一个变量。它是 Java 中最基础、最常用的运算符之一。

(1)基本赋值运算符 =

这是最简单、最直接的赋值方式。

  • 语法: 变量 = 表达式;

  • 运算顺序: 赋值运算从右向左执行。首先计算 =​ 右侧表达式的值,然后将该值存入 =​ 左侧的变量中。

  • 规则:

    • =​ 的左侧必须是一个变量
    • =​ 的右侧可以是任何能够产生值的形式,包括常量、变量或更复杂的表达式
int a;
int b = 10;
a = b; // 将变量 b 的值 (10) 赋给 a
a = b + 5; // 先计算 b + 5 (结果为15),再将结果赋给 a

(2)复合赋值运算符

复合赋值运算符是算术/位运算符基本赋值运算符 = ​ 的结合,用于简化代码。

运算符示例概念上等价于
+=a += b;a = a + b;
-=a -= b;a = a - b;
*=a *= b;a = a * b;
/=a /= b;a = a / b;
%=a %= b;a = a % b;
&=a &= b;a = a & b;
|=a |= b;a = a | b;
^=a ^= b;a = a ^ b;
<<=a <<= b;a = a << b;
>>=a >>= b;a = a >> b;
>>>=a >>>= b;a = a >>> b;

注意

  • 为什么没有 ~=​ 这样的运算符?这是因为按位取反 ~​ 是一个一元运算符(只需要一个操作数),而复合赋值运算符是为二元运算符(需要两个操作数)设计的简化写法。
  • 自增 ++​ 和自减 --​ 也是一元运算符,因此不存在 ++=​ 或 --=​ 这样的复合运算符。

(3)隐藏的强制类型转换

这是复合赋值运算符最重要也最容易被忽略的特性。

当使用复合赋值运算符时,如果右侧表达式计算出的结果类型与左侧变量的类型不匹配,编译器会自动进行一次隐藏的强制类型转换,将结果转换为左侧变量的类型。

这与 变量 = 变量 + 值的形式有本质区别!

// 场景:byte 类型最大值为 127
byte a = 127;

// 尝试方式一:使用基本运算符
// a = a + 1; // 这行代码会编译失败!
// 失败原因:a 在运算时被提升为 int 类型,a + 1 的结果也是 int (128)。
//          将 int 类型的 128 赋给 byte 类型的 a,需要显式强转,否则报错。

// 尝试方式二:使用复合赋值运算符
a += 1; // 这行代码编译通过!
// 背后发生的是:a = (byte)(a + 1);
// 编译器自动加入了强制类型转换。
// 结果:int 类型的 128 被强转为 byte,导致溢出。

System.out.println(a); // 输出: -128

注意 :警惕精度损失和数据溢出

复合赋值运算符的这种自动类型转换虽然方便,但也带来了风险。它会静默地处理类型不匹配问题,可能导致数据精度丢失(如 double​ 转 int​)或数据溢出(如上例),从而引发难以察觉的逻辑错误。在处理不同数据类型时,必须清楚地意识到这一行为。

5、 比较运算符

比较运算符,也称为“关系运算符”,用于比较两个操作数之间的关系。其运算结果总是一个布尔值(boolean ——true​ 或 false​。因此,它们是构成程序控制流(如 if​ 条件判断、while​ 和 for​ 循环)的基石。

(1)运算符列表

比较运算符均为二元运算符,即需要两个操作数。它们的运算顺序遵循标准的从左到右的结合性。

运算符名称示例 (int a = 10, b = 20;​)结果
==等于a == bfalse
!=不等于a != btrue
>大于a > bfalse
<小于a < btrue
>=大于或等于a >= 10true
<=小于或等于a <= 20true

(2)==​ 的双重语义

==​ 运算符的行为完全取决于它所比较的操作数的类型:是基本数据类型还是引用数据类型

  • 用于基本数据类型:比较“值”==​ 用于 byte​, short​, int​, long​, float​, double​, char​ 时,它比较的是两个变量所存储的字面值是否相等。

    int a = 100;
    int b = 100;
    double c = 100.0;
    char d = 'd'; // 'd' 的 ASCII 值是 100
    
    System.out.println(a == b); // true, 值相等
    System.out.println(a == c); // true, a 会自动类型提升为 double 100.0 后再比较
    System.out.println(a == d); // true, d 会自动类型提升为 int 100 后再比较
    
  • 用于引用数据类型:比较“内存地址”==​ 用于对象、数组等引用类型时,它比较的是两个引用变量是否指向堆内存中的同一个对象实例。它不关心对象的内容是否相同。

    String str1 = new String("hello");
    String str2 = new String("hello");
    String str3 = str1;
    
    System.out.println(str1 == str2); // false, str1 和 str2 指向内存中两个不同的 "hello" 对象
    System.out.println(str1 == str3); // true, str3 和 str1 指向同一个对象
    
    // 提示:要比较对象的内容是否相等,应使用 .equals() 方法。
    System.out.println(str1.equals(str2)); // true, 因为 "hello" 的内容相同
    

(3)类型兼容性要求

  • 对于 == != ​: 参与比较的两个操作数类型必须相同或相互兼容(即,一个类型可以自动提升为另一个类型)。否则,代码将无法通过编译。

    int i = 10;
    double d = 10.0;
    boolean b = true;
    
    System.out.println(i == d); // 合法, int 可以自动提升为 double
    
    // 编译错误:操作数类型不兼容 (Incompatible operand types int and boolean)
    // System.out.println(i == b); 
    
  • 对于 > ​ **、**​ < ​ **、**​ >=<= ​: 这些排序相关的运算符仅适用于数值类型和 char类型。它们不能用于 boolean​ 类型或任何引用类型。

    int a = 10;
    char c = 'C'; // 'C' 的 ASCII 值为 67
    
    System.out.println(a < c); // true, a(10) < c(67)
    
    // 编译错误:运算符 '>' 不能应用于 java.lang.String, java.lang.String
    // String s1 = "abc";
    // String s2 = "def";
    // System.out.println(s1 > s2);
    

(4)总结

  1. 结果为布尔:所有比较运算的最终结果都是 true​ 或 false​,是流程控制的基础。

  2. ==的本质

    • 基本类型,比较的是
    • 引用类型,比较的是内存地址(是否为同一对象)。若要比较内容,请使用 .equals()​ 方法。
  3. 排序运算符的局限性>​、<​、>=​、<=​ 仅能用于 boolean的基本数据类型。

  4. 类型需兼容:进行比较的两个操作数必须是相同或兼容的类型,否则会导致编译失败。

6、 逻辑运算符

逻辑运算符用于组合一个或多个布尔表达式,其最终运算结果也是一个布尔值(boolean 。它们是在 if​、while​、for​ 等控制语句中构建复合条件的核心工具。

(1)运算符总览与真值表

逻辑运算符的操作数必须是 boolean​ 类型。除逻辑非 !​ 为一元运算符外,其余均为二元运算符。

逻辑类别运算符名称运算规则说明
与 (AND)
&&短路与A && B​:仅当 A 和 B 都为 true​ 时,结果为 true若 A 为 false​,则不再判断 B (推荐使用)
&逻辑与A & B​:仅当 A 和 B 都为 true​ 时,结果为 true无论 A 的结果如何,总是会判断 B
或 (OR)
||短路或A || B​:当 A 或 B 任一为 true​ 时,结果为 true若 A 为 true​,则不再判断 B (推荐使用)
|逻辑或A | B​:当 A 或 B 任一为 true​ 时,结果为 true无论 A 的结果如何,总是会判断 B
异或 (XOR)^逻辑异或A ^ B​:当 A 和 B 的值不同时,结果为 true必须计算两端才能判断是否“不同”
非 (NOT)!逻辑非!A​:反转 A 的布尔值一元运算,只作用于单个操作数

真值表 (Truth Table):

aba && ba || ba & ba | ba ^ b
truetruetruetruetruetruefalse
truefalsefalsetruefalsetruetrue
falsetruefalsetruefalsetruetrue
falsefalsefalsefalsefalsefalsefalse

(2)重要区别:短路

在逻辑判断中,“短路”是 &&​ 和 ||​ 的一个关键特性,它能提升效率并避免不必要的计算和潜在的运行时错误。

  • 短路与 (&&​) vs. 逻辑与 (&​)

    • &&(短路与) : 从左到右计算。如果左侧表达式的结果为 false​,那么整个表达式的结果必定为 false​。此时,右侧的表达式将不会被执行(被短路)
    • &(逻辑与) : 无论左侧表达式的结果是什么,两侧的表达式总会被完整执行
  • 短路或 (||​) vs. 逻辑或 (|​)

    • ||(短路或) : 从左到右计算。如果左侧表达式的结果为 true​,那么整个表达式的结果必定为 true​。此时,右侧的表达式将不会被执行(被短路)
    • |(逻辑或) : 无论左侧表达式的结果是什么,两侧的表达式总会被完整执行

(3)开发建议与总结

  1. 首选短路运算符:在进行逻辑判断时,应始终优先使用 && || ​。这不仅能带来微小的性能提升,更重要的是能编写出更安全、更健壮的代码,有效避免类似 NullPointerException​ 的运行时错误。
  2. & |的用途:虽然 &​ 和 |​ 在逻辑运算中不被推荐,但它们在位运算(Bitwise Operations) 中扮演着核心角色。当操作数是整数(如 int​, byte​)时,&​ 和 |​ 会对这些数字的二进制位进行操作。请务必区分这两种完全不同的应用场景。
  3. 逻辑异或 (^)^​ 主要用于判断两个条件是否恰好有一个成立的场景,或在算法(如加密、校验)中作为位运算符使用。在常规业务逻辑中不如 &&​ 和 ||​ 常用。
  4. **逻辑非 (**​ !)!​ 用于反转一个布尔表达式的结果,非常直观和常用。例如 !user.isActive()​。

7、 条件运算符

条件运算符是 Java 中唯一的三元运算符(Ternary Operator),因为它需要三个操作数。它提供了一种极为简洁的方式来替代简单的 if-else​ 语句,非常适合用于对变量进行二选一的赋值操作。

(1)格式

(布尔类型的条件表达式) ? 表达式1 : 表达式2;

(2)执行流程

  1. 求值条件:首先,计算 条件表达式​ 的值。这个表达式的结果必须是一个 boolean​ 值 (true​ 或 false​)。

  2. 选择分支

    • 如果条件为 true​,则计算并返回 表达式1​ 的值,此时 表达式2不会被计算
    • 如果条件为 false​,则计算并返回 表达式2​ 的值,此时 表达式1不会被计算

代码示例:

int score = 85;
String result = (score >= 60) ? "及格" : "不及格";
System.out.println(result); // 输出: "及格"

(3)类型兼容性规则

  • 表达式1 表达式2的类型必须相同或相互兼容
  • 所谓“兼容”,是指其中一个类型可以通过自动类型提升转换为另一个类型(例如,int​ 和 double​ 是兼容的)。
  • 整个条件运算符表达式的最终结果类型,由 表达式1​ 和 表达式2​ 中范围更大的那个类型决定。

(4)开发建议

  • 适用场景:简洁赋值

条件运算符最适合用于简单的、二选一的赋值场景。它能让代码更紧凑。

  • 避坑指南:避免过度嵌套
int score = 75;
// 这段代码虽然能工作,但非常难以阅读
String grade = (score >= 90) ? "优秀" : ((score >= 60) ? "及格" : "不及格");

虽然语法上允许,但强烈不建议嵌套使用条件运算符。嵌套的条件运算符会使代码的可读性急剧下降,变得难以理解和维护。对于复杂的条件逻辑,使用流程控制语句。代码的清晰性和可维护性远比表面的简洁更重要。

8、 位运算符(了解)

(1)前置知识:计算机如何表示整数 (二进制补码)

计算机使用二进制的 补码 (Two's Complement) 形式来存储所有整数。Java 中所有的整数运算,包括位运算,都是在数字的二进制补码形式上进行的。

Java 的 byte​ 类型宽度 1 字节(8bit)、short ​类型宽度 2 字节(16bit)、int ​类型宽度 4 字节(32bit)、long​ 类型宽度 8 字节(64bit)

  • 符号位:二进制的最高位是符号位,0​ 代表正数,1​ 代表负数。

  • 为什么使用补码?

    1. 统一加减法:使用补码可以将减法运算转变为加法运算,简化了 CPU 的电路设计。A - B​ 等价于 A + (-B的补码)​。
    2. 解决了“+0”和“-0” :在原码表示中,00000000​ (+0) 和 10000000​ (-0) 是两个不同的编码,而补码中 0​ 的表示是唯一的。
  • 正数:其原码、反码、补码三码合一。最高位(符号位)为 0

    • 例如,正数 5​ : 00000000 00000000 00000000 00000101
  • 负数

    1. 原码 (Sign-Magnitude) :将十进制绝对值转换为二进制,然后将最高符号位置为 1​。
    2. 反码 (One's Complement) :在原码的基础上,符号位不变,其余各位按位取反(0 ​变 1​,1 ​变 0​)。
    3. 补码 (Two's Complement) :在其反码的末位加 1​。
// 示例:计算 -5 的补码 (以8位演示)
// 正数5的原码:
    0000 0101
// 得到-5的原码 (仅改变符号位):
    1000 0101
// 计算-5的反码 (符号位不变,数值位取反):
    1111 1010
// 计算-5的补码 (反码 + 1):
    1111 1010  (反码)
    +       1
    -----------
    1111 1011  (补码)
因此,在计算机内部,-5是以 11111111 11111111 11111111 11111011 (32位补码) 的形式存储和运算的。

解析 byte 类型的数值范围:-128~127

一个 byte​ 占 8 位,能表示 2^8 = 256​ 个不同的值。

  • 正数 (包括 0) :从 0000 0000​ 到 0111 1111

    • 0000 0000​ -> 0
    • 0111 1111​ -> 127
    • 128​ 个。
  • 负数:从 1000 0000​ 到 1111 1111

    • 1111 1111​ -> (反码 1000 0000​ -> 原码 1000 0001​) -> -1
    • 1000 0001​ -> (反码 1111 1110​ -> 原码 1111 1111​) -> -127
  • 特殊的 -128​:

    • 补码 1000 0000​ 是一个特殊值。按照规则,它的反码是 0111 1111​,再加 1 得到原码,会超出位数。
    • 在补码系统中,1000 0000​ 被规定-128​。这恰好填满了 256​ 个值的空位,没有浪费。

(2)位运算符概述

~​ (按位取反) 是一元运算符外,其余六个均为二元运算符。

运算符名称描述
&按位与对应位都为 1​ 时,结果位才为 1​。
|按位或对应位有一位为 1​,结果为 1​,否则为 0
^按位异或对应位不同时为 1​,相同时为 0​。
~按位取反将所有二进制位反转(0 ​变 1​,1 ​变 0​)。
<<左移各二进制位全部向左移动指定位数,右侧补 0​。
>>有符号右移各二进制位向右移动,用原符号位填充高位。
>>>无符号右移各二进制位向右移动,用 0 ​填充高位。

(3)**左移 (**​ <<)

  • 规则:各二进制位全部向左移动指定位数,右侧补 0​。x << n​ 在数值上约等于 x * 2^n​。

  • 示例 (3 << 2) :

    • 3​ 的补码 (以 8 位演示): 0000 0011
    • 向左移动 2 位,右侧补 0​: 0000 1100
    • 结果转为十进制: 12

(4)**有符号右移 (**​ >>)

  • 规则:各二进制位向右移动,用原符号位填充高位。x >> n​ 在数值上约等于 x / 2^n​ (向下取整)。

  • **示例 (**​ -5 >> 2) :

    • -5​ 的补码 (以 8 位演示): 1111 1011
    • 向右移动 2 位,高位用符号位 1​ 填充: 1111 1110
    • 这个补码 1111 1110​ 代表的十进制是 -2​。
    • 验证: -5 / 2^2 = -5 / 4 = -1.25​,向下取整为 -2​。

(5)**无符号右移 (**​ >>>)

  • 规则:各二进制位向右移动,用 0 ​填充高位,无论正负。因此,对负数进行此操作会得到一个巨大的正数。

  • **示例 (**​ -5 >>> 2) :

    • -5​ 的补码 (必须用 32 位): ​11111111 11111111 11111111 11111011
    • 向右移动 2 位,高位补 0​: ​00111111 11111111 11111111 11111110
    • 结果是一个非常大的正数 (1,073,741,822​)。

(6)**按位与 (**​ &)

  • 规则:两位都为 1​,结果为 1​,否则为 0​。

  • 示例 (6 & 3)(以 8 位补码演示) :

      0000 0110  (6)
    & 0000 0011  (3)
    -----------
      0000 0010  (2)
    

(7)**按位或 (**​ |)

  • 规则:只要有一位为 1​,结果为 1​,否则为 0​。

  • 示例 (6 | 3)(以 8 位补码演示) :

      0000 0110  (6)
    | 0000 0011  (3)
    -----------
      0000 0111  (7)
    

(8)**按位异或 (**​ ^)

  • 规则:两位不同,结果为 1​,否则为 0​。

  • 示例 (6 ^ 3)(以 8 位补码演示) :

      0000 0110  (6)
    ^ 0000 0011  (3)
    -----------
      0000 0101  (5)
    

(9)**按位取反 (**​ ~)

  • 规则0​ 变 1​,1​ 变 0​。~x​ 在数值上等于 -x - 1​。

  • **示例 (**​ ~6) :

    • 6​ 的补码 (以 8 位演示): 0000 0110
    • 按位取反: 1111 1001
    • 此为负数的补码,其表示的十进制值为 -7​。

(10)小结

  1. 为什么没有 “无符号左移” 因为“无符号左移”在逻辑上是多余的,它的行为与现有的“有符号左移 <<​”完全没有区别。 问题的关键在于:左移运算空出来的是“低位”,而右移运算空出来的是“高位”。 不同的位置决定了是否需要区分“有符号”和“无符号”。
  2. 区分:逻辑运算符 vs. 位运算符 当 &​, |​, ^​ 的操作数是 boolean​ 类型时,它们执行逻辑运算;当操作数是整数类型时,它们执行位运算。这是由上下文决定的。
  3. 高效计算 (面试题)
// 问题:如何最高效地计算 2 * 8?
// 解答:使用位移运算。`2 << 3` 或 `8 << 1`。CPU 执行位移指令远快于执行乘法指令。
// 不使用临时变量交换两个整数,如:int m = 10, n = 5;,如何在不使用第三个变量的情况下交换 m 和 n 的值?
// 方案一:按位异或 (推荐,无溢出风险)
m = m ^ n; // m = 10^5 = 15
n = m ^ n; // n = 15^5 = 10
m = m ^ n; // m = 15^10 = 5
// 方案二:加减法 (有数值溢出风险)
m = m + n;
n = m - n;
m = m - n;

9、运算符优先级(了解)

image

说明:

  • 运算符有不同的优先级,所谓优先级就是在表达式中的运算顺序。
  • 上一行中的运算符总是优先于下一行的。

开发建议:

  • 不要过多的依赖运算的优先级来控制表达式的执行顺序,这样可读性太差,尽量 使用小括号()来控制​ 表达式的执行顺序。

  • 不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则把它 分成几步​ 来完成。

    反例如:(num1 + num2) * 2 > num3 && num2 > num3 ? num3 : num1 + num2;

九、标点符号

Java 语言中列出的 50 个关键字(含保留字)和 3 个特殊值的单词(true、false、null)具有特殊意义;

Java 语言中定义了 38 个运算符对应具体的运算指令;

Java 语言中还规定了下画线 _​ 和美元符号 $2 个字符可以出现在标识符中,而斜线字符 \​ 字符用作转义。

除此之外,Java 语言还包含了 12 个标点符号,它们是:

( ) { } [ ] ; , . ... @ ::

  • 小括号 () 用于强制类型转换、表示优先运算表达式、方法参数列表。
  • 大括号 {} 用于数组元素列表、类体、方法体、复合语句代码块边界符。
  • 中括号 [] 用于数组。
  • 分号 ; 用于结束语句。
  • 逗号 , 用于多个赋值表达式的分隔符和方法参数列表分隔符。
  • 英文句号 . 用于成员访问和包目录结构分隔符。
  • 英文省略号 ... 用于可变参数。
  • @ 用于注解。
  • 双冒号 :: 用于方法引用。

十、字符集与字符编码【拓展】

计算机中储存的信息都是用 二进制数​ 表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。

按照某种规则,将字符存储到计算机中,称为 编码​ 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为 解码​ 。

在软件开发中,我们遇到的几乎所有“乱码”问题(如 ?????​ ),其本质都是字符集与编码方式不匹配导致的。

1 、 字符集 vs. 字符编码

初学者常将二者混淆,但它们是两个层面的概念:

  1. 字符集

    • 定义:也叫编码表,是一个系统支持的所有字符的集合,并为每个字符分配了一个唯一的数字编号(称为“码点”或 "Code Point")。
    • 目的:解决“是什么”的问题。它是一本字典,定义了'A'是 65,'你'是 20320。
    • 例子:ASCII 字符集、Unicode 字符集
  2. 字符编码

    • 定义:一套具体的规则,用于将字符集中的数字编号(码点)转换并存储为计算机内存或文件中的二进制字节序列。
    • 目的:解决“怎么存”的问题。它是将字典内容写入硬盘的方法。
    • 例子:UTF-8、GB 系列、ISO-8859-1

简单地说:Unicode(字符集)告诉你“汉”字的身份证号是 U+6C49,而 UTF-8(字符编码)则规定了这个号码应该以 E6 B1 89这三个字节的形态存储在计算机里。

2 、 编码的演进之路:从混乱到统一

阶段一:ASCII 时代 (美国标准)

  • ASCII (美国信息交换标准代码) :上世纪 60 年代为英语设计,使用 7 位二进制表示 128 个字符(英文字母、数字、控制符)。它奠定了现代编码的基础,其码点定义至今仍在沿用。
  • 局限:纯粹的英语中心设计,无法表示任何其他语言的字符。

阶段二:“代码页”的混乱时代 (区域标准)

为了在本国电脑上显示自己的语言,世界各地在 ASCII 的基础上进行了扩展,导致了大量不兼容的区域性编码,也称为“代码页 (Codepage)”。

  • ISO-8859-1 (Latin-1) :西欧语言的扩展,使用 8 位(1 字节)编码,兼容 ASCII,并增加了西欧常用字符。

  • GB 系列 (中国国标)

    • GB2312:最早的简体中文方案,使用双字节编码,收录了约 7000 个汉字,向下兼容 ASCII
    • GBK:GB2312 的超集,支持了繁体字和更多汉字,总数达 21003 个,是中文 Windows 系统默认的编码方式
    • GB18030:最新的国标,进一步扩充了字符,支持少数民族文字。

这个时代的核心问题是:一个 GBK 编码的文件,在只支持 ISO-8859-1 的系统上打开,必然是乱码。

阶段三:Unicode 革命 (全球统一标准)

  • Unicode (统一码) :为了终结“代码页”的混乱,Unicode 字符集应运而生。它的目标是为世界上每一种语言的每一个字符都提供一个全球唯一的码点,从而实现跨语言、跨平台的文本处理。

  • Unicode 的缺点:这里有三个问题:

    • 第一,英文字母只用一个字节表示就够了,如果用更多的字节存储是 极大的浪费​。
    • 第二,如何才能 区别Unicode和ASCII​?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?
    • 第三,如果和 GBK 等双字节编码方式一样,用最高位是 1 或 0 表示两个字节和一个字节,就少了很多值无法用于表示字符,不够表示所有字符​。

阶段四:UTF-8 的胜利 (最高效的实现)

Unicode 字符集在很长一段时间内无法推广,直到互联网的出现,为解决 Unicode 如何在网络上传输的问题,于是面向传输的众多 UTF(UCS Transfer Format)标准出现。具体来说,有三种编码方案,UTF-8、UTF-16 和 UTF-32。

UTF-8、UTF-16、UTF-32 是三种 将数字转换到程序数据​ 的编码方案。顾名思义,UTF-8 就是每次 8 个位传输数据,而 UTF-16 就是每次 16 个位。其中,UTF-8 是在互联网上 使用最广​ 的一种 Unicode 的实现方式。

  • 核心优势

    1. 完美兼容 ASCII:ASCII 字符(码点 0-127)只用 1 个字节表示,与 ASCII 文件一模一样。
    2. 空间高效:根据字符的码点大小,使用 1 到 4 个字节进行编码。拉丁文常用 2 字节,绝大多数汉字用 3 字节。
    3. 自同步:编码规则使得程序可以从字节流的任何位置准确地找到一个字符的起始字节,非常健壮。

3 、 开发建议

核心原则:在数据流经的每一个环节,都明确指定并统一使用 UTF-8编码。

  1. 源码文件编码

    在 IDE(如 IntelliJ IDEA、VS Code)中,将项目和所有文件的默认编码设置为 UTF-8​。这能防止源码中的中文字符串在编译时出错。

  2. 数据库编码

    • 创建数据库和表时,必须显式指定字符集。对于 MySQL,应使用 utf8mb4 ​而非 utf8​。(在 UTF-8 刚出现时,大部分字符都在 3 字节范围内,utf8 是 MySQL 为了节省空间而做的早期实现)
    • 示例: CREATE DATABASE my_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  3. 数据库连接

    • 行动:在应用的数据库连接字符串(JDBC URL)中,必须明确指定编码。
    • 示例: jdbc:mysql://localhost:3306/my_db?useUnicode=true&characterEncoding=UTF-8
  4. HTTP 通信

    • HTTP 请求:前端提交的数据,应确保以 UTF-8 编码。现代浏览器默认如此。后端框架(如 Spring)也应配置为按 UTF-8 解析请求体。
    • HTTP 响应:后端返回给前端的数据(HTML, JSON 等),必须在 Content-Type​ 响应头中明确声明 charset=UTF-8​。
    • 示例: Content-Type: application/json; charset=UTF-8
  5. 文件 I/O

    • 行动:在 Java 中读写文件时,必须使用接受 Charset​ 参数的构造函数,而不能依赖操作系统的默认编码。
    • 示例: new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
  6. JVM 启动参数

    • 行动:为避免某些依赖系统默认编码的第三方库出现问题,建议为 JVM 设置启动参数。
    • 示例: java -Dfile.encoding=UTF-8 -jar my_app.jar

4、 “ANSI”编码

在 Windows 记事本的“另存为”对话框中,你会看到一个叫 “ANSI” 的编码选项。

  • “ANSI”不是一种具体的编码。它是一个在不同语言环境下指向不同默认编码的别名
  • 在中国大陆地区的 Windows 上,ANSI 指向 GBK
  • 在西欧语言的 Windows 上,ANSI 指向 ISO-8859-1
  • 建议不要使用“ANSI”编码。

5、小结

十一、进制【拓展】

计算机的硬件逻辑(高低电平)决定了其内部世界只有二进制。我们编写的所有代码,处理的所有数据——无论是数字、文本、图片还是音视频——在底层都必须被翻译成二进制的 0​ 和 1​ 才能被存储和运算。

1 、 进制概览:不同的计数体系

进制 (Base)构成字符进位规则前缀示例说明
二进制 (Binary)0​, 1逢二进一0b​ 或 0Bint num = 0b101;告诉编译器,101​ 是一个二进制数,其值等于十进制的 5。
八进制 (Octal)0​ - 7逢八进一0​ (单个零)int num = 010;告诉编译器,10​ 是一个八进制数,其值等于十进制的 8。
十六进制 (Hex)0​-9​, a​-f逢十六进一0x​ 或 0Xint num = 0x1A;告诉编译器,1A​ 是一个十六进制数,其值等于十进制的 26。
十进制 (Decimal)0​ - 9逢十进一无前缀int num = 123;如果一个数字字面量没有任何前缀,编译器会默认它就是我们日常使用的十进制数。

2 、 进制转换

(1)其他进制 -> 十进制:权相加法

“权相加法”是将任何非十进制数转换为十进制数的通用法则。其核心思想是:将数中每一位上的数字乘以其对应的权重,然后将所有乘积相加。

这个“权重”就是基数(Radix)的 n次方,其中 n​ 是该数字所在的位置(从右向左,从 0 开始计数)。

  • 对于二进制,基数是 2
  • 对于八进制,基数是 8
  • 对于十六进制,基数是 16

示例:

  • 将二进制数 110101​ 转换为十进制。
二进制110101
位置543210
权重2^52^42^32^22^12^0
十进制(1 × 2^5) + (1 × 2^4) + (0 × 2^3) + (1 × 2^2) + (0 × 2^1) + (1 × 2^0)=53
  • 将八进制数 0372​ 转换为十进制。
八进制位372
位置210
权重8^28^18^0
十进制(3 × 8^2) + (7 × 8^1) + (2 × 8^0) = 250
  • 将十六进制数 1A5​ 转换为十进制。
十六进制位1A (10)5
位置210
权重16^216^116^0
十进制(1 × 16^2) + (10 × 16^1) + (5 × 16^0) = 421

特别注意: 在十六进制中,字母 A​ 到 F​ 分别代表十进制的 10​ 到 15​。在计算时需要进行替换。

(2)十进制 -> 二进制:除 2 取余法 (逆序)

对十进制数反复进行除以 2 的运算,直到商为 0,然后将每一步的余数倒序排列

示例:十进制 13​ -> 二进制:

  • 13 / 2 = 6 ... 1
  • 6 / 2 = 3 ... 0
  • 3 / 2 = 1 ... 1
  • 1 / 2 = 0 ... 1
  • 逆序读取余数:1101

(3)二进制 <=> 十六进制/八进制:分组法

十六进制4 位 二进制八进制3 位 二进制
000000000
100011001
200102010
300113011
401004100
501015101
601106110
701117111
81000--
91001--
A1010--
B1011--
C1100--
D1101--
E1110--
F1111--

二进制 转 十六进制

规则:从右向左,每 4 位一组。

示例:将二进制 110101​ 转换为十六进制。

  1. 分组:从右边开始,每 4 个数字为一组。

    • 11 | 0101
    • 我们得到了两组:右边是 0101​,左边是 11​。
  2. 补零:检查每一组是否都满 4 位。

    • 0101​ 已经是 4 位了。
    • 11​ 只有 2 位,不够。我们需要在左边给它补 0​,直到补满 4 位。补零不会改变它的值。11​ -> 0011
  3. 查表转换:现在我们有了两组完整的 4 位二进制 0011​ 和 0101​。

    • 对照上面的表格,0011​ 对应十六进制的 3​。
    • 对照上面的表格,0101​ 对应十六进制的 5​。
  4. 拼接结果:将转换后的数字按顺序拼接起来。

    • 3​ 和 5​ -> 35
    • 加上十六进制前缀 0x​,最终结果是 0x35​。

二进制 转 八进制

规则:从右向左,每 3 位一组。

示例:将二进制 110101​ 转换为八进制。

  1. 分组:从右边开始,每 3 个数字为一组。

    • 110 | 101
    • 我们得到了两组:右边是 101​,左边是 110​。
  2. 补零:检查每一组是否都满 3 位。

    • 101​ 已经是 3 位了。
    • 110​ 已经是 3 位了。
  3. 查表转换

    • 对照表格,110​ 对应八进制的 6​。
    • 对照表格,101​ 对应八进制的 5​。
  4. 拼接结果

    • 6​ 和 5​ -> 65
    • 加上八进制前缀 0​,最终结果是 065​。

十六进制 转 二进制

规则:将每一位十六进制,独立地转换为其对应的 4 位 二进制数。

示例:将十六进制 0x3A​ 转换为二进制。

  1. 拆分:将十六进制数的每一位拆开。

    • 3​ 和 A
  2. 查表转换 (补足位数)

    • 3​ -> 0011​ (因为是十六进制,必须转成 4 位二进制)
    • A​ (就是 10) -> 1010​ (同样,必须是 4 位)
  3. 拼接结果:将得到的二进制串直接拼接起来。

    • 0011​ 和 1010​ -> 00111010
  4. 美化 (可选) :可以去掉开头的 0​,并加上二进制前缀 0b​。

    • 00111010​ -> 111010​ -> 0b111010

八进制 转 二进制

规则:将每一位八进制,独立地转换为其对应的 3 二进制数。

示例:将八进制 0123​ 转换为二进制。

  1. 拆分:将十六进制数的每一位拆开。

    • 1​、2​、3
  2. 查表转换 (补足位数)

    • 1​ -> 001​ (因为是八进制,必须转成 3 位二进制)
    • 2​ -> 010​ (同样,必须是 3 位)
    • 3​ -> 011​ (同样,必须是 3 位)
  3. 拼接结果:将得到的二进制串直接拼接起来。

    • 001​、010​、011​ -> 001010011
  4. 美化 (可选) :可以去掉开头的 0​,并加上二进制前缀 0b​。

    • 001010011​ -> 1010011​ -> 0b1010011