软件研发 --- Java
第一阶段:Java基础
1. 安装配置JDK、开发工具安装(IDEA)
JDK(Java Development Kit)
什么是JDK:
JDK是Java开发工具包,是Java开发的基础环境。它包含了:
- JRE(Java Runtime Environment):Java运行时环境
- JVM(Java Virtual Machine):Java虚拟机,负责执行字节码
- 编译器(javac):将.java源文件编译成.class字节码文件
- 调试工具(jdb):用于调试Java程序
- 文档工具(javadoc):生成API文档
JDK、JRE、JVM的关系:
JDK(最大)
├── JRE(中间)
│ ├── JVM(最小)
│ └── 核心类库(rt.jar等)
├── 编译器(javac)
├── 调试器(jdb)
└── 其他开发工具
安装步骤:
- 下载JDK(推荐JDK 8或JDK 11 LTS版本)
- 安装到指定目录(如:C:\Program Files\Java\jdk1.8.0_xxx)
- 配置环境变量:
- JAVA_HOME:指向JDK安装目录
- Path:添加
%JAVA_HOME%\bin - CLASSPATH:
.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar
- 验证:命令行输入
java -version和javac -version
为什么需要配置环境变量:
- JAVA_HOME:让其他工具(如IDEA、Maven、Tomcat)知道JDK安装在哪里
- Path:让操作系统在任何目录下都能找到java和javac命令
- CLASSPATH:告诉JVM去哪里找.class文件
IDEA(IntelliJ IDEA)
什么是IDEA:
IDEA是JetBrains公司开发的Java集成开发环境(IDE),被认为是最强大的Java IDE。
IDEA vs Eclipse:
| 特性 | IDEA | Eclipse |
|---|---|---|
| 智能提示 | 极其强大 | 一般 |
| 代码重构 | 强大 | 一般 |
| 插件生态 | 丰富 | 丰富 |
| 性能 | 需要较多内存 | 相对轻量 |
| 价格 | 社区版免费/旗舰版收费 | 完全免费 |
IDEA核心概念:
- Project(项目):最顶层的结构,相当于Eclipse的Workspace
- Module(模块):一个项目可以包含多个模块,相当于Eclipse的Project
- Package(包):组织Java类的文件夹结构
- Class(类):Java源代码文件
IDEA常用快捷键:
psvm + Tab → public static void main(String[] args) {}
sout + Tab → System.out.println();
Ctrl + D → 复制当前行
Ctrl + Y → 删除当前行
Ctrl + / → 单行注释
Ctrl + Shift + / → 多行注释
Alt + Enter → 智能修复
Ctrl + Alt + L → 格式化代码
Shift + F6 → 重命名
第一个Java程序:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
程序执行流程:
HelloWorld.java → javac编译 → HelloWorld.class → java执行 → JVM运行 → 输出结果
(源文件) (字节码文件)
2. 数据类型、变量、常量
数据类型
Java是强类型语言,每个变量都必须声明其类型。
Java数据类型分类:
数据类型
├── 基本数据类型(Primitive Types)—— 8种
│ ├── 整数型
│ │ ├── byte (1字节,-128 ~ 127)
│ │ ├── short (2字节,-32768 ~ 32767)
│ │ ├── int (4字节,约±21亿)【默认】
│ │ └── long (8字节,非常大)
│ ├── 浮点型
│ │ ├── float (4字节,单精度)
│ │ └── double (8字节,双精度)【默认】
│ ├── 字符型
│ │ └── char (2字节,0 ~ 65535,存储Unicode字符)
│ └── 布尔型
│ └── boolean (true / false)
│
└── 引用数据类型(Reference Types)
├── 类(Class)—— 如 String
├── 接口(Interface)
└── 数组(Array)
各类型详细说明:
// 整数型
byte b = 127; // 最小的整数类型,节省空间
short s = 32767; // 短整型,较少使用
int i = 2147483647; // 最常用的整数类型
long l = 9223372036854775807L; // 需要加L后缀
// 浮点型
float f = 3.14F; // 需要加F后缀
double d = 3.141592653; // 默认浮点类型,精度更高
// 字符型
char c1 = 'A'; // 单个字符,用单引号
char c2 = 65; // 也可以用ASCII码值
char c3 = '\u0041'; // 也可以用Unicode编码
// 布尔型
boolean flag = true; // 只有true和false两个值
类型转换:
自动类型转换(隐式):小类型 → 大类型(安全,不丢失精度)
byte → short → int → long → float → double
char ↗
强制类型转换(显式):大类型 → 小类型(可能丢失精度)
// 自动类型转换
int num = 100;
double d = num; // int → double,自动转换,d = 100.0
// 强制类型转换
double pi = 3.14;
int n = (int) pi; // double → int,强制转换,n = 3(丢失小数部分)
// 注意:byte、short运算时自动提升为int
byte b1 = 10, b2 = 20;
// byte b3 = b1 + b2; // 错误!b1+b2结果是int
byte b3 = (byte)(b1 + b2); // 正确
int b4 = b1 + b2; // 正确
变量
什么是变量:
变量是程序中用于存储数据的容器,其值可以改变。
变量的三要素:
- 数据类型:决定分配多少内存空间
- 变量名:访问这块内存的标识
- 值:存储在内存中的数据
// 声明变量的方式
int age; // 先声明
age = 25; // 后赋值
int score = 100; // 声明并赋值
int a = 1, b = 2, c = 3; // 同时声明多个同类型变量
命名规则:
- 由字母、数字、下划线
_、美元符$组成 - 不能以数字开头
- 不能使用Java关键字(如class、int、public等)
- 区分大小写
- 建议使用驼峰命名法:
studentName、totalScore
常量
什么是常量:
常量是程序运行过程中值不可改变的量。
// 使用final关键字定义常量
final double PI = 3.14159265;
final int MAX_SIZE = 100;
// PI = 3.14; // 错误!常量不能重新赋值
// 常量命名规范:全部大写,多个单词用下划线分隔
final String DATABASE_URL = "jdbc:mysql://localhost:3306/test";
字面常量:
100 // 整数常量
3.14 // 浮点数常量
'A' // 字符常量
"Hello" // 字符串常量
true/false // 布尔常量
null // 空常量
3. 运算符号
算术运算符
int a = 10, b = 3;
System.out.println(a + b); // 13 加法
System.out.println(a - b); // 7 减法
System.out.println(a * b); // 30 乘法
System.out.println(a / b); // 3 整数除法(取整)
System.out.println(a % b); // 1 取余(模运算)
// 注意:整数除法会截断小数
System.out.println(10 / 3); // 3(不是3.333...)
System.out.println(10.0 / 3); // 3.333...(有一个是浮点数就保留小数)
// 自增自减运算符
int x = 5;
x++; // x = 6,先使用后自增
++x; // x = 7,先自增后使用
x--; // x = 6,先使用后自减
--x; // x = 5,先自减后使用
// 重要区别
int m = 5;
int n1 = m++; // n1 = 5, m = 6(先赋值,再自增)
int n2 = ++m; // n2 = 7, m = 7(先自增,再赋值)
字符串拼接中的 +:
System.out.println("Hello" + "World"); // HelloWorld
System.out.println("Age: " + 25); // Age: 25
System.out.println(1 + 2 + "abc"); // 3abc(先算1+2=3,再拼接)
System.out.println("abc" + 1 + 2); // abc12(遇到字符串后都是拼接)
赋值运算符
int a = 10; // 基本赋值
a += 5; // a = a + 5; → a = 15
a -= 3; // a = a - 3; → a = 12
a *= 2; // a = a * 2; → a = 24
a /= 4; // a = a / 4; → a = 6
a %= 4; // a = a % 4; → a = 2
// 注意:复合赋值运算符会自动进行类型转换
byte b = 10;
// b = b + 5; // 错误!b+5结果是int
b += 5; // 正确!等价于 b = (byte)(b + 5)
比较运算符(关系运算符)
结果都是boolean类型(true或false)。
int a = 10, b = 20;
System.out.println(a == b); // false 等于
System.out.println(a != b); // true 不等于
System.out.println(a > b); // false 大于
System.out.println(a < b); // true 小于
System.out.println(a >= b); // false 大于等于
System.out.println(a <= b); // true 小于等于
// 注意:== 和 = 的区别
// == 是比较运算符,判断是否相等
// = 是赋值运算符,将右边的值赋给左边
逻辑运算符
操作布尔值,结果也是布尔值。
boolean a = true, b = false;
// 与(AND):两个都为true,结果才为true
System.out.println(a & b); // false (逻辑与)
System.out.println(a && b); // false (短路与)
// 或(OR):只要一个为true,结果就为true
System.out.println(a | b); // true (逻辑或)
System.out.println(a || b); // true (短路或)
// 非(NOT):取反
System.out.println(!a); // false
// 异或(XOR):两个不同为true,相同为false
System.out.println(a ^ b); // true
短路与&& vs 逻辑与&:
int x = 5;
// 短路与:如果第一个条件为false,第二个条件不执行
if (x > 10 && ++x > 5) { } // x = 5(++x没有执行)
// 逻辑与:两个条件都会执行
if (x > 10 & ++x > 5) { } // x = 6(++x被执行了)
三元运算符(条件运算符)
// 格式:条件表达式 ? 值1 : 值2
// 如果条件为true,结果是值1;如果条件为false,结果是值2
int a = 10, b = 20;
int max = (a > b) ? a : b; // max = 20
String result = (score >= 60) ? "及格" : "不及格";
// 三元运算符可以嵌套(但不推荐,影响可读性)
int x = 10, y = 20, z = 15;
int max3 = (x > y) ? (x > z ? x : z) : (y > z ? y : z); // max3 = 20
运算符优先级(从高到低):
() 括号最高
++ -- ! 一元运算符
* / % 乘除取余
+ - 加减
> < >= <= 比较
== != 等于不等于
&& 短路与
|| 短路或
?: 三元运算符
= += -= *= /= %= 赋值
4. 流程控制:选择结构 if switch
if 语句
程序默认是从上到下顺序执行的,选择结构可以根据条件决定执行哪段代码。
// ============ 形式1:单分支 ============
if (条件) {
// 条件为true时执行
}
// 例:
int age = 20;
if (age >= 18) {
System.out.println("你已成年");
}
// ============ 形式2:双分支 ============
if (条件) {
// 条件为true时执行
} else {
// 条件为false时执行
}
// 例:
int score = 55;
if (score >= 60) {
System.out.println("及格");
} else {
System.out.println("不及格");
}
// ============ 形式3:多分支 ============
if (条件1) {
// 条件1为true时执行
} else if (条件2) {
// 条件2为true时执行
} else if (条件3) {
// 条件3为true时执行
} else {
// 以上条件都不满足时执行
}
// 例:
int score = 85;
if (score >= 90) {
System.out.println("优秀");
} else if (score >= 80) {
System.out.println("良好"); // 输出这个
} else if (score >= 60) {
System.out.println("及格");
} else {
System.out.println("不及格");
}
if 嵌套
// 在if内部再嵌套if
int score = 85;
String gender = "男";
if (score >= 60) {
System.out.println("考试通过");
if (gender.equals("男")) {
System.out.println("帅哥,恭喜你通过");
} else {
System.out.println("美女,恭喜你通过");
}
} else {
System.out.println("考试未通过");
}
// 实际应用:判断闰年
int year = 2024;
if (year % 4 == 0) {
if (year % 100 != 0) {
System.out.println(year + "是闰年");
} else {
if (year % 400 == 0) {
System.out.println(year + "是闰年");
} else {
System.out.println(year + "不是闰年");
}
}
} else {
System.out.println(year + "不是闰年");
}
// 简化写法
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
System.out.println("闰年");
}
switch 语句
当有多个固定值需要判断时,switch比多个if-else更清晰。
// 基本语法
switch (表达式) {
case 值1:
语句1;
break;
case 值2:
语句2;
break;
case 值3:
语句3;
break;
default:
默认语句;
break;
}
// 例:根据星期数输出
int day = 3;
switch (day) {
case 1:
System.out.println("星期一");
break;
case 2:
System.out.println("星期二");
break;
case 3:
System.out.println("星期三"); // 输出这个
break;
case 4:
System.out.println("星期四");
break;
case 5:
System.out.println("星期五");
break;
case 6:
System.out.println("星期六");
break;
case 7:
System.out.println("星期日");
break;
default:
System.out.println("无效的星期");
break;
}
switch支持的数据类型:
- byte、short、int、char
- String(JDK 7+)
- 枚举类型(enum)
- 不支持:long、float、double、boolean
break的重要性(case穿透):
int day = 2;
switch (day) {
case 1:
System.out.println("星期一");
// 没有break,会继续执行下面的case
case 2:
System.out.println("星期二");
// 没有break,继续穿透
case 3:
System.out.println("星期三");
break; // 遇到break才停止
case 4:
System.out.println("星期四");
break;
}
// 输出:星期二 星期三(穿透到break为止)
// case穿透的应用:多个case执行相同代码
switch (day) {
case 1:
case 2:
case 3:
case 4:
case 5:
System.out.println("工作日");
break;
case 6:
case 7:
System.out.println("休息日");
break;
}
switch嵌套:
int year = 2024;
int month = 2;
switch (month) {
case 1: case 3: case 5: case 7: case 8: case 10: case 12:
System.out.println("31天");
break;
case 4: case 6: case 9: case 11:
System.out.println("30天");
break;
case 2:
// 嵌套判断是否闰年
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
System.out.println("29天");
} else {
System.out.println("28天");
}
break;
}
if vs switch 选择:
| 场景 | 推荐 |
|---|---|
| 判断区间范围(score >= 60) | if |
| 判断固定的等值(day == 1) | switch |
| 判断布尔条件组合 | if |
| 分支很多且都是等值判断 | switch |
5. 流程控制:循环结构 while do-while for
while 循环
先判断条件,再执行循环体。如果条件一开始就是false,循环体一次都不执行。
// 语法
while (条件) {
循环体;
迭代语句; // 改变条件的语句,防止死循环
}
// 例1:打印1~10
int i = 1;
while (i <= 10) {
System.out.println(i);
i++;
}
// 例2:计算1+2+3+...+100
int sum = 0;
int n = 1;
while (n <= 100) {
sum += n;
n++;
}
System.out.println("1到100的和:" + sum); // 5050
// 例3:猜数字游戏
Scanner sc = new Scanner(System.in);
int target = 50;
int guess = 0;
while (guess != target) {
System.out.print("请猜一个数:");
guess = sc.nextInt();
if (guess > target) {
System.out.println("猜大了");
} else if (guess < target) {
System.out.println("猜小了");
}
}
System.out.println("恭喜你猜对了!");
do-while 循环
先执行循环体,再判断条件。循环体至少执行一次。
// 语法
do {
循环体;
迭代语句;
} while (条件); // 注意这里有分号!
// 例1:打印1~10
int i = 1;
do {
System.out.println(i);
i++;
} while (i <= 10);
// 例2:do-while至少执行一次的体现
int x = 100;
// while版:条件一开始就是false,不执行
while (x < 10) {
System.out.println("while: " + x); // 不会执行
x++;
}
// do-while版:先执行一次,再判断条件
do {
System.out.println("do-while: " + x); // 会执行一次,输出100
x++;
} while (x < 10);
// 实际应用:输入验证(必须先输入一次)
Scanner sc = new Scanner(System.in);
int input;
do {
System.out.print("请输入1~100之间的数:");
input = sc.nextInt();
} while (input < 1 || input > 100);
System.out.println("你输入的是:" + input);
for 循环
最常用的循环,适合已知循环次数的场景。
// 语法
for (初始化语句; 条件表达式; 迭代语句) {
循环体;
}
// 执行顺序:
// 1. 执行初始化语句(只执行一次)
// 2. 判断条件表达式
// 3. 如果为true,执行循环体
// 4. 执行迭代语句
// 5. 回到第2步
// 例1:打印1~10
for (int i = 1; i <= 10; i++) {
System.out.println(i);
}
// 例2:计算阶乘 5! = 1×2×3×4×5
int factorial = 1;
for (int i = 1; i <= 5; i++) {
factorial *= i;
}
System.out.println("5! = " + factorial); // 120
// 例3:遍历数组
int[] arr = {10, 20, 30, 40, 50};
for (int i = 0; i < arr.length; i++) {
System.out.println("arr[" + i + "] = " + arr[i]);
}
// 例4:倒序循环
for (int i = 10; i >= 1; i--) {
System.out.println(i);
}
// 例5:步长为2
for (int i = 0; i <= 100; i += 2) {
System.out.println(i); // 0, 2, 4, 6, 8, ...
}
三种循环的对比:
| 特性 | while | do-while | for |
|---|---|---|---|
| 先判断还是先执行 | 先判断 | 先执行 | 先判断 |
| 最少执行次数 | 0次 | 1次 | 0次 |
| 适用场景 | 不确定循环次数 | 至少执行一次 | 已知循环次数 |
| 变量作用域 | 循环外 | 循环外 | 可以在循环内 |
6. 流程控制:多重循环(嵌套、continue、break)
循环嵌套
// 外层循环控制行数,内层循环控制每行的内容
// 例1:打印矩形
// *****
// *****
// *****
for (int i = 0; i < 3; i++) { // 3行
for (int j = 0; j < 5; j++) { // 每行5个*
System.out.print("*");
}
System.out.println(); // 换行
}
// 例2:打印直角三角形
// *
// **
// ***
// ****
// *****
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= i; j++) {
System.out.print("*");
}
System.out.println();
}
// 例3:打印九九乘法表
// 1×1=1
// 1×2=2 2×2=4
// 1×3=3 2×3=6 3×3=9
// ...
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
System.out.print(j + "×" + i + "=" + (i * j) + "\t");
}
System.out.println();
}
// 例4:打印等腰三角形
// *
// ***
// *****
// *******
// *********
int n = 5;
for (int i = 1; i <= n; i++) {
// 打印空格
for (int j = 1; j <= n - i; j++) {
System.out.print(" ");
}
// 打印星号
for (int j = 1; j <= 2 * i - 1; j++) {
System.out.print("*");
}
System.out.println();
}
break 语句
break用于立即终止当前循环。
// 例1:找到第一个能被7整除的数
for (int i = 1; i <= 100; i++) {
if (i % 7 == 0) {
System.out.println("第一个能被7整除的数:" + i); // 7
break; // 找到后立即退出循环
}
}
// 例2:在嵌套循环中,break只跳出当前层循环
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (j == 2) {
break; // 只跳出内层循环
}
System.out.println("i=" + i + ", j=" + j);
}
}
// 输出:
// i=1, j=1
// i=2, j=1
// i=3, j=1
// 例3:使用标签跳出外层循环
outer:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (j == 2) {
break outer; // 跳出外层循环
}
System.out.println("i=" + i + ", j=" + j);
}
}
// 输出:i=1, j=1(只输出一行就跳出了整个嵌套循环)
continue 语句
continue用于跳过当次循环的剩余代码,进入下一次循环。
// 例1:打印1~10中所有奇数
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) {
continue; // 跳过偶数
}
System.out.println(i); // 1, 3, 5, 7, 9
}
// 例2:跳过特定值
for (int i = 1; i <= 5; i++) {
if (i == 3) {
continue; // 跳过3
}
System.out.println(i); // 1, 2, 4, 5
}
// break vs continue 对比
System.out.println("=== break ===");
for (int i = 1; i <= 5; i++) {
if (i == 3) break;
System.out.print(i + " "); // 1 2(遇到3直接终止循环)
}
System.out.println("\n=== continue ===");
for (int i = 1; i <= 5; i++) {
if (i == 3) continue;
System.out.print(i + " "); // 1 2 4 5(跳过3,继续循环)
}
7. 程序调试
什么是程序调试
程序调试(Debug)是找出程序中错误(Bug)并修复的过程。
错误类型
程序错误
├── 语法错误(编译错误)
│ └── IDE和编译器会直接提示,如拼写错误、缺少分号
├── 运行时错误(异常)
│ └── 程序运行时才出现,如除以零、数组越界、空指针
└── 逻辑错误(最难发现)
└── 程序能运行但结果不对,如算法错误
IDEA调试方法
1. 设置断点:
- 在代码行号左侧点击,出现红色圆点即为断点
- 程序运行到断点会暂停
2. 启动调试:
- 点击Debug按钮(虫子图标)或按
Shift + F9
3. 调试操作:
F7 → Step Into(步入):进入方法内部
F8 → Step Over(步过):执行当前行,不进入方法内部
Shift + F8 → Step Out(步出):从当前方法跳出
F9 → Resume(恢复):继续执行到下一个断点
4. 调试窗口:
- Variables(变量窗口):查看当前作用域内所有变量的值
- Watches(监视窗口):添加表达式进行监控
- Frames(调用栈):查看方法的调用层次
- Console(控制台):查看程序输出
// 调试示例:找出逻辑错误
public static int sumOfEven(int n) {
int sum = 0;
for (int i = 1; i <= n; i++) { // 在这行设断点
if (i % 2 == 0) { // 观察i的值和条件判断
sum += i; // 观察sum的变化
}
}
return sum; // 在这行设断点查看最终结果
}
5. 条件断点:
- 右键点击断点,可以设置条件
- 如:只有当
i == 50时才暂停 - 适用于循环中定位特定迭代
6. 常用调试技巧:
// 技巧1:使用System.out.println输出关键变量值
System.out.println("DEBUG: i = " + i + ", sum = " + sum);
// 技巧2:使用断言(assert)
assert sum > 0 : "sum应该大于0";
// 技巧3:分段注释法
// 将代码分段注释,逐段放开,定位错误所在区域
第二阶段:Java面向对象基础 + 数组
1. 一维数组
什么是数组
数组是存储相同数据类型的固定长度的有序集合。
数组在内存中的存储:
栈内存 堆内存
┌─────┐ ┌───┬───┬───┬───┬───┐
│ arr ├─────────────→│ 0 │ 0 │ 0 │ 0 │ 0 │
└─────┘ └───┴───┴───┴───┴───┘
[0] [1] [2] [3] [4]
- 数组变量(arr)存储在栈中,保存的是数组在堆中的地址(引用)
- 数组元素存储在堆中,是连续的内存空间
- 通过索引(下标)访问元素,索引从0开始
数组的声明和创建
// ============ 方式1:先声明后创建 ============
int[] arr; // 声明(推荐写法)
int arr2[]; // 声明(C风格写法,不推荐)
arr = new int[5]; // 创建,分配5个int空间,默认值为0
// ============ 方式2:声明同时创建 ============
int[] arr3 = new int[5];
// ============ 方式3:声明并初始化 ============
int[] arr4 = new int[]{10, 20, 30, 40, 50};
// ============ 方式4:简化初始化 ============
int[] arr5 = {10, 20, 30, 40, 50};
// 各类型数组的默认值:
// int/short/byte/long → 0
// float/double → 0.0
// char → '\u0000'(空字符)
// boolean → false
// 引用类型 → null
数组的基本操作
int[] arr = {10, 20, 30, 40, 50};
// 获取数组长度
System.out.println(arr.length); // 5
// 访问元素(通过索引)
System.out.println(arr[0]); // 10(第一个元素)
System.out.println(arr[4]); // 50(最后一个元素)
// System.out.println(arr[5]); // 错误!ArrayIndexOutOfBoundsException
// 修改元素
arr[2] = 100; // 将第3个元素改为100
// 遍历数组
// 方式1:普通for循环
for (int i = 0; i < arr.length; i++) {
System.out.println("arr[" + i + "] = " + arr[i]);
}
// 方式2:增强for循环(for-each)
for (int num : arr) {
System.out.println(num);
}
数组的常见操作
// ============ 求最大值 ============
int[] arr = {35, 12, 98, 45, 67};
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
System.out.println("最大值:" + max); // 98
// ============ 求和与平均值 ============
int sum = 0;
for (int num : arr) {
sum += num;
}
double avg = (double) sum / arr.length;
// ============ 数组排序(冒泡排序) ============
int[] arr2 = {5, 3, 8, 1, 9, 2};
for (int i = 0; i < arr2.length - 1; i++) {
for (int j = 0; j < arr2.length - 1 - i; j++) {
if (arr2[j] > arr2[j + 1]) {
int temp = arr2[j];
arr2[j] = arr2[j + 1];
arr2[j + 1] = temp;
}
}
}
// 排序后:{1, 2, 3, 5, 8, 9}
// ============ 使用Arrays工具类 ============
import java.util.Arrays;
Arrays.sort(arr); // 排序
System.out.println(Arrays.toString(arr)); // 输出为字符串
int index = Arrays.binarySearch(arr, 45); // 二分查找(需先排序)
int[] copy = Arrays.copyOf(arr, 10); // 复制数组
Arrays.fill(arr, 0); // 填充所有元素为0
2. 二维数组
什么是二维数组
二维数组可以看作是"数组的数组",像一个表格(行和列)。
内存模型:
arr → [ 引用1, 引用2, 引用3 ]
↓ ↓ ↓
[1,2,3] [4,5,6] [7,8,9]
声明和使用
// ============ 声明方式 ============
// 方式1:指定行数和列数
int[][] arr = new int[3][4]; // 3行4列
// 方式2:只指定行数(列数可以不同——锯齿数组)
int[][] arr2 = new int[3][];
arr2[0] = new int[3]; // 第一行3列
arr2[1] = new int[5]; // 第二行5列
arr2[2] = new int[2]; // 第三行2列
// 方式3:直接初始化
int[][] arr3 = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
// ============ 访问元素 ============
System.out.println(arr3[0][0]); // 1(第1行第1列)
System.out.println(arr3[1][2]); // 6(第2行第3列)
System.out.println(arr3[2][1]); // 8(第3行第2列)
// 修改元素
arr3[1][1] = 100;
// ============ 遍历二维数组 ============
for (int i = 0; i < arr3.length; i++) { // 遍历行
for (int j = 0; j < arr3[i].length; j++) { // 遍历列
System.out.print(arr3[i][j] + "\t");
}
System.out.println();
}
// 增强for循环版
for (int[] row : arr3) {
for (int num : row) {
System.out.print(num + "\t");
}
System.out.println();
}
二维数组应用
// 例1:求所有元素的和
int[][] scores = {
{80, 90, 75}, // 学生1的三科成绩
{85, 70, 95}, // 学生2
{90, 88, 92} // 学生3
};
int total = 0;
for (int[] row : scores) {
for (int score : row) {
total += score;
}
}
System.out.println("总分:" + total);
// 例2:矩阵转置
int[][] matrix = {{1, 2, 3}, {4, 5, 6}}; // 2×3
int[][] transposed = new int[3][2]; // 3×2
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
transposed[j][i] = matrix[i][j];
}
}
3. 类和对象基础:方法、局部变量、成员变量、类变量
面向对象编程(OOP)概念
面向过程编程:关注解决问题的步骤
→ 第一步做什么,第二步做什么...
面向对象编程:关注解决问题的角色(对象)
→ 谁来做什么事
类(Class):是对象的模板/蓝图,定义了对象的属性和行为。
对象(Object):是类的实例,是实际存在的个体。
类比:
类 → 房屋设计图纸
对象 → 根据图纸建造的实际房屋(可以建多个)
定义类和创建对象
// ============ 定义类 ============
public class Student {
// 成员变量(属性/字段)—— 描述对象的特征
String name;
int age;
String gender;
// 成员方法(行为)—— 描述对象能做什么
public void study() {
System.out.println(name + "正在学习Java");
}
public void introduce() {
System.out.println("我叫" + name + ",今年" + age + "岁,性别" + gender);
}
}
// ============ 创建对象 ============
public class TestStudent {
public static void main(String[] args) {
// 创建对象:类名 对象名 = new 类名();
Student stu1 = new Student();
// 设置属性
stu1.name = "张三";
stu1.age = 20;
stu1.gender = "男";
// 调用方法
stu1.study(); // 张三正在学习Java
stu1.introduce(); // 我叫张三,今年20岁,性别男
// 可以创建多个对象
Student stu2 = new Student();
stu2.name = "李四";
stu2.age = 22;
}
}
对象在内存中的存储:
栈内存 堆内存
┌──────┐ ┌──────────────────┐
│ stu1 ├───────────→│ name = "张三" │
└──────┘ │ age = 20 │
│ gender = "男" │
┌──────┐ └──────────────────┘
│ stu2 ├───────────→┌──────────────────┐
└──────┘ │ name = "李四" │
│ age = 22 │
│ gender = null │
└──────────────────┘
方法
// 方法的完整定义
访问修饰符 返回值类型 方法名(参数类型1 参数名1, 参数类型2 参数名2, ...) {
方法体;
return 返回值; // 如果返回值类型是void,可以省略return
}
// ============ 无参无返回值 ============
public void sayHello() {
System.out.println("Hello!");
}
// ============ 有参无返回值 ============
public void greet(String name) {
System.out.println("Hello, " + name + "!");
}
// ============ 无参有返回值 ============
public int getAge() {
return this.age;
}
// ============ 有参有返回值 ============
public int add(int a, int b) {
return a + b;
}
// ============ 方法调用 ============
Student stu = new Student();
stu.sayHello(); // 无参无返回
stu.greet("张三"); // 有参无返回
int age = stu.getAge(); // 无参有返回
int result = stu.add(10, 20); // 有参有返回
构造方法
public class Student {
String name;
int age;
// 无参构造方法(如果不写任何构造方法,系统默认提供)
public Student() {
System.out.println("创建了一个Student对象");
}
// 有参构造方法
public Student(String name, int age) {
this.name = name; // this指当前对象
this.age = age;
}
// 注意:如果定义了有参构造,系统不再提供无参构造
// 建议:总是同时提供无参和有参构造
}
// 使用
Student stu1 = new Student(); // 调用无参构造
Student stu2 = new Student("张三", 20); // 调用有参构造
变量的分类
public class VariableDemo {
// ============ 成员变量(实例变量) ============
// 定义在类中,方法外
// 属于对象,通过对象访问
// 有默认值(int=0, String=null, boolean=false)
// 生命周期:随对象创建而创建,随对象销毁而销毁
String name;
int age;
// ============ 类变量(静态变量) ============
// 使用static修饰
// 属于类,通过类名访问(也可以通过对象访问)
// 所有对象共享同一个值
// 生命周期:随类的加载而创建
static int count = 0; // 记录创建了多少个对象
static String schoolName = "清华大学";
public VariableDemo() {
count++; // 每创建一个对象,计数器+1
}
public void method() {
// ============ 局部变量 ============
// 定义在方法内部(或代码块内)
// 没有默认值,必须先赋值再使用
// 生命周期:方法执行时创建,方法结束时销毁
int localVar = 10;
System.out.println(localVar);
// 如果局部变量和成员变量同名,局部变量优先(就近原则)
String name = "局部的name";
System.out.println(name); // 局部的name
System.out.println(this.name); // 成员的name
}
}
// 使用类变量
System.out.println(VariableDemo.schoolName); // 通过类名访问
System.out.println(VariableDemo.count); // 查看创建了多少个对象
三种变量对比:
| 特性 | 局部变量 | 成员变量 | 类变量(static) |
|---|---|---|---|
| 定义位置 | 方法内 | 类中,方法外 | 类中,方法外,加static |
| 默认值 | 无,必须初始化 | 有默认值 | 有默认值 |
| 作用范围 | 方法内 | 整个类 | 整个类,所有对象共享 |
| 内存位置 | 栈 | 堆(对象中) | 方法区(静态区) |
| 生命周期 | 方法开始到结束 | 对象创建到销毁 | 类加载到卸载 |
| 访问方式 | 直接使用 | 通过对象 | 通过类名(推荐) |
4. String类
String的特点
// String是引用数据类型,但使用起来像基本类型
// String对象是不可变的(immutable)—— 一旦创建,内容不能改变
// String存储在字符串常量池中(方法区)
// ============ 创建String的两种方式 ============
// 方式1:字面量方式(推荐)
// 会先检查常量池中是否已有该字符串,有则直接引用,没有则创建
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true(指向常量池中同一个对象)
// 方式2:new方式
// 会在堆中创建新对象,即使常量池中已有相同字符串
String s3 = new String("Hello");
String s4 = new String("Hello");
System.out.println(s3 == s4); // false(堆中两个不同对象)
System.out.println(s3.equals(s4)); // true(内容相同)
内存模型:
栈内存 堆内存 常量池(方法区)
┌────┐ ┌─────────┐
│ s1 ├─────────────────────────────→│ "Hello" │
├────┤ └─────────┘
│ s2 ├─────────────────────────────→ ↑ (同一个)
├────┤ ┌─────────────┐
│ s3 ├───────→│ String对象1 ├────→ "Hello"
├────┤ └─────────────┘
│ s4 ├───────→┌─────────────┐
│ │ │ String对象2 ├────→ "Hello"
└────┘ └─────────────┘
String常用方法
String str = "Hello, World!";
// ============ 获取信息 ============
str.length() // 13(字符串长度)
str.charAt(0) // 'H'(获取指定索引的字符)
str.indexOf("World") // 7(查找子串首次出现的位置)
str.lastIndexOf("l") // 10(最后一次出现的位置)
str.isEmpty() // false(是否为空字符串)
// ============ 判断 ============
str.equals("Hello, World!") // true(比较内容是否相等)
str.equalsIgnoreCase("HELLO, WORLD!") // true(忽略大小写比较)
str.contains("World") // true(是否包含指定子串)
str.startsWith("Hello") // true(是否以指定前缀开头)
str.endsWith("!") // true(是否以指定后缀结尾)
// ============ 转换 ============
str.toUpperCase() // "HELLO, WORLD!"
str.toLowerCase() // "hello, world!"
str.trim() // 去除首尾空格
str.replace("World", "Java") // "Hello, Java!"
str.substring(7) // "World!"(从索引7到末尾)
str.substring(7, 12) // "World"(从索引7到11)
// ============ 分割 ============
String csv = "张三,李四,王五";
String[] names = csv.split(","); // ["张三", "李四", "王五"]
// ============ 拼接 ============
String s = "Hello" + " " + "World"; // 用+号拼接
String.join(",", "a", "b", "c"); // "a,b,c"
// ============ 转换与值 ============
String.valueOf(100) // "100"(int转String)
Integer.parseInt("100") // 100(String转int)
str.toCharArray() // 转为字符数组
// ============ 重要:== 和 equals 的区别 ============
// == 比较的是引用(地址),equals比较的是内容
String a = "abc";
String b = "abc";
String c = new String("abc");
a == b // true(常量池中同一个对象)
a == c // false(一个在常量池,一个在堆中)
a.equals(c) // true(内容相同)
String不可变性
// String对象一旦创建,其内容不能被修改
// 所有看似"修改"的操作,实际上都是创建了新的String对象
String s = "Hello";
s = s + " World"; // 不是修改了原来的"Hello",而是创建了新的"Hello World"
// 原来的"Hello"在常量池中仍然存在,只是s不再指向它了
// 这就是为什么大量字符串拼接时不推荐用String + 的方式
// 因为会创建大量临时对象,影响性能
String result = "";
for (int i = 0; i < 10000; i++) {
result += i; // 每次都创建新对象,非常低效!
}
// 应该使用StringBuilder或StringBuffer
5. StringBuffer类
StringBuffer的特点
// StringBuffer是可变的字符序列
// 线程安全(方法使用synchronized修饰)
// 适合在多线程环境中使用
// StringBuilder是StringBuffer的非线程安全版本
// 性能更高,适合在单线程环境中使用
// 性能对比:StringBuilder > StringBuffer >> String(大量拼接时)
StringBuffer常用方法
// ============ 创建 ============
StringBuffer sb = new StringBuffer(); // 空的,默认容量16
StringBuffer sb2 = new StringBuffer("Hello"); // 初始内容"Hello"
StringBuffer sb3 = new StringBuffer(100); // 指定初始容量
// ============ 增(追加) ============
sb.append("Hello");
sb.append(" ");
sb.append("World");
sb.append(100); // 可以追加各种类型
System.out.println(sb); // "Hello World100"
// 链式调用
sb.append("a").append("b").append("c");
// ============ 删 ============
sb.delete(5, 11); // 删除索引5到10的字符
sb.deleteCharAt(0); // 删除指定索引的字符
// ============ 改(修改/替换) ============
sb.replace(0, 5, "Java"); // 将索引0到4的内容替换为"Java"
sb.setCharAt(0, 'j'); // 修改指定索引的字符
// ============ 插入 ============
sb.insert(5, "Beautiful "); // 在索引5处插入
// ============ 反转 ============
sb.reverse(); // 反转字符串
// ============ 其他 ============
sb.length() // 长度
sb.capacity() // 容量
sb.toString() // 转为String
// ============ 与String的转换 ============
// String → StringBuffer
String str = "Hello";
StringBuffer sbuf = new StringBuffer(str);
// StringBuffer → String
String result = sbuf.toString();
String、StringBuffer、StringBuilder对比
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(不可变) | 安全(synchronized) | 不安全 |
| 性能 | 拼接慢 | 中等 | 最快 |
| 使用场景 | 少量字符串操作 | 多线程大量拼接 | 单线程大量拼接 |
// 性能对比演示
long start = System.currentTimeMillis();
// String方式(非常慢)
String s = "";
for (int i = 0; i < 100000; i++) {
s += i;
}
System.out.println("String耗时:" + (System.currentTimeMillis() - start) + "ms");
start = System.currentTimeMillis();
// StringBuilder方式(很快)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append(i);
}
String result = sb.toString();
System.out.println("StringBuilder耗时:" + (System.currentTimeMillis() - start) + "ms");
6. 方法传参(多参)返回值
方法参数
// ============ 单个参数 ============
public void greet(String name) {
System.out.println("Hello, " + name);
}
// ============ 多个参数 ============
public int add(int a, int b) {
return a + b;
}
// ============ 可变参数(不定参数) ============
// 使用...表示,本质是数组
// 必须是方法的最后一个参数
// 一个方法只能有一个可变参数
public int sum(int... numbers) {
int total = 0;
for (int num : numbers) {
total += num;
}
return total;
}
// 调用
sum(1, 2); // 3
sum(1, 2, 3); // 6
sum(1, 2, 3, 4, 5); // 15
值传递 vs 引用传递
Java只有值传递!
// ============ 基本数据类型:传递的是值的副本 ============
public static void change(int num) {
num = 100; // 只改变了副本,不影响原始变量
}
int a = 10;
change(a);
System.out.println(a); // 10(没有改变)
// ============ 引用数据类型:传递的是地址的副本 ============
public static void change(int[] arr) {
arr[0] = 100; // 通过地址副本找到同一个数组,修改了数组内容
}
int[] myArr = {1, 2, 3};
change(myArr);
System.out.println(myArr[0]); // 100(改变了!)
// ============ String特殊情况 ============
public static void change(String s) {
s = "World"; // s指向了新的String对象,原来的s没有受影响
}
String str = "Hello";
change(str);
System.out.println(str); // "Hello"(没有改变!因为String不可变)
基本类型传参内存图:
main方法栈 change方法栈
┌───────┐ ┌───────┐
│ a = 10│ │num=100│ ← 改的是副本
└───────┘ └───────┘
引用类型传参内存图:
main方法栈 change方法栈 堆内存
┌──────┐ ┌──────┐ ┌───────────┐
│myArr ├──────┐ │ arr ├────────→│{100, 2, 3}│
└──────┘ └───────────────────→ └───────────┘
两个引用指向同一个数组
返回值
// ============ 无返回值:void ============
public void printInfo() {
System.out.println("Hello");
// 可以使用return;提前结束方法(但不返回值)
return; // 可以省略
}
// ============ 有返回值 ============
public int getMax(int a, int b) {
return (a > b) ? a : b;
}
// ============ 返回数组 ============
public int[] getScores() {
int[] scores = {90, 85, 78};
return scores;
}
// ============ 返回对象 ============
public Student createStudent(String name, int age) {
Student stu = new Student();
stu.name = name;
stu.age = age;
return stu;
}
// 注意:方法只能返回一个值
// 如果需要返回多个值,可以:
// 1. 返回数组
// 2. 返回对象
// 3. 使用集合
第三阶段:HTML基础
1. 常用标签
HTML概述
HTML(HyperText Markup Language):超文本标记语言
- 不是编程语言,是标记语言
- 用于构建网页的结构和内容
- 浏览器解析HTML文件并渲染页面
基本结构
<!DOCTYPE html> <!-- 声明文档类型为HTML5 -->
<html lang="zh-CN"> <!-- 根元素 -->
<head> <!-- 头部:元数据 -->
<meta charset="UTF-8"> <!-- 字符编码 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网页标题</title> <!-- 浏览器标签页显示的标题 -->
</head>
<body> <!-- 主体:页面内容 -->
<h1>这是一个网页</h1>
</body>
</html>
常用标签详解
<!-- ============ 标题标签 h1~h6 ============ -->
<h1>一级标题(最大)</h1>
<h2>二级标题</h2>
<h3>三级标题</h3>
<h4>四级标题</h4>
<h5>五级标题</h5>
<h6>六级标题(最小)</h6>
<!-- ============ 段落和换行 ============ -->
<p>这是一个段落。段落之间有间距。</p>
<p>这是另一个段落。</p>
<br> <!-- 换行(单标签) -->
<hr> <!-- 水平分割线(单标签) -->
<!-- ============ 文本格式化 ============ -->
<b>加粗</b> 或 <strong>加粗(语义更强)</strong>
<i>斜体</i> 或 <em>斜体(强调)</em>
<u>下划线</u>
<del>删除线</del> 或 <s>删除线</s>
<sup>上标</sup> 如:x<sup>2</sup>
<sub>下标</sub> 如:H<sub>2</sub>O
<!-- ============ 容器标签 ============ -->
<div>块级容器(独占一行)</div>
<span>行内容器(不换行)</span>
<!-- ============ 图片标签 ============ -->
<img src="图片路径" alt="图片描述" width="200" height="150">
<!-- src: 图片源路径 -->
<!-- alt: 图片加载失败时显示的文字,也用于无障碍 -->
<!-- width/height: 宽高(可以用像素或百分比) -->
<!-- ============ 特殊字符(实体) ============ -->
< <!-- < 小于号 -->
> <!-- > 大于号 -->
& <!-- & 和号 -->
<!-- 空格 -->
© <!-- © 版权 -->
" <!-- " 引号 -->
块级元素 vs 行内元素:
| 类型 | 特点 | 示例 |
|---|---|---|
| 块级元素 | 独占一行,可设宽高 | div, h1-h6, p, ul, ol, table |
| 行内元素 | 不换行,宽高由内容决定 | span, a, b, i, img, input |
2. 超链接
<!-- ============ 基本超链接 ============ -->
<a href="https://www.baidu.com">百度一下</a>
<!-- ============ target属性 ============ -->
<a href="page2.html" target="_self">在当前窗口打开(默认)</a>
<a href="page2.html" target="_blank">在新窗口打开</a>
<a href="page2.html" target="_parent">在父框架打开</a>
<a href="page2.html" target="_top">在顶层框架打开</a>
<!-- ============ 锚点链接(页面内跳转) ============ -->
<!-- 定义锚点 -->
<h2 id="chapter1">第一章</h2>
<p>这是第一章的内容...</p>
<h2 id="chapter2">第二章</h2>
<p>这是第二章的内容...</p>
<!-- 跳转到锚点 -->
<a href="#chapter1">跳转到第一章</a>
<a href="#chapter2">跳转到第二章</a>
<a href="#">回到顶部</a>
<!-- ============ 链接到邮箱/电话 ============ -->
<a href="mailto:example@email.com">发送邮件</a>
<a href="tel:13800138000">拨打电话</a>
<!-- ============ 下载链接 ============ -->
<a href="file.zip" download>下载文件</a>
<a href="image.png" download="新文件名.png">下载图片</a>
<!-- ============ 图片链接 ============ -->
<a href="https://www.baidu.com">
<img src="logo.png" alt="百度Logo">
</a>
<!-- ============ 空链接(占位) ============ -->
<a href="#">链接1</a>
<a href="javascript:void(0)">链接2</a>
<a href="javascript:;">链接3</a>
3. 列表、表格
列表
<!-- ============ 无序列表 (ul) ============ -->
<ul>
<li>苹果</li>
<li>香蕉</li>
<li>橘子</li>
</ul>
<!-- 显示效果:
• 苹果
• 香蕉
• 橘子
-->
<!-- type属性:disc(实心圆)、circle(空心圆)、square(方块) -->
<ul type="circle">
<li>项目1</li>
<li>项目2</li>
</ul>
<!-- ============ 有序列表 (ol) ============ -->
<ol>
<li>第一步</li>
<li>第二步</li>
<li>第三步</li>
</ol>
<!-- 显示效果:
1. 第一步
2. 第二步
3. 第三步
-->
<!-- type属性:1(数字)、a(小写字母)、A(大写字母)、i(小写罗马)、I(大写罗马) -->
<ol type="A" start="3"> <!-- 从C开始 -->
<li>项目C</li>
<li>项目D</li>
</ol>
<!-- ============ 定义列表 (dl) ============ -->
<dl>
<dt>HTML</dt> <!-- 定义术语 -->
<dd>超文本标记语言</dd> <!-- 术语描述 -->
<dt>CSS</dt>
<dd>层叠样式表</dd>
<dt>JavaScript</dt>
<dd>脚本编程语言</dd>
</dl>
<!-- ============ 嵌套列表 ============ -->
<ul>
<li>前端技术
<ul>
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
</ul>
</li>
<li>后端技术
<ul>
<li>Java</li>
<li>Python</li>
</ul>
</li>
</ul>
表格
<!-- ============ 基本表格 ============ -->
<table border="1" cellspacing="0" cellpadding="5" width="500">
<!-- border: 边框粗细 -->
<!-- cellspacing: 单元格之间的间距 -->
<!-- cellpadding: 单元格内容与边框的距离 -->
<caption>学生成绩表</caption> <!-- 表格标题 -->
<thead> <!-- 表头区域 -->
<tr> <!-- 一行 -->
<th>姓名</th> <!-- 表头单元格(加粗居中) -->
<th>语文</th>
<th>数学</th>
<th>英语</th>
</tr>
</thead>
<tbody> <!-- 表体区域 -->
<tr>
<td>张三</td> <!-- 普通单元格 -->
<td>85</td>
<td>92</td>
<td>78</td>
</tr>
<tr>
<td>李四</td>
<td>90</td>
<td>88</td>
<td>95</td>
</tr>
</tbody>
<tfoot> <!-- 表尾区域 -->
<tr>
<td>平均</td>
<td>87.5</td>
<td>90</td>
<td>86.5</td>
</tr>
</tfoot>
</table>
<!-- ============ 合并单元格 ============ -->
<table border="1">
<tr>
<td colspan="3">跨3列合并(水平合并)</td>
<!-- colspan: 向右合并n列 -->
</tr>
<tr>
<td rowspan="2">跨2行合并(垂直合并)</td>
<!-- rowspan: 向下合并n行 -->
<td>B</td>
<td>C</td>
</tr>
<tr>
<!-- 这里少写一个td,因为上面的rowspan已经占了位置 -->
<td>E</td>
<td>F</td>
</tr>
</table>
4. 内联框架、框架集
内联框架(iframe)
<!-- iframe:在当前页面中嵌入另一个页面 -->
<iframe src="https://www.baidu.com" width="800" height="600"
frameborder="0" scrolling="auto" name="myFrame">
您的浏览器不支持iframe
</iframe>
<!-- 常用属性:
src : 嵌入的页面URL
width : 宽度
height : 高度
frameborder: 边框(0无/1有)
scrolling : 滚动条(auto/yes/no)
name : 框架名称(配合target使用)
-->
<!-- 配合超链接使用 -->
<a href="page1.html" target="myFrame">页面1</a>
<a href="page2.html" target="myFrame">页面2</a>
<iframe name="myFrame" width="800" height="600"></iframe>
<!-- 点击链接,内容在iframe中显示 -->
<!-- 实际应用:嵌入地图、视频 -->
<iframe src="https://www.youtube.com/embed/xxxxx" allowfullscreen></iframe>
框架集(frameset)— HTML5已废弃
<!-- 框架集将浏览器窗口分成多个区域,每个区域显示不同页面 -->
<!-- 注意:frameset不能和body共存 -->
<!DOCTYPE html>
<html>
<head><title>框架集</title></head>
<!-- 上下分割 -->
<frameset rows="100px, *">
<frame src="header.html" name="top" noresize>
<frame src="content.html" name="main">
</frameset>
<!-- 左右分割 -->
<frameset cols="200px, *">
<frame src="menu.html" name="left">
<frame src="content.html" name="right">
</frameset>
<!-- 嵌套分割 -->
<frameset rows="100px, *">
<frame src="header.html">
<frameset cols="200px, *">
<frame src="menu.html" name="left">
<frame src="content.html" name="right">
</frameset>
</frameset>
<!-- 注意:框架集在现代开发中已很少使用 -->
<!-- 替代方案:CSS布局 + iframe / SPA单页应用 -->
</html>
第四阶段:Java面向对象进阶
1. 封装
什么是封装
封装是面向对象三大特征之一(封装、继承、多态),指将对象的属性和行为隐藏在对象内部,只对外提供访问接口。
封装的好处:
1. 隐藏实现细节,只暴露安全的接口
2. 保护数据的完整性(通过验证)
3. 提高代码的可维护性
4. 降低耦合度
实现封装
// ============ 封装前(不安全) ============
public class Student {
String name;
int age;
}
Student stu = new Student();
stu.age = -100; // 不合理的值也能设置!没有任何限制
// ============ 封装后(安全) ============
public class Student {
// 1. 将属性设为private(私有),外部无法直接访问
private String name;
private int age;
// 2. 提供public的getter和setter方法,作为外部访问的接口
// getter:获取属性值
public String getName() {
return name;
}
// setter:设置属性值(可以添加验证逻辑)
public void setName(String name) {
if (name != null && !name.isEmpty()) {
this.name = name;
} else {
System.out.println("姓名不能为空");
}
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age >= 0 && age <= 150) {
this.age = age;
} else {
System.out.println("年龄不合理");
}
}
}
// 使用封装后的类
Student stu = new Student();
stu.setName("张三");
stu.setAge(20);
// stu.setAge(-100); // 会提示"年龄不合理"
System.out.println(stu.getName()); // 张三
System.out.println(stu.getAge()); // 20
访问修饰符
| 修饰符 | 同一类 | 同一包 | 子类 | 不同包 |
|---|---|---|---|---|
| private | ✅ | ❌ | ❌ | ❌ |
| 默认(不写) | ✅ | ✅ | ❌ | ❌ |
| protected | ✅ | ✅ | ✅ | ❌ |
| public | ✅ | ✅ | ✅ | ✅ |
public class Person {
public String name; // 任何地方都能访问
protected int age; // 同包 + 子类可访问
String gender; // 默认:同包可访问
private double salary; // 只有本类可访问
}
this关键字
public class Student {
private String name;
private int age;
// this指当前对象
public void setName(String name) {
this.name = name; // this.name是成员变量,name是参数
}
// this调用构造方法
public Student() {
this("未知", 0); // 调用有参构造,必须是构造方法第一行
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// this作为返回值(链式调用)
public Student setNameChain(String name) {
this.name = name;
return this;
}
public Student setAgeChain(int age) {
this.age = age;
return this;
}
}
// 链式调用
new Student().setNameChain("张三").setAgeChain(20);
2. 方法重载
什么是方法重载(Overload)
在同一个类中,方法名相同,但参数列表不同(参数个数、类型、顺序不同)。
public class Calculator {
// 方法重载:同名不同参
// 两个int相加
public int add(int a, int b) {
return a + b;
}
// 三个int相加(参数个数不同)
public int add(int a, int b, int c) {
return a + b + c;
}
// 两个double相加(参数类型不同)
public double add(double a, double b) {
return a + b;
}
// int和double相加(参数类型顺序不同)
public double add(int a, double b) {
return a + b;
}
public double add(double a, int b) {
return a + b;
}
}
// 使用
Calculator calc = new Calculator();
calc.add(10, 20); // 调用 add(int, int) → 30
calc.add(10, 20, 30); // 调用 add(int, int, int) → 60
calc.add(1.5, 2.5); // 调用 add(double, double) → 4.0
calc.add(10, 1.5); // 调用 add(int, double) → 11.5
重载的规则:
✅ 构成重载的条件:
- 方法名相同
- 参数列表不同(个数、类型、顺序任一不同)
❌ 不构成重载(编译错误):
- 仅返回值类型不同
- 仅访问修饰符不同
- 仅参数名不同
// 以下不构成重载:
public int add(int a, int b) { return a + b; }
// public double add(int a, int b) { return a + b; } // 错误!仅返回值不同
// public int add(int x, int y) { return x + y; } // 错误!仅参数名不同
常见的重载例子:
// System.out.println就是重载的典型
System.out.println(100); // println(int)
System.out.println(3.14); // println(double)
System.out.println("Hello"); // println(String)
System.out.println(true); // println(boolean)
System.out.println('A'); // println(char)
3. 继承
什么是继承
继承是面向对象的第二大特征。子类继承父类的属性和方法,实现代码复用。
继承的概念:
动物(父类/超类)
├── 狗(子类):继承了动物的特征,还有自己特有的
├── 猫(子类):继承了动物的特征,还有自己特有的
└── 鸟(子类):继承了动物的特征,还有自己特有的
实现继承
// ============ 父类(基类/超类) ============
public class Animal {
private String name;
private int age;
public Animal() {}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + "在吃东西");
}
public void sleep() {
System.out.println(name + "在睡觉");
}
// getter/setter省略
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
// ============ 子类 使用extends关键字继承 ============
public class Dog extends Animal {
private String breed; // 子类特有属性:品种
public Dog() {}
public Dog(String name, int age, String breed) {
super(name, age); // 调用父类构造方法
this.breed = breed;
}
// 子类特有方法
public void bark() {
System.out.println(getName() + "在汪汪叫");
}
}
public class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
public void catchMouse() {
System.out.println(getName() + "在抓老鼠");
}
}
// ============ 使用 ============
Dog dog = new Dog("旺财", 3, "金毛");
dog.eat(); // 继承的方法:旺财在吃东西
dog.sleep(); // 继承的方法:旺财在睡觉
dog.bark(); // 特有的方法:旺财在汪汪叫
Cat cat = new Cat("咪咪", 2);
cat.eat(); // 继承的方法
cat.catchMouse(); // 特有的方法
继承的特点
// 1. Java是单继承(一个类只能有一个直接父类)
public class A {}
public class B extends A {}
// public class C extends A, B {} // 错误!不能多继承
// 2. 支持多层继承
public class Animal {}
public class Dog extends Animal {}
public class GoldenRetriever extends Dog {} // 金毛 → 狗 → 动物
// 3. 所有类都直接或间接继承自Object类
// Object是所有类的根类
// 4. 子类不能继承父类的:
// - private修饰的属性和方法
// - 构造方法(但可以通过super调用)
// 5. super关键字
public class Dog extends Animal {
public Dog(String name, int age) {
super(name, age); // 调用父类构造方法,必须是第一行
}
public void show() {
super.eat(); // 调用父类的方法
// super.name; // 如果name是private,不能直接访问
System.out.println(super.getName()); // 可以通过getter访问
}
}
// 6. 构造方法的调用顺序
// 创建子类对象时,先执行父类构造,再执行子类构造
// 如果不写super(),系统会自动调用父类无参构造
4. 多态(方法重写)
方法重写(Override)
子类重新定义父类中已有的方法,方法名、参数列表、返回值类型都相同。
// 父类
public class Animal {
public void eat() {
System.out.println("动物在吃东西");
}
public void makeSound() {
System.out.println("动物在叫");
}
}
// 子类重写父类方法
public class Dog extends Animal {
@Override // 注解:标识这是重写的方法,编译器会检查
public void eat() {
System.out.println("狗在啃骨头"); // 重写了父类的eat方法
}
@Override
public void makeSound() {
System.out.println("汪汪汪!"); // 重写了父类的makeSound方法
}
}
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫在吃鱼");
}
@Override
public void makeSound() {
System.out.println("喵喵喵~");
}
}
重写的规则:
1. 方法名、参数列表必须相同
2. 返回值类型相同(或子类类型)
3. 访问权限不能比父类更严格(可以更宽松)
4. 不能重写private方法(因为子类看不到)
5. 不能重写static方法
6. 不能重写final方法
重载(Overload) vs 重写(Override):
| 特性 | 重载 | 重写 |
|---|---|---|
| 位置 | 同一个类中 | 子类和父类之间 |
| 方法名 | 相同 | 相同 |
| 参数列表 | 必须不同 | 必须相同 |
| 返回值 | 无要求 | 相同或子类型 |
| 访问权限 | 无要求 | 不能更严格 |
多态
多态是面向对象的第三大特征。同一个方法调用,根据对象的不同,产生不同的行为。
// ============ 多态的前提条件 ============
// 1. 有继承/实现关系
// 2. 有方法重写
// 3. 父类引用指向子类对象
// ============ 多态的核心:父类引用指向子类对象 ============
Animal animal1 = new Dog(); // 编译类型是Animal,运行类型是Dog
Animal animal2 = new Cat(); // 编译类型是Animal,运行类型是Cat
animal1.eat(); // 狗在啃骨头(运行时调用Dog的eat方法)
animal2.eat(); // 猫在吃鱼(运行时调用Cat的eat方法)
animal1.makeSound(); // 汪汪汪!
animal2.makeSound(); // 喵喵喵~
// ============ 多态的实际应用 ============
// 方法参数使用父类类型,可以接收任何子类对象
public class AnimalShow {
// 不使用多态:需要为每种动物写一个方法
// public void showEat(Dog dog) { dog.eat(); }
// public void showEat(Cat cat) { cat.eat(); }
// 使用多态:一个方法搞定
public void showEat(Animal animal) {
animal.eat(); // 根据传入的实际对象调用对应的eat方法
}
}
AnimalShow show = new AnimalShow();
show.showEat(new Dog()); // 狗在啃骨头
show.showEat(new Cat()); // 猫在吃鱼
// ============ 向上转型和向下转型 ============
// 向上转型(自动):子类 → 父类
Animal a = new Dog(); // 自动转型
// a.bark(); // 错误!编译类型是Animal,没有bark方法
// 向下转型(强制):父类 → 子类(需要instanceof判断)
if (a instanceof Dog) {
Dog dog = (Dog) a; // 强制转型
dog.bark(); // 现在可以调用Dog特有的方法了
}
多态的内存原理:
编译时:看左边(Animal类型),决定能调用哪些方法
运行时:看右边(Dog对象),决定实际执行哪个方法
编译时检查:Animal类有eat()方法吗? → 有,通过编译
运行时执行:实际对象是Dog → 执行Dog的eat()
5. 抽象类、接口
抽象类
抽象类是不能被实例化的类,用于定义规范,由子类来实现。
// 使用abstract关键字定义抽象类
public abstract class Shape {
private String color;
public Shape(String color) {
this.color = color;
}
// 抽象方法:只有方法签名,没有方法体
// 子类必须重写所有抽象方法
public abstract double getArea();
public abstract double getPerimeter();
// 普通方法:抽象类中也可以有普通方法
public void show() {
System.out.println("颜色:" + color + ",面积:" + getArea());
}
public String getColor() { return color; }
}
// 子类必须实现所有抽象方法(除非子类也是抽象类)
public class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
@Override
public double getPerimeter() {
return 2 * Math.PI * radius;
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(String color, double width, double height) {
super(color);
this.width = width;
this.height = height;
}
@Override
public double getArea() {
return width * height;
}
@Override
public double getPerimeter() {
return 2 * (width + height);
}
}
// 使用(多态)
Shape shape1 = new Circle("红色", 5);
Shape shape2 = new Rectangle("蓝色", 4, 6);
shape1.show(); // 颜色:红色,面积:78.54
shape2.show(); // 颜色:蓝色,面积:24.0
// Shape shape = new Shape("绿色"); // 错误!抽象类不能实例化
抽象类的特点:
1. 使用abstract修饰
2. 不能被实例化(不能new)
3. 可以有抽象方法(也可以没有)
4. 可以有普通方法、构造方法、成员变量
5. 子类必须重写所有抽象方法(除非子类也是抽象类)
6. 抽象方法不能是private、static、final
接口
接口是完全抽象的"规范",定义了一组行为标准。
// 使用interface关键字定义接口
public interface Flyable {
// 接口中的变量默认是 public static final
int MAX_HEIGHT = 10000; // 常量
// 接口中的方法默认是 public abstract(JDK 7及之前)
void fly();
void land();
// JDK 8+ 可以有默认方法
default void showInfo() {
System.out.println("这是一个可以飞行的对象");
}
// JDK 8+ 可以有静态方法
static void staticMethod() {
System.out.println("接口的静态方法");
}
}
public interface Swimmable {
void swim();
}
// 使用implements关键字实现接口
// 一个类可以实现多个接口(弥补单继承的不足)
public class Duck extends Animal implements Flyable, Swimmable {
@Override
public void fly() {
System.out.println("鸭子在飞");
}
@Override
public void land() {
System.out.println("鸭子降落了");
}
@Override
public void swim() {
System.out.println("鸭子在游泳");
}
}
// 接口也可以实现多态
Flyable f = new Duck();
f.fly(); // 鸭子在飞
Swimmable s = new Duck();
s.swim(); // 鸭子在游泳
抽象类 vs 接口:
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 关键字 | abstract class | interface |
| 继承/实现 | extends(单继承) | implements(多实现) |
| 构造方法 | 有 | 无 |
| 成员变量 | 可以有各种变量 | 只能有常量(public static final) |
| 方法 | 抽象方法 + 普通方法 | 抽象方法 + 默认方法(JDK8) |
| 设计理念 | is-a(是什么) | can-do(能做什么) |
| 使用场景 | 有共同的属性和部分方法 | 定义行为规范 |
6. 程序异常处理
异常体系
Throwable(所有异常和错误的父类)
├── Error(错误)—— 严重问题,程序无法处理
│ ├── OutOfMemoryError(内存溢出)
│ ├── StackOverflowError(栈溢出)
│ └── ...
│
└── Exception(异常)—— 程序可以处理
├── RuntimeException(运行时异常/非受检异常)
│ ├── NullPointerException(空指针)
│ ├── ArrayIndexOutOfBoundsException(数组越界)
│ ├── ClassCastException(类型转换异常)
│ ├── ArithmeticException(算术异常,如除以零)
│ ├── NumberFormatException(数字格式异常)
│ └── ...
│
└── 受检异常(编译时异常)—— 必须处理
├── IOException(IO异常)
├── FileNotFoundException(文件未找到)
├── SQLException(数据库异常)
├── ClassNotFoundException(类未找到)
└── ...
异常处理方式
// ============ 方式1:try-catch-finally ============
try {
// 可能发生异常的代码
int result = 10 / 0;
System.out.println("这行不会执行");
} catch (ArithmeticException e) {
// 捕获并处理异常
System.out.println("发生算术异常:" + e.getMessage());
e.printStackTrace(); // 打印异常堆栈信息
} catch (Exception e) {
// 可以有多个catch,从小到大排列
System.out.println("其他异常:" + e.getMessage());
} finally {
// 无论是否发生异常,finally中的代码都会执行
// 通常用于释放资源(关闭连接、关闭文件等)
System.out.println("finally块一定会执行");
}
// ============ 方式2:throws声明异常 ============
// 在方法签名上声明可能抛出的异常,交给调用者处理
public void readFile(String path) throws FileNotFoundException, IOException {
FileInputStream fis = new FileInputStream(path); // 可能抛出FileNotFoundException
fis.read(); // 可能抛出IOException
fis.close();
}
// 调用者处理
try {
readFile("test.txt");
} catch (FileNotFoundException e) {
System.out.println("文件不存在");
} catch (IOException e) {
System.out.println("读取文件失败");
}
// ============ 方式3:throw手动抛出异常 ============
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄不合理:" + age);
}
this.age = age;
}
// ============ 自定义异常 ============
public class AgeException extends RuntimeException {
public AgeException(String message) {
super(message);
}
}
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new AgeException("年龄必须在0~150之间,当前值:" + age);
}
this.age = age;
}
throws vs throw:
| throws | throw | |
|---|---|---|
| 位置 | 方法签名上 | 方法体内 |
| 作用 | 声明方法可能抛出的异常 | 创建并抛出异常对象 |
| 后面跟 | 异常类名 | 异常对象 |
第五阶段:HTML5 + CSS基础
1. HTML5表单
<!-- ============ form标签 ============ -->
<form action="服务器URL" method="get/post" enctype="multipart/form-data">
<!-- action: 表单提交的目标地址 -->
<!-- method: 提交方式(get/post) -->
<!-- enctype: 编码类型(文件上传时使用multipart/form-data) -->
<!-- ============ 文本输入 ============ -->
<label for="username">用户名:</label>
<input type="text" id="username" name="username"
placeholder="请输入用户名" maxlength="20" required>
<!-- ============ 密码输入 ============ -->
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
<!-- ============ HTML5新增输入类型 ============ -->
<input type="email" placeholder="邮箱"> <!-- 自动验证邮箱格式 -->
<input type="url" placeholder="网址"> <!-- 自动验证URL -->
<input type="tel" placeholder="手机号"> <!-- 移动端弹出数字键盘 -->
<input type="number" min="0" max="100" step="5"> <!-- 数字输入 -->
<input type="range" min="0" max="100"> <!-- 滑块 -->
<input type="date"> <!-- 日期选择器 -->
<input type="time"> <!-- 时间选择器 -->
<input type="datetime-local"> <!-- 日期时间选择器 -->
<input type="month"> <!-- 月份选择器 -->
<input type="week"> <!-- 周选择器 -->
<input type="color"> <!-- 颜色选择器 -->
<input type="search" placeholder="搜索"> <!-- 搜索框 -->
<input type="hidden" name="token" value="xxx"> <!-- 隐藏域 -->
<!-- ============ 单选按钮 ============ -->
<input type="radio" name="gender" value="male" checked> 男
<input type="radio" name="gender" value="female"> 女
<!-- 同name的radio为一组,只能选一个 -->
<!-- ============ 复选框 ============ -->
<input type="checkbox" name="hobby" value="reading"> 阅读
<input type="checkbox" name="hobby" value="gaming"> 游戏
<input type="checkbox" name="hobby" value="sports" checked> 运动
<!-- ============ 下拉列表 ============ -->
<select name="city">
<option value="">请选择</option>
<option value="beijing">北京</option>
<option value="shanghai" selected>上海</option>
<option value="guangzhou">广州</option>
</select>
<!-- ============ 多行文本 ============ -->
<textarea name="intro" rows="5" cols="40" placeholder="自我介绍"></textarea>
<!-- ============ 文件上传 ============ -->
<input type="file" name="avatar" accept="image/*" multiple>
<!-- accept: 限制文件类型 -->
<!-- multiple: 允许多选 -->
<!-- ============ 按钮 ============ -->
<input type="submit" value="提交"> <!-- 提交按钮 -->
<input type="reset" value="重置"> <!-- 重置按钮 -->
<input type="button" value="普通按钮"> <!-- 普通按钮 -->
<button type="submit">提交按钮</button> <!-- button标签 -->
<!-- ============ HTML5表单属性 ============ -->
<input type="text" required> <!-- 必填 -->
<input type="text" autofocus> <!-- 自动获取焦点 -->
<input type="text" autocomplete="off"> <!-- 关闭自动完成 -->
<input type="text" pattern="[0-9]{6}"> <!-- 正则验证 -->
<input type="text" readonly> <!-- 只读 -->
<input type="text" disabled> <!-- 禁用 -->
<!-- ============ datalist(数据列表) ============ -->
<input type="text" list="browsers">
<datalist id="browsers">
<option value="Chrome">
<option value="Firefox">
<option value="Safari">
<option value="Edge">
</datalist>
</form>
GET vs POST:
| 特性 | GET | POST |
|---|---|---|
| 数据位置 | URL中(可见) | 请求体中(不可见) |
| 数据长度 | 有限制(约2KB) | 无限制 |
| 安全性 | 低(数据在URL中暴露) | 较高 |
| 书签 | 可以收藏 | 不能收藏 |
| 缓存 | 会被缓存 | 不会被缓存 |
| 使用场景 | 查询数据 | 提交数据(登录、注册等) |
2. 样式表分类
CSS简介
CSS(Cascading Style Sheets):层叠样式表
- 用于控制HTML元素的显示样式(颜色、大小、位置等)
- 实现内容(HTML)与表现(CSS)的分离
三种引入方式
<!-- ============ 1. 行内样式(内联样式) ============ -->
<!-- 直接写在标签的style属性中 -->
<p style="color: red; font-size: 16px;">红色文字</p>
<!-- 优先级最高,但不推荐(维护困难) -->
<!-- ============ 2. 内部样式(嵌入样式) ============ -->
<!-- 写在head标签内的style标签中 -->
<head>
<style>
p {
color: blue;
font-size: 14px;
}
.highlight {
background-color: yellow;
}
</style>
</head>
<!-- 适合单个页面的样式 -->
<!-- ============ 3. 外部样式(推荐) ============ -->
<!-- 写在单独的.css文件中,通过link标签引入 -->
<head>
<link rel="stylesheet" href="style.css">
</head>
<!-- 适合多个页面共用样式,最推荐的方式 -->
<!-- style.css文件内容:
p {
color: green;
font-size: 16px;
}
-->
三种方式对比:
| 方式 | 优先级 | 复用性 | 维护性 | 推荐度 |
|---|---|---|---|---|
| 行内样式 | 最高 | 差 | 差 | ⭐ |
| 内部样式 | 中 | 单页 | 一般 | ⭐⭐ |
| 外部样式 | 中 | 好 | 好 | ⭐⭐⭐ |
3. 选择器(CSS3选择器)
基本选择器
/* ============ 1. 元素选择器(标签选择器) ============ */
p { color: red; } /* 所有<p>标签 */
h1 { font-size: 24px; } /* 所有<h1>标签 */
div { background: #f0f0f0; }
/* ============ 2. 类选择器 ============ */
.highlight { background: yellow; } /* class="highlight"的元素 */
.btn { padding: 10px 20px; }
p.intro { font-style: italic; } /* 同时是<p>标签且class="intro" */
/* HTML: <p class="highlight">高亮文字</p> */
/* 一个元素可以有多个类: <p class="highlight intro">多个类</p> */
/* ============ 3. ID选择器 ============ */
#header { height: 80px; } /* id="header"的元素 */
#footer { background: #333; }
/* ID在页面中必须唯一 */
/* ============ 4. 通配符选择器 ============ */
* { margin: 0; padding: 0; } /* 所有元素 */
/* ============ 5. 属性选择器(CSS3) ============ */
[href] { color: blue; } /* 有href属性的元素 */
[type="text"] { border: 1px solid #ccc; } /* type="text"的元素 */
[class~="btn"] { } /* class包含"btn"(空格分隔)*/
[href^="https"] { } /* href以"https"开头 */
[href$=".pdf"] { } /* href以".pdf"结尾 */
[href*="baidu"] { } /* href包含"baidu" */
组合选择器
/* ============ 后代选择器(空格) ============ */
div p { color: red; } /* div内部所有p(包括嵌套的) */
/* ============ 子元素选择器(>) ============ */
div > p { color: blue; } /* div的直接子元素p */
/* ============ 相邻兄弟选择器(+) ============ */
h1 + p { font-size: 18px; } /* 紧跟在h1后面的第一个p */
/* ============ 通用兄弟选择器(~) ============ */
h1 ~ p { color: gray; } /* h1后面所有的兄弟p */
/* ============ 分组选择器(,) ============ */
h1, h2, h3 { color: navy; } /* h1、h2、h3共用样式 */
伪类选择器
/* ============ 链接伪类 ============ */
a:link { color: blue; } /* 未访问的链接 */
a:visited { color: purple; } /* 已访问的链接 */
a:hover { color: red; } /* 鼠标悬停 */
a:active { color: orange; } /* 点击时 */
/* 顺序必须是 LVHA:link → visited → hover → active */
/* ============ 结构伪类(CSS3) ============ */
li:first-child { } /* 第一个子元素 */
li:last-child { } /* 最后一个子元素 */
li:nth-child(3) { } /* 第3个子元素 */
li:nth-child(2n) { } /* 偶数子元素 */
li:nth-child(2n+1) { } /* 奇数子元素 */
li:nth-child(odd) { } /* 奇数(同上) */
li:nth-child(even) { } /* 偶数 */
p:first-of-type { } /* 同类型中的第一个 */
p:last-of-type { } /* 同类型中的最后一个 */
p:nth-of-type(2) { } /* 同类型中的第2个 */
/* ============ 表单伪类 ============ */
input:focus { border-color: blue; } /* 获取焦点时 */
input:disabled { background: #eee; } /* 禁用状态 */
input:checked { } /* 选中状态 */
input:required { } /* 必填字段 */
input:valid { } /* 验证通过 */
input:invalid { } /* 验证失败 */
/* ============ 否定伪类 ============ */
p:not(.intro) { } /* 不是.intro类的p */
伪元素选择器
/* ============ 伪元素(CSS3用::) ============ */
p::before { content: "★ "; } /* 在元素内容前面插入 */
p::after { content: " →"; } /* 在元素内容后面插入 */
p::first-line { font-weight: bold; } /* 第一行 */
p::first-letter { font-size: 2em; } /* 第一个字母 */
::selection { background: yellow; } /* 被选中的文本 */
选择器优先级
!important > 行内样式 > ID选择器 > 类选择器 > 元素选择器 > 通配符
权重计算:
行内样式 1000
ID选择器 100
类/伪类/属性 10
元素/伪元素 1
通配符 0
例:
#nav .list li a → 100 + 10 + 1 + 1 = 112
.content p → 10 + 1 = 11
#header → 100
第六阶段:CSS布局
1. 基本样式
字体样式
p {
font-family: "微软雅黑", Arial, sans-serif; /* 字体族 */
font-size: 16px; /* 字体大小 */
font-weight: bold; /* 字体粗细:normal/bold/100~900 */
font-style: italic; /* 字体风格:normal/italic/oblique */
font-variant: small-caps; /* 小型大写字母 */
/* 简写 */
font: italic bold 16px/1.5 "微软雅黑", Arial;
/* 顺序:style weight size/line-height family */
}
文本样式
p {
color: #333; /* 文字颜色 */
text-align: center; /* 对齐:left/right/center/justify */
text-decoration: underline; /* 装饰:none/underline/overline/line-through */
text-indent: 2em; /* 首行缩进(2个字符) */
text-transform: uppercase; /* 大小写:uppercase/lowercase/capitalize */
letter-spacing: 2px; /* 字符间距 */
word-spacing: 5px; /* 单词间距 */
line-height: 1.5; /* 行高 */
white-space: nowrap; /* 不换行 */
text-overflow: ellipsis; /* 文字溢出显示省略号 */
overflow: hidden; /* 配合text-overflow使用 */
text-shadow: 2px 2px 4px #666; /* 文字阴影 */
}
超链接样式
/* 去除默认下划线 */
a {
text-decoration: none;
color: #333;
}
/* 悬停效果 */
a:hover {
color: #ff6600;
text-decoration: underline;
}
/* 导航链接样式 */
.nav a {
display: inline-block;
padding: 10px 20px;
color: white;
background-color: #333;
text-decoration: none;
}
.nav a:hover {
background-color: #ff6600;
}
背景样式
div {
background-color: #f0f0f0; /* 背景颜色 */
background-image: url('bg.jpg'); /* 背景图片 */
background-repeat: no-repeat; /* 重复:repeat/no-repeat/repeat-x/repeat-y */
background-position: center center; /* 位置:left/right/center top/bottom/center 或 px */
background-size: cover; /* 大小:cover/contain/100% 50%/200px 100px */
background-attachment: fixed; /* 固定:scroll/fixed */
/* 简写 */
background: #f0f0f0 url('bg.jpg') no-repeat center center / cover;
/* CSS3: 多背景 */
background: url('img1.png') no-repeat top left,
url('img2.png') no-repeat bottom right,
#f0f0f0;
/* CSS3: 渐变背景 */
background: linear-gradient(to right, #ff6600, #ff0066); /* 线性渐变 */
background: radial-gradient(circle, #ff6600, #ff0066); /* 径向渐变 */
}
列表样式
ul {
list-style-type: none; /* 去掉列表符号 */
/* 可选值:disc/circle/square/decimal/none */
list-style-position: inside; /* 符号位置:inside/outside */
list-style-image: url('bullet.png'); /* 自定义列表图标 */
/* 简写 */
list-style: none;
}
2. 盒子模型
盒子模型概念
每个HTML元素都可以看作一个"盒子",由以下部分组成:
┌──────────────────────────────────────┐
│ margin(外边距) │
│ ┌───────────────────────────────┐ │
│ │ border(边框) │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ padding(内边距) │ │ │
│ │ │ ┌───────────────────┐ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ content(内容) │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ └───────────────────┘ │ │ │
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────┘ │
└──────────────────────────────────────┘
各部分设置
div {
/* ============ 内容区域 ============ */
width: 200px; /* 内容宽度 */
height: 100px; /* 内容高度 */
/* ============ 内边距 padding ============ */
padding-top: 10px;
padding-right: 20px;
padding-bottom: 10px;
padding-left: 20px;
/* 简写 */
padding: 10px; /* 四边相同 */
padding: 10px 20px; /* 上下10,左右20 */
padding: 10px 20px 30px; /* 上10,左右20,下30 */
padding: 10px 20px 30px 40px; /* 上10,右20,下30,左40(顺时针) */
/* ============ 边框 border ============ */
border-width: 2px;
border-style: solid; /* solid/dashed/dotted/double/none */
border-color: #333;
/* 简写 */
border: 2px solid #333;
/* 单独设置某一边 */
border-top: 3px dashed red;
border-bottom: none;
/* CSS3: 圆角边框 */
border-radius: 10px; /* 四角相同 */
border-radius: 50%; /* 圆形 */
border-radius: 10px 20px 30px 40px; /* 四角不同 */
/* CSS3: 盒子阴影 */
box-shadow: 5px 5px 10px rgba(0,0,0,0.3);
/* 水平偏移 垂直偏移 模糊半径 颜色 */
/* ============ 外边距 margin ============ */
margin-top: 10px;
margin-right: auto; /* auto: 自动居中 */
margin-bottom: 10px;
margin-left: auto;
/* 简写(与padding相同) */
margin: 10px auto; /* 上下10,左右自动(水平居中) */
margin: 0 auto; /* 最常用的水平居中方式 */
}
盒子大小计算
/* ============ 标准盒模型(默认) ============ */
/* 总宽度 = width + padding-left + padding-right + border-left + border-right + margin-left + margin-right */
/* 总高度 = height + padding-top + padding-bottom + border-top + border-bottom + margin-top + margin-bottom */
div {
width: 200px;
padding: 20px;
border: 5px solid #333;
margin: 10px;
}
/* 实际占据宽度 = 200 + 20*2 + 5*2 + 10*2 = 270px */
/* 盒子可见宽度 = 200 + 20*2 + 5*2 = 250px */
/* ============ CSS3: box-sizing ============ */
/* 改变盒子大小的计算方式 */
div {
box-sizing: content-box; /* 默认:width只包含content */
box-sizing: border-box; /* width包含content+padding+border */
}
/* border-box更符合直觉 */
div {
box-sizing: border-box;
width: 200px;
padding: 20px;
border: 5px solid #333;
}
/* 设置width=200px,盒子可见宽度就是200px */
/* content宽度 = 200 - 20*2 - 5*2 = 150px */
/* 全局设置(推荐) */
* {
box-sizing: border-box;
}
外边距合并问题
/* 垂直方向的margin会合并(取较大值) */
.box1 { margin-bottom: 30px; }
.box2 { margin-top: 20px; }
/* 两个盒子之间的间距是30px,不是50px */
/* 解决方法:只给一个元素设置margin */
3. 页面布局(浮动)
浮动(float)
/* 浮动最初用于实现文字环绕图片效果 */
/* 现在主要用于横向排列元素 */
.left-box {
float: left; /* 向左浮动 */
width: 200px;
height: 300px;
}
.right-box {
float: right; /* 向右浮动 */
width: 200px;
height: 300px;
}
/* float的值:left / right / none(默认) */
浮动的特点:
1. 脱离标准文档流(不再占据原来的位置)
2. 浮动元素会向左或向右移动,直到碰到父元素边界或其他浮动元素
3. 浮动元素不会超出父元素的padding区域
4. 多个浮动元素会在同一行排列(空间不够会换行)
5. 浮动元素变成"块级"(可以设宽高)
清除浮动
问题:父元素没有设高度时,浮动子元素不会撑开父元素(高度塌陷)
/* ============ 方法1:clear属性 ============ */
.clearfix {
clear: both; /* 清除两侧浮动的影响 */
/* clear: left / right / both / none */
}
/* ============ 方法2:父元素overflow ============ */
.parent {
overflow: hidden; /* 触发BFC,自动包裹浮动子元素 */
}
/* ============ 方法3:伪元素清除(推荐) ============ */
.clearfix::after {
content: "";
display: block;
clear: both;
}
/* 给父元素添加class="clearfix"即可 */
/* ============ 方法4:双伪元素清除 ============ */
.clearfix::before,
.clearfix::after {
content: "";
display: table;
}
.clearfix::after {
clear: both;
}
常见布局
/* ============ 两列布局 ============ */
.left {
float: left;
width: 200px;
}
.right {
margin-left: 220px; /* 左边距 = 左栏宽度 + 间距 */
}
/* ============ 三列布局 ============ */
.left {
float: left;
width: 200px;
}
.right {
float: right;
width: 200px;
}
.center {
margin: 0 220px; /* 两边留出空间 */
}
4. 定位机制
position属性
/* ============ 1. 静态定位 static(默认) ============ */
div {
position: static;
/* 默认值,元素按标准文档流排列 */
/* top/right/bottom/left无效 */
}
/* ============ 2. 相对定位 relative ============ */
div {
position: relative;
top: 20px; /* 相对于原位置向下移动20px */
left: 30px; /* 相对于原位置向右移动30px */
}
/* 特点:
- 相对于元素原来的位置进行偏移
- 不脱离文档流(原位置仍然保留)
- 常用作子元素绝对定位的参照
*/
/* ============ 3. 绝对定位 absolute ============ */
div {
position: absolute;
top: 50px;
right: 20px;
}
/* 特点:
- 相对于最近的已定位(非static)祖先元素定位
- 如果没有已定位的祖先,则相对于body
- 脱离文档流(不占据原位置)
- 常用的套路:"子绝父相"(子元素absolute,父元素relative)
*/
/* "子绝父相"示例 */
.parent {
position: relative; /* 作为参照 */
width: 500px;
height: 300px;
}
.child {
position: absolute; /* 相对于parent定位 */
top: 10px;
right: 10px;
/* 在parent的右上角 */
}
/* ============ 4. 固定定位 fixed ============ */
div {
position: fixed;
bottom: 20px;
right: 20px;
}
/* 特点:
- 相对于浏览器窗口定位
- 脱离文档流
- 页面滚动时位置不变
- 常用于:固定导航栏、回到顶部按钮、悬浮广告
*/
/* 固定导航栏 */
.navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 60px;
background: #333;
z-index: 999;
}
/* ============ 5. 粘性定位 sticky(CSS3) ============ */
div {
position: sticky;
top: 0;
}
/* 特点:
- 在到达指定位置前是relative,到达后变成fixed
- 常用于:滚动时固定的表头
*/
z-index(层叠顺序)
/* 当定位元素重叠时,z-index决定谁在上面 */
.box1 {
position: absolute;
z-index: 1; /* 较小的在下面 */
}
.box2 {
position: absolute;
z-index: 10; /* 较大的在上面 */
}
/* z-index只对定位元素有效(position不是static) */
/* 值可以是正数、负数、0 */
居中技巧
/* ============ 水平居中 ============ */
/* 块级元素 */
.center-block {
width: 200px;
margin: 0 auto;
}
/* 行内元素/文字 */
.center-text {
text-align: center;
}
/* ============ 垂直居中 ============ */
/* 单行文字 */
.center-line {
height: 50px;
line-height: 50px; /* line-height = height */
}
/* ============ 水平垂直居中(定位方式) ============ */
.parent {
position: relative;
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* ============ 水平垂直居中(Flex方式,最推荐) ============ */
.parent {
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
第七阶段:MySQL数据库
1. MySQL安装与管理(SQLyog)
MySQL简介
MySQL是最流行的开源关系型数据库管理系统(RDBMS)
- 关系型:数据以表格形式存储,表之间通过关系(外键)连接
- SQL:Structured Query Language(结构化查询语言)
- 特点:开源免费、性能高、跨平台、社区活跃
安装与配置
安装MySQL:
1. 下载MySQL Server(推荐5.7或8.0版本)
2. 安装时设置root密码
3. 选择字符集:UTF-8(utf8mb4)
4. 配置端口:默认3306
5. 将MySQL添加到系统服务(自动启动)
管理工具SQLyog:
- 图形化的MySQL管理工具
- 可以直观地创建数据库、表
- 可以直接执行SQL语句
- 可以导入导出数据
基本操作
-- 连接MySQL
mysql -u root -p
-- 查看所有数据库
SHOW DATABASES;
-- 创建数据库
CREATE DATABASE school CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 使用数据库
USE school;
-- 查看当前数据库的所有表
SHOW TABLES;
-- 删除数据库
DROP DATABASE school;
2. 数据库约束
-- 约束用于保证数据的完整性和正确性
-- ============ 1. 主键约束 PRIMARY KEY ============
-- 唯一标识表中的每一行,不能为NULL,不能重复
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT, -- 主键 + 自增
name VARCHAR(20)
);
-- ============ 2. 非空约束 NOT NULL ============
-- 该列不能为NULL
name VARCHAR(20) NOT NULL,
-- ============ 3. 唯一约束 UNIQUE ============
-- 该列的值不能重复(但可以有多个NULL)
email VARCHAR(50) UNIQUE,
-- ============ 4. 默认约束 DEFAULT ============
-- 未指定值时使用默认值
gender VARCHAR(2) DEFAULT '男',
-- ============ 5. 外键约束 FOREIGN KEY ============
-- 建立表之间的关联关系,保证引用完整性
CREATE TABLE score (
id INT PRIMARY KEY AUTO_INCREMENT,
student_id INT,
course VARCHAR(20),
grade DECIMAL(5,2),
FOREIGN KEY (student_id) REFERENCES student(id)
);
-- ============ 6. 检查约束 CHECK(MySQL 8.0.16+支持) ============
age INT CHECK(age >= 0 AND age <= 150),
-- ============ 综合示例 ============
CREATE TABLE employee (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
email VARCHAR(50) UNIQUE,
age INT CHECK(age >= 18 AND age <= 65),
gender CHAR(1) DEFAULT '男',
dept_id INT,
hire_date DATE NOT NULL,
salary DECIMAL(10,2) DEFAULT 0,
FOREIGN KEY (dept_id) REFERENCES department(id)
);
3. DDL、DML、DQL、DCL语句
DDL(Data Definition Language)数据定义语言
-- 操作数据库结构(库、表、字段)
-- ============ 创建表 ============
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '学生ID',
name VARCHAR(20) NOT NULL COMMENT '姓名',
age INT COMMENT '年龄',
gender CHAR(1) DEFAULT '男' COMMENT '性别',
email VARCHAR(50) UNIQUE COMMENT '邮箱',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表';
-- MySQL常用数据类型:
-- 整数:TINYINT, SMALLINT, INT, BIGINT
-- 小数:FLOAT, DOUBLE, DECIMAL(M,D)
-- 字符串:CHAR(定长), VARCHAR(变长), TEXT(大文本)
-- 日期:DATE, TIME, DATETIME, TIMESTAMP
-- 二进制:BLOB
-- ============ 修改表 ============
-- 添加列
ALTER TABLE student ADD phone VARCHAR(20);
ALTER TABLE student ADD address VARCHAR(100) AFTER email; -- 指定位置
-- 修改列类型
ALTER TABLE student MODIFY name VARCHAR(50) NOT NULL;
-- 修改列名
ALTER TABLE student CHANGE phone telephone VARCHAR(20);
-- 删除列
ALTER TABLE student DROP telephone;
-- 重命名表
ALTER TABLE student RENAME TO stu;
RENAME TABLE stu TO student;
-- ============ 删除表 ============
DROP TABLE student; -- 直接删除
DROP TABLE IF EXISTS student; -- 存在才删除
-- ============ 清空表 ============
TRUNCATE TABLE student; -- 删除所有数据,重置自增ID,不可回滚
DML(Data Manipulation Language)数据操作语言
-- 操作表中的数据(增删改)
-- ============ 插入数据 INSERT ============
-- 方式1:指定列名
INSERT INTO student (name, age, gender, email)
VALUES ('张三', 20, '男', 'zhangsan@email.com');
-- 方式2:所有列(不推荐,列顺序改变会出问题)
INSERT INTO student
VALUES (NULL, '李四', 22, '女', 'lisi@email.com', NOW());
-- 方式3:批量插入
INSERT INTO student (name, age, gender) VALUES
('王五', 21, '男'),
('赵六', 23, '女'),
('钱七', 20, '男');
-- ============ 更新数据 UPDATE ============
-- 修改指定条件的数据
UPDATE student SET age = 25 WHERE name = '张三';
-- 修改多个字段
UPDATE student SET age = 26, email = 'new@email.com' WHERE id = 1;
-- 不加WHERE条件会修改所有行!(危险操作)
UPDATE student SET age = 20; -- 所有学生年龄都变成20
-- ============ 删除数据 DELETE ============
-- 删除指定条件的数据
DELETE FROM student WHERE id = 5;
-- 删除所有数据(可回滚)
DELETE FROM student;
-- TRUNCATE vs DELETE:
-- TRUNCATE: 速度快,重置自增ID,不能回滚,不触发触发器
-- DELETE: 可以条件删除,可以回滚,触发触发器
DQL(Data Query Language)数据查询语言
-- ============ 基础查询 ============
SELECT * FROM student; -- 查询所有列
SELECT name, age FROM student; -- 查询指定列
SELECT name AS '姓名', age AS '年龄' FROM student; -- 别名
SELECT DISTINCT gender FROM student; -- 去重
-- ============ 条件查询 WHERE ============
SELECT * FROM student WHERE age > 20;
SELECT * FROM student WHERE age >= 20 AND gender = '男';
SELECT * FROM student WHERE age = 20 OR age = 22;
SELECT * FROM student WHERE age IN (20, 22, 25);
SELECT * FROM student WHERE age BETWEEN 20 AND 25;
SELECT * FROM student WHERE email IS NULL;
SELECT * FROM student WHERE email IS NOT NULL;
-- ============ 排序 ORDER BY ============
SELECT * FROM student ORDER BY age ASC; -- 升序(默认)
SELECT * FROM student ORDER BY age DESC; -- 降序
SELECT * FROM student ORDER BY age DESC, name ASC; -- 多字段排序
-- ============ 限制结果 LIMIT ============
SELECT * FROM student LIMIT 5; -- 前5条
SELECT * FROM student LIMIT 5, 10; -- 从第6条开始取10条(分页)
-- LIMIT offset, count
-- 第1页: LIMIT 0, 10
-- 第2页: LIMIT 10, 10
-- 第n页: LIMIT (n-1)*pageSize, pageSize
详细见后面的查询部分。
DCL(Data Control Language)数据控制语言
-- 管理用户权限
-- ============ 用户管理 ============
-- 创建用户
CREATE USER 'zhangsan'@'localhost' IDENTIFIED BY '123456';
CREATE USER 'zhangsan'@'%' IDENTIFIED BY '123456'; -- %表示任何主机
-- 删除用户
DROP USER 'zhangsan'@'localhost';
-- 修改密码
ALTER USER 'zhangsan'@'localhost' IDENTIFIED BY 'newpassword';
-- ============ 权限管理 ============
-- 授权
GRANT SELECT, INSERT ON school.* TO 'zhangsan'@'localhost';
GRANT ALL PRIVILEGES ON school.* TO 'zhangsan'@'localhost'; -- 所有权限
-- 撤销权限
REVOKE INSERT ON school.* FROM 'zhangsan'@'localhost';
-- 查看权限
SHOW GRANTS FOR 'zhangsan'@'localhost';
-- 刷新权限
FLUSH PRIVILEGES;
4. 各种查询
模糊查询
-- LIKE + 通配符
-- %:匹配任意多个字符
-- _:匹配一个字符
SELECT * FROM student WHERE name LIKE '张%'; -- 张开头的
SELECT * FROM student WHERE name LIKE '%三'; -- 三结尾的
SELECT * FROM student WHERE name LIKE '%小%'; -- 包含小的
SELECT * FROM student WHERE name LIKE '张_'; -- 张+一个字符(张三、张四)
SELECT * FROM student WHERE name LIKE '张__'; -- 张+两个字符(张三丰)
连表查询(多表查询)
-- 准备数据
-- student表:id, name, class_id
-- class表:id, class_name
-- ============ 内连接(INNER JOIN)============
-- 只返回两个表中匹配的记录
SELECT s.name, c.class_name
FROM student s
INNER JOIN class c ON s.class_id = c.id;
-- 等价写法(隐式内连接)
SELECT s.name, c.class_name
FROM student s, class c
WHERE s.class_id = c.id;
-- ============ 左连接(LEFT JOIN)============
-- 返回左表所有记录 + 右表匹配的记录(不匹配的显示NULL)
SELECT s.name, c.class_name
FROM student s
LEFT JOIN class c ON s.class_id = c.id;
-- ============ 右连接(RIGHT JOIN)============
-- 返回右表所有记录 + 左表匹配的记录
SELECT s.name, c.class_name
FROM student s
RIGHT JOIN class c ON s.class_id = c.id;
-- ============ 自连接 ============
-- 表和自己连接
SELECT e.name AS '员工', m.name AS '上级'
FROM employee e
LEFT JOIN employee m ON e.manager_id = m.id;
子查询
-- 子查询:SQL语句中嵌套另一个SELECT语句
-- ============ 单行子查询 ============
-- 查询比张三年龄大的所有学生
SELECT * FROM student
WHERE age > (SELECT age FROM student WHERE name = '张三');
-- ============ 多行子查询 ============
-- IN:在子查询结果中
SELECT * FROM student
WHERE class_id IN (SELECT id FROM class WHERE class_name LIKE '%一%');
-- ANY/SOME:任意一个
SELECT * FROM student
WHERE age > ANY (SELECT age FROM student WHERE gender = '女');
-- ALL:所有
SELECT * FROM student
WHERE age > ALL (SELECT age FROM student WHERE gender = '女');
-- ============ EXISTS子查询 ============
SELECT * FROM student s
WHERE EXISTS (SELECT 1 FROM score sc WHERE sc.student_id = s.id);
-- ============ 子查询作为临时表 ============
SELECT t.name, t.avg_score
FROM (
SELECT s.name, AVG(sc.grade) AS avg_score
FROM student s
JOIN score sc ON s.id = sc.student_id
GROUP BY s.id
) t
WHERE t.avg_score > 80;
分组查询
-- GROUP BY + 聚合函数
-- ============ 聚合函数 ============
SELECT COUNT(*) FROM student; -- 总数
SELECT COUNT(email) FROM student; -- 非NULL数量
SELECT SUM(age) FROM student; -- 求和
SELECT AVG(age) FROM student; -- 平均值
SELECT MAX(age) FROM student; -- 最大值
SELECT MIN(age) FROM student; -- 最小值
-- ============ 分组查询 ============
-- 按性别统计人数
SELECT gender, COUNT(*) AS '人数'
FROM student
GROUP BY gender;
-- 按班级统计平均成绩
SELECT class_id, AVG(grade) AS '平均分'
FROM score
GROUP BY class_id;
-- ============ HAVING(分组后的条件过滤) ============
-- 查询平均分大于80的班级
SELECT class_id, AVG(grade) AS avg_grade
FROM score
GROUP BY class_id
HAVING avg_grade > 80;
-- WHERE vs HAVING:
-- WHERE:分组前过滤,不能使用聚合函数
-- HAVING:分组后过滤,可以使用聚合函数
SQL执行顺序
-- 书写顺序:
SELECT → FROM → JOIN → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT
-- 执行顺序:
FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
-- 理解执行顺序很重要!
-- 1. FROM/JOIN: 确定数据来源(哪些表)
-- 2. WHERE: 过滤行
-- 3. GROUP BY: 分组
-- 4. HAVING: 过滤分组
-- 5. SELECT: 选择列(此时才能用别名)
-- 6. ORDER BY: 排序(可以用别名)
-- 7. LIMIT: 限制结果数量
5. 数据库函数
-- ============ 字符串函数 ============
SELECT CONCAT('Hello', ' ', 'World'); -- 拼接:Hello World
SELECT LENGTH('Hello'); -- 长度:5
SELECT UPPER('hello'); -- 大写:HELLO
SELECT LOWER('HELLO'); -- 小写:hello
SELECT SUBSTRING('Hello World', 7, 5); -- 截取:World(从第7个开始取5个)
SELECT TRIM(' Hello '); -- 去空格:Hello
SELECT REPLACE('Hello', 'l', 'L'); -- 替换:HeLLo
SELECT REVERSE('Hello'); -- 反转:olleH
SELECT LPAD('5', 3, '0'); -- 左填充:005
SELECT RPAD('5', 3, '0'); -- 右填充:500
-- ============ 数值函数 ============
SELECT ABS(-10); -- 绝对值:10
SELECT CEIL(3.2); -- 向上取整:4
SELECT FLOOR(3.8); -- 向下取整:3
SELECT ROUND(3.1415, 2); -- 四舍五入:3.14
SELECT MOD(10, 3); -- 取余:1
SELECT RAND(); -- 随机数:0~1之间
-- ============ 日期函数 ============
SELECT NOW(); -- 当前日期时间:2024-01-15 10:30:00
SELECT CURDATE(); -- 当前日期:2024-01-15
SELECT CURTIME(); -- 当前时间:10:30:00
SELECT YEAR(NOW()); -- 年份
SELECT MONTH(NOW()); -- 月份
SELECT DAY(NOW()); -- 日
SELECT DATEDIFF('2024-12-31', '2024-01-01'); -- 日期差:365
SELECT DATE_ADD(NOW(), INTERVAL 30 DAY); -- 加30天
SELECT DATE_FORMAT(NOW(), '%Y年%m月%d日'); -- 格式化
-- ============ 流程控制函数 ============
SELECT IF(score >= 60, '及格', '不及格');
SELECT IFNULL(email, '未设置'); -- 如果email为NULL,返回'未设置'
SELECT CASE
WHEN score >= 90 THEN '优秀'
WHEN score >= 80 THEN '良好'
WHEN score >= 60 THEN '及格'
ELSE '不及格'
END AS '等级'
FROM score;
6. 数据库事务
事务概念
-- 事务(Transaction):一组操作要么全部成功,要么全部失败
-- 经典案例:银行转账
-- A给B转100元:
-- 1. A账户 -100
-- 2. B账户 +100
-- 如果第1步成功,第2步失败?→ A少了100,B没收到 → 数据不一致!
-- 事务保证这两步要么都成功,要么都回滚
ACID特性
A - Atomicity(原子性)
事务中的操作要么全部执行,要么全部不执行
C - Consistency(一致性)
事务执行前后,数据保持一致(如转账前后总金额不变)
I - Isolation(隔离性)
多个事务并发执行时,互不干扰
D - Durability(持久性)
事务提交后,数据永久保存,即使系统故障也不丢失
事务操作
-- ============ 手动控制事务 ============
-- 开始事务
START TRANSACTION; -- 或 BEGIN;
-- 执行操作
UPDATE account SET balance = balance - 100 WHERE name = 'A';
UPDATE account SET balance = balance + 100 WHERE name = 'B';
-- 提交事务(所有操作生效)
COMMIT;
-- 或 回滚事务(所有操作撤销)
ROLLBACK;
-- ============ 自动提交 ============
-- MySQL默认自动提交(每条SQL都是一个事务)
-- 查看自动提交状态
SELECT @@autocommit; -- 1=开启,0=关闭
-- 关闭自动提交
SET autocommit = 0;
-- ============ 保存点 ============
START TRANSACTION;
INSERT INTO student VALUES (1, '张三', 20);
SAVEPOINT sp1; -- 设置保存点
INSERT INTO student VALUES (2, '李四', 22);
ROLLBACK TO sp1; -- 回滚到保存点(只撤销李四的插入)
COMMIT;
事务隔离级别
-- 并发问题:
-- 1. 脏读:读到其他事务未提交的数据
-- 2. 不可重复读:同一事务中两次读取结果不同(其他事务修改了数据)
-- 3. 幻读:同一事务中两次查询结果条数不同(其他事务插入了数据)
-- 隔离级别(从低到高):
-- READ UNCOMMITTED(读未提交):都不能防止
-- READ COMMITTED(读已提交):防止脏读
-- REPEATABLE READ(可重复读):防止脏读和不可重复读(MySQL默认)
-- SERIALIZABLE(序列化):全部防止(性能最差)
-- 查看隔离级别
SELECT @@transaction_isolation;
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
7. 数据库视图
-- 视图(View):虚拟的表,是SQL查询结果的命名引用
-- 不存储数据,每次使用时都执行底层SQL
-- 作用:简化复杂查询、控制数据访问权限
-- ============ 创建视图 ============
CREATE VIEW v_student_score AS
SELECT s.name, s.age, c.class_name, sc.grade
FROM student s
JOIN class c ON s.class_id = c.id
JOIN score sc ON s.id = sc.student_id;
-- ============ 使用视图(像表一样查询) ============
SELECT * FROM v_student_score WHERE grade > 80;
-- ============ 修改视图 ============
CREATE OR REPLACE VIEW v_student_score AS
SELECT s.name, AVG(sc.grade) AS avg_grade
FROM student s
JOIN score sc ON s.id = sc.student_id
GROUP BY s.id;
-- ============ 删除视图 ============
DROP VIEW v_student_score;
DROP VIEW IF EXISTS v_student_score;
-- ============ 查看视图 ============
SHOW CREATE VIEW v_student_score;
8. 数据库索引
-- 索引(Index):类似于书的目录,加速查询
-- 原理:通常使用B+树数据结构
-- 优点:大幅提高查询速度
-- 缺点:占用存储空间,降低增删改的速度
-- ============ 创建索引 ============
-- 普通索引
CREATE INDEX idx_name ON student(name);
-- 唯一索引
CREATE UNIQUE INDEX idx_email ON student(email);
-- 复合索引(多列索引)
CREATE INDEX idx_name_age ON student(name, age);
-- 建表时创建索引
CREATE TABLE student (
id INT PRIMARY KEY, -- 主键自动创建索引
name VARCHAR(20),
email VARCHAR(50),
INDEX idx_name(name),
UNIQUE INDEX idx_email(email)
);
-- ============ 查看索引 ============
SHOW INDEX FROM student;
-- ============ 删除索引 ============
DROP INDEX idx_name ON student;
-- ============ 哪些列适合建索引 ============
-- ✅ 主键(自动创建)
-- ✅ 外键
-- ✅ WHERE条件频繁使用的列
-- ✅ ORDER BY、GROUP BY使用的列
-- ✅ 数据量大且区分度高的列
-- ❌ 频繁更新的列
-- ❌ 数据量小的表
-- ❌ 大量重复值的列(如性别)
-- ============ EXPLAIN 分析查询计划 ============
EXPLAIN SELECT * FROM student WHERE name = '张三';
-- type列:ALL(全表扫描) > index > range > ref > eq_ref > const
-- key列:实际使用的索引
-- rows列:预估扫描行数
9. 数据库备份恢复
-- ============ 命令行备份 ============
-- 备份整个数据库
mysqldump -u root -p school > school_backup.sql
-- 备份指定表
mysqldump -u root -p school student score > tables_backup.sql
-- 备份所有数据库
mysqldump -u root -p --all-databases > all_backup.sql
-- ============ 命令行恢复 ============
-- 方式1:命令行
mysql -u root -p school < school_backup.sql
-- 方式2:登录MySQL后
mysql> USE school;
mysql> SOURCE /path/to/school_backup.sql;
-- ============ SQLyog中操作 ============
-- 备份:右键数据库 → 备份/导出 → 导出为SQL
-- 恢复:右键数据库 → 导入 → 执行SQL文件
第八阶段:Java集合框架
1. List(ArrayList、LinkedList)
集合框架概览
Java集合框架
├── Collection(单列集合)
│ ├── List(有序、可重复)
│ │ ├── ArrayList(数组实现)
│ │ ├── LinkedList(链表实现)
│ │ └── Vector(线程安全,已过时)
│ └── Set(无序、不可重复)
│ ├── HashSet(哈希表实现)
│ ├── LinkedHashSet(有序HashSet)
│ └── TreeSet(红黑树,有序)
│
└── Map(双列集合,键值对)
├── HashMap(哈希表实现)
├── LinkedHashMap(有序HashMap)
├── TreeMap(红黑树,有序)
└── Hashtable(线程安全,已过时)
ArrayList
// ArrayList基于动态数组实现
// 特点:查询快(通过索引O(1)),增删慢(需要移动元素O(n))
import java.util.ArrayList;
// ============ 创建 ============
ArrayList<String> list = new ArrayList<>(); // 泛型指定元素类型
// ============ 增 ============
list.add("张三"); // 添加到末尾
list.add("李四");
list.add("王五");
list.add(1, "赵六"); // 插入到索引1的位置
// ============ 删 ============
list.remove("李四"); // 按元素删除(删除第一个匹配的)
list.remove(0); // 按索引删除
list.clear(); // 清空
// ============ 改 ============
list.set(0, "新张三"); // 修改索引0的元素
// ============ 查 ============
String name = list.get(0); // 获取索引0的元素
int size = list.size(); // 获取长度
boolean has = list.contains("张三"); // 是否包含
int idx = list.indexOf("张三"); // 查找索引(-1表示不存在)
boolean empty = list.isEmpty(); // 是否为空
// ============ 遍历 ============
// 方式1:普通for
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 方式2:增强for
for (String s : list) {
System.out.println(s);
}
// 方式3:迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
// 方式4:forEach(Java 8+)
list.forEach(s -> System.out.println(s));
ArrayList底层原理:
初始容量:10
扩容机制:当数组满时,创建新数组(原容量的1.5倍),将旧数组复制过去
添加元素:
[张三][李四][王五][ ][ ][ ][ ][ ][ ][ ] ← 初始容量10
↑ 新元素添加到这里
插入元素到索引1:
[张三][赵六][李四][王五][ ][ ][ ][ ][ ][ ]
↑ 李四和王五需要向后移动 → 所以插入慢
LinkedList
// LinkedList基于双向链表实现
// 特点:增删快(O(1)),查询慢(需要遍历O(n))
import java.util.LinkedList;
LinkedList<String> list = new LinkedList<>();
// 基本操作和ArrayList相同(因为都实现了List接口)
list.add("A");
list.add("B");
list.get(0);
list.remove(0);
// LinkedList特有方法(作为双端队列/栈使用)
list.addFirst("First"); // 添加到头部
list.addLast("Last"); // 添加到尾部
list.getFirst(); // 获取第一个
list.getLast(); // 获取最后一个
list.removeFirst(); // 删除第一个
list.removeLast(); // 删除最后一个
list.push("Push"); // 压栈(添加到头部)
list.pop(); // 弹栈(删除并返回第一个)
list.peek(); // 查看栈顶(不删除)
LinkedList底层原理:
双向链表:每个节点包含 [前驱指针|数据|后继指针]
null ← [A] ⇄ [B] ⇄ [C] ⇄ [D] → null
↑ ↑
first last
插入/删除:只需要修改前后节点的指针,不需要移动其他元素
查询:需要从head或tail开始遍历
ArrayList vs LinkedList:
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层 | 动态数组 | 双向链表 |
| 查询 | 快(O(1)) | 慢(O(n)) |
| 增删 | 慢(O(n)) | 快(O(1)) |
| 内存 | 连续空间 | 不连续,每个节点有额外指针 |
| 适用场景 | 频繁查询 | 频繁增删 |
2. Set
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.TreeSet;
// ============ HashSet ============
// 无序、不可重复、允许null
// 底层:哈希表(HashMap)
HashSet<String> set = new HashSet<>();
set.add("张三");
set.add("李四");
set.add("王五");
set.add("张三"); // 重复元素不会被添加
set.add(null); // 可以添加null
System.out.println(set); // [null, 李四, 张三, 王五](顺序不确定)
System.out.println(set.size()); // 4
System.out.println(set.contains("张三")); // true
set.remove("李四");
// 遍历
for (String s : set) {
System.out.println(s);
}
// ============ LinkedHashSet ============
// 有序(插入顺序)、不可重复
// 底层:哈希表 + 链表
LinkedHashSet<String> linkedSet = new LinkedHashSet<>();
linkedSet.add("C");
linkedSet.add("A");
linkedSet.add("B");
System.out.println(linkedSet); // [C, A, B](保持插入顺序)
// ============ TreeSet ============
// 有序(自然排序或自定义排序)、不可重复
// 底层:红黑树
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(30);
treeSet.add(10);
treeSet.add(20);
System.out.println(treeSet); // [10, 20, 30](自动排序)
// 自定义对象需要实现Comparable接口或传入Comparator
TreeSet<Student> stuSet = new TreeSet<>((s1, s2) -> s1.getAge() - s2.getAge());
HashSet去重原理:
1. 计算对象的hashCode
2. 根据hashCode确定存储位置
3. 如果位置为空,直接存入
4. 如果位置不为空,调用equals比较
5. equals为true → 重复,不存入
6. equals为false → 不重复,链表/红黑树存入
所以自定义对象要放入HashSet,必须重写hashCode()和equals()方法!
3. Map
import java.util.HashMap;
import java.util.Map;
// Map存储键值对(key-value),key不重复,value可以重复
// ============ 基本操作 ============
Map<String, Integer> map = new HashMap<>();
// 添加/修改
map.put("张三", 85);
map.put("李四", 92);
map.put("王五", 78);
map.put("张三", 90); // key重复,覆盖旧值
// 获取
int score = map.get("张三"); // 90
int score2 = map.getOrDefault("赵六", 0); // key不存在返回默认值0
// 删除
map.remove("王五");
// 判断
map.containsKey("张三"); // true
map.containsValue(90); // true
map.isEmpty(); // false
map.size(); // 2
// ============ 遍历 ============
// 方式1:keySet遍历
for (String key : map.keySet()) {
System.out.println(key + " = " + map.get(key));
}
// 方式2:entrySet遍历(推荐,效率更高)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
// 方式3:forEach(Java 8+)
map.forEach((key, value) -> System.out.println(key + " = " + value));
// 方式4:values遍历(只需要值)
for (Integer value : map.values()) {
System.out.println(value);
}
HashMap底层原理:
JDK 8:数组 + 链表 + 红黑树
1. 计算key的hashCode
2. 通过hash值确定数组位置
3. 如果该位置为空,直接存入
4. 如果不为空(哈希冲突),用链表连接
5. 当链表长度 > 8 且数组长度 >= 64 时,链表转为红黑树
6. 数组默认大小16,负载因子0.75,超过12个元素就扩容(2倍)
4. Collections工具类
import java.util.Collections;
import java.util.ArrayList;
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5, 9));
// ============ 排序 ============
Collections.sort(list); // 升序排序
Collections.sort(list, (a, b) -> b - a); // 降序排序
Collections.reverse(list); // 反转
// ============ 查找 ============
int max = Collections.max(list); // 最大值
int min = Collections.min(list); // 最小值
int freq = Collections.frequency(list, 1); // 元素出现次数
// ============ 修改 ============
Collections.shuffle(list); // 随机打乱
Collections.fill(list, 0); // 全部填充为0
Collections.swap(list, 0, 1); // 交换两个位置的元素
Collections.replaceAll(list, 1, 10); // 将所有1替换为10
// ============ 其他 ============
Collections.unmodifiableList(list); // 创建不可修改的列表
Collections.synchronizedList(list); // 创建线程安全的列表
Collections.singletonList("only"); // 创建只有一个元素的列表
Collections.emptyList(); // 创建空列表
5. Iterator和增强型for循环
// ============ Iterator(迭代器) ============
// 所有Collection都可以使用Iterator遍历
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
Iterator<String> it = list.iterator();
while (it.hasNext()) { // 是否有下一个元素
String element = it.next(); // 获取下一个元素
System.out.println(element);
if (element.equals("B")) {
it.remove(); // 安全删除当前元素(Iterator的remove方法)
}
}
// 注意:遍历时不能用list.remove()删除元素(会抛ConcurrentModificationException)
// 必须用it.remove()
// ============ 增强型for循环(for-each) ============
// 简化了Iterator的写法
// 适用于:数组、实现了Iterable接口的集合
// 遍历数组
int[] arr = {1, 2, 3, 4, 5};
for (int num : arr) {
System.out.println(num);
}
// 遍历集合
List<String> names = Arrays.asList("张三", "李四", "王五");
for (String name : names) {
System.out.println(name);
}
// 遍历Map
Map<String, Integer> map = new HashMap<>();
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 注意:增强for循环中不能修改集合结构(不能添加/删除元素)
// 如果需要删除,使用Iterator
6. 泛型
// 泛型(Generics):参数化类型,在编译时检查类型安全
// ============ 没有泛型的问题 ============
ArrayList list = new ArrayList(); // 没有泛型
list.add("Hello");
list.add(100); // 可以添加任何类型
list.add(new Dog()); // 不安全!
String s = (String) list.get(1); // 运行时ClassCastException!
// ============ 使用泛型 ============
ArrayList<String> list = new ArrayList<>(); // 限定只能存String
list.add("Hello");
// list.add(100); // 编译错误!类型安全
String s = list.get(0); // 不需要强制转换
// ============ 泛型类 ============
public class Box<T> { // T是类型参数
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
String s = stringBox.getContent(); // 不需要强转
Box<Integer> intBox = new Box<>();
intBox.setContent(100);
int n = intBox.getContent();
// ============ 泛型方法 ============
public <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
// ============ 泛型接口 ============
public interface Comparable<T> {
int compareTo(T other);
}
public class Student implements Comparable<Student> {
@Override
public int compareTo(Student other) {
return this.age - other.age;
}
}
// ============ 通配符 ============
// ? 表示未知类型
public void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
// 上界通配符:只能读取,不能添加
public void processNumbers(List<? extends Number> list) {
// 可以读取为Number类型
Number n = list.get(0);
// list.add(1); // 编译错误!
}
// 下界通配符:可以添加,读取只能用Object
public void addIntegers(List<? super Integer> list) {
list.add(1); // 可以添加Integer
list.add(2);
Object obj = list.get(0); // 只能用Object接收
}
泛型擦除:
Java泛型是"伪泛型",只在编译时检查类型
编译后泛型信息被擦除,运行时不存在泛型
ArrayList<String> 和 ArrayList<Integer> 在运行时都是 ArrayList
第九阶段:JDBC
1. JDBC基础
JDBC概念
JDBC(Java Database Connectivity):Java数据库连接
- Java程序访问数据库的标准接口
- Sun公司定义了一套接口规范(java.sql包)
- 各数据库厂商提供具体的实现(驱动jar包)
程序 ←→ JDBC接口 ←→ MySQL驱动 ←→ MySQL数据库
←→ Oracle驱动 ←→ Oracle数据库
JDBC核心类
DriverManager → 管理数据库驱动,获取数据库连接
Connection → 数据库连接对象
Statement → 执行SQL语句(有SQL注入风险)
PreparedStatement → 预编译SQL语句(推荐,防SQL注入)
ResultSet → 查询结果集
JDBC基本步骤
import java.sql.*;
public class JDBCDemo {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// 1. 注册驱动(MySQL 8.0+)
Class.forName("com.mysql.cj.jdbc.Driver");
// MySQL 5.x: com.mysql.jdbc.Driver
// 2. 获取连接
String url = "jdbc:mysql://localhost:3306/school?useSSL=false&serverTimezone=UTC&characterEncoding=utf8";
String username = "root";
String password = "123456";
conn = DriverManager.getConnection(url, username, password);
// 3. 创建PreparedStatement(预编译SQL)
String sql = "SELECT * FROM student WHERE age > ?";
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, 20); // 设置第1个参数为20
// 4. 执行查询
rs = pstmt.executeQuery();
// 5. 处理结果集
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
int age = rs.getInt("age");
System.out.println(id + " " + name + " " + age);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 关闭资源(后开先关)
try {
if (rs != null) rs.close();
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
2. JDBC实现建表操作
public static void createTable() {
Connection conn = null;
Statement stmt = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/school", "root", "123456");
stmt = conn.createStatement();
String sql = "CREATE TABLE IF NOT EXISTS employee (" +
"id INT PRIMARY KEY AUTO_INCREMENT, " +
"name VARCHAR(20) NOT NULL, " +
"age INT, " +
"salary DECIMAL(10,2), " +
"dept VARCHAR(20)" +
")";
stmt.executeUpdate(sql); // executeUpdate用于DDL和DML
System.out.println("表创建成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源...
}
}
3. JDBC实现增删改查(批量、分页)
// ============ 增:INSERT ============
public static void insert() throws Exception {
Connection conn = getConnection();
String sql = "INSERT INTO employee (name, age, salary, dept) VALUES (?, ?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "张三");
pstmt.setInt(2, 25);
pstmt.setDouble(3, 8000.00);
pstmt.setString(4, "技术部");
int rows = pstmt.executeUpdate(); // 返回受影响的行数
System.out.println("插入了" + rows + "行");
close(conn, pstmt, null);
}
// ============ 批量增:Batch Insert ============
public static void batchInsert() throws Exception {
Connection conn = getConnection();
conn.setAutoCommit(false); // 关闭自动提交,提高性能
String sql = "INSERT INTO employee (name, age, salary, dept) VALUES (?, ?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
for (int i = 0; i < 1000; i++) {
pstmt.setString(1, "员工" + i);
pstmt.setInt(2, 20 + i % 30);
pstmt.setDouble(3, 5000 + Math.random() * 10000);
pstmt.setString(4, "部门" + (i % 5));
pstmt.addBatch(); // 添加到批处理
if (i % 100 == 0) {
pstmt.executeBatch(); // 每100条执行一次
pstmt.clearBatch();
}
}
pstmt.executeBatch(); // 执行剩余的
conn.commit(); // 提交事务
close(conn, pstmt, null);
}
// ============ 删:DELETE ============
public static void delete(int id) throws Exception {
Connection conn = getConnection();
String sql = "DELETE FROM employee WHERE id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, id);
int rows = pstmt.executeUpdate();
System.out.println("删除了" + rows + "行");
close(conn, pstmt, null);
}
// ============ 批量删 ============
public static void batchDelete(int[] ids) throws Exception {
Connection conn = getConnection();
String sql = "DELETE FROM employee WHERE id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
for (int id : ids) {
pstmt.setInt(1, id);
pstmt.addBatch();
}
pstmt.executeBatch();
close(conn, pstmt, null);
}
// ============ 改:UPDATE ============
public static void update(int id, double newSalary) throws Exception {
Connection conn = getConnection();
String sql = "UPDATE employee SET salary = ? WHERE id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setDouble(1, newSalary);
pstmt.setInt(2, id);
int rows = pstmt.executeUpdate();
System.out.println("更新了" + rows + "行");
close(conn, pstmt, null);
}
// ============ 查:SELECT ============
public static void query() throws Exception {
Connection conn = getConnection();
String sql = "SELECT * FROM employee WHERE dept = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "技术部");
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
System.out.println(
rs.getInt("id") + " | " +
rs.getString("name") + " | " +
rs.getInt("age") + " | " +
rs.getDouble("salary") + " | " +
rs.getString("dept")
);
}
close(conn, pstmt, rs);
}
// ============ 分页查询 ============
public static void queryByPage(int pageNum, int pageSize) throws Exception {
Connection conn = getConnection();
String sql = "SELECT * FROM employee LIMIT ?, ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, (pageNum - 1) * pageSize); // offset
pstmt.setInt(2, pageSize); // count
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
// 处理数据...
}
// 查询总数
String countSql = "SELECT COUNT(*) FROM employee";
PreparedStatement countPstmt = conn.prepareStatement(countSql);
ResultSet countRs = countPstmt.executeQuery();
if (countRs.next()) {
int total = countRs.getInt(1);
int totalPages = (int) Math.ceil((double) total / pageSize);
System.out.println("总共" + total + "条," + totalPages + "页");
}
close(conn, pstmt, rs);
}
详细剖析:JDBC对事务支持 及后续所有课程内容
一、事务处理:JDBC对事务支持
1.1 事务的基本概念
事务(Transaction)是数据库操作的最小工作单元,是一组不可分割的操作序列。
事务的四大特性(ACID)
A - Atomicity(原子性):事务中的所有操作要么全部成功,要么全部失败回滚
C - Consistency(一致性):事务执行前后,数据库从一个一致性状态变到另一个一致性状态
I - Isolation(隔离性):多个事务并发执行时,彼此互不干扰
D - Durability(持久性):事务一旦提交,对数据库的改变是永久的
经典案例:银行转账
张三 → 李四 转账 1000元
操作1:张三账户 -1000
操作2:李四账户 +1000
如果操作1成功,操作2失败 → 钱凭空消失了!
所以必须保证:要么两个操作都成功,要么都失败
1.2 JDBC默认的事务行为
// 默认情况下,JDBC是自动提交模式
// 每执行一条SQL语句,就自动提交一次事务
Connection conn = DriverManager.getConnection(url, user, password);
// conn.getAutoCommit() == true (默认为true,自动提交)
Statement stmt = conn.createStatement();
stmt.executeUpdate("UPDATE account SET balance = balance - 1000 WHERE name = '张三'");
// ↑ 这条SQL执行完毕后,立即自动提交,无法回滚
stmt.executeUpdate("UPDATE account SET balance = balance + 1000 WHERE name = '李四'");
// ↑ 如果这条SQL执行失败,上面的操作已经提交了,无法撤销!
1.3 JDBC手动控制事务
核心API
// Connection 接口中的事务相关方法:
conn.setAutoCommit(false); // 关闭自动提交,开启手动事务
conn.commit(); // 手动提交事务
conn.rollback(); // 回滚事务
conn.setSavepoint(); // 设置保存点
conn.rollback(savepoint); // 回滚到保存点
conn.setTransactionIsolation(level); // 设置事务隔离级别
完整的事务控制代码
import java.sql.*;
public class TransactionDemo {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt1 = null;
PreparedStatement pstmt2 = null;
try {
// 1. 获取连接
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/bank_db?useSSL=false&serverTimezone=UTC",
"root",
"123456"
);
// ★★★ 2. 关闭自动提交(开启事务)★★★
conn.setAutoCommit(false);
// 3. 执行业务操作
// 操作1:张三扣款
String sql1 = "UPDATE account SET balance = balance - ? WHERE name = ?";
pstmt1 = conn.prepareStatement(sql1);
pstmt1.setDouble(1, 1000);
pstmt1.setString(2, "张三");
int rows1 = pstmt1.executeUpdate();
// 模拟异常(除以零异常)
// int x = 1 / 0; // 取消注释可测试回滚
// 操作2:李四收款
String sql2 = "UPDATE account SET balance = balance + ? WHERE name = ?";
pstmt2 = conn.prepareStatement(sql2);
pstmt2.setDouble(1, 1000);
pstmt2.setString(2, "李四");
int rows2 = pstmt2.executeUpdate();
// 4. 验证业务逻辑
if (rows1 > 0 && rows2 > 0) {
// ★★★ 5. 两个操作都成功,提交事务 ★★★
conn.commit();
System.out.println("转账成功!");
} else {
// 有操作未影响到数据,回滚
conn.rollback();
System.out.println("转账失败,已回滚!");
}
} catch (Exception e) {
// ★★★ 6. 出现异常,回滚事务 ★★★
try {
if (conn != null) {
conn.rollback();
System.out.println("发生异常,事务已回滚!");
}
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} finally {
// 7. 释放资源
try {
if (pstmt1 != null) pstmt1.close();
if (pstmt2 != null) pstmt2.close();
// 恢复自动提交(如果连接要归还连接池)
if (conn != null) {
conn.setAutoCommit(true);
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
1.4 事务控制流程图
开始
│
▼
conn.setAutoCommit(false) ← 关闭自动提交
│
▼
┌─────────────────┐
│ 执行SQL操作1 │
│ 执行SQL操作2 │
│ 执行SQL操作3 │
│ ... │
└────────┬────────┘
│
是否有异常?
┌────┴────┐
│ │
无异常 有异常
│ │
▼ ▼
conn.commit() conn.rollback()
│ │
▼ ▼
提交成功 回滚成功
│ │
└────┬────┘
│
▼
释放资源(finally)
1.5 Savepoint(保存点)
保存点允许你回滚到事务中的某个特定点,而不是回滚整个事务。
Connection conn = null;
Savepoint sp = null;
try {
conn = getConnection();
conn.setAutoCommit(false);
// 操作1:插入订单
String sql1 = "INSERT INTO orders(order_no, amount) VALUES(?, ?)";
PreparedStatement pstmt1 = conn.prepareStatement(sql1);
pstmt1.setString(1, "ORD001");
pstmt1.setDouble(2, 5000);
pstmt1.executeUpdate();
// ★★★ 设置保存点 ★★★
sp = conn.setSavepoint("afterOrder");
// 操作2:插入订单详情(可能失败)
try {
String sql2 = "INSERT INTO order_detail(order_no, product, qty) VALUES(?, ?, ?)";
PreparedStatement pstmt2 = conn.prepareStatement(sql2);
pstmt2.setString(1, "ORD001");
pstmt2.setString(2, "iPhone");
pstmt2.setInt(3, -1); // 假设数量不能为负数,触发异常
pstmt2.executeUpdate();
} catch (Exception e) {
// ★★★ 回滚到保存点(只撤销操作2,保留操作1)★★★
conn.rollback(sp);
System.out.println("订单详情插入失败,已回滚到保存点");
// 可以在这里执行替代操作
String sql3 = "INSERT INTO order_detail(order_no, product, qty) VALUES(?, ?, ?)";
PreparedStatement pstmt3 = conn.prepareStatement(sql3);
pstmt3.setString(1, "ORD001");
pstmt3.setString(2, "iPhone");
pstmt3.setInt(3, 1); // 使用默认数量
pstmt3.executeUpdate();
}
conn.commit();
} catch (Exception e) {
if (conn != null) conn.rollback();
e.printStackTrace();
}
1.6 事务隔离级别
并发问题
┌──────────────┬───────────────────────────────────────────────────┐
│ 并发问题 │ 说明 │
├──────────────┼───────────────────────────────────────────────────┤
│ 脏读 │ 事务A读到了事务B未提交的数据 │
│ (Dirty Read) │ 如果事务B回滚,事务A读到的就是无效数据 │
├──────────────┼───────────────────────────────────────────────────┤
│ 不可重复读 │ 事务A两次读取同一数据,期间事务B修改并提交了该数据 │
│ (Non- │ 导致事务A两次读取结果不一致 │
│ Repeatable) │ │
├──────────────┼───────────────────────────────────────────────────┤
│ 幻读 │ 事务A按条件查询,期间事务B插入了符合条件的新数据 │
│ (Phantom) │ 导致事务A再次查询时多出了"幻影"记录 │
└──────────────┴───────────────────────────────────────────────────┘
四种隔离级别
// JDBC中设置隔离级别
conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); // 1
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // 2
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); // 4(MySQL默认)
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); // 8
┌─────────────────────────┬────────┬──────────────┬────────┐
│ 隔离级别 │ 脏读 │ 不可重复读 │ 幻读 │
├─────────────────────────┼────────┼──────────────┼────────┤
│ READ_UNCOMMITTED(读未提交)│ ✓ │ ✓ │ ✓ │
│ READ_COMMITTED(读已提交) │ ✗ │ ✓ │ ✓ │
│ REPEATABLE_READ(可重复读) │ ✗ │ ✗ │ ✓ │
│ SERIALIZABLE(串行化) │ ✗ │ ✗ │ ✗ │
└─────────────────────────┴────────┴──────────────┴────────┘
✓ = 可能发生 ✗ = 不会发生
隔离级别越高 → 数据越安全 → 性能越差
隔离级别越低 → 数据越不安全 → 性能越好
1.7 实际开发中的事务模板(最佳实践)
/**
* JDBC事务操作模板方法
*/
public boolean transferMoney(String fromUser, String toUser, double amount) {
Connection conn = null;
try {
conn = DBUtil.getConnection();
// 1. 开启事务
conn.setAutoCommit(false);
// 2. 业务操作(可能是多个DAO操作)
AccountDao accountDao = new AccountDao();
// 检查余额
double balance = accountDao.getBalance(conn, fromUser);
if (balance < amount) {
throw new RuntimeException("余额不足!");
}
// 扣款
accountDao.decreaseBalance(conn, fromUser, amount);
// 加款
accountDao.increaseBalance(conn, toUser, amount);
// 记录流水
TransactionLogDao logDao = new TransactionLogDao();
logDao.addLog(conn, fromUser, toUser, amount);
// 3. 提交事务
conn.commit();
return true;
} catch (Exception e) {
// 4. 回滚事务
DBUtil.rollback(conn);
e.printStackTrace();
return false;
} finally {
// 5. 恢复并关闭
DBUtil.close(conn);
}
}
二、封装BaseDao
2.1 为什么需要BaseDao
在实际开发中,每个DAO类都需要:获取连接 → 创建Statement → 执行SQL → 处理结果集 → 关闭资源。大量重复代码!
没有BaseDao时:
UserDao:
getConnection() → prepareStatement() → executeQuery() → close()
getConnection() → prepareStatement() → executeUpdate() → close()
ProductDao:
getConnection() → prepareStatement() → executeQuery() → close()
getConnection() → prepareStatement() → executeUpdate() → close()
OrderDao:
getConnection() → prepareStatement() → executeQuery() → close()
getConnection() → prepareStatement() → executeUpdate() → close()
↑ 大量重复代码!
2.2 BaseDao完整实现
import java.sql.*;
import java.util.*;
/**
* 数据访问层基类
* 封装了通用的JDBC操作
*/
public class BaseDao {
// 数据库连接参数
private static final String DRIVER = "com.mysql.cj.jdbc.Driver";
private static final String URL = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8";
private static final String USER = "root";
private static final String PASSWORD = "123456";
// 静态代码块,加载驱动(只执行一次)
static {
try {
Class.forName(DRIVER);
} catch (ClassNotFoundException e) {
throw new RuntimeException("数据库驱动加载失败!", e);
}
}
/**
* 获取数据库连接
*/
protected Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
/**
* 通用的增删改方法
* @param sql SQL语句(带?占位符)
* @param params 参数数组
* @return 受影响的行数
*/
protected int executeUpdate(String sql, Object... params) {
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
// 设置参数
if (params != null) {
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
}
return pstmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("执行更新操作失败:" + sql, e);
} finally {
close(null, pstmt, conn);
}
}
/**
* 带事务的通用增删改方法
* @param conn 外部传入的连接(事务控制用)
* @param sql SQL语句
* @param params 参数
* @return 受影响的行数
*/
protected int executeUpdate(Connection conn, String sql, Object... params) {
PreparedStatement pstmt = null;
try {
pstmt = conn.prepareStatement(sql);
if (params != null) {
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
}
return pstmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("执行更新操作失败:" + sql, e);
} finally {
close(null, pstmt, null); // 注意:不关闭conn,由外部控制
}
}
/**
* 批量执行增删改
* @param sql SQL语句
* @param paramList 参数列表(每个Object[]对应一条SQL的参数)
* @return 每条SQL受影响的行数数组
*/
protected int[] executeBatch(String sql, List<Object[]> paramList) {
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = getConnection();
conn.setAutoCommit(false); // 批量操作使用事务
pstmt = conn.prepareStatement(sql);
for (Object[] params : paramList) {
if (params != null) {
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
}
pstmt.addBatch();
}
int[] results = pstmt.executeBatch();
conn.commit();
return results;
} catch (SQLException e) {
rollback(conn);
throw new RuntimeException("批量操作失败:" + sql, e);
} finally {
close(null, pstmt, conn);
}
}
/**
* 查询返回单个值(如:count、max、sum等)
*/
protected Object queryScalar(String sql, Object... params) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
if (params != null) {
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
}
rs = pstmt.executeQuery();
if (rs.next()) {
return rs.getObject(1);
}
return null;
} catch (SQLException e) {
throw new RuntimeException("查询标量值失败:" + sql, e);
} finally {
close(rs, pstmt, conn);
}
}
/**
* 查询返回List<Map<String, Object>>
* 每个Map代表一行数据,key是列名,value是列值
*/
protected List<Map<String, Object>> queryForList(String sql, Object... params) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
if (params != null) {
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
}
rs = pstmt.executeQuery();
// 获取结果集元数据(列信息)
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
List<Map<String, Object>> list = new ArrayList<>();
while (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnLabel(i); // 获取列别名
Object value = rs.getObject(i);
row.put(columnName, value);
}
list.add(row);
}
return list;
} catch (SQLException e) {
throw new RuntimeException("查询列表失败:" + sql, e);
} finally {
close(rs, pstmt, conn);
}
}
/**
* 查询返回单个Map(查询一条记录)
*/
protected Map<String, Object> queryForMap(String sql, Object... params) {
List<Map<String, Object>> list = queryForList(sql, params);
return list.isEmpty() ? null : list.get(0);
}
/**
* 分页查询
* @param sql 原始SQL(不含LIMIT)
* @param pageNum 当前页码(从1开始)
* @param pageSize 每页条数
* @param params 查询参数
* @return 分页结果
*/
protected Map<String, Object> queryForPage(String sql, int pageNum, int pageSize, Object... params) {
Map<String, Object> result = new HashMap<>();
// 1. 查询总记录数
String countSql = "SELECT COUNT(*) FROM (" + sql + ") temp";
Object countObj = queryScalar(countSql, params);
long totalCount = countObj != null ? ((Number) countObj).longValue() : 0;
// 2. 计算总页数
long totalPages = (totalCount + pageSize - 1) / pageSize;
// 3. 计算偏移量
int offset = (pageNum - 1) * pageSize;
// 4. 拼接分页SQL
String pageSql = sql + " LIMIT " + offset + ", " + pageSize;
List<Map<String, Object>> data = queryForList(pageSql, params);
// 5. 封装结果
result.put("data", data); // 当前页数据
result.put("pageNum", pageNum); // 当前页码
result.put("pageSize", pageSize); // 每页条数
result.put("totalCount", totalCount); // 总记录数
result.put("totalPages", totalPages); // 总页数
return result;
}
/**