第一章 Java基础
1.1 变量与数据类型
1.1.1 基本数据类型
1.1.1.1 整数类型(byte、short、int、long)
在 Java 中,整数类型用于表示没有小数部分的数字,不同的整数类型有不同的取值范围和占用的存储空间:
- byte:
- 占用 1 个字节(8 位)。
- 取值范围是 -128 到 127。
- 常用于节省内存,比如在处理文件或网络数据时。示例代码:
byte myByte = 100;
- short:
- 占用 2 个字节(16 位)。
- 取值范围是 -32768 到 32767。
- 也用于节省内存,不过比 byte 能表示的范围更大。示例代码:
short myShort = 20000;
- int:
- 占用 4 个字节(32 位)。
- 取值范围是 -2147483648 到 2147483647。
- 是最常用的整数类型,一般情况下整数都使用 int 类型。示例代码:
int myInt = 1000000;
- long:
- 占用 8 个字节(64 位)。
- 取值范围非常大,用于表示很大的整数。
- 在赋值时,需要在数字后面加
L
或l
来表示这是一个 long 类型的数值。示例代码:long myLong = 1234567890123L;
1.1.1.2 浮点类型(float、double)
浮点类型用于表示带有小数部分的数字:
- float:
- 占用 4 个字节。
- 单精度浮点型,能表示大约 6 - 7 位有效数字。
- 在赋值时,需要在数字后面加
F
或f
。示例代码:float myFloat = 3.14f;
- double:
- 占用 8 个字节。
- 双精度浮点型,能表示大约 15 位有效数字,精度比 float 高。
- 是 Java 中默认的浮点类型,一般使用 double 来处理浮点数。示例代码:
double myDouble = 3.1415926;
1.1.1.3 字符类型(char)
- char 类型用于表示单个字符。
- 占用 2 个字节(16 位)。
- 可以用单引号括起来表示一个字符,也可以用 Unicode 编码来表示。示例代码:
char myChar = 'A';
char unicodeChar = '\u0041'; // 表示字符 'A'
1.1.1.4 布尔类型(boolean)
- 布尔类型只有两个值:
true
和false
。 - 常用于逻辑判断。示例代码:
boolean isTrue = true;
1.1.2 引用数据类型
1.1.2.1 类(Class)
- 类是 Java 中最基本的引用数据类型,它是对象的模板。
- 可以定义类的属性和方法,通过类可以创建多个对象。示例代码:
class Person {String name;int age;void sayHello() {System.out.println("Hello, my name is " + name + " and I'm " + age + " years old.");}
}
1.1.2.2 接口(Interface)
- 接口是一种特殊的抽象类型,它只包含抽象方法和常量。
- 一个类可以实现多个接口,用于实现多继承的效果。示例代码:
interface Animal {void eat();void sleep();
}class Dog implements Animal {@Overridepublic void eat() {System.out.println("Dog is eating.");}@Overridepublic void sleep() {System.out.println("Dog is sleeping.");}
}
1.1.2.3 数组(Array)
- 数组是一种用于存储相同类型元素的集合。
- 数组的长度在创建时确定,并且不能改变。示例代码:
int[] myArray = new int[5];
myArray[0] = 1;
myArray[1] = 2;
// 也可以直接初始化
int[] anotherArray = {1, 2, 3, 4, 5};
1.1.3 变量声明与初始化
- 变量声明:在 Java 中,声明变量需要指定变量的类型和名称。示例代码:
int number;
- 变量初始化:声明变量后,可以为其赋值。可以在声明时初始化,也可以在声明后再初始化。示例代码:
// 声明时初始化
int num1 = 10;
// 声明后初始化
int num2;
num2 = 20;
1.2 运算符
1.2.1 算术运算符
算术运算符用于进行基本的数学运算:
+
:加法运算符,用于两个数相加。示例:int result = 5 + 3;
-
:减法运算符,用于两个数相减。示例:int result = 5 - 3;
*
:乘法运算符,用于两个数相乘。示例:int result = 5 * 3;
/
:除法运算符,用于两个数相除。如果是整数相除,结果会取整。示例:int result = 5 / 3;
(结果为 1)%
:取模运算符,用于求两个数相除的余数。示例:int result = 5 % 3;
(结果为 2)
1.2.2 赋值运算符
赋值运算符用于给变量赋值:
=
:基本赋值运算符,将右边的值赋给左边的变量。示例:int num = 10;
+=
:加赋值运算符,相当于a = a + b
。示例:int a = 5; a += 3;
(此时 a 的值为 8)-=
:减赋值运算符,相当于a = a - b
。示例:int a = 5; a -= 3;
(此时 a 的值为 2)*=
:乘赋值运算符,相当于a = a * b
。示例:int a = 5; a *= 3;
(此时 a 的值为 15)/=
:除赋值运算符,相当于a = a / b
。示例:int a = 6; a /= 3;
(此时 a 的值为 2)%=
:取模赋值运算符,相当于a = a % b
。示例:int a = 5; a %= 3;
(此时 a 的值为 2)
1.2.3 比较运算符
比较运算符用于比较两个值的大小关系,结果为布尔类型:
==
:等于运算符,判断两个值是否相等。示例:boolean result = 5 == 3;
(结果为 false)!=
:不等于运算符,判断两个值是否不相等。示例:boolean result = 5 != 3;
(结果为 true)>
:大于运算符,判断左边的值是否大于右边的值。示例:boolean result = 5 > 3;
(结果为 true)<
:小于运算符,判断左边的值是否小于右边的值。示例:boolean result = 5 < 3;
(结果为 false)>=
:大于等于运算符,判断左边的值是否大于等于右边的值。示例:boolean result = 5 >= 3;
(结果为 true)<=
:小于等于运算符,判断左边的值是否小于等于右边的值。示例:boolean result = 5 <= 3;
(结果为 false)
1.2.4 逻辑运算符
逻辑运算符用于对布尔值进行逻辑运算:
&&
:逻辑与运算符,只有当两个操作数都为 true 时,结果才为 true。示例:boolean result = (5 > 3) && (2 < 4);
(结果为 true)||
:逻辑或运算符,只要两个操作数中有一个为 true,结果就为 true。示例:boolean result = (5 < 3) || (2 < 4);
(结果为 true)!
:逻辑非运算符,用于取反操作。示例:boolean result = !(5 > 3);
(结果为 false)
1.2.5 位运算符
位运算符用于对二进制位进行操作:
&
:按位与运算符,对应位都为 1 时结果为 1,否则为 0。示例:int result = 5 & 3;
(5 的二进制是 101,3 的二进制是 011,结果为 001,即 1)|
:按位或运算符,对应位只要有一个为 1 结果就为 1。示例:int result = 5 | 3;
(结果为 7,二进制 111)^
:按位异或运算符,对应位不同时结果为 1,相同为 0。示例:int result = 5 ^ 3;
(结果为 6,二进制 110)~
:按位取反运算符,将每一位取反。示例:int result = ~5;
<<
:左移运算符,将二进制位向左移动指定的位数,右边补 0。示例:int result = 5 << 2;
(5 的二进制 101 左移 2 位变为 10100,即 20)>>
:右移运算符,将二进制位向右移动指定的位数,左边补符号位。示例:int result = 5 >> 1;
(结果为 2,二进制 10)>>>
:无符号右移运算符,将二进制位向右移动指定的位数,左边补 0。
1.2.6 三元运算符
三元运算符也叫条件运算符,语法为 条件表达式 ? 表达式1 : 表达式2
。如果条件表达式为 true,则返回表达式 1 的值,否则返回表达式 2 的值。示例:
int a = 5;
int b = 3;
int max = a > b ? a : b; // max 的值为 5
1.3 控制语句
1.3.1 条件语句
1.3.1.1 if - else语句
if - else 语句用于根据条件执行不同的代码块:
int num = 10;
if (num > 5) {System.out.println("num 大于 5");
} else {System.out.println("num 小于等于 5");
}
还可以使用 else if 进行多条件判断:
int score = 80;
if (score >= 90) {System.out.println("优秀");
} else if (score >= 80) {System.out.println("良好");
} else if (score >= 60) {System.out.println("及格");
} else {System.out.println("不及格");
}
1.3.1.2 switch - case语句
switch - case 语句用于根据一个变量的值进行多分支选择:
int day = 3;
switch (day) {case 1:System.out.println("星期一");break;case 2:System.out.println("星期二");break;case 3:System.out.println("星期三");break;default:System.out.println("其他");
}
1.3.2 循环语句
1.3.2.1 for循环
for 循环用于已知循环次数的情况,语法为 for (初始化; 条件判断; 迭代) { 循环体 }
:
for (int i = 0; i < 5; i++) {System.out.println(i);
}
1.3.2.2 while循环
while 循环用于未知循环次数,只要条件为 true 就会一直循环:
int i = 0;
while (i < 5) {System.out.println(i);i++;
}
1.3.2.3 do - while循环
do - while 循环会先执行一次循环体,然后再判断条件,所以至少会执行一次:
int i = 0;
do {System.out.println(i);i++;
} while (i < 5);
1.3.3 跳转语句
1.3.3.1 break语句
break 语句用于跳出当前所在的循环或 switch 语句:
for (int i = 0; i < 10; i++) {if (i == 5) {break;}System.out.println(i);
}
1.3.3.2 continue语句
continue 语句用于跳过当前循环的剩余部分,直接进入下一次循环:
for (int i = 0; i < 10; i++) {if (i == 5) {continue;}System.out.println(i);
}
1.3.3.3 return语句
return 语句用于从方法中返回一个值,并结束当前方法的执行:
public static int add(int a, int b) {return a + b;
}
1.4 面向对象编程基础
1.4.1 类与对象
1.4.1.1 类的定义
类是对象的抽象描述,包含属性和方法。示例:
class Rectangle {double width;double height;double getArea() {return width * height;}
}
1.4.1.2 对象的创建与使用
通过类可以创建对象,使用 new
关键字。示例:
Rectangle rect = new Rectangle();
rect.width = 5;
rect.height = 3;
double area = rect.getArea();
System.out.println("矩形的面积是:" + area);
1.4.2 封装
1.4.2.1 访问修饰符(private、protected、public)
- private:只能在本类中访问。
- protected:可以在本类、同一个包中的类以及不同包中的子类中访问。
- public:可以在任何地方访问。示例:
class Person {private String name; // 私有属性protected int age; // 受保护属性public String address; // 公共属性// 其他方法
}
1.4.2.2 getter和setter方法
getter 和 setter 方法用于访问和修改私有属性:
class Person {private String name;public String getName() {return name;}public void setName(String name) {this.name = name;}
}
1.4.3 继承
1.4.3.1 子类与父类
子类可以继承父类的属性和方法,使用 extends
关键字。示例:
class Animal {void eat() {System.out.println("Animal is eating.");}
}class Dog extends Animal {// 可以继承 Animal 的 eat 方法
}
1.4.3.2 方法重写
子类可以重写父类的方法,提供自己的实现。示例:
class Animal {void eat() {System.out.println("Animal is eating.");}
}class Dog extends Animal {@Overridevoid eat() {System.out.println("Dog is eating.");}
}
1.4.3.3 super关键字
super 关键字用于引用父类的属性、方法或构造方法。示例:
class Animal {String name;Animal(String name) {this.name = name;}
}class Dog extends Animal {Dog(String name) {super(name); // 调用父类的构造方法}
}
1.4.4 多态
1.4.4.1 方法重载
方法重载是指在一个类中可以有多个同名的方法,但参数列表不同。示例:
class Calculator {int add(int a, int b) {return a + b;}double add(double a, double b) {return a + b;}
}
1.4.4.2 向上转型与向下转型
- 向上转型:将
第二章 Java高级特性
2.1 异常处理
2.1.1 异常的概念
在 Java 中,异常是指在程序运行过程中出现的不正常情况。想象一下,你编写了一个程序,就像是在建造一座房子,而异常就像是房子里突然出现的小故障,比如水管漏水、电路短路等。这些故障会干扰程序的正常运行,可能导致程序崩溃。例如,当你尝试打开一个不存在的文件时,就会引发异常。异常的出现提醒我们程序中存在问题,需要进行处理,以保证程序的健壮性和稳定性。😟
2.1.2 异常的分类
2.1.2.1 受检查异常(Checked Exception)
受检查异常就像是你出门前必须要检查的事项,比如带钥匙、带手机等。在 Java 中,这类异常是编译器会检查的异常,也就是说,在编译阶段,编译器会强制要求你对这类异常进行处理。如果不处理,程序将无法通过编译。常见的受检查异常有 IOException
、SQLException
等。例如,当你使用 FileInputStream
打开一个文件时,如果文件不存在,就会抛出 FileNotFoundException
,这是一个受检查异常,你必须使用 try - catch
语句捕获它或者使用 throws
关键字声明抛出它。🤔
2.1.2.2 运行时异常(Runtime Exception)
运行时异常则像是突然发生的意外事件,比如走路时突然被石头绊倒。这类异常在编译阶段不会被编译器检查,也就是说,即使你不处理它们,程序也能通过编译。但是在程序运行时,如果出现了这类异常,程序可能会崩溃。常见的运行时异常有 NullPointerException
(当你尝试访问一个空对象的方法或属性时抛出)、ArrayIndexOutOfBoundsException
(当你访问数组时越界抛出)等。😱
2.1.3 异常处理机制
2.1.3.1 try - catch - finally语句
try - catch - finally
语句是 Java 中最常用的异常处理方式。可以把它想象成一个“保护罩”,将可能出现异常的代码放在 try
块中,就像是把珍贵的物品放在保护罩里。如果在 try
块中出现了异常,程序会立即跳转到相应的 catch
块中进行异常处理。catch
块就像是一个“急救箱”,可以对异常进行处理,比如输出错误信息、记录日志等。而 finally
块则像是一个“善后小组”,无论 try
块中是否出现异常,finally
块中的代码都会被执行,通常用于释放资源,比如关闭文件、关闭数据库连接等。示例代码如下:
try {// 可能出现异常的代码int result = 10 / 0;
} catch (ArithmeticException e) {// 处理异常System.out.println("发生了算术异常:" + e.getMessage());
} finally {// 无论是否出现异常,都会执行的代码System.out.println("finally 块被执行");
}
2.1.3.2 throws关键字
throws
关键字用于在方法声明中声明该方法可能抛出的异常。就像是在告诉调用者:“我这个方法可能会出现这些异常,你要做好处理的准备”。当一个方法可能会抛出受检查异常时,通常会使用 throws
关键字声明。例如:
public void readFile() throws java.io.IOException {// 可能抛出 IOException 的代码java.io.FileInputStream fis = new java.io.FileInputStream("test.txt");
}
2.1.3.3 throw关键字
throw
关键字用于在方法内部手动抛出一个异常。就像是你发现了一个问题,主动把这个问题“扔出去”。例如,当你编写一个方法,要求传入的参数不能为负数,如果传入了负数,就可以使用 throw
关键字抛出一个异常:
public void checkNumber(int num) {if (num < 0) {throw new IllegalArgumentException("传入的参数不能为负数");}
}
2.1.4 自定义异常
有时候,Java 提供的异常类型不能满足我们的需求,这时候就可以自定义异常。自定义异常就像是你自己制作了一个特殊的“急救箱”,用于处理特定的问题。自定义异常通常继承自 Exception
或 RuntimeException
。例如,我们可以自定义一个 AgeException
来处理年龄不合法的情况:
class AgeException extends Exception {public AgeException(String message) {super(message);}
}public class Main {public static void main(String[] args) {try {checkAge(-5);} catch (AgeException e) {System.out.println(e.getMessage());}}public static void checkAge(int age) throws AgeException {if (age < 0) {throw new AgeException("年龄不能为负数");}}
}
2.2 多线程编程
2.2.1 线程的概念
线程就像是一个程序中的多个“小工人”,每个线程都可以独立执行任务。在 Java 中,一个程序可以同时运行多个线程,这些线程可以并行执行不同的任务,从而提高程序的执行效率。例如,一个浏览器可以同时有一个线程负责下载网页内容,另一个线程负责显示网页界面,这样可以让用户在下载的同时也能看到部分内容,提高用户体验。🚀
2.2.2 线程的创建方式
2.2.2.1 继承Thread类
继承 Thread
类是创建线程的一种方式。就像是你让一个工人专门负责一项任务,这个工人就是一个线程。你需要创建一个类继承自 Thread
类,并重写 run()
方法,在 run()
方法中定义线程要执行的任务。示例代码如下:
class MyThread extends Thread {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println("线程 " + Thread.currentThread().getName() + " 执行:" + i);}}
}public class Main {public static void main(String[] args) {MyThread thread = new MyThread();thread.start();}
}
2.2.2.2 实现Runnable接口
实现 Runnable
接口是另一种创建线程的方式。这种方式就像是你把任务写在一张纸上,然后找一个工人来执行这个任务。你需要创建一个类实现 Runnable
接口,并重写 run()
方法,然后将这个类的实例传递给 Thread
类的构造函数。示例代码如下:
class MyRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println("线程 " + Thread.currentThread().getName() + " 执行:" + i);}}
}public class Main {public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();Thread thread = new Thread(myRunnable);thread.start();}
}
2.2.2.3 实现Callable接口
实现 Callable
接口也是创建线程的一种方式,与前两种方式不同的是,Callable
接口的 call()
方法可以有返回值。就像是你让工人去完成一个任务,并且要求他完成后给你带回一个结果。示例代码如下:
import java.util.concurrent.*;class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i < 10; i++) {sum += i;}return sum;}
}public class Main {public static void main(String[] args) throws ExecutionException, InterruptedException {MyCallable myCallable = new MyCallable();ExecutorService executorService = Executors.newSingleThreadExecutor();Future<Integer> future = executorService.submit(myCallable);int result = future.get();System.out.println("计算结果:" + result);executorService.shutdown();}
}
2.2.3 线程的生命周期
线程的生命周期就像是一个人的成长过程,有出生、成长、工作、休息等阶段。在 Java 中,线程的生命周期包括以下几个状态:
- 新建状态(New):当创建一个
Thread
对象时,线程处于新建状态,就像是一个婴儿刚刚出生。 - 就绪状态(Runnable):当调用
start()
方法后,线程进入就绪状态,等待 CPU 分配时间片,就像是一个人准备好了去工作,等待分配任务。 - 运行状态(Running):当 CPU 分配时间片给线程时,线程进入运行状态,开始执行
run()
方法中的代码,就像是一个人开始工作。 - 阻塞状态(Blocked):当线程由于某些原因(如等待输入、等待锁等)暂停执行时,进入阻塞状态,就像是一个人在工作过程中遇到了问题,暂时停止工作。
- 死亡状态(Terminated):当
run()
方法执行完毕或者线程被异常终止时,线程进入死亡状态,就像是一个人完成了工作,结束了一生。
2.2.4 线程同步
2.2.4.1 synchronized关键字
synchronized
关键字用于实现线程同步,就像是给一个房间上了一把锁,同一时间只允许一个人进入房间。在 Java 中,当多个线程访问共享资源时,可能会出现数据不一致的问题,使用 synchronized
关键字可以保证同一时间只有一个线程可以访问共享资源。可以将 synchronized
关键字用于方法或代码块。示例代码如下:
class Counter {private int count = 0;public synchronized void increment() {count++;}public int getCount() {return count;}
}public class Main {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.increment();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println("计数器的值:" + counter.getCount());}
}
2.2.4.2 Lock接口
Lock
接口是 Java 提供的另一种实现线程同步的方式,与 synchronized
关键字相比,Lock
接口提供了更灵活的锁机制。就像是你有一把更高级的锁,可以根据需要进行加锁和解锁操作。Lock
接口有多个实现类,如 ReentrantLock
。示例代码如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class Counter {private int count = 0;private Lock lock = new ReentrantLock();public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}public int getCount() {return count;}
}public class Main {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.increment();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println("计数器的值:" + counter.getCount());}
}
2.2.5 线程通信
2.2.5.1 wait()、notify()和notifyAll()方法
wait()
、notify()
和 notifyAll()
方法用于实现线程之间的通信。可以把它们想象成线程之间的“对讲机”,线程可以通过 wait()
方法进入等待状态,就像是一个人拿起对讲机说“我先等一下”,然后释放对象的锁;其他线程可以通过 notify()
方法唤醒一个正在等待的线程,就像是另一个人拿起对讲机说“你可以继续了”;notifyAll()
方法则可以唤醒所有正在等待的线程。示例代码如下:
class Message {private String content;private boolean available = false;public synchronized String read() {while (!available) {try {wait();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}available = false;notifyAll();return content;}public synchronized void write(String content) {while (available) {try {wait();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}this.content = content;available = true;notifyAll();}
}public class Main {public static void main(String[] args) {Message message = new Message();Thread writer = new Thread(() -> {message.write("Hello, World!");});Thread reader = new Thread(() -> {System.out.println("读取的消息:" + message.read());});writer.start();reader.start();}
}
2.2.5.2 Condition接口
Condition
接口是 Java 提供的另一种实现线程通信的方式,与 wait()
、notify()
和 notifyAll()
方法相比,Condition
接口提供了更灵活的线程通信机制。可以把 Condition
接口想象成多个“对讲机频道”,不同的线程可以在不同的频道上进行通信。示例代码如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class Message {private String content;private boolean available = false;private Lock lock = new ReentrantLock();private Condition notFull = lock.newCondition();private Condition notEmpty = lock.newCondition();public String read() {lock.lock();try {while (!available) {notEmpty.await();}available = false;notFull.signalAll();return content;} catch (InterruptedException e) {Thread.currentThread().interrupt();return null;} finally {lock.unlock();}}public void write(String content) {lock.lock();try {while (available) {notFull.await();}this.content = content;available = true;notEmpty.signalAll();} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();}}
}public class Main {public static void main(String[] args) {Message message = new Message();Thread writer = new Thread(() -> {message.write("Hello, World!");});Thread reader = new Thread(() -> {System.out.println("读取的消息:" + message.read());});writer.start();reader.start();}
}
2.3 泛型编程
2.3.1 泛型的概念
泛型就像是一个“万能容器”,可以在编译时指定容器中存放的数据类型。在 Java 中,使用泛型可以提高代码的复用性和类型安全性。例如,你可以创建一个泛型列表,这个列表可以存放不同类型的数据,但是在编译时就可以确定列表中存放的数据类型,避免了在运行时出现类型转换错误。😎
2.3.2 泛型类
泛型类是指在类的定义中使用泛型。就像是你制作了一个通用的容器,这个容器可以根据需要存放不同类型的物品。示例代码如下:
class Box<T> {private T content;public void setContent(T content) {this.content = content;}public T getContent() {return content;}
}public class Main {public static void main(String[] args) {Box<String> box = new Box<>();box.setContent("Hello");System.out.println(box.getContent());}
}
2.3.3 泛型方法
泛型方法是指在方法的定义中使用泛型。就像是你制作了一个通用的工具,这个工具可以处理不同类型的数据。
第三章 Java集合框架
3.1 集合概述
3.1.1 集合的概念
在 Java 里,集合就像是一个“容器”🎁,专门用来存放多个对象。数组也能存放多个对象,不过集合比数组更灵活。数组一旦创建,长度就固定了,而集合的大小可以动态变化。
集合可以用来存储不同类型的数据,例如可以把整数、字符串、自定义对象等都放到集合里。集合在处理数据时非常方便,比如对数据进行添加、删除、查找等操作。它还能提高代码的复用性和可维护性,让我们的编程工作更加高效👍。
3.1.2 集合框架的体系结构
Java 集合框架有一个清晰的体系结构,主要分为两大接口:Collection
和 Map
。
Collection
接口:它是存储单个元素的根接口,下面又有一些子接口和实现类。List
接口:有序、可重复的集合。就像排队一样,元素有固定的顺序,而且可以有重复的元素。Set
接口:无序、不可重复的集合。每个元素都是独一无二的,就像人的身份证号码一样。
Map
接口:存储键值对的集合。就像一本字典,通过键(key)可以快速找到对应的值(value)。
这个体系结构就像一棵大树🌳,Collection
和 Map
是树干,下面的子接口和实现类是树枝和树叶,它们共同构成了 Java 强大的集合框架。
3.2 列表(List)
3.2.1 ArrayList
ArrayList
是 List
接口的一个常用实现类,它的底层是用数组实现的。可以把它想象成一个动态数组📋,当元素数量超过数组容量时,它会自动扩容。
- 优点:
- 随机访问速度快。就像在书架上找书一样,知道书的位置就能快速拿到。通过索引可以快速访问元素,时间复杂度是 O(1)。
- 实现简单,使用方便。
- 缺点:
- 插入和删除操作效率低。因为插入或删除元素时,后面的元素都要移动位置,时间复杂度是 O(n)。
- 使用示例:
import java.util.ArrayList;
import java.util.List;public class ArrayListExample {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("apple");list.add("banana");System.out.println(list.get(0)); // 输出 apple}
}
3.2.2 LinkedList
LinkedList
也是 List
接口的实现类,它的底层是用双向链表实现的。可以把它想象成一列火车🚂,每个车厢(节点)都有前后两个连接点,通过这些连接点可以方便地找到前后的车厢。
- 优点:
- 插入和删除操作效率高。只需要修改节点的前后连接关系,时间复杂度是 O(1)。
- 适合频繁进行插入和删除操作的场景。
- 缺点:
- 随机访问速度慢。要访问某个元素,需要从头或尾开始遍历链表,时间复杂度是 O(n)。
- 使用示例:
import java.util.LinkedList;
import java.util.List;public class LinkedListExample {public static void main(String[] args) {List<String> list = new LinkedList<>();list.add("cherry");list.add("date");list.remove(0);System.out.println(list.get(0)); // 输出 date}
}
3.2.3 Vector
Vector
同样实现了 List
接口,它的底层也是数组。不过 Vector
是线程安全的,而 ArrayList
是非线程安全的。
- 优点:
- 线程安全。在多线程环境下使用比较安全。
- 缺点:
- 性能相对较低。因为要保证线程安全,会有一些额外的开销。
- 使用示例:
import java.util.Vector;
import java.util.List;public class VectorExample {public static void main(String[] args) {List<String> list = new Vector<>();list.add("elderberry");System.out.println(list.get(0)); // 输出 elderberry}
}
3.3 集合(Set)
3.3.1 HashSet
HashSet
是 Set
接口的实现类,它的底层是用哈希表实现的。可以把它想象成一个大仓库,每个物品(元素)都有一个独特的编号(哈希值),通过这个编号可以快速找到物品。
- 特点:
- 无序、不可重复。元素没有固定的顺序,而且每个元素只能出现一次。
- 插入、删除和查找操作效率高。时间复杂度接近 O(1)。
- 使用示例:
import java.util.HashSet;
import java.util.Set;public class HashSetExample {public static void main(String[] args) {Set<String> set = new HashSet<>();set.add("fig");set.add("grape");System.out.println(set.contains("fig")); // 输出 true}
}
3.3.2 LinkedHashSet
LinkedHashSet
是 HashSet
的子类,它在 HashSet
的基础上维护了一个双向链表,用来记录元素的插入顺序。就像在仓库里给物品排了队,按照插入的顺序依次摆放。
- 特点:
- 有序、不可重复。元素按照插入的顺序排列,且每个元素只能出现一次。
- 插入、删除和查找操作效率也比较高。时间复杂度接近 O(1)。
- 使用示例:
import java.util.LinkedHashSet;
import java.util.Set;public class LinkedHashSetExample {public static void main(String[] args) {Set<String> set = new LinkedHashSet<>();set.add("honeydew");set.add("kiwi");for (String fruit : set) {System.out.println(fruit);}}
}
3.3.3 TreeSet
TreeSet
是 Set
接口的另一个实现类,它的底层是用红黑树实现的。红黑树是一种自平衡的二叉搜索树,可以保证元素按照自然顺序或指定的比较器顺序排列。就像在书架上按照字母顺序摆放书籍一样。
- 特点:
- 有序、不可重复。元素按照排序顺序排列,且每个元素只能出现一次。
- 可以对元素进行排序。
- 使用示例:
import java.util.TreeSet;
import java.util.Set;public class TreeSetExample {public static void main(String[] args) {Set<Integer> set = new TreeSet<>();set.add(3);set.add(1);set.add(2);for (Integer num : set) {System.out.println(num); // 输出 1 2 3}}
}
3.4 映射(Map)
3.4.1 HashMap
HashMap
是 Map
接口的常用实现类,它的底层是用哈希表实现的。可以把它想象成一个大柜子,每个抽屉(键)都对应着一个物品(值),通过抽屉的编号(键)可以快速找到物品(值)。
- 特点:
- 键不可重复,值可以重复。每个键只能对应一个值,但不同的键可以对应相同的值。
- 插入、删除和查找操作效率高。时间复杂度接近 O(1)。
- 使用示例:
import java.util.HashMap;
import java.util.Map;public class HashMapExample {public static void main(String[] args) {Map<String, Integer> map = new HashMap<>();map.put("apple", 1);map.put("banana", 2);System.out.println(map.get("apple")); // 输出 1}
}
3.4.2 LinkedHashMap
LinkedHashMap
是 HashMap
的子类,它在 HashMap
的基础上维护了一个双向链表,用来记录键值对的插入顺序或访问顺序。就像在柜子里给抽屉排了队,按照插入或访问的顺序依次摆放。
- 特点:
- 键不可重复,值可以重复。
- 可以保持键值对的插入顺序或访问顺序。
- 使用示例:
import java.util.LinkedHashMap;
import java.util.Map;public class LinkedHashMapExample {public static void main(String[] args) {Map<String, Integer> map = new LinkedHashMap<>();map.put("cherry", 3);map.put("date", 4);for (Map.Entry<String, Integer> entry : map.entrySet()) {System.out.println(entry.getKey() + ": " + entry.getValue());}}
}
3.4.3 TreeMap
TreeMap
是 Map
接口的实现类,它的底层是用红黑树实现的。可以保证键按照自然顺序或指定的比较器顺序排列。就像在柜子里按照抽屉编号的大小顺序摆放一样。
- 特点:
- 键不可重复,值可以重复。
- 键按照排序顺序排列。
- 使用示例:
import java.util.TreeMap;
import java.util.Map;public class TreeMapExample {public static void main(String[] args) {Map<Integer, String> map = new TreeMap<>();map.put(3, "elderberry");map.put(1, "fig");map.put(2, "grape");for (Map.Entry<Integer, String> entry : map.entrySet()) {System.out.println(entry.getKey() + ": " + entry.getValue());}}
}
3.4.4 Hashtable
Hashtable
也是 Map
接口的实现类,它和 HashMap
类似,但 Hashtable
是线程安全的,而 HashMap
是非线程安全的。
- 特点:
- 键不可重复,值可以重复。
- 线程安全。
- 使用示例:
import java.util.Hashtable;
import java.util.Map;public class HashtableExample {public static void main(String[] args) {Map<String, Integer> map = new Hashtable<>();map.put("honeydew", 5);System.out.println(map.get("honeydew")); // 输出 5}
}
3.5 迭代器与遍历
3.5.1 Iterator接口
Iterator
是 Java 集合框架中的一个接口,它提供了一种统一的方式来遍历集合中的元素。可以把它想象成一个“指针”,从集合的第一个元素开始,依次向后移动,直到遍历完所有元素。
- 使用示例:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;public class IteratorExample {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("apple");list.add("banana");Iterator<String> iterator = list.iterator();while (iterator.hasNext()) {System.out.println(iterator.next());}}
}
3.5.2 ListIterator接口
ListIterator
是 Iterator
的子接口,它专门用于遍历 List
集合。和 Iterator
不同的是,ListIterator
可以双向遍历,还可以在遍历过程中进行添加、修改和删除操作。
- 使用示例:
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;public class ListIteratorExample {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("cherry");list.add("date");ListIterator<String> listIterator = list.listIterator();while (listIterator.hasNext()) {System.out.println(listIterator.next());}while (listIterator.hasPrevious()) {System.out.println(listIterator.previous());}}
}
3.5.3 增强for循环
增强 for 循环是 Java 5 引入的一种简化的遍历方式,它可以更方便地遍历数组和集合。
- 使用示例:
import java.util.ArrayList;
import java.util.List;public class EnhancedForLoopExample {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("elderberry");list.add("fig");for (String fruit : list) {System.out.println(fruit);}}
}
3.5.4 Lambda表达式与Stream API遍历
Java 8 引入了 Lambda 表达式和 Stream API,它们可以让集合的遍历和操作更加简洁和高效。
- 使用示例:
import java.util.ArrayList;
import java.util.List;public class LambdaStreamExample {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("grape");list.add("honeydew");list.stream().forEach(fruit -> System.out.println(fruit));}
}
通过这些迭代器和遍历方式,我们可以根据不同的需求选择最合适的方法来处理集合中的元素。🎉
第四章 Java输入输出(IO)
4.1 IO流的概念
4.1.1 输入流与输出流
想象一下,Java程序就像是一个忙碌的小工厂🏭,数据就像是原材料和产品。输入流和输出流就是这个工厂的两条运输通道。
- 输入流:它就像是工厂的进货通道🚚,负责将外部的数据(如文件、网络数据等)传输到Java程序中。程序可以从输入流中读取数据,就像工厂从进货通道接收原材料一样。例如,当你想要读取一个文本文件的内容时,就需要使用输入流。
- 输出流:它好比工厂的出货通道🚛,用于将Java程序中的数据传输到外部。程序可以向输出流中写入数据,就像工厂把生产好的产品通过出货通道发送出去。比如,你要把程序中的一些数据保存到文件中,就会用到输出流。
4.1.2 字节流与字符流
在数据传输的过程中,根据传输数据的单位不同,又可以分为字节流和字符流。
- 字节流:以字节(byte)为单位进行数据传输。一个字节是8位二进制数,它可以处理任何类型的数据,包括文本、图片、音频、视频等。字节流就像是一个万能的运输工🧑🚚,不管是什么货物(数据)都能运输。在Java中,字节流的基类是
InputStream
和OutputStream
。 - 字符流:以字符(char)为单位进行数据传输。一个字符通常由一个或多个字节组成,主要用于处理文本数据。字符流在处理文本时更加方便,因为它会自动处理字符编码的问题。可以把字符流想象成专门运输文本货物的快递员📦,处理文本数据更加得心应手。在Java中,字符流的基类是
Reader
和Writer
。
4.2 字节流
4.2.1 FileInputStream和FileOutputStream
- FileInputStream:它是用于从文件中读取字节数据的输入流。就像是一个小矿工⛏️,专门从文件这个“矿山”中挖掘字节数据。以下是一个简单的示例代码:
import java.io.FileInputStream;
import java.io.IOException;public class FileInputStreamExample {public static void main(String[] args) {try (FileInputStream fis = new FileInputStream("test.txt")) {int data;while ((data = fis.read()) != -1) {System.out.print((char) data);}} catch (IOException e) {e.printStackTrace();}}
}
- FileOutputStream:用于向文件中写入字节数据的输出流。它就像一个小画家🖌️,把字节数据“画”到文件这个“画布”上。示例代码如下:
import java.io.FileOutputStream;
import java.io.IOException;public class FileOutputStreamExample {public static void main(String[] args) {try (FileOutputStream fos = new FileOutputStream("test.txt")) {String message = "Hello, World!";byte[] bytes = message.getBytes();fos.write(bytes);} catch (IOException e) {e.printStackTrace();}}
}
4.2.2 BufferedInputStream和BufferedOutputStream
- BufferedInputStream:它是
FileInputStream
的增强版,在内部有一个缓冲区(可以想象成一个小仓库🏬)。当程序需要读取数据时,它会先从缓冲区中读取,如果缓冲区中的数据不够,再从文件中读取一批数据到缓冲区。这样可以减少与文件系统的交互次数,提高读取效率。示例代码:
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;public class BufferedInputStreamExample {public static void main(String[] args) {try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.txt"))) {int data;while ((data = bis.read()) != -1) {System.out.print((char) data);}} catch (IOException e) {e.printStackTrace();}}
}
- BufferedOutputStream:同样是
FileOutputStream
的增强版,也有一个缓冲区。程序向它写入数据时,会先把数据写入缓冲区,当缓冲区满了或者调用flush()
方法时,才会把缓冲区中的数据一次性写入文件。这样可以减少文件写入的次数,提高写入效率。示例代码:
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;public class BufferedOutputStreamExample {public static void main(String[] args) {try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("test.txt"))) {String message = "Hello, Buffered World!";byte[] bytes = message.getBytes();bos.write(bytes);bos.flush();} catch (IOException e) {e.printStackTrace();}}
}
4.2.3 ObjectInputStream和ObjectOutputStream
- ObjectInputStream:用于从输入流中读取对象。它就像是一个神奇的解码器🧑🔬,可以把字节数据解码成Java对象。在使用它之前,对象的类必须实现
Serializable
接口。示例代码:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;public class ObjectInputStreamExample {public static void main(String[] args) {try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.ser"))) {Object obj = ois.readObject();System.out.println(obj);} catch (IOException | ClassNotFoundException e) {e.printStackTrace();}}
}
- ObjectOutputStream:用于将对象写入输出流。它就像一个神奇的编码器🧑💻,可以把Java对象编码成字节数据。示例代码:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;class Person implements java.io.Serializable {String name;int age;public Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "Person{name='" + name + "', age=" + age + "}";}
}public class ObjectOutputStreamExample {public static void main(String[] args) {try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.ser"))) {Person person = new Person("John", 30);oos.writeObject(person);} catch (IOException e) {e.printStackTrace();}}
}
4.3 字符流
4.3.1 FileReader和FileWriter
- FileReader:用于从文件中读取字符数据。它就像一个小书虫📖,专门从文件这个“大书库”中读取字符。示例代码:
import java.io.FileReader;
import java.io.IOException;public class FileReaderExample {public static void main(String[] args) {try (FileReader fr = new FileReader("test.txt")) {int data;while ((data = fr.read()) != -1) {System.out.print((char) data);}} catch (IOException e) {e.printStackTrace();}}
}
- FileWriter:用于向文件中写入字符数据。它就像一个小作家✍️,把字符数据写入文件这个“笔记本”。示例代码:
import java.io.FileWriter;
import java.io.IOException;public class FileWriterExample {public static void main(String[] args) {try (FileWriter fw = new FileWriter("test.txt")) {String message = "Hello, Character World!";fw.write(message);} catch (IOException e) {e.printStackTrace();}}
}
4.3.2 BufferedReader和BufferedWriter
- BufferedReader:是
FileReader
的增强版,有一个缓冲区。它可以一次读取一行文本,就像一个快速的阅读者📚,能够高效地读取文本文件。示例代码:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;public class BufferedReaderExample {public static void main(String[] args) {try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {String line;while ((line = br.readLine()) != null) {System.out.println(line);}} catch (IOException e) {e.printStackTrace();}}
}
- BufferedWriter:是
FileWriter
的增强版,也有缓冲区。它可以高效地写入文本数据,并且可以使用newLine()
方法写入换行符。示例代码:
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;public class BufferedWriterExample {public static void main(String[] args) {try (BufferedWriter bw = new BufferedWriter(new FileWriter("test.txt"))) {bw.write("Hello, Buffered Character World!");bw.newLine();bw.write("This is a new line.");} catch (IOException e) {e.printStackTrace();}}
}
4.3.3 PrintWriter
PrintWriter
是一个非常方便的字符输出流,它可以像使用 System.out.println()
一样方便地输出数据。它可以自动刷新缓冲区,还支持格式化输出。示例代码:
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;public class PrintWriterExample {public static void main(String[] args) {try (PrintWriter pw = new PrintWriter(new FileWriter("test.txt"))) {pw.println("Hello, PrintWriter!");int num = 10;pw.printf("The number is %d.\n", num);} catch (IOException e) {e.printStackTrace();}}
}
4.4 序列化与反序列化
4.4.1 Serializable接口
序列化是指将Java对象转换为字节序列的过程,反序列化则是将字节序列恢复为Java对象的过程。在Java中,要实现序列化,对象的类必须实现 Serializable
接口。这个接口是一个标记接口,没有任何方法,只是告诉Java虚拟机这个类的对象可以被序列化。例如上面的 Person
类:
class Person implements java.io.Serializable {String name;int age;public Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "Person{name='" + name + "', age=" + age + "}";}
}
通过 ObjectOutputStream
可以将 Person
对象序列化到文件中,通过 ObjectInputStream
可以从文件中反序列化出 Person
对象。
4.4.2 Externalizable接口
Externalizable
接口是 Serializable
接口的扩展,它定义了两个方法:writeExternal()
和 readExternal()
。实现 Externalizable
接口的类可以自己控制对象的序列化和反序列化过程。示例代码:
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;class Student implements Externalizable {String name;int age;public Student() {}public Student(String name, int age) {this.name = name;this.age = age;}@Overridepublic void writeExternal(ObjectOutput out) throws IOException {out.writeObject(name);out.writeInt(age);}@Overridepublic void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {name = (String) in.readObject();age = in.readInt();}@Overridepublic String toString() {return "Student{name='" + name + "', age=" + age + "}";}
}
4.5 NIO(New IO)
4.5.1 缓冲区(Buffer)
缓冲区是 NIO 中的一个重要概念,它是一个可以读写数据的容器。可以把缓冲区想象成一个水桶💧,数据就像水一样可以被装进桶里或者从桶里倒出来。在 Java 中,Buffer
是一个抽象类,它有多个子类,如 ByteBuffer
、CharBuffer
等。以下是一个简单的 ByteBuffer
使用示例:
import java.nio.ByteBuffer;public class BufferExample {public static void main(String[] args) {ByteBuffer buffer = ByteBuffer.allocate(10);buffer.put((byte) 'H');buffer.put((byte) 'e');buffer.put((byte) 'l');buffer.put((byte) 'l');buffer.put((byte) 'o');buffer.flip();while (buffer.hasRemaining()) {System.out.print((char) buffer.get());}}
}
4.5.2 通道(Channel)
通道就像是数据传输的高速公路🚗,它可以直接与文件、网络等数据源进行数据交换。通道可以异步地进行数据读写操作,提高了数据传输的效率。在 Java 中,Channel
是一个接口,常见的实现类有 FileChannel
、SocketChannel
等。以下是一个使用 FileChannel
进行文件复制的示例:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;public class ChannelExample {public static void main(String[] args) {try (FileInputStream fis = new FileInputStream("source.txt");FileOutputStream fos = new FileOutputStream("destination.txt");FileChannel inChannel = fis.getChannel();FileChannel outChannel = fos.getChannel()) {inChannel.transferTo(0, inChannel.size(), outChannel);} catch (IOException e) {e.printStackTrace();}}
}
4.5.3 选择器(Selector)
选择器是 NIO 中用于实现多路复用的关键组件。它可以同时监控多个通道的 IO 事件(如可读、可写等),就像一个聪明的调度员👨调度员,能够合理地分配资源,提高系统的并发性能。以下是一个简单的 Selector
使用示例:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;
import java.util.Set;public class SelectorExample {public static void main(String[] args) {try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {serverSocketChannel.socket().bind(new InetSocketAddress(8080));serverSocketChannel.configureBlocking(false);Selector selector = Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);while (true) {int readyChannels = selector.select();if (readyChannels == 0) continue;Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();if (key.isAcceptable()) {// 处理连接事件}keyIterator.remove();}}} catch (IOException e) {e.printStackTrace();}}
}
通过学习 Java 的输入输出(IO)知识,你可以更好地处理文件、网络等数据的读写操作,让你的 Java 程序更加灵活和强大💪!
第五章 Java数据库编程
5.1 JDBC概述
5.1.1 JDBC的概念
JDBC(Java Database Connectivity)即 Java 数据库连接,它是 Java 语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。简单来说,JDBC 就像是一座桥梁,它让 Java 程序能够和各种不同的数据库进行“对话”,不管是 MySQL、Oracle 还是 SQL Server 等,Java 程序都可以通过 JDBC 来操作这些数据库中的数据😃。
5.1.2 JDBC的体系结构
JDBC 的体系结构主要分为两层:
- 应用程序层:这是我们编写的 Java 应用程序所在的层。在这个层中,我们会使用 JDBC API 来编写代码,实现与数据库的交互。例如,我们可以编写代码来查询数据库中的数据、插入新的数据或者更新已有的数据。
- 驱动程序层:这一层包含了各种数据库的驱动程序。不同的数据库有不同的驱动程序,这些驱动程序负责将 JDBC API 的调用转换为数据库能够理解的指令。例如,MySQL 有 MySQL 的 JDBC 驱动,Oracle 有 Oracle 的 JDBC 驱动。当 Java 应用程序通过 JDBC API 发出操作数据库的请求时,驱动程序会将这些请求翻译成对应数据库的特定指令,并发送给数据库进行执行📡。
5.2 JDBC编程步骤
5.2.1 加载数据库驱动
在使用 JDBC 连接数据库之前,我们需要先加载相应的数据库驱动。不同的数据库有不同的驱动类,例如 MySQL 的驱动类是 com.mysql.cj.jdbc.Driver
。我们可以使用 Class.forName()
方法来加载驱动类,示例代码如下:
try {Class.forName("com.mysql.cj.jdbc.Driver");System.out.println("数据库驱动加载成功!");
} catch (ClassNotFoundException e) {e.printStackTrace();
}
这段代码尝试加载 MySQL 的驱动类,如果加载成功,会输出“数据库驱动加载成功!”;如果加载失败,会打印出异常信息。
5.2.2 建立数据库连接
加载完驱动后,我们就可以建立与数据库的连接了。我们需要使用 DriverManager.getConnection()
方法,该方法需要传入数据库的 URL、用户名和密码。示例代码如下:
String url = "jdbc:mysql://localhost:3306/testdb";
String username = "root";
String password = "123456";
try {Connection connection = DriverManager.getConnection(url, username, password);System.out.println("数据库连接成功!");
} catch (SQLException e) {e.printStackTrace();
}
这里的 url
是数据库的连接地址,username
和 password
分别是数据库的用户名和密码。如果连接成功,会输出“数据库连接成功!”;如果连接失败,会打印出异常信息。
5.2.3 创建Statement对象
建立好数据库连接后,我们需要创建一个 Statement
对象,用于执行 SQL 语句。Statement
对象可以通过 Connection
对象的 createStatement()
方法来创建,示例代码如下:
try {Statement statement = connection.createStatement();System.out.println("Statement对象创建成功!");
} catch (SQLException e) {e.printStackTrace();
}
这里的 connection
是前面建立的数据库连接对象。如果 Statement
对象创建成功,会输出“Statement对象创建成功!”;如果创建失败,会打印出异常信息。
5.2.4 执行SQL语句
创建好 Statement
对象后,我们就可以使用它来执行 SQL 语句了。Statement
对象提供了不同的方法来执行不同类型的 SQL 语句,例如 executeQuery()
方法用于执行查询语句,executeUpdate()
方法用于执行插入、更新和删除语句。示例代码如下:
// 执行查询语句
try {String sql = "SELECT * FROM users";ResultSet resultSet = statement.executeQuery(sql);System.out.println("查询语句执行成功!");
} catch (SQLException e) {e.printStackTrace();
}// 执行插入语句
try {String insertSql = "INSERT INTO users (name, age) VALUES ('John', 25)";int rows = statement.executeUpdate(insertSql);System.out.println("插入语句执行成功,影响行数:" + rows);
} catch (SQLException e) {e.printStackTrace();
}
这里分别展示了执行查询语句和插入语句的示例。如果语句执行成功,会输出相应的提示信息;如果执行失败,会打印出异常信息。
5.2.5 处理结果集
当执行查询语句后,会返回一个 ResultSet
对象,它包含了查询结果。我们可以使用 ResultSet
对象的方法来遍历结果集,获取其中的数据。示例代码如下:
try {while (resultSet.next()) {int id = resultSet.getInt("id");String name = resultSet.getString("name");int age = resultSet.getInt("age");System.out.println("ID: " + id + ", Name: " + name + ", Age: " + age);}
} catch (SQLException e) {e.printStackTrace();
}
这里使用 resultSet.next()
方法来遍历结果集,每次调用该方法会将指针移动到下一行。如果还有下一行数据,返回 true
;否则返回 false
。然后使用 getInt()
和 getString()
方法来获取相应列的数据。
5.2.6 关闭资源
在完成数据库操作后,我们需要关闭所有打开的资源,包括 ResultSet
、Statement
和 Connection
对象。关闭资源的顺序是先关闭 ResultSet
,再关闭 Statement
,最后关闭 Connection
。示例代码如下:
try {if (resultSet != null) {resultSet.close();}if (statement != null) {statement.close();}if (connection != null) {connection.close();}System.out.println("资源关闭成功!");
} catch (SQLException e) {e.printStackTrace();
}
这里使用 close()
方法来关闭资源。如果关闭成功,会输出“资源关闭成功!”;如果关闭失败,会打印出异常信息。
5.3 预处理语句(PreparedStatement)
5.3.1 预处理语句的优点
- 提高性能:预处理语句会在数据库端进行预编译,当多次执行相同结构的 SQL 语句时,只需要编译一次,后续执行时直接使用编译好的语句,避免了重复编译的开销,从而提高了执行效率。
- 防止 SQL 注入攻击:预处理语句使用占位符(
?
)来表示参数,在执行时会对参数进行严格的类型检查和转义处理,能够有效防止 SQL 注入攻击。例如,用户输入的恶意 SQL 语句会被当作普通的字符串处理,而不会被当作 SQL 指令执行。
5.3.2 预处理语句的使用
使用预处理语句的步骤如下:
- 编写包含占位符的 SQL 语句。
- 创建
PreparedStatement
对象。 - 设置占位符的值。
- 执行 SQL 语句。
示例代码如下:
// 编写包含占位符的 SQL 语句
String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
try {// 创建 PreparedStatement 对象PreparedStatement preparedStatement = connection.prepareStatement(sql);// 设置占位符的值preparedStatement.setString(1, "Alice");preparedStatement.setInt(2, 30);// 执行 SQL 语句int rows = preparedStatement.executeUpdate();System.out.println("插入语句执行成功,影响行数:" + rows);
} catch (SQLException e) {e.printStackTrace();
}
这里使用 prepareStatement()
方法创建 PreparedStatement
对象,使用 setString()
和 setInt()
方法设置占位符的值,最后使用 executeUpdate()
方法执行 SQL 语句。
5.4 事务处理
5.4.1 事务的概念
事务是一组不可分割的数据库操作序列,这些操作要么全部成功执行,要么全部失败回滚。事务具有四个特性,简称 ACID:
- 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成,不会处于中间状态。例如,在银行转账操作中,从一个账户扣款和向另一个账户存款这两个操作必须同时成功或同时失败。
- 一致性(Consistency):事务执行前后,数据库的状态必须保持一致。例如,在转账操作中,转账前后两个账户的总金额应该保持不变。
- 隔离性(Isolation):多个事务并发执行时,一个事务的执行不能被其他事务干扰。例如,在多个用户同时进行转账操作时,每个用户的操作应该相互隔离,不会相互影响。
- 持久性(Durability):事务一旦提交,它对数据库的改变就是永久性的,即使数据库发生故障也不会丢失。
5.4.2 JDBC中的事务管理
在 JDBC 中,默认情况下,每个 SQL 语句都是一个独立的事务,会自动提交。如果我们需要手动管理事务,可以通过以下步骤实现:
- 关闭自动提交模式。
- 执行一系列 SQL 语句。
- 如果所有操作都成功,提交事务;如果有任何操作失败,回滚事务。
示例代码如下:
try {// 关闭自动提交模式connection.setAutoCommit(false);// 执行一系列 SQL 语句Statement statement = connection.createStatement();statement.executeUpdate("UPDATE accounts SET balance = balance - 100 WHERE id = 1");statement.executeUpdate("UPDATE accounts SET balance = balance + 100 WHERE id = 2");// 提交事务connection.commit();System.out.println("事务提交成功!");
} catch (SQLException e) {try {// 回滚事务connection.rollback();System.out.println("事务回滚成功!");} catch (SQLException ex) {ex.printStackTrace();}e.printStackTrace();
} finally {try {// 恢复自动提交模式connection.setAutoCommit(true);} catch (SQLException e) {e.printStackTrace();}
}
这里使用 setAutoCommit(false)
方法关闭自动提交模式,使用 commit()
方法提交事务,使用 rollback()
方法回滚事务,最后使用 setAutoCommit(true)
方法恢复自动提交模式。
5.5 数据库连接池
5.5.1 数据库连接池的概念
数据库连接池是一种数据库连接的管理技术,它预先创建一定数量的数据库连接,将这些连接存储在一个连接池中。当应用程序需要访问数据库时,从连接池中获取一个可用的连接;使用完后,将连接归还给连接池,而不是直接关闭连接。这样可以避免频繁地创建和销毁数据库连接,从而提高应用程序的性能和响应速度。就像一个自行车租赁站,里面有很多自行车(数据库连接),用户(应用程序)需要时可以租一辆,用完后归还,而不是每次都去买一辆新的自行车(创建新的数据库连接)🚲。
5.5.2 常见的数据库连接池实现
5.5.2.1 C3P0
C3P0 是一个开源的 JDBC 连接池,它提供了很多配置选项,可以方便地进行连接池的管理和优化。使用 C3P0 的步骤如下:
- 引入 C3P0 的依赖。
- 配置 C3P0 的连接池参数。
- 获取数据库连接。
示例代码如下:
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;public class C3P0Example {public static void main(String[] args) {// 创建 C3P0 连接池数据源ComboPooledDataSource dataSource = new ComboPooledDataSource();try {// 设置数据库驱动类dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");// 设置数据库连接 URLdataSource.setJdbcUrl("jdbc:mysql://localhost:3306/testdb");// 设置数据库用户名dataSource.setUser("root");// 设置数据库密码dataSource.setPassword("123456");// 设置连接池的最小连接数dataSource.setMinPoolSize(5);// 设置连接池的最大连接数dataSource.setMaxPoolSize(20);// 获取数据库连接Connection connection = dataSource.getConnection();System.out.println("从 C3P0 连接池获取数据库连接成功!");// 使用完后关闭连接(实际上是归还给连接池)connection.close();} catch (Exception e) {e.printStackTrace();}}
}
这里使用 ComboPooledDataSource
类来创建 C3P0 连接池数据源,通过设置各种参数来配置连接池,最后使用 getConnection()
方法获取数据库连接。
5.5.2.2 Druid
Druid 是阿里巴巴开源的数据库连接池,它结合了 C3P0、DBCP 等连接池的优点,同时还提供了强大的监控和防御 SQL 注入的功能。使用 Druid 的步骤如下:
- 引入 Druid 的依赖。
- 配置 Druid 的连接池参数。
- 获取数据库连接。
示例代码如下:
import com.alibaba.druid.pool.DruidDataSource;
import java.sql.Connection;
import java.sql.SQLException;public class DruidExample {public static void main(String[] args) {// 创建 Druid 连接池数据源DruidDataSource dataSource = new DruidDataSource();// 设置数据库驱动类dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");// 设置数据库连接 URLdataSource.setUrl("jdbc:mysql://localhost:3306/testdb");// 设置数据库用户名dataSource.setUsername("root");// 设置数据库密码dataSource.setPassword("123456");// 设置连接池的初始连接数dataSource.setInitialSize(5);// 设置连接池的最大活动连接数dataSource.setMaxActive(20);try {// 获取数据库连接Connection connection = dataSource.getConnection();System.out.println("从 Druid 连接池获取数据库连接成功!");// 使用完后关闭连接(实际上是归还给连接池)connection.close();} catch (SQLException e) {e.printStackTrace();}}
}
这里使用 DruidDataSource
类来创建 Druid 连接池数据源,通过设置各种参数来配置连接池,最后使用 getConnection()
方法获取数据库连接。
第六章 Java Web开发
6.1 Servlet
6.1.1 Servlet的概念
Servlet 是 Java 语言编写的服务器端程序,它运行在支持 Java 的应用服务器中,如 Tomcat。可以把 Servlet 想象成一个“小管家”👨管家,它负责接收客户端(比如浏览器)的请求,然后根据请求进行相应的处理,最后把处理结果返回给客户端。它是 Java Web 开发的基础组件之一,常用于处理表单数据、生成动态网页内容等。
6.1.2 Servlet的生命周期
Servlet 的生命周期包含以下几个重要阶段:
- 加载和实例化:当客户端首次请求访问某个 Servlet 时,服务器会加载该 Servlet 的类文件,并创建它的实例。这就像是为“小管家”安排了一个工作岗位💼。
- 初始化:实例化后,服务器会调用 Servlet 的
init()
方法进行初始化操作。这个方法只会被调用一次,通常用于初始化一些资源,比如数据库连接等。就好像“小管家”在正式工作前要做好各项准备工作📋。 - 服务:当有请求到来时,服务器会调用 Servlet 的
service()
方法来处理请求。service()
方法会根据请求的类型(如 GET、POST)调用相应的doGet()
或doPost()
方法。这就如同“小管家”开始接待客人,根据客人的需求提供服务🍵。 - 销毁:当服务器关闭或者 Servlet 被卸载时,会调用 Servlet 的
destroy()
方法,用于释放 Servlet 占用的资源。这就好比“小管家”下班了,要把工作场所清理干净🧹。
6.1.3 Servlet的开发与部署
开发步骤
- 创建 Servlet 类:编写一个类,继承自
HttpServlet
类,并重写doGet()
或doPost()
方法。例如:
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;public class MyServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/html");PrintWriter out = resp.getWriter();out.println("<html><body>");out.println("<h1>Hello, Servlet!</h1>");out.println("</body></html>");}
}
- 配置 Servlet:在
web.xml
文件中配置 Servlet 的映射信息,告诉服务器哪个 URL 对应哪个 Servlet。例如:
<servlet><servlet-name>MyServlet</servlet-name><servlet-class>com.example.MyServlet</servlet-class>
</servlet>
<servlet-mapping><servlet-name>MyServlet</servlet-name><url-pattern>/myServlet</url-pattern>
</servlet-mapping>
部署
将开发好的 Servlet 打包成 WAR 文件,然后将 WAR 文件部署到支持 Java 的应用服务器(如 Tomcat)中。部署完成后,就可以通过浏览器访问配置好的 URL 来调用 Servlet 了。
6.1.4 Servlet的请求与响应处理
请求处理
客户端发送的请求包含了很多信息,如请求方法(GET、POST 等)、请求参数、请求头信息等。Servlet 可以通过 HttpServletRequest
对象来获取这些信息。例如,获取请求参数:
String username = req.getParameter("username");
响应处理
Servlet 处理完请求后,需要将处理结果返回给客户端。可以通过 HttpServletResponse
对象来设置响应的内容类型、状态码等信息,并将响应数据输出到客户端。例如,设置响应内容类型为 HTML,并输出一段 HTML 内容:
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html><body>");
out.println("<h1>Response from Servlet</h1>");
out.println("</body></html>");
6.1.5 Servlet的过滤器(Filter)
Servlet 过滤器就像是一个“安检员”🚻,它可以在请求到达 Servlet 之前对请求进行预处理,也可以在响应返回客户端之前对响应进行后处理。过滤器可以用于实现一些通用的功能,如字符编码过滤、权限验证、日志记录等。
开发步骤
- 创建过滤器类:编写一个类,实现
javax.servlet.Filter
接口,并重写doFilter()
方法。例如:
import javax.servlet.*;
import java.io.IOException;public class CharacterEncodingFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {request.setCharacterEncoding("UTF-8");response.setCharacterEncoding("UTF-8");chain.doFilter(request, response);}
}
- 配置过滤器:在
web.xml
文件中配置过滤器的映射信息,指定过滤器要拦截的 URL 模式。例如:
<filter><filter-name>CharacterEncodingFilter</filter-name><filter-class>com.example.CharacterEncodingFilter</filter-class>
</filter>
<filter-mapping><filter-name>CharacterEncodingFilter</filter-name><url-pattern>/*</url-pattern>
</filter-mapping>
6.1.6 Servlet的监听器(Listener)
Servlet 监听器就像是一个“观察者”👀,它可以监听 Servlet 容器中的各种事件,如 ServletContext 的创建和销毁、HttpSession 的创建和销毁、ServletRequest 的创建和销毁等。监听器可以用于实现一些与事件相关的功能,如统计在线用户数、记录系统启动和关闭时间等。
常见监听器类型
- ServletContextListener:监听 ServletContext 的创建和销毁事件。
- HttpSessionListener:监听 HttpSession 的创建和销毁事件。
- ServletRequestListener:监听 ServletRequest 的创建和销毁事件。
开发步骤
- 创建监听器类:编写一个类,实现相应的监听器接口,并重写相应的方法。例如,实现
ServletContextListener
接口:
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;public class MyServletContextListener implements ServletContextListener {@Overridepublic void contextInitialized(ServletContextEvent sce) {System.out.println("ServletContext initialized");}@Overridepublic void contextDestroyed(ServletContextEvent sce) {System.out.println("ServletContext destroyed");}
}
- 配置监听器:在
web.xml
文件中配置监听器。例如:
<listener><listener-class>com.example.MyServletContextListener</listener-class>
</listener>
6.2 JSP(JavaServer Pages)
6.2.1 JSP的概念
JSP 是一种动态网页技术,它允许在 HTML 页面中嵌入 Java 代码。可以把 JSP 看作是 HTML 和 Java 的结合体,它可以根据不同的请求动态生成 HTML 页面。JSP 页面在服务器端被编译成 Servlet,然后由 Servlet 来处理请求并生成响应。就好像 JSP 是一个“智能模板”,可以根据不同的情况生成不同的网页内容🎨。
6.2.2 JSP的语法
JSP 指令
JSP 指令用于设置 JSP 页面的一些全局属性,如页面的编码、导入的包等。常见的 JSP 指令有 <%@ page %>
、<%@ include %>
和 <%@ taglib %>
。例如:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
JSP 脚本元素
JSP 脚本元素用于在 JSP 页面中嵌入 Java 代码。常见的 JSP 脚本元素有 <% %>
、<%= %>
和 <%! %>
。例如:
<%String message = "Hello, JSP!";out.println(message);
%>
JSP 动作标签
JSP 动作标签用于在 JSP 页面中实现一些特定的功能,如包含其他页面、转发请求等。常见的 JSP 动作标签有 <jsp:include>
、<jsp:forward>
等。例如:
<jsp:include page="header.jsp" />
6.2.3 JSP的内置对象
JSP 提供了一些内置对象,这些对象可以在 JSP 页面中直接使用,无需显式声明。常见的 JSP 内置对象有:
- request:代表客户端的请求,用于获取请求参数、请求头信息等。
- response:代表服务器的响应,用于设置响应内容类型、状态码等信息。
- session:代表客户端的会话,用于在多个请求之间共享数据。
- application:代表整个 Web 应用程序,用于在整个应用程序中共享数据。
- out:用于向客户端输出响应数据。
例如,使用 request
对象获取请求参数:
<%String username = request.getParameter("username");out.println("Username: " + username);
%>
6.2.4 JSP的标签库
6.2.4.1 JSTL(JavaServer Pages Standard Tag Library)
JSTL 是 JSP 标准标签库,它提供了一组标准的标签,用于简化 JSP 页面的开发。JSTL 标签库可以分为核心标签库、格式化标签库、SQL 标签库和 XML 标签库等。
使用步骤
- 导入 JSTL 标签库:在 JSP 页面中使用
<%@ taglib %>
指令导入 JSTL 标签库。例如:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
- 使用 JSTL 标签:在 JSP 页面中使用 JSTL 标签来实现各种功能。例如,使用核心标签库的
<c:forEach>
标签遍历集合:
<c:forEach items="${list}" var="item"><p>${item}</p>
</c:forEach>
6.3 MVC设计模式
6.3.1 MVC的概念
MVC(Model-View-Controller)是一种软件设计模式,它将一个应用程序分为三个主要部分:模型(Model)、视图(View)和控制器(Controller)。
- 模型(Model):负责处理业务逻辑和数据的存储与操作。可以把模型想象成一个“数据仓库”📦,它管理着应用程序的数据,并提供对数据的访问和操作方法。
- 视图(View):负责将模型中的数据以用户友好的方式呈现给用户。视图就像是一个“展示窗口”🖼️,它从模型中获取数据,并将其渲染成 HTML 页面或其他形式的界面。
- 控制器(Controller):负责接收用户的请求,调用模型进行相应的处理,并根据处理结果选择合适的视图进行显示。控制器就像是一个“调度员”📞,它协调模型和视图之间的交互。
6.3.2 MVC在Java Web开发中的应用
在 Java Web 开发中,MVC 设计模式可以帮助我们将业务逻辑、数据处理和页面显示分离,提高代码的可维护性和可扩展性。
- 模型(Model):可以使用 JavaBean 来表示业务数据和业务逻辑,使用 DAO(Data Access Object)模式来实现数据的持久化操作。
- 视图(View):可以使用 JSP 或其他前端技术来实现用户界面的显示。
- 控制器(Controller):可以使用 Servlet 或其他控制器框架(如 Spring MVC)来接收用户的请求,调用模型进行处理,并选择合适的视图进行显示。
例如,用户在浏览器中输入一个 URL 发送请求,请求首先到达控制器,控制器根据请求的信息调用相应的模型进行处理,模型处理完后将结果返回给控制器,控制器再根据处理结果选择合适的视图,将视图渲染后返回给客户端。
6.4 Java Web框架
6.4.1 Spring
6.4.1.1 Spring的核心特性
Spring 是一个轻量级的 Java 开发框架,它具有以下核心特性:
- IoC(Inversion of Control):控制反转,将对象的创建和依赖关系的管理交给 Spring 容器,降低了组件之间的耦合度。就好像 Spring 是一个“对象管家”🧑💼,它负责管理和提供应用程序中所需的对象。
- AOP(Aspect - Oriented Programming):面向切面编程,允许在不修改原有代码的情况下,对程序的功能进行增强。可以把 AOP 看作是一个“代码补丁”🧩,它可以在程序的特定位置插入额外的功能,如日志记录、事务管理等。
- 事务管理:提供了统一的事务管理机制,简化了事务处理的开发。
- 集成其他框架:可以方便地与其他框架(如 Hibernate、MyBatis 等)集成,提高开发效率。
6.4.1.2 Spring的IoC(Inversion of Control)
IoC 是 Spring 的核心特性之一,它通过依赖注入(Dependency Injection)的方式来实现。依赖注入是指将对象的依赖关系通过外部配置或注解的方式注入到对象中,而不是在对象内部直接创建依赖对象。
实现方式
- 基于 XML 配置:在 Spring 的配置文件中通过
<bean>
标签来定义对象,并通过<property>
标签来注入依赖。例如:
<bean id="userService" class="com.example.UserService"><property name="userDao" ref="userDao" />
</bean>
<bean id="userDao" class="com.example.UserDaoImpl" />
- 基于注解:使用
@Component
、@Service
、@Repository
等注解来标识对象,并使用@Autowired
注解来注入依赖。例如:
@Service
public class UserService {@Autowiredprivate UserDao userDao;
}@Repository
public class UserDaoImpl implements UserDao {// 实现方法
}
6.4.1.3 Spring的AOP(Aspect - Oriented Programming)
AOP 允许我们将一些通用的功能(如日志记录、事务管理等)从业务逻辑中分离出来,形成独立的切面。在 Spring 中,AOP 可以通过 XML 配置或注解的方式来实现。
实现步骤
- 定义切面类:使用
@Aspect
注解来标识切面类,并在切面类中定义切入点和通知。例如:
@Aspect
@Component
public class LoggingAspect {@Before("execution(* com.example.service.*.*(..))")public void beforeAdvice() {System.out.println("Before method execution");}
}
- 启用 AOP 自动代理:在 Spring 的配置文件中启用 AOP 自动代理。例如:
<aop:aspectj-autoproxy />
6.4.2 Spring MVC
6.4.2.1 Spring MVC的工作原理
Spring MVC 是基于 MVC 设计模式的 Web 框架,它的工作原理如下:
- 客户端发送请求:客户端(如浏览器)向服务器发送请求。
- DispatcherServlet 接收请求:请求首先到达
DispatcherServlet
,它是 Spring MVC 的核心控制器,负责接收和分发请求。 - HandlerMapping 查找处理器:
DispatcherServlet
根据请求的 URL 调用HandlerMapping
来查找处理该请求的处理器(Controller)。 - 处理器执行处理逻辑:找到处理器后,
DispatcherServlet
调用处理器的相应方法来处理请求,并返回一个ModelAndView
对象。 - 视图解析器解析视图:
DispatcherServlet
将ModelAndView
对象传递给视图解析器,视图解析器根据视图名称解析出实际的视图。 - 视图渲染:
DispatcherServlet
将模型数据传递给视图,视图将模型数据渲染成 HTML 页面,并返回给客户端。
6.4.2.2 Spring MVC的控制器与视图解析器
- 控制器(Controller):负责处理客户端的请求,调用业务逻辑进行处理,并返回一个
ModelAndView
对象。可以使用@Controller
注解来标识控制器类,并使用@RequestMapping
注解来映射请求的 URL。例如:
@Controller
public class UserController {@RequestMapping("/user")public ModelAndView getUser() {ModelAndView mav = new ModelAndView();mav.addObject("user", new User("John", 25));mav.setViewName("user");return mav;}
}
- 视图解析器(ViewResolver):负责将视图名称解析成实际的视图。常见的视图解析器有
InternalResourceViewResolver
,它可以将视图名称解析成 JSP 页面。例如:
@Bean
public InternalResourceViewResolver viewResolver() {InternalResourceViewResolver resolver = new InternalResourceViewResolver();resolver.setPrefix("/WEB-INF/views/");resolver.setSuffix(".jsp");return resolver;
}
第七章 Java性能优化与测试
7.1 Java性能优化
7.1.1 代码优化
7.1.1.1 避免创建过多的对象
在 Java 中,对象的创建和销毁是有成本的。频繁地创建大量对象会增加垃圾回收器的负担,降低程序的性能。以下是一些避免创建过多对象的方法:
- 使用对象池:对于一些创建和销毁成本较高的对象,如数据库连接、线程等,可以使用对象池来复用这些对象。例如,在 Apache Commons Pool 中提供了对象池的实现,我们可以使用它来管理数据库连接池。
- 使用基本数据类型:基本数据类型(如 int、double 等)比包装类(如 Integer、Double 等)的创建和操作成本更低。在不需要使用对象特性时,优先使用基本数据类型。
- 字符串拼接使用 StringBuilder 或 StringBuffer:在进行字符串拼接时,使用
+
操作符会创建新的字符串对象。而StringBuilder
或StringBuffer
可以在原对象上进行操作,避免了频繁创建新对象。示例代码如下:
// 不推荐
String result = "";
for (int i = 0; i < 100; i++) {result += i;
}// 推荐
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {sb.append(i);
}
String result2 = sb.toString();
7.1.1.2 合理使用数据结构
Java 提供了丰富的数据结构,如数组、链表、栈、队列、哈希表等。选择合适的数据结构可以显著提高程序的性能。以下是一些常见数据结构的使用场景:
- 数组:适合随机访问元素,当需要频繁根据索引访问元素时,使用数组是一个不错的选择。但数组的大小是固定的,不适合动态添加或删除元素。
- 链表:适合频繁插入和删除元素的场景,因为链表的插入和删除操作只需要修改指针,时间复杂度为 O(1)。但链表的随机访问性能较差,时间复杂度为 O(n)。
- 哈希表:如
HashMap
,适合快速查找元素,其查找、插入和删除操作的平均时间复杂度为 O(1)。当需要根据键快速查找值时,使用哈希表是一个很好的选择。 - 栈和队列:栈遵循后进先出(LIFO)原则,队列遵循先进先出(FIFO)原则。当需要实现特定的算法,如深度优先搜索(DFS)使用栈,广度优先搜索(BFS)使用队列。
7.1.1.3 减少方法调用的开销
方法调用会有一定的开销,包括参数传递、栈帧的创建和销毁等。以下是一些减少方法调用开销的方法:
- 内联方法:对于一些简单的方法,如果方法体较小且被频繁调用,可以考虑将方法内联到调用处,避免方法调用的开销。例如:
// 原方法
public int add(int a, int b) {return a + b;
}// 调用处内联
int result = 3 + 5;
- 减少递归调用:递归调用会不断创建新的栈帧,可能会导致栈溢出。如果可能,将递归算法改为迭代算法。例如,计算斐波那契数列:
// 递归实现
public int fibonacciRecursive(int n) {if (n <= 1) {return n;}return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);
}// 迭代实现
public int fibonacciIterative(int n) {if (n <= 1) {return n;}int a = 0, b = 1, c;for (int i = 2; i <= n; i++) {c = a + b;a = b;b = c;}return b;
}
7.1.2 内存管理优化
7.1.2.1 垃圾回收机制
Java 的垃圾回收机制(GC)负责自动回收不再使用的对象所占用的内存。了解垃圾回收机制可以帮助我们优化内存使用。以下是一些关于垃圾回收机制的要点:
- 垃圾回收算法:常见的垃圾回收算法有标记 - 清除算法、标记 - 整理算法、复制算法等。不同的垃圾回收器可能会采用不同的算法组合。
- 垃圾回收器:Java 提供了多种垃圾回收器,如 Serial 垃圾回收器、Parallel 垃圾回收器、CMS 垃圾回收器、G1 垃圾回收器等。不同的垃圾回收器适用于不同的场景,可以根据应用的特点选择合适的垃圾回收器。
- 对象的生命周期:对象在内存中有不同的生命周期阶段,如新生代、老年代、永久代(Java 8 及以后为元空间)。了解对象的生命周期可以帮助我们理解垃圾回收的过程。
7.1.2.2 堆内存和栈内存的优化
Java 内存主要分为堆内存和栈内存。以下是一些堆内存和栈内存的优化方法:
-
堆内存优化:
- 合理设置堆内存大小:可以通过
-Xms
和-Xmx
参数来设置堆内存的初始大小和最大大小。如果堆内存设置过小,可能会导致频繁的垃圾回收,影响性能;如果设置过大,可能会浪费系统资源。 - 避免内存泄漏:内存泄漏是指对象不再使用,但由于某些原因无法被垃圾回收器回收。常见的内存泄漏原因包括静态集合持有对象引用、未关闭的资源等。
- 合理设置堆内存大小:可以通过
-
栈内存优化:
- 避免深度递归:递归调用会不断创建新的栈帧,可能会导致栈溢出。尽量将递归算法改为迭代算法。
- 合理设置栈大小:可以通过
-Xss
参数来设置线程栈的大小。如果栈大小设置过小,可能会导致栈溢出;如果设置过大,会占用过多的系统资源。
7.1.3 多线程优化
7.1.3.1 线程池的使用
线程池可以管理和复用线程,避免频繁创建和销毁线程带来的开销。以下是使用线程池的好处和示例代码:
-
好处:
- 提高性能:减少线程创建和销毁的开销。
- 控制并发线程数:避免过多线程导致系统资源耗尽。
- 便于管理:可以对线程进行统一的管理和监控。
-
示例代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadPoolExample {public static void main(String[] args) {// 创建一个固定大小的线程池ExecutorService executor = Executors.newFixedThreadPool(5);// 提交任务for (int i = 0; i < 10; i++) {final int taskId = i;executor.submit(() -> {System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());});}// 关闭线程池executor.shutdown();}
}
7.1.3.2 减少锁的竞争
在多线程编程中,锁的竞争会导致线程阻塞,降低程序的性能。以下是一些减少锁竞争的方法:
- 缩小锁的范围:只在必要的代码块上加锁,避免长时间持有锁。例如:
public class LockExample {private final Object lock = new Object();private int count = 0;public void increment() {// 只在需要修改共享变量时加锁synchronized (lock) {count++;}// 其他不需要加锁的操作System.out.println("Count: " + count);}
}
- 使用读写锁:对于读多写少的场景,可以使用读写锁。读写锁允许多个线程同时进行读操作,但在写操作时会互斥。示例代码如下:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockExample {private final ReadWriteLock lock = new ReentrantReadWriteLock();private int data = 0;public int readData() {lock.readLock().lock();try {return data;} finally {lock.readLock().unlock();}}public void writeData(int newData) {lock.writeLock().lock();try {data = newData;} finally {lock.writeLock().unlock();}}
}
7.2 Java测试
7.2.1 单元测试
7.2.1.1 JUnit框架
JUnit 是 Java 中最常用的单元测试框架,它可以帮助我们编写和运行单元测试。以下是使用 JUnit 进行单元测试的基本步骤:
- 添加依赖:在项目中添加 JUnit 的依赖,例如在 Maven 项目中,可以在
pom.xml
中添加以下依赖:
<dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version><scope>test</scope>
</dependency>
- 编写测试类和测试方法:测试类需要使用
@RunWith(JUnit4.class)
注解,测试方法需要使用@Test
注解。示例代码如下:
import org.junit.Test;
import static org.junit.Assert.assertEquals;public class CalculatorTest {@Testpublic void testAdd() {Calculator calculator = new Calculator();int result = calculator.add(2, 3);assertEquals(5, result);}
}class Calculator {public int add(int a, int b) {return a + b;}
}
- 运行测试:可以使用 IDE 自带的测试运行器或 Maven 的
mvn test
命令来运行测试。
7.2.1.2 Mockito框架
Mockito 是一个 Java 的模拟对象框架,它可以帮助我们在单元测试中创建和管理模拟对象。以下是使用 Mockito 进行单元测试的基本步骤:
- 添加依赖:在项目中添加 Mockito 的依赖,例如在 Maven 项目中,可以在
pom.xml
中添加以下依赖:
<dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><version>3.12.4</version><scope>test</scope>
</dependency>
- 创建和使用模拟对象:可以使用
Mockito.mock()
方法创建模拟对象,并使用Mockito.when()
方法设置模拟对象的行为。示例代码如下:
import org.junit.Test;
import static org.mockito.Mockito.*;
import static org.junit.Assert.assertEquals;public class UserServiceTest {@Testpublic void testFindUserById() {// 创建模拟对象UserDao userDao = mock(UserDao.class);UserService userService = new UserService(userDao);// 设置模拟对象的行为User mockUser = new User(1, "John");when(userDao.findUserById(1)).thenReturn(mockUser);// 调用被测试方法User result = userService.findUserById(1);// 验证结果assertEquals(mockUser, result);// 验证方法调用verify(userDao, times(1)).findUserById(1);}
}class User {private int id;private String name;public User(int id, String name) {this.id = id;this.name = name;}public int getId() {return id;}public String getName() {return name;}
}interface UserDao {User findUserById(int id);
}class UserService {private UserDao userDao;public UserService(UserDao userDao) {this.userDao = userDao;}public User findUserById(int id) {return userDao.findUserById(id);}
}
7.2.2 集成测试
集成测试是测试多个组件之间的交互是否正常。与单元测试不同,集成测试关注的是组件之间的协作和接口的正确性。常见的集成测试方法包括:
- 使用测试框架:可以使用 JUnit 等测试框架编写集成测试用例。
- 模拟依赖组件:在集成测试中,可能需要模拟一些外部依赖组件,如数据库、网络服务等。可以使用 Mockito 等框架来创建模拟对象。
- 搭建测试环境:需要搭建一个与生产环境相似的测试环境,包括数据库、服务器等。
7.2.3 性能测试
7.2.3.1 JMeter工具
JMeter 是一个开源的性能测试工具,它可以用于测试 Web 应用、数据库、FTP 等多种类型的应用。以下是使用 JMeter 进行性能测试的基本步骤:
- 安装和启动 JMeter:从 JMeter 官方网站下载并安装 JMeter,然后启动 JMeter。
- 创建测试计划:在 JMeter 中创建一个新的测试计划,并添加线程组、采样器、监听器等组件。
- 配置线程组:设置线程数、循环次数、启动时间等参数,模拟多个用户并发访问。
- 配置采样器:根据测试的目标,选择合适的采样器,如 HTTP 请求采样器、JDBC 请求采样器等,并配置请求的参数。
- 添加监听器:添加监听器来收集和分析测试结果,如聚合报告、图形结果等。
- 运行测试:点击运行按钮开始执行测试,然后查看监听器中的测试结果。
例如,使用 JMeter 对一个 Web 应用进行性能测试的步骤如下:
- 创建一个新的测试计划。
- 添加一个线程组,设置线程数为 100,循环次数为 10。
- 添加一个 HTTP 请求采样器,配置请求的 URL、请求方法等参数。
- 添加一个聚合报告监听器。
- 运行测试,查看聚合报告中的响应时间、吞吐量等指标。
🎉 通过以上的性能优化和测试方法,可以帮助我们提高 Java 程序的性能和质量。