总结
* 标准流
* `System.err`: 标准错误流. (命令行输出错误信息)
* `System.in`: 标准输入流. (键盘输入)
* 是`InputStream`类, 属于字节输入流
* `System.out`: 标准输出流. (控制台输出)
* 是`OutputStream`的子类PrintStream, 属于字节输出流
* 转换流
* `InputStreamReader`: 字节输入流转字符输入流
* 是`Reader`的子类, 属于字符输入流
* `OutputStreamWriter`: 字符输出流转字节输出流
* 是`Writer`的子类, 属于字符输出流
* 特点
* 转换流也是包装类, 需要传入实际的输入输出流对象
* 打印流
* `PrintStream`: 字节打印流
* `PrintWriter`: 字符打印流
* 注意
* 打印是输出操作, 所以打印流只有输出, 没有输入
* PrintWriter打印流的特点
* 可以自动换行, println(). 会根据系统自动确定换行符
* 不能输出字节, 可以输出其他任意类型(要输出字节需要使用PrintStream)
* 通过构造方法的配置可以实现自动刷新(flush)(只在调用println, printf, format时有用)
* 也是包装流, 自身没有写出功能
* 可以把字节输出流转换为字符输出流
* 关流不会抛出异常(此类中的方法不会抛出 I/O 异常,尽管其某些构造方法可能抛出异常)
* 方法
* 构造方法
* `PrintWriter PrintWriter(String filepath)`
* `PrintWriter PrintWriter(Writer out, boolean autoFlush)`: 创建对象, 同时设置是否自动刷新
* `PrintWriter(OutputStream out, boolean autoFlush)`: 创建对象, 同时设置是否自动刷新
* 成员方法
* `void write(String s)`: 写一个字符串
* `void print(String s)`: 输出字符串, 没有换行
* `void println(String s)`: 输出字符串并换行. 如果启动了自动刷新, 则会执行自动刷新写入数据
* `void printf(Locale l, String format, Object... args)`: 使用指定格式字符串和参数将格式化的字符串写入输出流. 如果启动了自动刷新, 则会执行自动
刷新写入数据
* `void format(Locale l, String format, Object... args)`: 使用指定格式字符串和参数将格式化的字符串写入输出流. 如果启动了自动刷新, 则会执行自动
刷新写入数据
* 对象操作流
* 作用: 读写对象到文件
* `ObjectInputStream`: 对象输入流
* `ObjectOutputStream`: 对象输出流
* 注意
* 使用对象输出流写出对象到文件, 该文件只能使用对象输入流读取对象
* 只能将实现了`java.io.Serializable`接口的对象写入流中
* Serializable接口
* 标记接口, 用于标记该类可以序列化
* `private static final long serialVersionUID`: 序列版本号. 用于确定对象和类定义是否一致
* `InvalidClassException`用对象读取流时发生该异常的原因:
1. 该类的序列版本号与从流中读取的类描述符的版本号不匹配
2. 该类包含未知数据类型(即可能添加了新的成员变量或方法)
3. 该类没有可访问的无参数构造方法
* 方法
* 构造方法:
* `ObjectOutputStream ObjectOutputStream(OutputStream out)`: 创建对象输出流
* `ObjectInputStream ObjectInputStream(InputStream in)`: 创建对象输入流
* 成员方法
* `void writeObject(Object o)`: 将对象写入对象输出流
* `Object readObject()`: 从对象输入流读取对象, 一次只读取一个对象. 当读不到时抛出`EOFException`.
* 读取对象异常的优化操作
* 在对象流中只保存一个对象, 通过该对象保存其他对象
* 比如用集合存储多个同类型的对象
* 定义一个类, 其中包含不同类型的其他类型对象
* Properties
* 继承`Hashtable<K, V>`, 实现`Map<K, V>`
* 作用: 以键值对方式保存信息到文本文件
* 应用场景: 作为程序的配置文件
* 注意
不能存`null`的键和值
* 只能保存英文字符和符号, 默认使用ISO-8859-1编码, 存中文会显示乱码
* 注意: 如果配置文件保存为`.txt`, 会变成GBK编码, 可以显示中文. 但一般都存为`.properties`, 使用ISO-8859-1, 显示不了中文
* 键和值都是字符串
* 方法
* 构造方法
* `Properties Properties()`
* 成员方法
* 可以使用Map的方法
* `String getProperty(String key)`: 根据键获取值. 如果找不到该键, 则返回null
* `String getProperty(String key, String defaultValue)`: 根据键获取值, 如果值不存在, 则使用指定的默认值
* `void setProperty(String key, String value)`: 设置键值对
* `void load(InputStream in)`: 从字节输入流中读取Properties
* `void load(Reader reader)`: 从字符输入流中读取Properties
* `void list(PrintStream out)`: 将Properties输出到指定的字节打印输出流. 会自动加一个`-- listing properties --`文件头
* `void list(PrintWriter out)`: 将Properties输出到指定的字符打印输出流. 会自动加一个`-- listing properties --`文件头
* `void store(Writer writer, String comments)`: 将Properties输出到指定的输出流, 并添加一个注释. 如果不想要注释可传null. 无论有没有注释, 都会添
加时间字符串的注释
* `void store(OutputStream os, String comments)`: 同上, 只是流不同
* list方法和store方法的区别
* list只能接收打印流(PrintStream, PrintWriter)
* store可以接收任何输出流(OutputStream, Writer)
* Properties读写操作步骤
* 写出到文件
* 创建Properties对象, 添加键值对, 使用list()或store()保存到文件中
* 从文件读取
* 创建Properties对象, 使用load()从文件加载数据, 使用getProperty()根据指定键获取值, 或使用遍历Map的方式遍历所有键和值
* 编码表
* 作用: 将计算机二进制数据转换为不同语言的字符
* 常见编码表
* `ASCII`: 美国标准码. 包含26个英文字母的大写和小写, 数字, 符号
* `ISO-8859-1`: 西方语言编码
* `GB2312`: 国标码
* `GBK`: 国标扩展码
* `Unicode`: 万国码. 支持多国语言字符.
* `UTF-8`: Unicode的一种实现方式, 长度可变的码表, 一个字符占用1个或2个字节
* `ANSI`: 本地编码表. 根据系统设置决定编码表
* Java String类对于字节和编码的操作
* `byte[] getBytes()`: 获取字符串的byte数组, 使用默认编码
* `byte[] getBytes(String charsetName)`: 获取字符串的byte数组, 使用指定编码
* `String String(byte[] bytes)`: 将byte数组转化为字符串, 使用默认编码
* `String String(byte[] bytes, String charsetName)`: 将byte数组转换为字符串, 使用指定编码
* `String String(byte[] bytes, int offset, int len, String charsetName)`: 将byte数组的一部分转换为字符串, 使用指定编码
* 乱码
* 原因: 读的编码与写的编码不一致
* 解决方法: 保证读和写的编码一致, 即可解决
* 处理乱码的2种方式:
1. String通过指定编码转为byte数组, 然后再创建String: (GBK字符串转UTF-8字符串写入文件)
* 先将String通过目标编码转为byte数组: `byte[] bys = "月薪过万".getBytes("UTF-8");`
* 再将byte数组转换为String: `String str = new String(bys);`
* 写入到文件: `fw.write(str);`
2. OutputStreamWriter可以指定编码写入文件, 免去使用String通过编码转换为byte数组的步骤
* `OutputStreamWriter OutputStreamWriter(OutputStream out, String charsetName)`: 创建转换流对象, 并指定编码
# day10 多线程
## 多线程概述
* 进程:
* Process, 一个应用程序在内存中的执行区域
* 举例:
* 一个正在运行的程序, 可以有一个或多个进程.
* 例如: 查看资源管理器, QQ有1个进程, chrome浏览器有多个进程
* 每个进程都会占用一部分系统资源
* 如CPU, 内存, 磁盘, 网络等
* 线程:
* Thread, 是一个进程中的一个执行路径.
* 举例:
* 一个进程中可以有一个或多个线程, 每个线程可以执行自己的代码
* 多个线程各自执行的任务可以同时进行
* 线程的执行依靠CPU调度分配
* 单核CPU: 靠CPU不断高速的随机的切换执行的线程, 达到看似同时进行的效果
* 多核CPU: 每个CPU可能完全执行一个线程, 或者多个CPU各自不断高速随机切换, 更高效
* 单线程和多线程特点
* 单线程: 同一时间只做一件事情, 安全性高, 效率低.
* 多线程: 同一时间做多件事, 安全性低, 效率高.
* 单线程和多线程的例子
* 假设各位同学吃坏了, 都想去卫生间, 只有1个空位
* 单线程方式: 同学1进去解决完毕, 同学2再去. ( 安全性高, 效率低)
* 多线程方式: 大家同时进去解决... (安全性低, 效率高)
* 扩展:
* 并发: 并行发生, 同时发生, 多线程就可以实现并发
* 同步: sync, 注意并不是同时的意思, 同步是指一步接一步的执行, 一个执行完毕再开始执行下一个.
* 单线程就是同步
* 异步: async, 不是一步一步执行, 而是同时执行多步, 每个步骤何时结束不确定.
* 多线程就是异步
* 同一个线程内的代码是同步执行的, 不同线程的代码是异步执行的
* 阻塞: 上一行代码正在执行, 还没有执行完毕, 程序就阻塞在这里了, 下一行代码必须等上一行不再阻塞后才能执行
## 创建线程的第一种方式: 继承Thread类
* `java.lang.Thread`类: 实现了`Runnable`接口
* 构造方法
* `Thread Thread()`: 创建Thead对象
* `Thread Thread(Runnable r)`: 通过Runnable对象创建Thread对象
* `Thread Thread(Runnable r, String threadName)`: 通过Runnable对象创建Thread对象并指定线程名
* 成员方法
* `void start()`: 启动线程, 即让线程开始执行`run()`方法中的代码
* `String getName()`: 获取线程的名称
* `void setName(String name)`: 设置线程名称
* 静态方法
* `static Thread currentThread()`: 返回对当前正在执行的线程对象的引用
* `static void sleep(long millis)`: 让所在线程睡眠指定的毫秒
* 多线程实现方式1:
* 定义类, 继承Thread类, 重写`run()`方法
* 步骤
1. 定义一个类, 继承Thread类
2. 在子类中重写父类的`run()`方法
3. 创建子类的对象
4. 调用子类对象的`start()`方法启动线程
```java
// 1. 定义一个类, 继承Thread类
public class MyThread extends Thread {
// 2. 在子类中重写父类的`run()`方法
@Override
public void run() {
// 该线程要做的事
}
}
public class Test {
public static void main(String[] args) {
// 3. 创建子类的对象
MyThread thread = new MyThread();
// 4. 调用子类对象的`start()`方法启动线程
thread.start();
}
}
```
* 说明
* 一个Thread对象, 就相当于一条线程. 一个类继承了`Thread`类, 该类对象就可以作为一个线程去单独执行任务
* `run()`方法中的代码, 就是该线程要执行的任务
* 如果想要多个线程同时执行相同的任务, 只需要定义一个类, 然后创建该类的多个对象来启动即可
* 如果想要多个线程同时执行不同的任务, 则需要定义多个类, 分别重写run方法, 然后创建这些类的对象来启动
* 注意:
* 线程何时启动?
* 线程对象调用`start()`方法开始启动, 启动后在新的线程中执行`run()`方法的代码
* 线程何时结束?
* 当`run()`方法中的代码执行完毕后, 线程就会自动结束并销毁.
* 如果`run()`方法中有死循环无法结束, 则该线程永远执行
* 不要调用`stop()`或`suspend()`方法来结束线程, 这两个方法不管用, 不安全, 而且已经过时
* 5分钟练习: 使用继承Thread方式创建多线程
* 创建项目s2-day11, 建包com.itheima.practice_01
* 建类MyThread
* 继承Thread类
* 重写run()方法
* 在run方法中循环100次打印线程名和i的值
* 建类Test
* 创建2个MyThread对象, 设置名称, 调用run()方法启动这两个线程
* 查看控制台结果
```java
// 1.定义类继承Thread类
public class MyThead extends Thread {
// 2. 重写run()方法
@Override
public void run() {
// run()方法中就是线程要执行的任务: 100次循环,每次循环打印出线程的名字和i的值
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":" + i);
}
}
}
/*
定义一个类继承Thread类,线程要执行的代码是
100次循环,每次循环打印出线程的名字和i的值
开启两个线程对象,都执行这段代码
*/
public class Test {
public static void main(String[] args) {
// 3.创建线程对象
MyThead t1 = new MyThead();
MyThead t2 = new MyThead();
// 修改线程名称
t1.setName("线程1");
t2.setName("线程222");
// 4.启动线程
t1.start();
t2.start();
System.out.println("main()方法执行完毕");
}
}
```
---
## main()方法是单线程的
* `main()`方法
* 也是执行在一个线程中的, 该线程的名称为`main`, 一般称为主线程, 主线程和其他线程没什么区别
* 所以main()方法是单线程的, 方法中的代码是一行一行向下执行的
## 创建线程的第二种方式: 实现Runnable接口
* `java.lang.Runnable`接口: 可将其对象看作是子线程要运行的任务
* 抽象方法:
* `void run()`
* `Runnable`接口出现的原因
* 因为Java只支持单继承, 如果继承了`Thread`类, 就无法继承其他类, 局限性太大, 所以使用实现`Runnable`接口的方式, 既能继承其他类, 还能实现多个接口, 不会影响类的扩
展性
* 多线程实现方式2:
* 实现Runnable接口, 重写`run()`方法
* 步骤
1. 定义一个类, 实现`Runnable`接口
2. 在子类中重写父类的`run()`方法
3. 创建一个子类的对象
4. 创建`Thread`对象, 在其构造方法中传入刚才创建的子类的对象
5. 调用`Thread`对象的`start()`方法启动一个线程
```java
// 定义一个类, 实现Runnable接口
public class MyRunnable implements Runnable {
// 2. 在子类中重写父类的`run()`方法
@Override
public void run() {
/ 这是线程要做的事
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class Test {
public static void main(String[] args) {
// 3. 创建一个子类的对象
MyRunnable myRunnable = new MyRunnable();
// 4. 创建Thread对象, 在其构造方法中传入刚才创建的子类的对象
Thread thread = new Thread(myRunnable);
// 5. 调用Thread对象的start()方法启动一个线程
thread.start();
}
}
```
* 注意:
* 这种方式仍然是通过多个`Thread`对象来启动多个线程
* 多个Thread对象使用同一个`Runnable`实现类对象, 就可以执行相同的任务
* 5分钟练习: 实现Runnable接口创建线程
* 继续使用项目s2-day11, 建包com.itheima.practice_02
* 建类MyRunnable
* 实现Runnable接口
* 重写run()方法
* 循环100次, 打印线程名和i
* 建类Test
* 先创建MyRunnable对象
* 再创建2个Thread对象, 传入MyRunnable对象
* 调用Thread对象的start()方法启动线程
* 观察打印结果
```java
// 1. 定义类实现Runnable接口
public class MyRunnable implements Runnable {
// 2. 重写run方法, 定义要执行的任务
@Override
public void run() {
// 这里是要执行的任务
for (int i = 0; i < 100; i++) {
// Thread.currentThread(): 哪个线程执行该方法, 就返回哪个线程对象
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
/*
定义一个类实现Runnable接口,在run方法中
执行的代码进行100次循环的打印,每次循环打印出i的值
创建两个两个线程都去执行这段代码
*/
public class Test {
public static void main(String[] args) {
// 先创建任务
MyRunnable runnable = new MyRunnable();
// 再创建线程对象, 多个线程执行相同的任务, 使用同一个runnable实现类对象即可
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
// 设置线程名字
t1.setName("线程1");
t2.setName("线程222");
// 启动线程
t1.start();
t2.start();
}
}
```
---
## 多线程中的线程安全问题: 售票发现问题
* Thread类
* 静态方法
* `static void sleep(long millis)`: 让所在线程睡眠指定的毫秒
* 售票案例分析
* 火车站有多个售票窗口, 每个售票窗口是同时售票的, 可以使用多个线程模拟售票
* 火车票是多个售票窗口共有的, 所以多个线程操作的都是相同的火车票. 可以理解为: 多个线程操作共享数据
* 火车票可以定义为一个int变量, 保存票数. 卖票通过票数--实现, 当票数<1时停止
* 该变量需要多个线程共享, 需要考虑将该变量定义在什么位置, 分析多线程的2种实现方式, 考虑哪种便于定义共享数据:
* 继承Thead类: 需要创建多个Thread对象, 共享的票数需要定义为静态变量
* 实现Runnable接口: 创建一个Runnable对象, 多个Thread对象共用, 所以票数在Runnable实现类中定义为非静态成员变量即可
* 这里选择实现Runnable接口
* 定义类TickerThread, 实现Runnable接口, 定义非静态变量`int ticket = 100;`, 重写run()方法
* 不知道什么时候卖完, 所以使用死循环卖票while(true), 先判断票数是否大于0, 大于0则打印线程名称和当前票数值, 然后票数--; 如果小于等于0, 则break结束
* 为了让多线程效果明显一些, 在卖票前增加Thread.sleep(100), 让线程睡100毫秒
* 5分钟练习: 模拟售票的问题
* 继续使用项目s2-day11, 建包com.itheima.practice_03
* 模拟售票
```java
public class SellTicketRunnable implements Runnable {
// 定义成员变量代表火车票数
int ticket = 100;
@Override
public void run() {
// 卖票操作
while (true) {
// 判断一下, 当票数大于0时就卖出一张票
if (ticket > 0) {
// 卖之前小睡一会儿
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票");
// 减少票数
ticket--;
}
}
}
}
/*
用三个线程模拟三个售票窗口
三个售票窗口共享100张票
在线程执行的代码中打印卖出当前第几张票
直到第0张票,停止打印
*/
public class Test {
public static void main(String[] args) {
// 创建卖票任务
SellTicketRunnable runnable = new SellTicketRunnable();
// 创建3个卖票窗口
Thread window1 = new Thread(runnable);
Thread window2 = new Thread(runnable);
Thread window3 = new Thread(runnable);
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
// 开始卖票
window1.start();
window2.start();
window3.start();
}
}
```
## 多线程中的线程安全问题: 售票问题原因分析
* 原因
* 线程会在执行过程中根据CPU调度来随机切换, 可能暂停在任何一行代码上
* 多个线程同时操作了共享数据, 在共享数据还没有发生变化时, 其他线程就对其继续操作, 导致数据不准确
* 票重复的原因: 拿最开始卖出3张第100张票举例
```java
@Override
public void run() {
while (true) {
// 每个线程进来后都会先判断票有没有, 最开始是100
if (ticket > 0) {
// 卖票
System.out.println(Thread.currentThread().getName() + "窗口卖出第" + ticket + "张票");
// 线程1进来, 卖出第100张票. 很不幸, 此时CPU切换, 线程1被挂起, 切换到线程2
// 线程2进来, 卖出第100张票. 很不幸, 此时CPU切换, 线程2被挂起, 切换到线程3
// 线程3进来, 卖出第100张票. 很不幸, 此时CPU切换, 线程3被挂起, 切换到线程1
ticket--; // 3个线程都卖出了第100张票, 然后才减少了票数到99
} else {
break;
}
}
}
```
* 有负数票的原因: 卖到最后
```java
@Override
public void run() {
while (true) {
// 当卖到最后第1张票时, 线程1来判断, 还有1张, 进入
if (ticket > 0) {
// 线程1刚进来, CPU切换了, 线程1挂起, 切换到线程2
// 线程2在外面判断还有1张票, 也进来了, 但CPU切换了, 线程2挂起, 切换到线程3
// 线程3在外面判断还有1张票, 也进来了, 但CPU切换了, 线程3挂起, 切换到线程1, 继续卖票
// 卖票
System.out.println(Thread.currentThread().getName() + "窗口卖出第" + ticket + "张票"); // 1:1 2:0 3:-1
// 线程1进来, 卖出第1张票.
ticket--;
// 然后线程1减少了票数, 当前ticket=0, 线程1出去后判断没票了, 所以break结束了
// 此时线程2还要继续执行, 线程2也要卖票, 此时ticket已经是0, 所以卖出第0张票, 然后减少票数, 当前ticket=-1. 出去后判断没票了, 所以break结束了
// 此时线程3还要继续执行, 线程3也要卖票, 此时ticket已经是-1, 所以卖出第-1张票, 然后减少票数. 出去后判断没票了, 所以break结束了
} else {
break;
}
}
}
```
---=
## 多线程中的线程安全问题: 使用同步代码块解决共享资源的问题
* 同步:
* 表示多个线程在执行同步的代码时, 这些线程只能按顺序一个一个去执行
* 优缺点
* 同步安全性高, 效率低
* 不同步安全性低, 效率高
* 使用`synchronized`关键字:
* 同步代码块: 使用`synchronized`修饰的代码块
* 作用:
* 被同步代码块包裹的代码相当于在一个房间内, 锁对象相当于房间的锁
* 一个线程进入同步代码块, 会把门锁上, 同步代码块外面的线程看到同步代码块内部有线程, 只能在外面等待
* 直到同步代码块内部的线程执行完毕从代码块中出来, 释放了锁, 外面等待的线程才能随机进去一个
* 应用位置:
* 哪些代码属于共享操作, 需要避免安全问题, 就将这些代码放入同步代码块
* 术语
* 线程进入同步代码块, 叫`获取锁`
* 线程出同步代码块, 叫`释放锁`
* 同步方法: 使用`synchronized`修饰的方法(下面讲)
* 锁对象
* 锁是一个**任意类型**的对象
* Object对象可以, String对象可以, Student对象等也可以...
* 锁对象**必须是被所有线程共享的**, 也就是说多个线程都共用一个锁
* 如使用同一个Runnable对象中的成员变量
* 或使用某个类的静态变量
* 或使用某个类的字节码对象
* 同步必须配合锁对象来使用
```java
// 同步代码块格式
synchronized (锁对象) {
// 要同步执行的代码
}
房间相当于同步代码块
+------+
| |
| \ 门锁
| |
+------+
```
* 一个便于理解的锁的例子
```
// 共用一把锁:
有一个卫生间 # 同步代码块
卫生间门上有一把锁 # 锁对象
乘客1进入卫生间, 门反锁不让外面人开 # 线程1进入同步代码块, 获取了锁
卫生间外的其他乘客打不开锁, 只能在门外等 # 其他线程在同步代码块外
乘客1解决完出来, 放开门锁 # 线程1出了同步代码块, 释放了锁
其他人谁抢到谁去 # 随机一个线程进入同步代码块
```
* **出现多线程的安全问题, 检查一下2个方面**
1. 对于操作了了共享数据的代码是否是同步的?
2. 同步使用的是否是同一个锁对象?
* 5分钟练习: 使用同步代码块修复问题
* 继续使用项目s2-day11, 建包com.itheima.practice_04
* 复制上次练习代码
* 修改TicketRunnable类
* 定义成员变量`Object lock = new Object();`
* 在while(true)循环内部, if(ticket>0)外部, 增加一个同步代码块, 使用lock对象作为锁
* 测试修改后的效果
```java
public class SellTicketRunnable implements Runnable {
// 定义成员变量代表火车票数
int ticket = 100;
// 定义一个多个线程共享的对象, 作为锁对象
Object lock = new Object();
@Override
public void run() {
// 卖票操作
while (true) {
// 同步代码块将操作共享数据的代码封装起来, 同一时间只有一个线程能够进入同步代码块执行
synchronized (lock) {
// 判断一下, 当票数大于0时就卖出一张票
if (ticket > 0) {
// 卖之前小睡一会儿
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票"); // 2:7 // "窗口2卖出了第7张票"
// 减少票数
ticket--;
} else {
System.out.println(Thread.currentThread().getName() + "卖完了");
break;
}
}
}
}
}
/*
用三个线程模拟三个售票窗口
三个售票窗口共享100张票
在线程执行的代码中打印卖出当前第几张票
直到第0张票,停止打印
*/
public class Test {
public static void main(String[] args) {
// 创建卖票任务
SellTicketRunnable runnable = new SellTicketRunnable();
// 创建3个卖票窗口
Thread window1 = new Thread(runnable);
Thread window2 = new Thread(runnable);
Thread window3 = new Thread(runnable);
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
// 开始卖票
window1.start();
window2.start();
window3.start();
}
}
```
* **多线程卖票常见错误分析**
* 错误1: while循环条件直接判断票数, 同步在while循环内, 导致卖出-1, -2的票
* 错误原因(`ticket > 0`也是操作共享数据, 也应该放在同步代码块中)
* 当剩下最后一张票时, 三个线程都在while条件中判断ticket条件成立, 然后进入while循环
* 此时遇到同步代码块, 有一个线程进入了, 其他2个在外面
* 进去的线程卖掉了最后一张票, 此时票数为0, 执行完毕后出了同步代码块结束
* 其他2个线程再分别进入同步代码块, 由于已经没有判断条件限制, 所以又各自卖掉了2个票, 所以出现了-1, -2
```java
@Override
public void run() {
while (ticket > 0) {
// 同步锁
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "窗口卖出了第 " + ticket + " 张票");
ticket--;
}
}
}
```
* 错误2: 锁加在while循环外, 导致只有一个窗口售票
* 错误分析:
* 同步代码块中只能有一个线程进入执行
* 当一个线程进入后, 一只执行while循环, 把票卖完, 然后出来
* 其他线程再进入, 发现票没有了, 直接结束了
```java
@Override
public void run() {
// 同步锁
synchronized (lock) {
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "窗口卖出了第 " + ticket + " 张票");
ticket--;
}
}
}
```
* 错误3: 锁对象不是共享的唯一一个, 而是多个, 导致没有锁住
* 错误原因
* 因为每个run方法中创建了一个局部变量作为锁对象, 而每个线程都有属于各自对象的run方法, 所以相当于每个线程各自有各自的锁
* 线程1进去, 上了线程1的锁, 但是这个锁只能锁住线程1自己, 线程2和3是锁不住的, 他们也能进去
* 可以想象为:
* 火车卫生间一个门上有3把锁, 开任何一个锁都能把门打开
* 乘客1进入, 用锁1锁上了门
* 乘客2用锁2又打开门进去, 用锁2锁上了门
* 乘客3用锁3又打开门进去, 用锁3锁上了门
* 3个乘客都在里面
* 这种方式相当于没有使用同步锁
```java
@Override
public void run() {
// 在各自线程要执行的方法中创建了一个锁对象, 没用
Object lock = new Object();
while (true) {
// 同步锁
synchronized (lock) {
if (ticket > 0) {
try {
Thread.sleep(100); // 睡一下效果明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "窗口卖出了第 " + ticket + " 张票");
ticket--;
} else {
break;
}
}
}
}
```
* 错误4: 使用继承Thread方式, 用成员变量定义票数, 导致票数不是共享资源, 每个窗口都重复卖各自的票. 同步锁也是成员变量, 每个线程都有一个锁, 相当于没用
* 错误原因
* 继承Thread类, 票数为成员变量
* 在这种方式创建线程时, 需要创建多个Thread对象, 相当于每个对象各自都有100张票, 各卖各的
* 解决方法:
* 把票数搞成静态的
* 把锁搞成静态的
```java
public class TicketWindow extends Thread{
int ticket = 100;
Object lock = new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票");
ticket--;
} else {
break;
}
}
}
}
}
```
## 多线程中的线程安全问题: 使用同步方法解决
* 同步方法:
* `synchronized`修饰的方法
* 作用:
* 该方法在同一时间只能被一个线程执行, 多个线程需要按顺序一个一个调用方法
* 同步方法的锁对象:
* 同步方法也有锁对象
* 静态同步方法:
* 是方法所在类的`Class`对象, 也就是该类的字节码对象
* 非静态同步方法:
* 是`this`, 也就是该方法所在类的对象
* 同步代码块和同步方法如何选择?
* 如果一段代码的功能是独立的, 会在多个地方重复执行, 且需要同步, 可以定义为同步方法, 便于复用
* 5分钟练习: 使用同步方法解决卖票问题
* 继续使用项目s2-day11, 建包com.itheima.practice_05
* 复制上次练习代码
* 修改TicketRunnable类
* 删除锁对象
* 定义同步方法, 内部执行卖票代码, 去掉同步代码块, else语句中改为返回boolean值, 作为判断是否结束循环的依据
* 将run()方法中的同步代码块内容删除, 留下循环, 循环内部改为调用同步方法
* 测试效果
```java
public class SellTicketRunnable implements Runnable {
// 定义成员变量代表火车票数
int ticket = 100;
@Override
public void run() {
// 卖票操作
while (true) {
sellTicket();
}
}
// 同步方法
private synchronized void sellTicket() {
// 判断一下, 当票数大于0时就卖出一张票
if (ticket > 0) {
// 卖之前小睡一会儿
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票"); // 2:7 // "窗口2卖出了第7张票"
// 减少票数
ticket--;
}
}
}
public class Test {
public static void main(String[] args) {
// 创建卖票任务
SellTicketRunnable runnable = new SellTicketRunnable();
// 创建3个卖票窗口
Thread window1 = new Thread(runnable);
Thread window2 = new Thread(runnable);
Thread window3 = new Thread(runnable);
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
// 开始卖票
window1.start();
window2.start();
window3.start();
}
}
```
--------------------------------------------
## 今日扩展
**如果你发现运行几次多线程代码后电脑越来越卡, 看一下Eclipse控制台上的小小显示器按钮, 点击右侧三角, 这里都是控制台运行的程序, 如果没有写`<terminated>`, 则说明你的多线程
程序可能没有结束, 仍然在运行, 点击红色方块结束**
### 继承Thread类和实现Runnable接口在共享资源上有什么区别
* 继承Thread类
* Thread类中的非静态成员变量是每个线程各自拥有的, 不能作为共享资源
* 静态成员变量是所有线程对象共享的, 可以作为共享资源
* 实现Runnable接口, 如果为所有Thread对象都传入同一个Runnable对象, 则
* Runnable对象中的非静态成员变量会被所有Thread对象(也就是所有线程)共享, 可以作为共享资源
* Runnable对象中的静态成员变量也会被所有Thread对象共享, 也可以作为共享资源
* 综上所述
* 继承Thread类方式下, 共享资源要定义为Thread类的静态资源
* 实现Runnable接口方式下, 共享资源要定义为Runnable接口的静态或非静态资源, 且所有Thread对象都要传入同一个Runnable对象
### 实现多线程的第3种方法: 创建线程池 thread pool
```
线程池(存放Thread对象的池子)
+-------------------------------------+
| |
| 线程1 o---------------o |
| 线程2 o---------------o |
| 线程3 o---------------o |
| 线程4 o---------------o |
| 线程5 o---------------o |
| |
+-------------------------------------+
需要使用3个线程, 从线程池中获取线程
+-------------------------------------+
| |
| | 执行 线程1 o---------------o
| | 执行 线程2 o---------------o
| | 执行 线程3 o---------------o
| 线程4 o---------------o |
| 线程5 o---------------o |
| |
+-------------------------------------+
线程执行完毕后再将Thread放回线程池
+-------------------------------------+
| |
| 线程1 o---------------o |
| 线程2 o---------------o |
| 线程3 o---------------o |
| 线程4 o---------------o |
| 线程5 o---------------o |
| |
+-------------------------------------+
```
* 方法3: `Executors`线程池+`Callable`实现类
* 定义类, 实现`Callable`接口, 重写call()方法, 注意Callable接口有泛型, call()方法要有泛型类的返回值.
* 使用`Executors`类创建线程池, 并提交`Callable`实现类启动任务
* 3种创建线程池的方式, 返回线程池实现类
* `static ExecutorService newCachedThreadPool()`: 创建新的线程池, 为每个新传入的任务创建一个线程
* `static ExecutorService newFixedThreadPool(int nThreads)`: 创建固定数量线程的线程池(常用)
* `static ExecutorService newSingleThreadExecutor()`: 创建只有一个线程的线程池
* 线程启动和销毁, 使用`ExecutorService`对象的成员方法
* `Future submit(Callable<T> task)`: 提交线程, 让线程池线程运行起来, 一个返回值的任务
* `Future submit(Runnable task)`: 同上, 只是参数不同
* `shutdown()`: 通知线程执行完任务后关闭. 注意该方法不会在调用时立刻停止线程, 而是让线程执行完任务后停止. 如果不调用此方法, 则线程任务执行完后
仍然在运行
* 创建几个线程就创建几个实现类对象并执行几个submit, submit会返回Future类的线程运行结果.
* 这种方法创建的线程只在最初创建一次, 执行完毕后回到线程池中等待, 除非调用线程池的shutdown方法关闭线程池才会销毁, 节省资源且效率高
* 示例代码
```java
/*
线程池
*/
public class Test {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// method1();
// method2();
}
// 执行Callable任务
private static void method2() throws InterruptedException, ExecutionException {
// 创建一个固定数量的线程池, 内部保存3个线程对象
ExecutorService executorService = Executors.newCachedThreadPool();
// 提交Callable实现类任务
MyCallable callable = new MyCallable();
Future<?> future1 = executorService.submit(callable);
Future<?> future2 = executorService.submit(callable);
Future<?> future3 = executorService.submit(callable);
// 仍然需要通知线程执行完后停止
executorService.shutdown();
// 查看结果, 是有结果的, 因为call()方法有返回值
System.out.println(future1.get());
System.out.println(future2.get());
System.out.println(future3.get());
}
// 执行Runnable任务
private static void method1() throws InterruptedException, ExecutionException {
// 创建一个固定数量的线程池, 内部保存3个线程对象
ExecutorService executorService = Executors.newFixedThreadPool(3);
// submit一次就启动一个线程
MyRunnable runnable = new MyRunnable();
Future<?> future1 = executorService.submit(runnable, "结果"); // 要想有结果, 要在这里传
Future<?> future2 = executorService.submit(runnable); // 不传就没有结果
Future<?> future3 = executorService.submit(runnable); // 不传就没有结果
// 调用shutdown()方法通知线程执行完任务后关闭
executorService.shutdown();
// 查看结果
System.out.println(future1.get()); // 结果
System.out.println(future2.get()); // null
System.out.println(future3.get()); // null
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
// 返回线程执行的结果
return Thread.currentThread().getName() + "执行完毕";
}
}
```
### 线程的生命周期
* 线程的声明周期中有5种状态
1. 创建: new一个线程对象, 此时还没有调用start()
2. 就绪: 调用start()方法后, 进入就绪状态, 等待CPU执行
3. 运行: 获取了CPU的执行权, 开始运行线程
4. 阻塞: 调用了sleep(), wait(), 或由于IO操作导致阻塞. 阻塞解除后仍会返回就绪状态, 等待CPU执行
5. 销毁: 线程执行完毕
```
+---------------------------+
| 阻塞 |
+--v----------------------^-+
| |
|解除阻塞 被阻塞|
new | |
+------+ +--v---+ +--^---+ +------+
| | start() | >--------------> | 线程执行结束 | |
| 创建 >------------> 就绪 | CPU调度 | 运行 >---------------> 销毁 |
| | | <--------------< | | |
+------+ +------+ +------+ +------+
```
### 线程之间的通信
* 使用`Object`类的成员方法
* `void wait()`: 使当前线程处于等待状态, 并且会立刻释放锁
* `void notify()`: 随机唤醒一个处于等待状态的线程
* `void notifyAll()`: 唤醒所有处于等待状态的线程
* 注意: **这三个方法必须在同步代码块中, 且只能用锁对象来调用, 否则会抛异常**
* `sleep()`和`wait()`的区别
* sleep
* 让当前线程在指定时间内睡眠, 时间结束后继续执行
* 是Thread类的静态方法
* 不会释放锁
* wait
* 让当前线程等待, 直到有人唤醒
* 是Object类的非静态方法
* 等待会立刻释放锁
* 注意:
* wait的线程必须要有人notify才能唤醒, 如果所有的线程都wait了, 那程序整个就全都在等待了. 所以在写代码时必须考虑仔细
```java
// 2个窗口交替卖票示例
public class Test {
public static void main(String[] args) {
Window window1 = new Window();
Window window2 = new Window();
window1.start();
window2.start();
}
}
class Window extends Thread {
private static int ticket = 10;
private static Object lock = Window.class;
@Override
public void run() {
while (true) {
synchronized (lock) {
if (ticket > 0) {
System.out.println(getName() + "卖出票:" + ticket);
ticket--;
// 卖出一张, 换线程
lock.notify();
System.out.println(getName() + "说: 肚子疼, 换人!");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 通知所有人下班
lock.notifyAll();
System.out.println(getName() + "说: 票卖完了, 大家下班吧!");
break;
}
}
}
}
}
```
### 线程释放锁的3种情况
1. synchronized同步代码执行完毕
2. 线程发生了异常导致线程终止
3. 线程调用了`wait()`方法进行等待, 等待会立刻释放锁
### 常见线程问题: 死锁
* 死锁: dead lock
* 同步代码块中的线程不出来, 也不释放锁; 同步代码块外的线程拿不到锁, 只能等在外面.
* 发生死锁的原因:
1. 同步代码块内的线程, 可能处在死循环, IO阻塞, sleep()状态, 导致内部持有锁的线程无法出同步代码块
2. 多个线程互相持有锁又不释放锁: 两个线程执行的任务都是双层同步代码块, 每层同步都需要一个锁, 两个线程中同步代码块的锁是相反的
* 死锁的结果: 程序卡死, 无法继续执行
* 如何避免死锁:
* 避免在同步代码块中执行死循环, IO阻塞操作, sleep()
* 避免多个线程互相持有锁又不释放锁的情况
```java
// 嵌套同步代码块演示死锁
public class Test {
// 定义2个锁对象
public static String lock1 = "{锁1}";
public static String lock2 = "{锁2}";
public static void main(String[] args) {
// 创建2个线程对象
MyThread1 thread1 = new MyThread1();
MyThread2 thread2 = new MyThread2();
thread1.setName("MyThread1");
thread2.setName("MyThread2");
// 开始执行
thread1.start();
thread2.start();
// 注意观察控制台的小红点
}
}
class MyThread1 extends Thread {
@Override
public void run() {
System.out.println(getName() + "线程开始执行run()");
// 外层同步代码块, 使用锁1
synchronized (Test.lock1) {
System.out.println(getName() + "线程拿到了" + Test.lock1);
// 睡一下
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 内层同步代码块, 使用锁2
synchronized (Test.lock2) {
System.out.println(getName() + "持有2把锁");
}
}
}
}
class MyThread2 extends Thread {
@Override
public void run() {
System.out.println(getName() + "线程开始执行run()");
// 外层同步代码块, 使用锁2
synchronized (Test.lock2) {
System.out.println(getName() + "线程拿到了" + Test.lock2);
// 睡一下
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 内层同步代码块, 使用锁1
synchronized (Test.lock1) {
System.out.println(getName() + "持有2把锁");
}
}
}
}
```
### 另一种加锁方式: Lock类
* JDK5增加
```java
// 创建锁对象, 该锁对象也要所有线程共享唯一一个
private Lock lock = new ReentrantLock();
lock.lock(); // 加锁, 相当于执行到synchronized
// 同步的代码
lock.unlock(); // 释放锁, 相当于同步代码块执行完毕
```
------------------------
## 今日总结
* 概念
* 进程: Process, 一个应用程序在内存中的执行区域
* 线程: Thread, 进程中的一条执行路径
* 并发: 并行发生, 同时发生, 多线程就可以实现并发
* 同步: 注意并不是同时的意思, 同步是指一步接一步的执行, 一个执行完毕再开始执行下一个. 单线程就是同步
* 异步: 不是一步一步执行, 而是同时执行多步, 每个步骤何时结束不确定. 多线程就是异步
* 阻塞: 上一行代码正在执行, 还没有执行完毕, 程序就阻塞在这里了, 下一行代码必须等上一行不再阻塞后才能执行
* 单线程和多线程的特点
* 单线程: 同一时间只做一件事, 安全性高, 效率低
* 多线程: 同一时间做多个事情, 安全性低, 效率高
* 多线程的实现方式
1. 继承`Thread`类, 重写run方法
2. 实现`Runnable`接口(仍然是创建Thread类对象), 重写run方法
* `java.lang.Thread`类: 实现了`Runnable`接口
* 构造方法
* `Thread Thread()`: 创建Thead对象
* `Thread Thread(Runnable r)`: 通过Runnable对象创建Thread对象
* `Thread Thread(Runnable r, String threadName)`: 通过Runnable对象创建Thread对象并指定线程名
* 成员方法
* `void start()`: 启动线程, 即让线程开始执行`run()`方法中的代码
* `String getName()`: 获取线程的名称
* `void setName(String name)`: 设置线程名称
* 静态方法
* `static Thread currentThread()`: 返回对当前正在执行的线程对象的引用
* `static void sleep(long millis)`: 让所在线程睡眠指定的毫秒
* 多线程中的常见问题
1. 资源共享: 卖票问题
* 共享资源定义位置: 共享资源要定义在多个线程能够共同使用的地方, 如:
* 多个Thread共用同一个Runnable实现类对象, 则定义为Runnable实现类的非静态成员变量
* 如果只用Thread子类, 则可以定义为Thread子类的静态成员变量
* 操作共享数据的线程安全问题: 使用同步解决
* 同步代码块
* `synchronized (锁对象) {}`
* 锁对象
* 必须是多个线程共享的对象:
* 一个类的Class对象
* 如果是实现Runnable, 则可以是this
* 同步方法
* `public (static) synchronized void method() {}`
* 锁对象
* 静态同步方法, 锁对象是: 方法所在类的Class对象
* 非静态同步方法, 锁对象是: this
2. 不同线程之间通信:
* wait(), notify(), notifyAll()
* 这三个方法必须在同步代码块中, 用锁对象来调用, 否则会抛异常
3. 死锁: 同步代码块中的线程不出来, 也不释放锁; 同步代码块外的线程拿不到锁, 只能等在外面.
* 发生死锁的原因:
* 同步代码块内的线程, 可能处在死循环, IO阻塞, 或sleep状态
* 多个线程互相持有锁又不释放锁
# day12 网络
## 网络编程概述
* 计算机网络
* 指将地理位置不同的具有独立功能的多态计算机及其外部设备, 通过通信线路连接起来, 在网络操作系统, 网络管理软件及网络通信协议的管理和协调下, 实现资源共享和信息传
递的计算机系统
* 网络编程
* 让不同计算机上运行的程序可以通过网络进行数据交换
* Socket: 中文译名为`套接字`(记住).
* 不是协议, 而是对协议的封装, 提供了编程规范接口, 让程序员可以通过它来实现相关网络协议
* 用于描述IP地址和端口. 是一种网络编程机制. 通信两端都有Socket
* 计算机上一般会有多个服务程序, 每个服务程序会使用多个Socket进行网络通信, 每个Socket会绑定到一个端口上, 网络通信就是Socket之间的通信, 数据在两个Socket之间通过
**IO**传输
* 实际上Socket名词是插座的意思, 动词是插入插座的意思. 两台计算机通过网络通信, 可以把这种网络连接看做把网线插头插入网口插座一样, 网线两端的插头插入两台计算机的
网口插座, 就可以通过网线进行网络通信.
## 网络通信三要素
* 网络通信三要素
* `传输协议`:
* 作用: 通信的规则
* 常见协议
* TCP: Transmission Control Protocol, 传输控制协议
* UDP: User Datagram Protocol, 用户数据报协议
* `IP地址`: 是一个二进制数字
* 作用: 网络设备的标识.
* IPv4: 192.168.100.255 (每位0~255, 32位, 4个无符号byte), `点分十进制表示法`
* IPv6: CDCD:910A:2222:5498:8475:1111:3900:2020 (128位, 16个无符号byte)
* IP可以使用主机名或域名代替, 更容易记忆
* `端口号`:
* 作用: 标识使用网络通信的进程的逻辑地址, 用于定位一个主机上的具体服务
* 0~65535个端口, 前1024个端口号是系统保留端口号
* 常见服务占用的端口
* 80: HTTP服务
* 443: HTTPS服务, 安全加密的HTTP
* 21: FTP服务, 文件传输
* 22: SSH服务, 安全加密的远程登录
* 23: Telnet服务, 远程登录
* 查看端口号: `netstat -an`
```
www.baidu.com - DNS -> 0.1.1.1
http:// 192.168.1.1 : 8080
------- ----------- ----
通信协议 IP 端口号
```
* 举例理解三要素(寄快递)
* 传输协议: 用顺丰, 申通还是中通?
* IP地址: XX省XX市XX街道XX号XX大厦
* 端口号: 8楼
* UDP和TCP区别
* UDP: USER DATAGRAM PROTOCOL, 用户数据报协议.
* 特点:
* 无连接的不可靠协议
* 数据传输大小限制为64K(一个包)
* 不需要建立连接即可传输
* 数据发送速度快, 发送方只发送数据, 不检查接收方是否真正接收到数据, 所以数据可能有丢包的情况
* 适用场景
* 适合实时性要求强的场合, 比如网络电话, 网络游戏等环境, 这种协议延迟很小
* TCP: TRANSMISSION CONTROL PROTOCOL, 传输控制协议
* 特点:
* 需要建立连接的可靠协议
* 没有数据传输大小的限制
* 在传输前需要先建立连接(三次握手)
* 它的重发机制保证了数据传输的准确性, 但因为需要接收方发回验证信息, 所以数据发送时间长, 数据流量大
* 适用场景
* 这种方式适合准确性要求强的场合, 比如金融系统, 视频点播, 用户可以等待数据传输但是不能忍受错误
## InetAddress概述和测试
* `java.net.InetAddress`类: 用于表示IP地址对象, 可以获取主机名, IP地址等信息
* 静态方法
* `static InetAddress getLocalHost()`: 获取本机的InetAddress对象
* `static InetAddress getByName(String host)`: 根据主机名或IP的字符串获取主机的InetAddress对象
* `static InetAddress getLoopbackAddress()`: 获取回环地址的InetAddress对象 127.0.0.1 localhost
* `static InetAddress getByAddress(byte[] addr)`: 根据IP获取InetAddress对象
* 如: `InetAddress.getByAddress(new byte[]{(byte)192, (byte)168, (byte)1, (byte)1});`
* 成员方法
* `String getHostAddress()`: 返回主机的IP地址
* `String getHostName()`: 返回主机名
* `String getCanonicalHostName()`: 获取此IP地址的完全限定域名
* 5分钟练习: 测试InetAddress方法
* 创建项目s2-day12, 建包com.itheima.practice_01
* 建类Test
* 获取InetAddress对象
* 获取本机IP地址和主机名, 并打印
```java
/*
利用InetAddress打印出自己电脑的IP地址和主机名
*/
public class Test {
public static void main(String[] args) throws UnknownHostException {
// 获取本机的InetAddress对象
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost);
// 回环地址
InetAddress loopbackAddress = InetAddress.getLoopbackAddress();
System.out.println(loopbackAddress); // localhost/127.0.0.1
// 通过主机名获取
InetAddress ipByName = InetAddress.getByName("lishaoqing");
System.out.println(ipByName);
// 可以根据IP获取吗? /192.168.12.52 返回的
InetAddress ipByAddress = InetAddress.getByAddress(new byte[]{(byte) 192, (byte) 168, 12, 52});
System.out.println(ipByAddress);
System.out.println(ipByAddress.getHostName()); // 主机名
}
}
```
---
## UDP协议发送数据
* `java.net.DatagramSocket`类: 基于UDP协议的Socket
* 构造方法
* `DatagramSocket()`: 创建DatagramSocket对象, 随机分配端口号
* `DatagramSocket(int port)`: 创建DatagramSocket对象, 指定端口号
* 成员方法
* `void send(DatagramPacket p)`: 发送数据包
* `void receive(DatagramPacket p)`: 接收数据, 数据保存在DatagramPacket对象中
* `void close()`: 关闭通信, 释放资源
* `java.net.DatagramPacket`类: UDP数据包
* 构造方法
* `DatagramPacket DatagramPackage(byte[] msg, int msgLength, InetAddress host, int port)`: 创建数据包对象, 指定数据, 目标主机对象, 端口
* `DatagramPacket(byte[] buf, int length)`: 创建数据包对象, 接收数据为length的数据, 存入byte数组中
* 成员方法
* `byte[] getData()`: 获取包中的数据, 以byte数组形式
* `int getLength()`: 获取数据包中数据的长度, 即byte数组的长度
* `int getPort()`: 获取发送方端口号
* `InetAddress getAddress()`: 获取数据包发送方的InetAddress对象
* UDP发送数据步骤
1. 为发送端创建Socket对象(DatagramSocket):
* `DatagramSocket udp = new DatagramSocket();`
2. 创建数据并打包:
* `DatagramPacket packet = new DatagramPacket(buf, buf.length, address, port);`
3. 发送数据:
* `DatagramSocket`对象的`void send(DatagramPackage p)`
4. 释放资源:
* `DatagramSocket`对象的`void close()`
* 5分钟练习: 发送UDP数据
* 继续使用项目s2-day12, 建包com.itheima.practice_02
* 建类Test
* 创建UDP Socket对象(DatagramSocket)
* 创建数据并打包
* 创建字符串数据, 转为byte数组
* 获取localhost的InetAddress对象
* 定义端口
* 创建DatagramPacket对象, 将数据传入
* 发送数据
* 释放资源
```java
/*
向本地主机,端口号8888的应用程序发送
”Hello,I am comming”
利用DatagramSocket和DatagramPacket完成发送端
*/
public class UDPSender {
public static void main(String[] args) throws IOException {
// 创建DatagramSocket对象
DatagramSocket socket = new DatagramSocket();
// 准备数据
String s = "hello, im udp!";
byte[] bys = s.getBytes();
InetAddress ip = InetAddress.getLocalHost();
// 创建数据包
DatagramPacket packet = new DatagramPacket(bys, bys.length, ip, 8888);
// 发送包
socket.send(packet);
// 释放资源
socket.close();
}
}
```
## UDP协议接收数据
* **UDP不区分发送端和客户端**, 所以都使用DatagramSocket即可
* UDP接收数据步骤
1. 创建接收端Socket对象:
* `DatagramSocket(int port)`
2. 接收数据
* 创建包对象: `DatagramPacket datagramPackage(byte[] buf, int length)`
* 接收包: `DatagramSocket`对象的`void receive(DatagramPacket p)`, 该方法会阻塞等待接收数据
3. 解析数据
* 获取发送端信息
* `DatagramPacket`对象的`InetAddress getAddress()`: 获取客户端
* `DatagramPacket`对象的`byte[] getData()`: 获取数据
* `DatagramPacket`对象的`int getLength()`: 获取数据长度
4. 输出数据
5. 释放资源:
* `DatagramSocket`对象的`void close()`
* 5分钟练习: 创建UDP接收端
* 继续使用项目s2-day12, 建包com.itheima.practice_03
* 建类Test
* 创建DatagramSocket对象, 指定端口8888
* 准备接收数据
* 定义byte数组, 长度可以是1024
* 创建DatagramPacket对象, 传入数组和长度
* 调用DatagramSocket对象的receive(DatagramPacket p)方法等待数据
* 解析数据
* 调用DatagramPacket的getData()方法获取byte数组, 创建String对象
* 调用DatagramPacket的getAddress()方法获取发送端InetAddress对象
* 通过InetAddress对象获取主机名和IP地址
* 打印字符串和发送方信息
* 释放资源
```java
```
## UDP收发数据注意事项
* 端口绑定异常:
* `java.net.BindException: Address already in use: Cannot bind`: 端口已经被占用
* 如何解决: 要更改为其他端口号, 或将占用端口的应用关闭
---
## TCP协议发送数据
* `java.net.Socket`类: 基于TCP协议的Socket, 作为客户端
* 构造方法
* `Socket(InetAddress add, int port)`: 创建TCP客户端对象
* 成员方法
* `OutputStream getOutputStream()`: 获取输出流对象, 用于发送数据
* `InputStream getInputStream()`: 获取输入流, 用于接收数据
* `void close()`: 释放资源
* TCP发送数据步骤
1. 创建客户端Socket对象(建立连接):
* `Socket(InetAddress add, int port)`
2. 获取输出流对象:
* `Socket`对象的`OutputStream getOutputStream()`
3. 发送数据:
* `OutputStream`对象的`void write(byte[] b)`
4. 释放资源:
* `OutputStream`对象的`close()`
* `Socket`对象的`close()`
* 连接失败异常:
* `ConnectException: Connection refused: connect`: 连接被拒绝, 无法创建连接. 一般是因为网络不通, IP地址不存在等导致无法连接
* 解决办法:
* 确认必须有接收端, 且可以连接
* 5分钟练习: TCP客户端
* 继续使用项目s2-day12, 建包com.itheima.practice_04
* 建类Test
* 创建客户端Socket对象, 指定本机和端口
* 获取字节输出流对象
* 使用字节输出流对象将数据写出
* 释放资源
```java
/*
利用TCP协议向本地主机,10086端口号的应用程序
发送”Hello tcp,im coming!!”
*/
public class TCPClient {
public static void main(String[] args) throws UnknownHostException, IOException {
// 创建客户端Socket对象
// java.net.ConnectException: Connection refused: connect
// TCP特有的连接异常, TCP必须先建立连接才能通信, 所以如果连接不上就会抛出异常报错
Socket socket = new Socket(InetAddress.getByName("192.168.80.1"), 10086);
// 通过socket获取字节输出流对象
OutputStream out = socket.getOutputStream();
// 通过流将数据发送出去
out.write("Hello tcp,im coming!!".getBytes());
// 释放资源
socket.close();
}
}
```
## TCP协议接收数据
* `java.net.ServerSocket`: TCP服务端
* 构造方法
* `ServerSocket(int port)`: 创建一个TCP服务端, 并监听指定端口
* 成员方法
* `Socket accept()`: 监听数据, 会阻塞. 收到数据后返回Socket对象
* `void close()`: 关闭Socket
* TCP接收数据步骤
1. 创建服务端ServerSocket对象:
* `ServerSocket(int port)`
2. 监听数据:
* `ServerSocket`对象的`Socket accept()`, 获取客户端Socket对象
* 监听时是阻塞的
3. 获取输入流对象:
* `Socket`对象的`InputStream getInputStream()`
4. 获取数据:
* `InputStream`对象的`int read(byte[] buf)`
5. 输出数据:
* 将获取的字节数组转换为String打印输出
6. 释放资源:
* `Socket`对象的`void close()`方法
* `ServerSocket`对象的`void close()`方法
* 5分钟练习: 创建TCP接收端
* 继续使用项目s2-day12, 建包com.itheima.practice_05
* 建类Test
* 创建ServerSocket对象, 指定端口
* 调用accept()方法监听客户端连接, 返回客户端Socket对象
* 使用客户端Socket对象获取字节输入流对象
* 使用字节输入流对象读取字节数组, 转换为String
*
```java
/*
利用TCP协议接收本地主机发送过来的
”Hello tcp,im coming!!”,打印到控制台上
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
// 创建服务端ServerSocker
ServerSocket serverSocket = new ServerSocket(10086);
// 等待客户端连接
System.out.println("TCP服务端已启动, 等待客户端连接...");
Socket socket = serverSocket.accept();
// 通过socket对象获取输入流
InputStream in = socket.getInputStream();
// 从输入流中获取字节数组
byte[] bys = new byte[1024];
int len;
len = in.read(bys);
// 将读取到的字节数组转换为String
String result = new String(bys, 0, len);
// 通过socket可以获取客户端信息
System.out.println(socket.getInetAddress());
System.out.println("服务端接收到数据:" + result);
// 释放资源
socket.close();
// ServerSocket可以不关
// serverSocket.close();
}
}
```
--=
## TCP协议案例: 客户端发送, 服务端处理后返回客户端
* 注意:
* Socket客户端
* 发送数据给服务端: 使用Socket对象获取输出流对象OutputStream, write()
* 接收服务端的数据: 使用Socket对象获取输入流对象InputStream, read()
* ServerSocket服务端
* 接收客户端的数据: 使用accpet()方法返回的Socket对象获取输入流对象InputStream, read()
* 发送数据给客户端: 使用accpet()方法返回的Socket对象获取输出流对象OutputStream, write()
* 5分钟练习: 服务端写回大写数据
* 继续使用项目s2-day12, 建包com.itheima.practice_06
* 建类TCPClient
* 创建客户端
* 发送数据
* 接收服务端返回的数据
* 释放资源
* 建类TCPServer
* 创建服务端
* 接收数据, 返回Socket对象
* 获取数据
* 转换大写
* 写回大写数据
* 释放资源
```java
/*
利用TCP协议完成以下需求:
客户端发给服务端 数据
服务端将客户端发送过来的数据中的
字母全部转换为大写,写给客户端
客户端打印转换后的数据
*/
public class TCPClient {
public static void main(String[] args) throws UnknownHostException, IOException {
// 创建客户端Socket对象
// java.net.ConnectException: Connection refused: connect
// TCP特有的连接异常, TCP必须先建立连接才能通信, 所以如果连接不上就会抛出异常报错
Socket socket = new Socket(InetAddress.getByName("192.168.80.1"), 10010);
// 通过socket获取字节输出流对象
OutputStream out = socket.getOutputStream();
// 通过流将数据发送出去
out.write("hello tcp,im coming!!".getBytes());
// 接收服务端返回的数据
InputStream in = socket.getInputStream();
byte[] bys = new byte[1024];
int len;
len = in.read(bys);
// 将byte[]转换为String
String response = new String(bys, 0, len);
System.out.println("客户端接收到服务端返回的数据:" + response);
// 释放资源
socket.close();
}
}
/*
利用TCP协议接收本地主机发送过来的
”Hello tcp,im coming!!”,打印到控制台上
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
// 创建服务端ServerSocker
ServerSocket serverSocket = new ServerSocket(10010);
// 等待客户端连接
System.out.println("TCP服务端已启动, 等待客户端连接...");
Socket socket = serverSocket.accept();
// 通过socket对象获取输入流
InputStream in = socket.getInputStream();
// 从输入流中获取字节数组
byte[] bys = new byte[1024];
int len;
len = in.read(bys);
// 将读取到的字节数组转换为String
String result = new String(bys, 0, len);
// 通过socket可以获取客户端信息
System.out.println("服务端接收到客户端发来数据:" + result);
// 将字符串转大写返回
String response = result.toUpperCase();
// 获取输出流
OutputStream out = socket.getOutputStream();
out.write(response.getBytes());
// 释放资源
socket.close();
// ServerSocket可以不关
// serverSocket.close();
}
}
```
---
## 模拟用户登录
* 要点
* 客户端
* 使用BufferedReader结合转换流InputStreamReader获取System.in键盘录入的数据
* 使用PrintWriter结合client的OutputStream发送数据
* 服务端
* 使用BufferedReader结合转换流InputStreamReader获取client的InputStream数据
* 使用PrintWriter结合client的OutputStream发送数据
* 5分钟练习: 模拟用户登录
* 继续使用项目s2-day12, 建包com.itheima.practice_06
* 建类TCPClient
* 创建Socket对象
* 将System.in通过转换流转换为BufferedReader
* 读取键盘录入的用户名和密码
* 使用PrintWriter包装client的OutputStream对象, 开启自动刷新
* 使用println()方法发送用户名和密码
* 将client的InputStream通过转换流转换为BufferedReader
* readLine()获取登录结果, 打印
* 释放资源
* 建类TCPServer
* 创建ServerSocket对象
* accept()接收客户端Socket
* 将client的InputStream通过转换流转换为BufferedReader
* readLine()两次获取用户名和密码
* 判断用户名和密码是否匹配, 根据其写回结果
* 使用PrintWriter包装client的OutputStream对象, 开启自动刷新
* 使用println()方法发送登录结果
* 释放资源
```java
public class Client {
public static void main(String[] args) throws UnknownHostException, IOException {
// 创建客户端
Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
// 获取键盘输入的用户名和密码
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 提示输入用户名
System.out.println("请输入用户名:");
String username = br.readLine();
// 提示输入密码
System.out.println("请输入密码:");
String password = br.readLine();
// 将输入的用户名和密码写出, 使用PrintWriter
PrintWriter pw = new PrintWriter(socket.getOutputStream(), true);
pw.println(username);
pw.println(password);
// 等待服务器返回结果
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String result = bufferedReader.readLine();
System.out.println(result);
// 释放资源
socket.close();
}
}
public class Server {
public static void main(String[] args) throws IOException {
String user = "zhangsan";
String pass = "123";
// 创建服务端
ServerSocket serverSocket = new ServerSocket(9999);
// 等待客户端连接
System.out.println("服务端已启动, 等待客户端连接");
Socket socket = serverSocket.accept();
// 读取客户端发来的用户名和密码, 使用BufferedReader
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 读取用户名和密码
String username = br.readLine();
String password = br.readLine();
// 判断用户名和密码是否相同
boolean canLogin = false;
if (user.equals(username) && pass.equals(password)) {
canLogin = true;
}
// 写回结果
PrintWriter pw = new PrintWriter(socket.getOutputStream(), true);
String result = "";
if (canLogin) {
result = "登录成功";
} else {
result = "登录失败";
}
pw.println(result);
// 释放资源
socket.close();
}
}
```
## 模拟用户登录案例改写
* 要点
* 定义集合保存已有用户数据
* 服务端判断用户信息是否匹配时, 按照集合中是否包含User对象判断
* 5分钟练习: 改写模拟用户登录
* 继续使用项目s2-day12, 建包com.itheima.practice_07
* 复制上一个练习代码
* 建类User
* 定义成员变量username, password
* 生成equals()
* 建类UserDB
* 定义静态成员变量private static List<User> users = new ArrayList<>();
* 在静态代码块中初始化users, 添加几个user对象作为已有用户数据
* 生成users的get, set方法
* 改写TCPServer
* 将获取到的username和password封装为User对象
* 调用UserDB的getUsers()方法获取用户数据集合, 调用contains()方法判断是否包含该对象, 用来判断是否登录成功
* 运行测试
```java
public class User {
private String username;
private String password;
public User() {
super();
}
public User(String username, String password) {
super();
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (password == null) {
if (other.password != null)
return false;
} else if (!password.equals(other.password))
return false;
if (username == null) {
if (other.username != null)
return false;
} else if (!username.equals(other.username))
return false;
return true;
}
@Override
public String toString() {
return "User [username=" + username + ", password=" + password + "]";
}
}
/*
模拟存储用户信息的数据库
*/
public class UserDB {
private static List<User> list = new ArrayList<>();
static {
list.add(new User("zhangsan", "123"));
list.add(new User("lisi", "234"));
list.add(new User("wangwu", "345"));
}
public static List<User> getList() {
return list;
}
}
public class Client {
public static void main(String[] args) throws UnknownHostException, IOException {
// 创建客户端
Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
// 获取键盘输入的用户名和密码
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 提示输入用户名
System.out.println("请输入用户名:");
String username = br.readLine();
// 提示输入密码
System.out.println("请输入密码:");
String password = br.readLine();
// 将输入的用户名和密码写出, 使用PrintWriter
PrintWriter pw = new PrintWriter(socket.getOutputStream(), true);
pw.println(username);
pw.println(password);
// 等待服务器返回结果
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String result = bufferedReader.readLine();
System.out.println(result);
// 释放资源
socket.close();
}
}
public class Server {
public static void main(String[] args) throws IOException {
// 创建服务端
ServerSocket serverSocket = new ServerSocket(9999);
// 等待客户端连接
System.out.println("服务端已启动, 等待客户端连接");
Socket socket = serverSocket.accept();
// 读取客户端发来的用户名和密码, 使用BufferedReader
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 读取用户名和密码
String username = br.readLine();
String password = br.readLine();
// 将用户名和密码封装为对象
User user = new User(username, password);
// 判断已经注册的用户中是否包含当前用户信息
boolean canLogin = false;
if (UserDB.getList().contains(user)) {
canLogin = true;
}
// 写回结果
PrintWriter pw = new PrintWriter(socket.getOutputStream(), true);
String result = "";
if (canLogin) {
result = "登录成功";
} else {
result = "登录失败";
}
pw.println(result);
// 释放资源
socket.close();
}
}
```
-------------------------------
## 今日扩展
### IP
* IP地址最后一位的作用
* 0: 是网段地址
* 255: 广播地址
* 1~254: 有效IP地址
* 特殊IP
* `127.0.0.1`: 本机回环地址, 也就是自己的电脑. 可以用`localhost`主机名代替
### 网络分层模型
* 2种分层模型
* OSI模型
* TCP/IP模型
```
对应关系
OSI模型 TCP/IP模型
+-----------+ +------------+
| 应用层 | | |
+-----------+ | |
| 表示层 | | 应用层 |
+-----------+ | |
| 会话层 | | |
+-----------+ +------------+
| 传输层 | | 传输层 |
+-----------+ +------------+
| 网络层 | | 网际层 |
+-----------+ +------------+
| 数据链路层 | | |
+-----------+ | 主机至网络层 |
| 物理层 | | |
+-----------+ +------------+
```
### Socket, IP, TCP, UDP, TCP/IP, HTTP之间有什么关系
* IP: Internet Protocol, 互联网协议, 对应网络层
* TCP, UDP协议对应传输层
* HTTP协议对应应用层
* TCP/IP: TCP协议和IP协议的组合, 但其中包含上百种协议
* Socket是对TCP/IP协议的封装, 它不是协议, 而是一种API, 使用Socket变成就可以使用TCP/IP协议, 也可以使用TCP, UDP
### TCP建立连接时的三次握手
* 第一次握手: 客户端发送syn包(syn=j)到服务器, 并进入SYN_SEND状态, 等待服务器确认
* 第二次握手: 服务器收到syn包, 必须确认客户的SYN(ack=j+1), 同时自己也发送一个SYN包(syn=k), 即SYN+ACK包,此时服务器进入SYN_RECV状态
* 第三次握手: 客户端收到服务器的SYN+ACK包, 向服务器发送确认包ACK(ack=k+1), 此包发送完毕, 客户端和服务器进入ESTABLISHED状态, 完成三次握手
* 握手过程中传送的包里不包含数据, 三次握手完毕后, 客户端与服务器才正式开始传送数据
### 一些常见名词
* 域名: domain name, 由一串用点分隔的名字组成的Internet上某一台计算机或计算机组的名称.
* 用于替代IP地址, 让主机名称容易记住
* 一个IP可以绑定多个域名, 一个域名只能指向一个IP
* 如`www.baidu.com`中,
* `.com`: 顶级域名
* `baidu.com`: 一级域名
* `www.baidu.com`: 二级域名
* DNS: Domain Name System, 域名系统
* 用于将域名映射到IP地址的一种服务
* DNS解析: 浏览器输入网址后, 请求发送到DNS服务器, DNS服务器在内部根据网址的域名查找对应的IP, 这一过程称为DNS解析. 解析完成后, 请求会根据IP找到对应的主机, 将请求发送到
主机的对应服务程序中
* CDN: Content Delivery Network, 内容分发网络.
* 在多个地理位置的服务器中存放相同的资源, 当用户请求时, CDN系统会根据节点的负载情况将用户的请求重新定向到离客户最近的服务节点上, 以达到加快响应速度的目的
* CDN一般存储的都是静态资源
### 使用TCP模拟浏览器访问百度
* 很简单, 正常创建Socket客户端, 只需要发送HTTP格式的报文即可
```
public class Test {
public static void main(String[] args) throws IOException {
// 创建客户端
InetAddress baidu = InetAddress.getByName("www.baidu.com");
Socket client = new Socket(baidu, 80); // HTTP默认80端口
// -----------发送HTTP请求
// 创建打印流
PrintWriter printWriter = new PrintWriter(client.getOutputStream(), true);
// 写出HTTP报文
printWriter.println(request);
// -----------接收HTTP响应
// 创建缓冲输入流
BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream(), "utf-8"));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
// 释放资源
client.close();
}
static String request =
// "GET / HTTP/1.1" + System.lineSeparator() + // 访问百度首页
"GET /s?wd=hello HTTP/1.1" + System.lineSeparator() + // 搜索hello
"Host: www.baidu.com" + System.lineSeparator();
// "Host: www.baidu.com" + System.lineSeparator()
}
```
---------------------
## 今日总结
* Socket
* 套接字
* 用于描述IP地址和端口, 是一种网络编程机制, 通信的两端都会有Socket
* 网络通信3要素
* 传输协议
* IP地址
* 端口号
* 常见通信协议
* UDP
* 它是一种无连接的不可靠协议
* 数据传输大小限制为64K(一个包)
* 不需要建立连接即可传输
* 数据发送速度快, 发送方只发送数据, 不检查接收方是否真正接收到数据, 所以数据有丢包的情况
* 这种方式适合实时性要求强的场合, 比如网络电话, 网络游戏等环境, 这种协议延迟很小
* TCP
* 它是一种需要建立连接的可靠协议
* 没有数据传输大小的限制
* 在传输前需要先建立连接(三次握手)
* 它的重发机制保证了数据传输的准确性, 但因为需要接收方发回验证信息, 所以数据发送时间长, 数据流量大
* 这种方式适合准确性要求强的场合, 比如金融系统, 视频点播, 用户可以等待数据传输但是不能忍受错误
* `java.net.InetAddress`类: 用于表示IP地址对象, 可以获取主机名, IP地址等信息
* 静态方法
* `static InetAddress getLocalHost()`: 获取本机的InetAddress对象
* `static InetAddress getByName(String host)`: 根据主机名或IP的字符串获取主机的InetAddress对象
* `static InetAddress getLoopbackAddress()`: 获取回环地址的InetAddress对象
* 成员方法
* `String getHostAddress()`: 返回主机的IP地址
* `String getHostName()`: 返回主机名
* `String getCanonicalHostName()`: 获取此IP地址的完全限定域名
* UDP相关类
* `java.net.DatagramSocket`类: 基于UDP协议的Socket
* 构造方法
* `DatagramSocket DatagramSocket()`: 创建DatagramSocket对象, 随机分配端口号
* `DatagramSocket DatagramSocket(int port)`: 创建DatagramSocket对象, 指定端口号
* 成员方法
* `void send(DatagramPacket p)`: 发送数据包
* `void receive(DatagramPacket p)`: 接收数据, 数据保存在DatagramPacket对象中
* `void close()`: 关闭通信, 释放资源
* `java.net.DatagramPacket`类: UDP数据包
* 构造方法
* `DatagramPacket DatagramPackage(byte[] msg, int msgLength, InetAddress host, int port)`: 创建数据包对象, 指定数据, 目标主机对象, 端口
* `DatagramPacket(byte[] buf, int length)`: 创建数据包对象, 接收数据为length的数据, 存入byte数组中
* 成员方法
* `byte[] getData()`: 获取包中的数据, 以byte数组形式
* `int getLength()`: 获取数据包中数据的长度, 即byte数组的长度
* `int getPort()`: 获取发送方端口号
* `InetAddress getAddress()`: 获取数据包发送方的InetAddress对象
* TCP相关类
* `java.net.Socket`类: 基于TCP协议的Socket
* 构造方法
* `Socket Socket(InetAddress add, int port)`
* 成员方法
* `OutputStream getOutputStream()`: 获取输出流对象, 用于发送数据
* `InputStream getInputStream()`: 获取输出流, 即接收数据
* `void close()`: 释放资源
* `java.net.ServerSocket`: TCP服务端
* 构造方法
* `ServerSocket ServerSocket(int port)`
* 成员方法
* `Socket accept()`: 监听数据, 会阻塞. 收到数据后返回Socket对象
* `void close()`: 关闭Socket
* UDP收发数据步骤
* 发送端
1. 为发送端创建Socket对象(DatagramSocket):
* `DatagramSocket udp = new DatagramSocket();`
2. 创建数据并打包:
* `DatagramPacket packet = new DatagramPacket(buf, buf.length, address, port);`
3. 发送数据:
* `DatagramSocket`对象的`void send(DatagramPackage p)`
4. 释放资源:
* `DatagramSocket`对象的`void close()`
* 接收端
1. 创建接收端Socket对象:
* `DatagramSocket DatagramSocket(int port)`
2. 接收数据
* 创建包对象: `DatagramPacket datagramPackage(byte[] buf, int length)`
* 接收包: `DatagramSocket`对象的`void receive(DatagramPacket p)`, 该方法会阻塞等待接收数据
3. 解析数据
* 获取发送端信息
* `DatagramPacket`对象的`InetAddress getAddress()`: 获取客户端
* `DatagramPacket`对象的`byte[] getData()`: 获取数据
* `DatagramPacket`对象的`int getLength()`: 获取数据长度
4. 输出数据
5. 释放资源:
* `DatagramSocket`对象的`void close()`
* TCP收发数据步骤
* 客户端
1. 创建客户端Socket对象(建立连接):
* `Socket Socket(InetAddress add, int port)`
2. 获取输出流对象:
* `Socket`对象的`OutputStream getOutputStream()`
3. 发送数据:
* `OutputStream`对象的`void write(byte[] b)`
4. 释放资源:
* `OutputStream`对象的`close()`
* `Socket`对象的`close()`
* 服务端
1. 创建服务端ServerSocket对象:
* `ServerSocket ServerSocket(int port)`
2. 监听数据:
* `ServerSocket`对象的`Socket accept()`, 获取客户端Socket对象
* 监听时是阻塞的
3. 获取输入流对象:
* `Socket`对象的`InputStream getInputStream()`
4. 获取数据:
* `InputStream`对象的`int read(byte[] buf)`
5. 输出数据:
* 将获取的字节数组转换为String打印输出
6. 释放资源:
* `Socket`对象的`void close()`方法
* `ServerSocket`对象的`void close()`方法
* 为什么要学反射:
* 反射的目的
* 反射可以在不修改源代码的前提下, 改变程序的运行方式
* 反射是后期web框架的底层实现基础, 帮助我们更好理解框架的原理
* 反射能干的事
* 前提条件: 获取一个类的字节码对象, 然后通过该字节码对象可以:
* 获取一个类的所有构造方法
* 创建一个类的对象
* 获取一个类的所有成员属性
* 获取属性值
* 设置属性值
* 获取一个类的所有成员方法
* 调用成员方法
## 反射机制概述, 获取字节码对象的3种方式
* 反射:
* Reflection. 在程序运行时, 获取任何一个类的所有属性和方法(包括私有的), 调用任意一个对象的所有属性和方法(包括私有的)
* 反射的前提
* 获取类的字节码对象
* 获取字节码对象的3种方法
* `对象.getClass()`
* `类名.class`
* `Class.forName(String clasName)`
## 反射获取构造方法并创建对象
* 反射使用的相关类和方法
* `java.lang.Class`类: 类的字节码对象
* 获取构造方法
* `Constructor<?>[] getConstructors()`: 以数组形式返回该类中所有public的构造方法. 如果没有public的, 则数组长度为0
* `Constructor<?>[] getDeclaredConstructors()`: 以数组形式返回该类中所有权限的构造方法, 包括private的. 如果该类是接口, 基本类型, 数组, void, 则
数组长度为0
* `Constructor<T> getConstructor(Class<?>... parameterTypes)`: 根据参数列表返回指定的public的构造方法. 参数列表填写参数的字节码对象
* `Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)`: 根据参数列表返回指定的所有权限的构造方法, 包括private的. 参数列表填写参
数的字节码对象
* 创建对象
* `T newInstance()`: 使用该类的无参构造创建一个对象
* `java.lang.reflect.Constructor`类: 构造方法对象
* `T newInstance()`: 通过无参构造方法对象创建一个类的对象
* `T newInstance(Object... initargs)`: 通过有参构造方法对象创建一个类的对象, 传入构造方法所需的参数列表
* `void setAccessible(boolean canAccess)`: 设置为true时禁用Java的访问安全检查, 可以访问所有权限的构造方法
* 注意:
* `Class`类和`Constructor`类中都有`T newInstance()`方法, 都使用类的无参构造创建对象
* 反射获取一个类的构造方法的步骤
1. 获取该类`Class`字节码对象(3种方式)
2. 通过`Class`对象获取构Constructor造方法对象
3. 通过`Constructor`对象创建该类的对象
* 5分钟练习: 获取类的构造方法创建对象
* 创建项目s2-day13, 建包com.itheima.practice_01
* 建类Test
* 获取类的字节码对象
* 通过类的字节码对象获取构造方法
* 通过构造方法创建对象
```java
/*
定义一个Student类
属性:name(私有的) age(公开的)
方法:无参构造、有参构造、setter和getter方法, toString方法
定义Test测试类
测试反射获取构造方法:
Class类
Constructor<?>[] getConstructors()
Constructor<T> getConstructor(Class<?>... parameterTypes)
Constructor类
T newInstance()
T newInstance(Object... initargs)
*/
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
// 反射前提: 获取一个类的字节码对象
Class studentClass = Class.forName("com.itheima.practice_01.Student");
// 使用字节码对象获取构造方法对象
//Constructor<?>[] getConstructors(): 获取所有public的构造方法
Constructor[] publicConstructors = studentClass.getConstructors();
for (Constructor constructor : publicConstructors) {
System.out.println(constructor);
}
// Constructor<T> getConstructor(Class<?>... parameterTypes): 通过指定参数类型获取对应的public构造方法
// 获取无参的
Constructor noParamConstructor = studentClass.getConstructor();
System.out.println(noParamConstructor);
// 获取有参的
Constructor twoParamConstructor = studentClass.getConstructor(String.class, int.class);
System.out.println(twoParamConstructor);
// 通过Constructor对象创建学生类的对象
// 通过无参构造创建
Object student1 = noParamConstructor.newInstance();
System.out.println(student1);
// 通过有参构造创建
Object student2 = twoParamConstructor.newInstance("zhangsan", 18);
System.out.println(student2);
// 同样也可以使用Class对象的newInstance()方法来通过无参构造创建对象
Object student3 = studentClass.newInstance();
System.out.println(student3);
}
}
```
---
## 反射获取public成员变量: 设置和获取值
* Class类中获取成员变量的方法
* `Field[] getFields()`: 获取所有public的成员变量
* `Field[] getDeclaredFields()`: 获取所有权限的成员变量, 包括private的
* `Field getField(String fieldName)`: 通过指定的成员变量名获取指定的public的成员变量
* `Field getDeclaredField(String fieldName)`: 通过指定的成员变量名获取指定的所有权限的成员变量, 包括private的
* `java.lang.reflect.Field`类: 成员变量对象
* `Object get(Object obj)`: 获取指定对象的属性值
* `void set(Object obj, Object value)`: 将指定对象的属性值设置为指定的值
* `void setAccessible(boolean canAccess)`: 设置为true时禁用Java的访问安全检查, 可以访问所有权限的成员属性
* 反射获取一个类的public成员变量的步骤
1. 获取该类`Class`字节码对象(3种方式)
2. 通过`Class`对象调用`newInstance()`方法创建该类的对象
3. 通过`Class`对象调用获取成员属性的方法获取属性对象
4. 通过`Field`对象设置或获取属性值
* 5分钟练习: 反射获取成员变量
* 继续使用项目s2-day13, 建包com.itheima.practice_02
* 复制上一个练习的Student类
* 建类Test
* 获取Student类的字节码对象
* 通过类的字节码对象创建Student对象
* 获取public的属性
* 设置属性值
* 获取属性值, 打印
```java
/*
定义一个Student类
属性:name(私有的) age(公开的)
方法:无参构造、有参构造、setter和getter方法, toString方法
定义Test测试类, 测试反射获取成员变量方法, 修改age的属性值, 然后获取修改后的值:
Class类
Field[] getFields
Field[] getDeclaredFields
Field getField(String fieldName)
Field getDeclaredField(String fieldName)
Field类
void set(Object obj, Object value)
Object get(Object obj)
*/
public class Test {
public static void main(String[] args) throws ReflectiveOperationException {
// 获取学生类的字节码对象
Class studentClass = Class.forName("com.itheima.practice_02.Student");
// 通过class对象创建学生对象
Object student = studentClass.newInstance();
// Field[] getFields: 获取所有public属性
Field[] publicFields = studentClass.getFields();
for (Field field : publicFields) {
System.out.println(field);
}
// Field[] getDeclaredFields: 获取所有的全部权限的属性
Field[] allFields = studentClass.getDeclaredFields();
for (Field field : allFields) {
System.out.println(field);
}
// Field getField(String fieldName): 获取指定名称的public属性
Field ageField = studentClass.getField("age");
// void set(Object obj, Object value): 修改指定对象的属性值
ageField.set(student, 30);
System.out.println(student);
//Object get(Object obj): 获取指定对象的属性值
int age = ageField.getInt(student);
System.out.println(age);
// Field getDeclaredField(String fieldName): 获取指定名称的所有权限的属性
Field nameField = studentClass.getDeclaredField("name");
System.out.println(nameField);
nameField.get(student); // 直接获取权限不足的属性会报错:
nameField.set(student, "李四"); // 直接修改权限不足的属性会报错:
// java.lang.IllegalAccessException: Class xxx can not access a member of class yyy with modifiers "private"
}
}
```
## 反射获取私有成员变量并修改
* 反射获取非public成员变量并修改的步骤
1. 获取该类`Class`字节码对象(3种方式)
2. 通过`Class`对象调用`newInstance()`方法创建该类的对象
3. 通过`Class`对象调用获取成员属性的方法获取属性对象
4. 设置`Field`对象的访问权限(也叫作`暴力访问`)
* `void setAccessible(boolean canAccess)`: 设置为true时可以访问所有权限的成员属性
5. 通过`Field`对象设置或获取属性值
* 小结
* `getXxx()`: 只能得到public的属性或方法
* `getDeclaredXxx()`: 可以得到所有权限的属性或方法. 但如果访问权限不足, 则无法操作
* `setAccessible(true)`: 获取成员变量, 构造方法, 成员方法的访问权限
* 5分钟练习: 反射获取私有属性并修改
* 继续使用项目s2-day13, 建包com.itheima.practice_03
* 复制上一个练习的Student类
* 建类Test
* 获取Student类的字节码对象
* 通过字节码对象创建Student类对象
* 获取所有权限的属性
* 设置访问权限
* 修改属性值
* 获取属性值并打印
```java
/*
定义一个Student类
属性:name(私有的) age(公开的)
方法:无参构造、有参构造、setter和getter方法, toString方法
定义Test测试类, 使用反射的手段获取两个属性并修改为“李四”, 24
Class类
Field[] getDeclaredField(String fieldName)
Field类
Object get(Object obj)
void set(Object obj, Object value)
void setAccessible(boolean canAccess)
*/
public class Test {
public static void main(String[] args) throws ReflectiveOperationException {
// 获取学生类的字节码对象
Class studentClass = Class.forName("com.itheima.practice_03.Student");
// 通过class对象创建学生对象
Object student = studentClass.newInstance();
// Field getDeclaredField(String fieldName): 获取指定名称的所有权限的属性
Field nameField = studentClass.getDeclaredField("name");
System.out.println(nameField);
// void setAccessible(boolean canAccess): 取消权限检查
nameField.setAccessible(true);
// 暴力访问设置后, 才能进行访问(修改和获取)的操作
nameField.set(student, "李四");
Object name = nameField.get(student);
System.out.println(name);
}
}
```
---
## 反射获取成员方法并调用
* 获取成员方法
* `Method[] getMethods()`: 返回所有public的方法数组
* `Method[] getDeclaredMethods()`: 返回所有权限的方法数组
* `Method getMethod(String name, Class<?>... parameterTypes)`: 获取public的方法, 传入方法名和方法形参字节码对象
* `Method getDeclaredMethod(String name, Class<?>... parameterTypes)`: 获取所有权限的指定方法, 传入方法名和方法形参字节码对象
* `java.lang.reflect.Method`类: 成员方法对象
* `Object invoke(Object obj, Object... args)`: 调用指定对象的成员方法
* `void setAccessible(boolean canAccess)`: 设置为true时禁用Java的访问安全检查, 可以访问所有权限的成员方法
* 反射获取成员方法并调用的步骤
1. 获取该类`Class`字节码对象(3种方式)
2. 通过`Class`对象调用`newInstance()`方法创建该类的对象
3. 通过`Class`对象调用获取成员方法的方法获取方法对象
4. 使用`Method`对象的`Object invoke(Object obj, Object... args)`方法调用方法, 传入该类的对象和参数, 返回方法的返回值
* 私有方法, 也是`setAccessible(true)`获取访问权限
* 5分钟练习: 反射获取成员方法并调用
* 继续使用项目s2-day13, 建包com.itheima.practice_04
* 复制上一个练习的Student类, 增加方法
* 无参无返回值方法: private void method(), 打印"无参无返回值方法"
* 建类Test
* 获取Student类的字节码对象
* 通过字节码对象创建Student类对象
* 调用setName, getName, method
```java
/*
属性:name(私有的) age(公开的)
方法:无参构造、有参构造、setter和getter方法, toString方法
*/
public class Student {
private String name;
public int age;
public Student() {
super();
}
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
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;
}
//定义private的无参无返回值方法method(), 打印”无参无返回值方法”
private void method() {
System.out.println("无参无返回值方法");
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
/*
定义一个Student类
属性:name(私有的) age(公开的)
方法:无参构造、有参构造、setter和getter方法, toString方法
定义private的无参无返回值方法method(), 打印”无参无返回值方法”
定义Test测试类, 测试反射分别调用:
有参无返回值方法: setName(String name)设置为lisi
无参有返回值方法: String getName()获取姓名并打印
无参无返回值方法: method(), 注意权限
Class类
Method getDeclaredMethod(String methodName)
Method类
Object invoke(Object obj, Object... args)
void setAccessible(boolean canAccess)
*/
public class Test {
public static void main(String[] args) throws ReflectiveOperationException {
// 获取学生类的字节码对象
Class studentClass = Class.forName("com.itheima.practice_04.Student");
// 通过class对象创建学生对象
Object student = studentClass.newInstance();
// 获取有参无返回值方法
Method setNameMethod = studentClass.getDeclaredMethod("setName", String.class);
Object returnedValue1 = setNameMethod.invoke(student, "lisi");
System.out.println(returnedValue1); // 无返回值的方法调用结果为: null
// 获取无参有返回值方法
Method getNameMethod = studentClass.getDeclaredMethod("getName");
Object returnedValue2 = getNameMethod.invoke(student);
System.out.println(returnedValue2); // 有返回值的方法调用结果为: lisi
// 获取无参无返回值方法
Method privateMethod = studentClass.getDeclaredMethod("method");
// 暴力访问
privateMethod.setAccessible(true);
// 权限不足的方法如果直接调用会报java.lang.IllegalAccessException, 需要设置暴力访问
privateMethod.invoke(student);
}
}
```
--=
## JavaBean概述和规范
* JavaBean:
* 概念: 就是一个类
* 作用: 封装数据
* 规范:
* 类必须是public修饰的
* 通过private的成员变量保存数据
* 通过public的get/set方法操作数据
* 至少提供一个无参构造方法
* 实现Serializable接口(用于写入文件中)
## BeanUtils概述和jar包
* jar包
* Java ARchive, 一个后缀名为`.jar`的文件, 类似于`rar`, 是一个压缩文件, 只不过专门用于压缩Java项目, 所以叫jar
* 作用
* jar包中是写好的代码编译出来的class文件, 有了这些类文件, 就可以调用其中的方法
* jar包从哪里来?
* 相关类库的官方网站上下载
* 自己导出jar包
* 如何使用jar包?
* **项目根目录下**创建名为`lib`的目录
* 复制jar文件, 粘贴到项目根目录下的`lib`目录下
* 选中项目中的jar包, 右键, 选择`Build Path`, 点击`Add to Build Path`. 此时项目中的`Referenced Libraries`中会出现jar包名称的奶瓶, 说明已经添加成功
* 导入的jar包整个项目都能使用
* `BeanUtils`
* Apache组织提供的第三方类库`Commons`中的一个组件
* 作用:
* 利用反射技术给一个类的对象的成员属性赋值或获取值, 用于快速封装数据到JavaBean
* 使用BeanUtils所需jar包
* `commons-beanutils`
* `commons-logging`
## BeanUtils的常用方法
* `org.apache.commons.beanutils.BeanUtils`类
* 常用静态方法
* `static void setProperty(Object bean, String name, Object value)`: 给对象的成员属性赋值. 传入对象, 成员属性名, 属性值
* `static String getProperty(Object bean, String name)`: 获取对象成员属性值. 传入对象, 成员属性名, 返回属性值的字符串形式
* `static void populate(Object bean, Map properties)`: 批量给对象的成员属性赋值, 传入对象, Map的key是属性名, value是属性值
* BeanUtils的`setProperty`和`getProperty`原理
* 方法底层是通过调用JavaBean的public的get/set方法来获取和设置属性
* get/set方法是通过反射来找到的
* 所以如果没有get/set方法则会报错
* 5分钟练习: 测试BeanUtils常用方法
* 继续使用项目s2-day13, 建包com.itheima.practice_05
* 建类Person
* 成员属性: name, age, gender
* 定义为标准JavaBean
* 建类Test
* 创建Person对象
* 测试BeanUtils的三个常用方法
```java
/*
定义一个标准的Person类
private属性:name, age, gender
方法:无参构造、有参构造、setter和getter方法, toString方法
实现序列化接口
*/
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String gender;
public Person() {
super();
}
public Person(String name, int age, String gender) {
super();
this.name = name;
this.age = age;
this.gender = gender;
}
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;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", gender=" + gender + "]";
}
}
/*
定义一个标准的Person类
private属性:name, age, gender
方法:无参构造、有参构造、setter和getter方法, toString方法
实现序列化接口
定义Test测试类, 测试BeanUtils常用方法:
创建Person对象, 将name设置为zhangsan, 将age设置为18
获取name属性值并打印
批量设置对象属性: lisi, 18, 男
static void setProperty(Object bean, String name, Object value)
static String getProperty(Object bean, String name)
static void populate(Object bean, Map properties)
*/
public class Test {
public static void main(String[] args) throws ReflectiveOperationException {
// 创建Person对象
Person p = new Person();
// static void setProperty(Object bean, String name, Object value): 给对象的属性设置值
BeanUtils.setProperty(p, "name", "zhangsan");
BeanUtils.setProperty(p, "age", 18);
System.out.println(p);
// static String getProperty(Object bean, String name): 获取对象的属性值
String name = BeanUtils.getProperty(p, "name");
System.out.println(name);
// static void populate(Object bean, Map properties): 批量给属性赋值
Person p2 = new Person();
Map<String, Object> map = new HashMap<>();
map.put("name", "lisi");
map.put("age", 18);
map.put("gender", "男");
BeanUtils.populate(p, map);
System.out.println(p);
}
}
```
---
## 自定义BeanUtils的赋值和获取方法实现
* 我们自己定义就直接通过反射操作Field即可
* 5分钟练习: 实现MyBeanUtils工具类
* 继续使用项目s2-day13, 建包com.itheima.practice_06
* 建类Person
* 成员变量: name, age, gender
* 定义为标准JavaBean
* 建类MyBeanUtils
* 私有构造方法
* 定义2个静态方法
* static void setProperty(Object bean, String name, Object value)
* static String getProperty(Object bean, String name)
```java
/*
定义一个标准的Person类
private属性:name, age, gender
方法:无参构造、有参构造、setter和getter方法, toString方法
实现序列化接口
*/
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String gender;
public Person() {
super();
}
public Person(String name, int age, String gender) {
super();
this.name = name;
this.age = age;
this.gender = gender;
}
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;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", gender=" + gender + "]";
}
}
public class MyBeanUtils {
private MyBeanUtils() {}
/**
* 给指定对象的指定属性赋值
* @param bean
* @param name
* @param value
* @throws ReflectiveOperationException
*/
public static void setProperty(Object bean, String name, Object value) throws ReflectiveOperationException {
// 通过对象获取字节码对象
Class clazz = bean.getClass();
// 通过字节码对象获取属性对象
Field field = clazz.getDeclaredField(name);
// 设置暴力访问
field.setAccessible(true);
// 设置值
field.set(bean, value);
}
/**
* 获取指定对象的指定属性值
* @param bean
* @param name
* @return
* @throws ReflectiveOperationException
*/
public static String getProperty(Object bean, String name) throws ReflectiveOperationException {
// 通过对象获取字节码对象
Class clazz = bean.getClass();
// 通过字节码对象获取属性对象
Field field = clazz.getDeclaredField(name);
// 设置暴力访问
field.setAccessible(true);
// 获取属性值
Object value = field.get(bean);
// 返回值的字符串
return value.toString();
}
}
/*
定义一个标准的Person类
private属性:name, age, gender
方法:无参构造、有参构造、setter和getter方法, toString方法
实现序列化接口
自定义MyBeanUtils工具类, 实现以下方法:
static void setProperty(Object bean, String name, Object value)
static String getProperty(Object bean, String name)
定义Test测试类测试
*/
public class Test {
public static void main(String[] args) throws ReflectiveOperationException {
// 创建Person对象
Person p1 = new Person();
// 赋值
MyBeanUtils.setProperty(p1, "name", "张三");
MyBeanUtils.setProperty(p1, "age", 30);
System.out.println(p1);
// 获取值
String age = MyBeanUtils.getProperty(p1, "age");
System.out.println(age);
}
}
```
## 自定义BeanUtils的populate方法
* 5分钟练习: 自己实现populate方法
* 继续使用项目s2-day13, 建包com.itheima.practice_07
* 修改MyBeanUtils类
* 增加方法static void populate(Object bean, Map properties)
* 通过捕获NoSuchFieldException方式, 避免设置不存在的属性产生异常
* 测试
```java
/**
* 给指定对象的属性批量赋值
* @param bean
* @param properties
* @throws ReflectiveOperationException
*/
public static void populate(Object bean, Map properties) throws ReflectiveOperationException {
// 通过对象获取字节码对象
Class clazz = bean.getClass();
// 遍历Map, 获取所有属性名
Set names = properties.keySet();
for (Object name : names) {
// 获取属性名对应的属性值
Object value = properties.get(name);
// 使用try catch 捕获异常
try {
// 通过反射获取属性对象
Field field = clazz.getDeclaredField(name.toString());
// 开启暴力访问
field.setAccessible(true);
// 设置属性值
field.set(bean, value);
} catch (NoSuchFieldException e) {
// 什么都不做
}
}
}
private static void method2() throws ReflectiveOperationException {
// 创建Person对象
Person p2 = new Person();
// 创建Map
Map map = new HashMap<>();
map.put("name", "张三");
map.put("age", 30);
map.put("gender", "男");
map.put("birthday", "1970-1-1");
// 测试
MyBeanUtils.populate(p2, map);
System.out.println(p2);
}
```
--------------
## 今日扩展
* 查看Apache Commons项目官网: http://commons.apache.org/
## 今日总结
* 反射
* 概念: 在程序运行时, 获取任何一个类的所有属性和方法(包括私有的). 调用任意一个对象的所有属性和方法(包括私有的)
* 前提:
* 获得字节码对象
* 获取字节码对象的3种方法
1. `对象.getClass()`
2. `类名.class`
3. `Class.forName(String fullClassName)`
* 反射能干的事
* 获取一个类的字节码对象, 通过该字节码对象:
* 获取一个类的构造方法(public或全部权限的)
* 创建一个类的对象
* 获取一个类的成员属性(public或全部权限的)
* 获取属性值
* 设置属性值
* 获取一个类的成员方法(public或全部权限的)
* 调用成员方法
* 反射使用的相关类和方法
* `java.lang.Class`类: 类的字节码对象
* 获取构造方法
* `Constructor<?>[] getConstructors()`: 以数组形式返回该类中所有public的构造方法. 如果没有public的, 则数组长度为0
* `Constructor<?>[] getDeclaredConstructors()`: 以数组形式返回该类中所有权限的构造方法, 包括private的. 如果该类是接口, 基本类型, 数组,
void, 则数组长度为0
* `Constructor<T> getConstructor(Class<?>... parameterTypes)`: 根据参数列表返回指定的public的构造方法. 参数列表填写参数的字节码对象
* `Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)`: 根据参数列表返回指定的所有权限的构造方法, 包括private的. 参数列
表填写参数的字节码对象
* 获取成员属性
* `Field[] getFields()`: 获取所有public的成员变量
* `Field[] getDeclaredFields()`: 获取所有权限的成员变量, 包括private的
* `Field getField(String fieldName)`: 通过指定的成员变量名获取指定的public的成员变量
* `Field getDeclaredField(String fieldName)`: 通过指定的成员变量名获取指定的所有权限的成员变量, 包括private的
* 获取成员方法
* `Method[] getMethods()`: 返回所有public的方法数组
* `Method[] getDeclaredMethods()`: 返回所有权限的方法数组
* `Method getMethod(String name, Class<?>... parameterTypes)`: 获取public的方法, 传入方法名和方法形参字节码对象
* `Method getDeclaredMethod(String name, Class<?>... parameterTypes)`: 获取所有权限的指定方法, 传入方法名和方法形参字节码对象
* 创建对象
* `T newInstance()`: 使用该类的无参构造创建一个对象
* `java.lang.reflect.Constructor`类: 构造方法对象
* `T newInstance()`: 通过无参构造方法对象创建一个类的对象
* `T newInstance(Object... initargs)`: 通过有参构造方法对象创建一个类的对象, 传入构造方法所需的参数列表
* `void setAccessible(boolean canAccess)`: 设置为true时禁用Java的访问安全检查, 可以访问所有权限的构造方法
* `java.lang.reflect.Field`类: 成员变量对象
* `Object get(Object obj)`: 获取指定对象的属性值
* `void set(Object obj, Object value)`: 将指定对象的属性值设置为指定的值
* `void setAccessible(boolean canAccess)`: 设置为true时禁用Java的访问安全检查, 可以访问所有权限的成员属性
* `java.lang.reflect.Method`类: 成员方法对象
* `Object invoke(Object obj, Object... args)`: 调用指定对象的成员方法
* `void setAccessible(boolean canAccess)`: 设置为true时禁用Java的访问安全检查, 可以访问所有权限的成员方法
* JavaBean
* 就是一个类
* 作用: 用于封装和存储数据
* 规范
* 类必须是public修饰的
* 成员变量必须是private的
* 必须有public的set/get方法
* 至少提供一个无参构造方法
* 实现Serializable接口
* Jar包
* Java ARchive, 后缀名为`.jar`, 是一个压缩文件, 里面包含编译后的class文件和说明信息
* 作用: jar包中是写好的代码编译出来的class文件, 有了这些类文件, 就可以调用其中的方法
* 导入jar包的步骤
* **项目根目录下**创建名为`lib`的目录
* 复制jar文件, 粘贴到项目根目录下的`lib`目录下
* 选中项目中的jar包, 右键, 选择`Build Path`, 点击`Add to Build Path`. 此时项目中的`Referenced Libraries`中会出现jar包名称的奶瓶, 说明已经添加成功
* 导入的jar包整个项目都能使用
* BeanUtils
* Apache组织提供的第三方类库`Commons`中的一个组件
* 作用:
* 利用反射技术给一个类的对象的成员属性赋值或获取值, 用于快速封装数据到JavaBean
* BeanUtils类常用的3个方法
* `static void setProperty(Object bean, String name, Object value)`: 给JavaBean对象的成员属性赋值, 传入对象, 成员属性名, 属性值
* `static String getProperty(Object bean, String name)`: 获取JavaBean成员属性的属性值, 传入对象, 成员属性名, 返回属性值的字符串形式
* `static void populate(Object bean, Map properties)`: 给JavaBean对象的成员属性赋值, 传入对象, Map的key是属性名, value是属性值
# day14 XML
## XML概述
* XML:
* Extensible Markup Language, 可扩展标记语言. 是一种标记语言, 类似于HTML. 是W3C组织发布的, 目前XML的规范是W3C于2000年发布的XML1.0规范
* 特点
* XML没有预定义标签, 所使用的标签都需要由用户自定义
* 作用:
* 用于描述数据, 而非显示数据.
* 擅长表示一对多的, 包含嵌套的数据关系
* 应用场景:
* 作为数据载体, 通过网络传递数据
* 作为应用程序的配置文件
* 标签:
* Tag, 也叫作`元素(element)`
* 格式:
* `<person></person>`: 开始标签和结束标签, 包含标签体
* `<name />`: 自封闭标签, 不包含标签体
```xml
<?xml version="1.0" encoding="UTF-8"?>
<中国>
<河北>
<石家庄></石家庄>
<保定></保定>
</河北>
<北京></北京>
</中国>
```
```xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
http://www.apache.org/licenses/LICENSE-2.0
-->
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
```
## XML语法: 文档声明
* XML文件
* 以`.xml`后缀结尾
* 组成部分(总览)
* 文档声明(实际上属于处理指令)
* 元素(即标签)
* 元素的属性
* 注释
* CDATA区
* 特殊字符
* 处理指令(PI: Processing Instruction)
* 文档声明
* 编写位置:
* 必须写在XML文档的第一行
* 格式:
* `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>`
* `version`属性: 声明XML的版本
* `encoding`属性: 声明文件的编码, 文件的编码必须与声明的编码一致.
* 不声明该属性则默认为`UTF-8`
* `standalone`属性: 声明文档是否独立, 即是否不依赖于其他文档.
* 不声明该属性则默认为`no`
* 5分钟练习: 编写xml文档声明
* 创建项目s2-day14, 建包com.itheima.practice_01
* 创建city.xml文件
* 编写xml文档声明
```xml
<?xml version='1.0' encoding='UTF-8'?>
<!--
手写的时候, 可以这样写:
先写: <>
然后在尖括号中添加2个问号: <??>
然后在第一个问号后添加xml: <?xml ?>
然后添加version和encoding属性, 不要忘了值有引号: <?xml version="1.0" encoding="UTF-8"?>
-->
```
## XML语法: 元素(标签)
* XML元素:
* 也称`标签`
* 组成部分:
* 开始标签: `<中国>`
* 标签体: 可选, 文本或子标签
* 结束标签: `</中国>`, 注意尖括号中有个斜杠
* 标签名命名规范:
* 可以包含字母, 数字, 减号, 下划线, 英文句点
* 必须遵守以下规范
* 区分大小写: `<p>`和`<P>`是不一样的
* 只能以字母或下划线开头: `<ab12>`, `<_ab12>`
* 不能以`xml`, `XML`, `Xml`等开头, 这个词属于保留词
* 标签名的字符之间不能有空格或制表符
* 标签名的字符之间不能使用冒号`:`, 这个冒号有特殊用途, 是用于区分标签前缀用的
* 标签的嵌套
* 标签中可以嵌套任意个子标签
* 标签不能交叉嵌套
* 标签的种类
* 闭合标签: 有开始标签和结束标签, 如`<name></name>`
* 有标签体: `<name>Bill Gates</name>`
* 无标签体: `<name></name>`
* 自闭合标签: `<name value="Bill Gates" />`
* 标签的属性
* 在开始标签内, 以`键=值`形式表示该标签的属性
* 属性名的命名规范与标签名相同
* 属性值必须使用引号引起来, 可以使用单引号`'`或双引号`"`
* 一个标签可以有多个属性, 属性名不能重复
* 标签的属性可以改用子标签来定义
* 如: `<中国 属性名1=值1 属性名2=值2></中国>`
* 注意:
* 一个XML文档**必须且仅有一个根标签**, 其他标签都是这个根标签的子标签
```xml
<!-- 标签的组成部分 -->
<name> Bill Gates </name>
------ ---------- -------
| | |
开始 标签体 结束
标签 标签
<!-- 正确的标签嵌套 -->
<father>
<son></son>
</father>
<!-- 错误的标签嵌套 -->
<father>
<son>
</father>
</son>
<!-- 自闭和标签直接使用属性来描述信息 -->
<student name="Tom" age="12" />
<!-- 标签使用子标签来描述信息 -->
<student>
<name>Tom</name>
<age>12</age>
</student>
```
* 5分钟练习: 练习xml标签
* 继续使用项目s2-day14, 建包com.itheima.practice_02
* 创建student.xml
* 编写文档声明
* 编写标签
```xml
<?xml version="1.0" encoding="UTF-8"?>
<students>
<!-- 使用属性表示学生的数据 -->
<student name="张三" age="30"/>
<student name="张三" age="30"></student>
<!-- 使用子标签表示学生的数据 -->
<student>
<name>李四</name>
<age>25</age>
</student>
</students>
<!--
新建student.xml文件
定义xml文档声明
定义元素
根标签为: <students></students>
其中嵌套2个<student>标签:
一个通过标签的属性name=“张三”, age=“30”表示数据;
另一个通过子标签<name>李四</name>, <age>25</age>表示数据
-->
```
---
## XML语法: 注释
* 注释的格式:
* `<!-- 注释内容 -->`
* 同时支持单行注释和多行注释
* 注意:
* 注释不会被当做标签解析
* 注释不能嵌套
* XML声明之前不能有注释
* 原因: 只有先解析到XML声明才知道该文档是一个XML文档, 才会把`<!-- -->`当做注释.
```xml
<!-- 单行注释 -->
<!--
多行注释
-->
```
## XML语法: 其他组成部分
* CDATA区: Character data
* 格式: `<![CDATA[你的内容]]>`
* 作用: CDATA区内的内容只会被当做普通字符串解析, 即使有标签也不会被解析
* 特殊字符的转义
* `&`: `&` ampersand
* `<`: `<` less than
* `>`: `>` greater than
* `"`: `"` quote
* `'`: `'` apostrophe
* 注意不要忘了开头的`&`和结尾的`;`
* 综上:
* 如果想显示xml代码, 而不被浏览器解析, 有2种方式
1. 将xml代码放在CDATA区中:
* `<![CDATA[<itheima>hi</itheima>]]>`
2. 使用特殊字符表示xml代码中的尖括号:
* `<itheima>hi</itheima>`
```xml
<!-- CDATA示例 -->
<url>
<![CDATA[<tag>这个标签只会被当做字符串, 不会被解析</tag>]]>
</url>
```
* 处理指令
* PI, Processing Instruction
* 作用: 用于指示软件如何解析XML文档
* 语法: 必须以`<?`作为开头, 以`?>`作为结尾
* 常用处理指令
* XML声明: `<?xml version="1.0" encoding="UTF-8" ?>`
* XML-Stylesheet指令: `<?xml-stylesheet type="text/css" href="some.css" ?>`
* 5分钟练习: 练习xml其他部分
* 继续使用项目s2-day14, 建包com.itheima.practice_03
* 创建city.xml
* 编写文档声明
* 编写标签
* 使用CDATA区和特殊字符2种方式显示xml代码
```xml
<?xml version="1.0" encoding="UTF-8"?>
<students>
<!-- 使用CDATA区 -->
<student>
<name>张三</name>
<url>
<![CDATA[<itheima>www.itheima.com</itheima>]]>
</url>
</student>
<!-- 使用转义字符 -->
<student>
<name>李四</name>
<url>
<itheima>www.itheima.com</itheima>
</url>
</student>
</students>
<!--
CDATA区的写法
先写一对尖括号: <>
在尖括号中添加一个叹号: <!>
在叹号后写一对中括号: <![]>
在中括号中添加CDATA: <![CDATA]>
在CDATA后添加一对中括号: <![CDATA[]]>
然后在中括号中填写内容: <![CDATA[内容]]>
-->
<!--
练习直接显示xml标签而不被解析
新建student.xml文件. 定义xml文档声明. 定义元素
根标签为: <students></students>, 其中嵌套2个<student>标签:
第一个通过子标签<name></name>, <url></url>表示数据
其中url标签的内部使用CDATA区显示文本“<itheima>www.itheima.com</itheima>“
第二个也通过子标签<name></name>, <url></url>表示数据
其中url使用特殊字符替换尖括号来显示网址文本
-->
```
## DTD约束: 入门案例
* DTD:
* Document Type Definition, 文档类型定义. 用于在XML文档中声明, 对XML文档进行约束
* 作用:
* 用于约束文档中使用的书写规范(即能使用哪些标签, 属性, 属性值, 标签的顺序, 次数等)
* 由于XML标签都是自定义标签, 所以标签和属性是否正确需要通过DTD约束来进行检查
* DTD和XML的关系:
* DTD不是XML
* XML中可以引入DTD文件, 用于对XML中的标签进行约束
* DTD约束文档的书写规范
* 以`.dtd`后缀结尾
* 也需要XML的文档声明, 且文件编码必须为`UTF-8`
* XML使用DTD约束的方式
* 直接在XML中定义DTD约束: 在XML中添加`<!DOCTYPE 根元素 [元素声明]>`
* 在XML中引入外部定义的DTD约束文件(xxx.dtd)
* 引入本地DTD约束文件: `<!DOCTYPE 根标签名 SYSTEM "dtd文档路径">`
* 引入网络DTD约束文件: `<!DOCTYPE 根元素 PUBLIC "dtd名称" "DTD文档的URL">`
* 示例:
* 将dtd定义在单独的文件中: book.dtd
* 说明 :
* 必须有XML文档声明
* 第一行声明了xml的根标签必须为"书架"标签
* 该书架标签内部可以有子标签"书"
* 且书标签可以有一个或多个(通过+表示)
* 第二行声明了了xml中可以有书标签
* 书标签内部可以有子标签"书名", "作者", "售价"标签
* 且通过逗号,规定子标签的顺序必须按照这种定义顺序
* 且每个标签要出现1次
* 第三到五行声明了了xml可以有书名, 作者, 售价三个标签
* 且通过(#PCDATA)来定义的标签的内容格式为普通字符串
```xml
这是book.dtd文件
<?xml version="1.0" encoding="UTF-8" ?>
<!ELEMENT 书架 (书+)>
<!ELEMENT 书 (书名, 作者, 售价)>
<!ELEMENT 书名 (#PCDATA)>
<!ELEMENT 作者 (#PCDATA)>
<!ELEMENT 售价 (#PCDATA)>
```
* 在xml中引入上面定义的dtd约束文件
```xml
这是引入了book.dtd约束的book.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE 书架 SYSTEM "book.dtd">
---
|
必须和dtd约束中的根标签一致
```
* 也可以直接将dtd定义在XML文档中并使用
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE persons [
<!ELEMENT person (name,age)>
<!ELEMENT name (#PCDATA)>
<!ELEMENT age (#PCDATA)>
]>
<persons>
<person>
<name>Jerry</name>
<age>13</age>
</person>
</persons>
```
## DTD约束: 规范细节
* 引入网络的DTD约束文档:
* `<!DOCTYPE 根元素 PUBLIC "dtd名称" "DTD文档的URL">`
```xml
<!-- 示例 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" dtd名称
"http://java.sun.com/dtd/web-app_2_3.dtd"> dtd文档URL
```
* DTD规范细节
* `!ELEMENT`用于声明一个XML标签:
* 格式: `<!ELEMENT 标签名称 使用规则>`
* 标签名称
* 定义标签名
* 使用规则
* `(#PCDATA)`: Parsed Character Data, 指示元素的主题内容只能是普通的文本
* `EMPTY`: 指示元素的主体为空, 如`<br/>`
* `ANY`: 指示元素的主体内容为任意类型
* `(子元素)`: 指示元素中包含的子元素
* 子元素用逗号`,`分隔: 则必须按声明顺序编写XML元素
* 子元素用竖线`|`分隔: 任选其一即可
* 子元素后什么都没有: 必须且只能出现一次
* 子元素用`+`分隔: 至少出现一次
* 子元素用`*`分隔: 出现零次或任意次
* 子元素用`?`分隔: 出现零次或一次
* `!ATTLIST`用于为一个标签声明内部属性: attribute list
* 格式: 见下方
* 标签名: 用于指定该属性用于哪个标签
* 属性名: 属性的名称
* 属性值类型
* `CDATA`: 属性的取值为普通的文本字符串
* `(选项1|选项2|选项3)`: 枚举, 从列表中任选其一
* `ID`: 属性的取值不能重复
* 设置说明
* `#REQUIRED`: 该属性必须出现
* `#IMPLIED`: 表示该属性可有可无
* `#FIXED="固定值"`: 表示属性的取值为一个固定值
* 值: 表示属性的取值为该默认值
```xml
<!ATTLIST 标签名
属性名1 属性值类型 设置说明
属性名2 属性值类型 设置说明
...
>
```
```xml
<!-- dtd完整示例 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE 购物篮 [
<!ELEMENT 水果 (苹果, 香蕉+)>
<!ELEMENT 苹果 ANY>
<!ELEMENT 香蕉 ANY>
<!ELEMENT 蔬菜 (#PCDATA)>
<!ELEMENT 肉 EMPTY>
<!ATTLIST 肉
品种 (鸡肉|牛肉|猪肉|鱼肉) "鸡肉"
价格 CDATA #REQUIRED
>
]>
<购物篮>
<水果>
<苹果></苹果>
<香蕉></香蕉>
<香蕉></香蕉>
</水果>
<蔬菜>白菜</蔬菜>
<肉 品种="牛肉" 价格="30"/>
</购物篮>
```
* 5分钟练习: 根据DTD约束编写xml
* 继续使用项目s2-day14, 建包com.itheima.practice_03
* 创建book.xml
* 编写文档声明
* 引入dtd约束: `<!DOCTYPE 书架 SYSTEM "book.dtd">`
* 根据约束编写xml
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!ELEMENT 书架 (书+)>
<!ELEMENT 书 (书名, 作者, 售价)>
<!ELEMENT 书名 (#PCDATA)>
<!ELEMENT 作者 (#PCDATA)>
<!ELEMENT 售价 (#PCDATA)>
```
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 引入约束 -->
<!DOCTYPE 书架 SYSTEM "book.dtd">
<书架>
<书>
<书名>Java编程思想</书名>
<作者>未知</作者>
<售价>99.00</售价>
</书>
<书>
<书名>Effective Java</书名>
<作者>未知</作者>
<售价>88.00</售价>
</书>
</书架>
<!--
创建以下book.dtd约束文件
<?xml version="1.0" encoding="UTF-8" ?>
<!ELEMENT 书架 (书+)>
<!ELEMENT 书 (书名, 作者, 售价)>
<!ELEMENT 书名 (#PCDATA)>
<!ELEMENT 作者 (#PCDATA)>
<!ELEMENT 售价 (#PCDATA)>
创建book.xml, 根据dtd约束编写xml
-->
```
---
## Schema约束: 概述
* Schema约束
* 也是一种XML约束, 用于取代DTD.
* Schema相较于DTD的优势
* Schema更符合XML语法结构
* DOM, SAX等XML解析库更容易解析Schema文档
* Schema对**名称空间(namespace, 或命名空间)**支持很好
* Schema比DTD支持更多的数据类型, 并支持用户自定义新的数据类型
* Schema定义约束的能力非常强大, 可以对XML实例文档做出细致的语言限制
* Schema文档
* Schema本身是一个XML文件, 但后缀名为`.xsd`
* Schema文档通常被称为`模式文档(约束文档)`, 遵循该文档的XML文件称为`实例文档`
* Schema文档必须有一个根节点, 且该根节点名称必须为`schema`
* 编写了一个Schema约束文档后, 需要把这个约束文档中声明的元素绑定到一个`URI`地址上. 这一过程叫做`将元素绑定到一个命名空间`, 以后XML文件就可以通过这个URI找到约束
文档
## Schema约束: 入门案例
* 定义Schema约束文档
* `xmlns`:
* 含义: XML name space, XML名称空间
* 作用: 不同约束文件中可能定义了相同名称的标签, 为了避免冲突, 所以为每个约束定义一个名称空间. 类似于Java的包来区分同名的类
* 定义命名空间的URI
* `targetNamespace='http://www.baidu.com'`
* 用于声明命名空间绑定的URI. 当XML文件引用该约束文档时, 只需要引用该URI即可找到约束文档
* 定义标签约束
* `<xs:element name='标签名'>`
* 定义一个标签
* 属性
* `name`: 定义标签名
* `type`: 定义属性值类型
* `<xs:complexType>`
* 声明一个复杂类型标签, 即可以有子标签
* `<xs:sequence>`:
* 约束子标签的定义顺序
* 属性
* `maxOccurs`: 出现次数
* `unbounded`值: 不限次数
* 常见问题:
* 为什么这个前缀取名为`xs`?
* 因为该前缀引用的URI是`http://www.w3.org/2001/XMLSchema`, `xs`就是`XMLSchema`的缩写.
* 以后还会见到`xsi`, 是`XMLSchema-instance`的缩写
* 其实就是一个变量名, 你也可以使用自己喜欢的名称
* 引用名称空间的2种方式
* 起别名:
* `xmlns:xs='http://www.w3.org/2001/XMLSchema'`
* 引入一个名称空间, 并起别名`xs`, 使用该命名空间的标签时, 要使用`xs:标签`来使用
* `xs:`也叫作前缀, 类似于一个变量, 值就是后面的URI
* 不起别名
* `xmlns='http://www.w3.org/2001/XMLSchema'`
* 引入一个名称空间, 没有别名, 使用时不用加前缀. 这种方式容易造成标签名冲突
* 两种方式都需要引入URI和文件路径
* `xsi:schemaLocation="命名空间URI 约束文件路径"`
* 将命名空间URI和约束文档路径对应起来
* 如: `xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web_-app_3_1.xsd"`
* `QName`: `前缀:标签名`这个东西称为QName, Qualified Name
```xml
<!-- 一个Schema约束文档示例 -->
<?xml version='1.0' encoding='UTF-8' ?>
<xs:schema
xmlns:xs='http://www.w3.org/2001/XMLSchema'
targetNamespace='http://www.itheima.com'>
<xs:element name='书架' >
<xs:complexType>
<xs:sequence maxOccurs='unbounded' >
<xs:element name='书' >
<xs:complexType>
<xs:sequence>
<xs:element name='书名' type='xs:string' />
<xs:element name='作者' type='xs:string' />
<xs:element name='售价' type='xs:string' />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
<!-- 一个XML使用Schema约束示例 -->
<?xml version="1.0" encoding="UTF-8"?>
<itheima:书架
xmlns:itheima="http://www.itheima.com"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="命名空间URI 约束文件路径">
<书>
<书名>Java学习</书名>
<作者>itheima</作者>
<售价>100</售价>
</书>
</itheima:书架>
```
---=
## XML解析: XML的解析方式
* 解析:
* 就是将XML中存储的数据取出来
* 文档树:
* XML文档中的标签嵌套方式可以看做一个树形结构
* 节点: 一对标签及其属性和内容, 如`<name attr="hi">Tom</name>`
* 2种解析方式
* DOM解析
* Document Object Model, 文档对象模型. 是W3C推荐的处理XML的方式
* SAX解析
* Simple API for XML, 是第三方开源解析器
* Dom解析和SAX解析的区别
* DOM: 基于树形模型原理. 无论要解析哪个节点, 先一次性将XML文档全部加载到内存中, 形成DOM树, 然后进行解析操作
* 好处: 文档加载完整, 操作节点简单
* 缺点: 消耗内存大
* SAX: 基于事件驱动原理. 从上往下读取XML文档, 当读取到要解析的节点的开始标签时触发一个事件, 开始从这里加载到内存, 当读取到要解析节点的结束标签时, 触发一个事件,
结束加载. 从而只会得到要解析的节点部分
* 好处: 节省内存
* 缺点: 文档加载不完整, 如果要进行增删改节点, 要反复多次读取文档, 效率低
* 第三方解析库
* `JAXP`: Java提供的解析XML规范, 规定了Dom和SAX两种解析方式
* `Dom4J`: 第三方开源Dom解析库(使用方便, 推荐), 同时支持DOM解析和SAX两种解析方式, 也实现了JAXP
## 使用Dom4J解析XML: DOM4J的API介绍
* `org.dom4j.io.SAXReader`类: SAX解析器
* 构造方法:
* `SAXReader()`: 创建一个SAX解析器
* 成员方法:
* `Document read(String systemId)`: 读取XML文件, 返回一个Document对象
* `org.dom4j.io.OutputFormat`类: 配置XML输出格式
* `static OutputFormat createPrettyPrint()`: 创建一个用户输出美观格式的OutputFormat对象
* `void setEncoding(String encoding)`: 设置字符集
* `org.dom4j.io.XMLWriter`类: 用于向XML中写入数据的输出流
* 构造方法:
* `XMLWriter(OutputStream out)`: 使用字节流创建一个XMLWriter对象
* 成员方法:
* `void write(Document doc)`: 将Document对象中的数据写入到XML文件
* `org.dom4j.Document`接口: 代表一个XML文档
* `Element getRootElement()`: 获取根元素对象. 需要用根元素获取其他子元素
* `org.dom4j.Element`接口: 代表一个元素
* 获取元素信息
* `String getName()`: 获取元素名
* 元素增删改查
* 获取元素
* `List<Element> elements()`: 获取调用元素对象的所有子元素
* `List<Element> elements(String name)`: 获取所有的指定名称的子元素
* `Element element(String name)`: 获取第一个该名称的子元素对象
* `Element getParent()`: 获取当前元素的父元素对象
* 添加元素
* `Element addElement(String elementName)`: 在当前元素下添加指定名称的子元素
* 删除元素
* `boolean remove(Element child)`: 删除子元素
* 操作元素中的文本
* `String elementText(String name)`: 获取第一个该名称的子元素的文本内容
* `String getText()`: 获取当前元素对象的文本
* `void setText(String text)`: 设置当前元素对象的文本
* 操作元素的属性
* `String attributeValue(String name)`: 根据指定的属性名获取属性值
* `Element addAttribute(String name, String value)`: 添加或修改指定属性名的属性值
* `org.dom4j.DocumentHelper`类:
* `static Element createElement(String name)`: 使用指定名称作为元素名, 创建一个元素对象
---
## Dom4J案例: 查询元素
* city.xml
```xml
<?xml version="1.0" encoding="UTF-8"?>
<State Code="37" Name="河南" description="郑州">
<City>
<Name>郑州</Name>
<Region>高薪区</Region>
</City>
<City>开封</City>
<City>洛阳</City>
<City>信阳</City>
</State>
```
* 项目需求:
```
// 1. 获取并打印郑州这个Name节点的文本
// 2. 遍历所有节点, 打印节点名称
// 3. 修改某个节点的主体内容, 信阳 -> 安阳
// 4. 删除元素 开封
// 5. 向指定节点添加新元素: 添加城市 南阳
// 6. 在指定元素之前添加元素: 在洛阳前添加三门峡
// 7. 操作XML属性: 打印State的Name属性值
// 8. 向根元素中添加属性: GDP="99999亿"
```
* 10分钟练习: 完成需求1和2
* 建包com.itheima.xmlparse
* 新建xml文件city.xml, 复制内容
* 导包
* 在项目根目录下的`lib`目录中, 放入`dom4j-1.6.1.jar`
* 右键点击`Build Path`, `Add to Build Path`
* 建类Dom4jUtils
* 内部定义getDucument(String xmlFilePath)方法, 获取Document对象
* 建类Test
* 完成需求1
* 获取Document对象
* 通过Document对象获取根元素
* 通过根元素对象获取所有子元素的List集合
* 获取List集合中第一个Element对象
* 使用Element对象调用elementText(String name)方法获取Name子标签的内容
* 使用Element对象调用elementText(String name)方法获取Region子标签的内容
* 完成需求2
* 定义方法用于打印当前元素名称, 并递归打印子元素名称
* 获取Document对象
* 通过Document对象获取根元素
* 调用递归方法传入根元素对象
```java
public class Dom4jUtils {
private Dom4jUtils () {}
/**
* 获取city.xml文件的Document对象
* @return
* @throws Exception
*/
public static Document getDocument() throws Exception {
SAXReader reader = new SAXReader();
Document document = reader.read("src/com/itheima/xmlparse/city.xml");
return document;
}
}
```
```java
public class Test {
// 1. 获取并打印郑州这个Name节点的文本
// 2. 遍历所有节点, 打印节点名称
// 3. 修改某个节点的主体内容, 信阳 -> 安阳
// 4. 删除元素 开封
// 5. 向指定节点添加新元素: 添加城市 南阳
// 6. 在指定元素之前添加元素: 在洛阳前添加三门峡
// 7. 操作XML属性: 打印State的Name属性值
// 8. 向根元素中添加属性: GDP="99999亿"
public static void main(String[] args) throws Exception {
// method1();
method2();
}
// 2. 遍历所有节点, 打印节点名称
private static void method2() throws Exception {
// 先获取Document对象
Document document = Dom4JUtils.getDocument();
// 获取根元素
Element rootElement = document.getRootElement();
// 调用方法递归打印根节点的名字
treeWalk(rootElement);
}
// 定义方法递归打印元素名
private static void treeWalk(Element element) {
// 先打印当前元素名
System.out.println(element.getName());
// 然后获取该元素的所有子元素
List<Element> childElements = element.elements();
// 遍历所有子元素, 递归调用方法
for (Element childElement : childElements) {
// 递归调用方法
treeWalk(childElement);
}
}
// 1. 获取并打印郑州这个Name节点的文本
private static void method1() throws Exception {
// 通过工具类获取Document对象
Document document = Dom4JUtils.getDocument();
// 通过document对象获取根节点元素
Element rootElement = document.getRootElement();
// 获取根元素中的所有子元素
List<Element> elements = rootElement.elements();
// 通过索引获取第一个子元素
Element firstCityElement = elements.get(0);
// 获取子元素name的文本
String text = firstCityElement.elementText("Name");
// 打印
System.out.println(text); // 郑州
}
}
```
## Dom4J案例: 修改元素, 删除元素
* 注意:
* 一个元素只能被其父元素删除
* Document对象在内存中操作后, 要写回文件中
* 10分钟练习: 完成需求3和4
```java
// 工具类中增加方法, 写回修改后的Document到文件
public static void writeXml2File(Document document) throws IOException {
OutputFormat format = OutputFormat.createPrettyPrint();
format.setEncoding("utf-8");
XMLWriter writer = new XMLWriter(new FileOutputStream("src/com/itheima/xmlparse/city.xml"));
writer.write(document);
}
```
```java
// 4. 删除元素 开封
private static void method4() throws Exception {
// 获取Document对象
Document document = Dom4JUtils.getDocument();
// 获取根元素
Element rootElement = document.getRootElement();
// 获取所有的子元素集合
List<Element> childElements = rootElement.elements();
// 根据索引获取开封元素
Element kaifeng = childElements.get(1);
// 获取开封元素的父元素
Element kaifengParent = kaifeng.getParent();
// 使用开封元素的父元素删除开封元素
kaifengParent.remove(kaifeng);
// 将修改写回xml
Dom4JUtils.writeXml2File(document);
}
// 3. 修改某个节点的主体内容, 信阳 -> 安阳
private static void method3() throws Exception {
// 获取Document对象
Document document = Dom4JUtils.getDocument();
// 获取根元素
Element rootElement = document.getRootElement();
// 获取根元素的所有子元素集合
List<Element> childElements = rootElement.elements();
// 根据索引获取信阳元素
Element xinyang = childElements.get(3);
// 修改信阳元素的文本
xinyang.setText("安阳");
// 将修改后的Document对象写回xml
Dom4JUtils.writeXml2File(document);
}
```
## Dom4J案例: 添加元素
* `Element addElement(String elementName)`: 在当前元素下添加指定名称的子元素
* 添加元素到指定位置:
* 获取元素List, 添加到指定位置, 写入xml文件
* `DocumentHelper`类:
* `static Element createElement(String name)`: 创建一个元素对象
* 10分钟练习: 完成需求5和6
```java
// 6. 在指定元素之前添加元素: 在洛阳前添加三门峡
private static void method6() throws Exception {
// 先创建一个三门峡元素
Element sanmenxia = DocumentHelper.createElement("City");
sanmenxia.setText("三门峡");
// 获取Document对象
Document document = Dom4JUtils.getDocument();
// 获取根元素
Element rootElement = document.getRootElement();
// 获取根元素中所有子元素的集合
List<Element> childElements = rootElement.elements();
// 将三门峡元素添加到第二个位置
childElements.add(1, sanmenxia);
// 将修改写回xml文件
Dom4JUtils.writeXml2File(document);
}
// 5. 向指定节点添加新元素: 添加城市 南阳
private static void method5() throws Exception {
// 获取Document对象
Document document = Dom4JUtils.getDocument();
// 获取根元素
Element rootElement = document.getRootElement();
// 使用根元素的方法添加子元素
Element nanyang = rootElement.addElement("City"); // 注意元素名的大小写
nanyang.setText("南阳");
// 将修改写回xml文件
Dom4JUtils.writeXml2File(document);
}
```
## Dom4J案例: 获取和添加元素的属性
* 10分钟练习: 完成需求7和8
```java
// 8. 向根元素中添加属性: GDP="99999亿"
private static void method8() throws Exception {
// 获取Document对象
Document document = Dom4JUtils.getDocument();
// 获取根元素
Element rootElement = document.getRootElement();
// 添加属性名和属性值
rootElement.addAttribute("GDP", "99999亿");
// 将修改写回xml文件
Dom4JUtils.writeXml2File(document);
}
// 7. 操作XML属性: 打印State的Name属性值
private static void method7() throws Exception {
// 获取Document对象
Document document = Dom4JUtils.getDocument();
// 获取根元素
Element rootElement = document.getRootElement();
// 通过属性名获取属性值
String value = rootElement.attributeValue("Name");
// 打印
System.out.println(value);
}
```
---------------------
## 今日总结
* XML
* 概念: Extensible Markup Language, 可扩展标记语言. 是一种标记语言, 类似于HTML. 是W3C组织发布的, 目前XML的规范是W3C于2000年发布的XML1.0规范
* 作用: 用于描述数据, 而非显示数据. 擅长表示包含嵌套的数据关系(一对多关系)
* 注意: XML没有预定义的标签, 所有标签都需要用户定义
* 应用场景:
* 作为数据载体
* 作为配置文件
* XML文档的组成部分
* 文档声明
* 必须在XML文档第一行
* 属性
* `version`: XML版本, 目前1.0
* `encoding`: XML文件编码, 文件编码必须和声明编码一致. 默认`UTF-8`
* `standalone`: 文档是否独立不依赖于其他文档. 默认`no`
* 元素
* 组成部分
* 开始标签
* 标签体
* 结束标签
* 标签种类
* 闭合标签: `<name></name>`
* 有标签体: `<name>Bill Gates</name>`
* 无标签体: `<name></name>`
* 自闭合标签: `<name value="Bill Gates" />`
* 标签名命名规范
* 可以包含字母, 数字, 减号, 下划线, 英文句点
* 区分大小写: <p>和<P>是不一样的
* 只能以字母或下划线开头: <ab12>, <_ab12>
* 不能以xml, XML, Xml等开头, 这个词属于保留词
* 名称字符之间不能有空格或制表符
* 名称字符之间不能使用冒号:, 这个冒号有特殊用途
* 标签的嵌套
* 标签可以嵌套任意个子标签
* 标签不能交叉嵌套
* XML文档有且只有一个根标签
* 元素的属性
* 在开始标签内, 以键值对形式表示该标签的属性
* 属性名的命名规范与标签名相同
* 属性值必须使用引号引起来, 可以使用单引号'或双引号"
* 一个标签可以有多个属性, 属性名不能重复
* 标签的属性可以改用子标签来定义
* 注释
* 格式: `<!-- 注释内容 -->`, 适用于单行和多行
* 注意:
* 注释不会被当做标签解析
* 注释不能嵌套
* XML声明之前不能有注释
* 原因: 只有先解析到XML声明才知道该文档是一个XML文档, 才会把`<!-- -->`当做注释.
* CDATA区
* 格式: `<![CDATA[你的内容]]>`
* 作用: CDATA区中的内容只会被当做普通字符串解析, 即使有标签也不会被解析
* 特殊字符
* `&`: `&`
* `<`: `<`
* `>`: `>`
* `"`: `"`
* `'`: `'`
* 处理指令
* PI, Processing Instruction
* 作用: 指示软件如何解析XML文档
* 语法: 必须以`<?`开头, 以`?>`结尾
* 常用处理指令
* XML声明: `<?xml version="1.0" encoding="UTF-8" ?>`
* XML-Stylesheet指令: `<?xml-stylesheet type="text/css" href="some.css" ?>`
* XML约束
* 约束的作用: 用于约束文档中使用的书写规范(即能使用哪些标签, 属性, 属性值, 标签的顺序, 次数等)
* 为什么要使用约束: 由于XML标签都是自定义标签, 所以标签和属性是否正确需要通过约束来进行检查
* DTD约束
* 概念: Document Type Defination, 文档类型定义.
* dtd文件规范:
* 以`.dtd`后缀结尾
* 需要XML文档声明, 文件编码必须为`UTF-8`
* DTD约束的定义
* `ELEMENT`: `<!ELEMENT 标签名称 使用规则>`
* 声明一个XML标签
* 使用规则
* `(#PCDATA)`: Parsed Character Data, 指示元素的主题内容只能是普通的文本
* `EMPTY`: 指示元素的主体为空, 如`<br/>`
* `ANY`: 指示元素的主题内容为任意类型
* `(子元素)`: 指示元素中包含的子元素
* 子元素用逗号`,`分隔: 则必须按声明顺序编写XML元素
* 子元素用竖线`|`分隔: 任选其一即可
* 子元素后什么都没有: 必须且只能出现一次
* 子元素用`+`分隔: 至少出现一次
* 子元素用`*`分隔: 出现零次或任意次
* 子元素用`?`分隔: 出现零次或一次
* `ATTLIST`: `<!ATTLIST 元素名 属性名 属性值类型 设置说明 ...>`
* 声明一个标签的属性
* 属性值类型
* `CDATA`: 属性的取值为普通的文本字符串
* `ENUMERATED`: 枚举, 从列表中任选其一
* `ID`: 属性的取值不能重复
* 设置说明
* `#REQUIRED`: 该属性必须出现
* `#IMPLIED`: 表示该属性可有可无
* `#FIXED="固定值"`: 表示属性的取值为一个固定值
* `值`: 表示属性的取值的默认值为该值
* DTD约束文件的引入
* 引入本地约束文件: `<!DOCTYPE 根标签名 SYSTEM "dtd文档路径">`
* 引入网络约束文件: `<!DOCTYPE 根元素 PUBLIC "dtd名称" "DTD文档的URL">`
* Schema约束
* 用于取代DTD
* Schema相较于DTD的优势
* Schema更符合XML语法结构
* DOM, SAX等XML解析库更容易解析Schema文档
* Schema对命名空间支持很好
* Schema比DTD支持更多的数据类型, 并支持用户自定义新的数据类型
* Schema定义约束的能力非常强大, 可以对XML实例文档做出细致的语言限制
* Schema要求
* Schema本身是一个XML文件, 但后缀名为`.xsd`
* Schema文档通常被称为`模式文档(约束文档)`, 遵循该文档的XML文件称为`实例文档`
* Schema文档必须有一个根节点, 且该根节点名称必须为`<schema>`
* 编写了一个Schema约束文档后, 需要把这个约束文档中声明的元素绑定到一个`URI`地址上. 这一过程叫做将元素绑定到一个`命名空间`, 以后XML文件就可以通
过这个URI找到约束文档
* Schema约束文件的定义
* 定义命名空间的URI
* `targetNamespace='http://www.baidu.com'`: 声明命名空间要绑定的URI. 当XML文件引用该约束文档时, 只需要引用该URI即可找到约束文档
* 定义标签约束
* `<xs:element name='标签名'>`: 定义一个标签
* `name`属性: 定义标签名
* `type`属性: 定义属性值类型
* `<xs:complexType>`: 声明一个复杂类型标签, 即可以有子标签
* `<xs:sequence>`: 约束子标签的定义顺序
* `maxOccurs`属性: 出现次数
* `uncounded`属性值: 不限次数
* Schema约束文件的引用
* 引用网络约束文件:
* 方式1: `xmlns:xs='http://www.w3.org/2001/XMLSchema'`
* 声明一个命名空间, 并且给这个命名空间起一个别名`xs`, 当使用该命名空间的标签时, 使用`xs:`标签来使用
* 方式2: `xmlns='http://www.w3.org/2001/XMLSchema'`
* 声明一个命名空间, 没有别名, 所以在使用该命名空间的标签时, 不用加前缀. 这种方式容易造成标签名冲突
* 引用本地约束文件
* 除了使用以上2种方式, 还要增加`xsi:schemaLocation="命名空间URI 约束文件路径"`: 将命名空间URI和约束文档路径对应起来(一般用于本地schema
约束).
* 标签前缀: `xs:标签`, xs就是前缀
* QName: `xs:标签`, 整体叫做QName, Qualified Name
* 解析XML
* DOM解析
* DOM: Document Object Model, 文档对象模型. 是W3C推荐的处理XML的方式
* 解析原理: 基于树形模型原理. 无论要解析哪个节点, 先一次性将XML文档全部加载到内存中, 形成DOM树, 然后进行解析操作
* 好处: 文档加载完整, 操作节点简单
* 缺点: 消耗内存大
* SAX解析
* SAX: Simple API for XML, 第三方开源解析器
* 解析原理: 基于事件驱动原理. 从上往下读取XML文档, 当读取到要解析的节点的开始标签时触发一个事件, 开始从这里加载到内存, 当读取到要解析节点的结束标签时,
触发一个事件, 结束加载. 从而只会得到要解析的节点部分
* 好处: 节省内存
* 缺点: 文档加载不完整, 如果要进行增删改节点, 要反复多次读取文档, 效率低
* 第三方解析库:
* `JAXP`: Java提供的解析XML规范, 规定了Dom和SAX两种解析方式
* `Dom4J`: 第三方开源Dom解析库(使用方便, 推荐), 同时支持DOM解析和SAX两种解析方式, 也实现了JAXP
* `org.dom4j.io.SAXReader`类: SAX解析器
* 构造方法:
* `SAXReader()`: 创建一个SAX解析器
* 成员方法:
* `Document read(String systemId)`: 读取XML文件, 返回一个Document对象
* `org.dom4j.io.OutputFormat`类: 配置XML输出格式
* `static OutputFormat createPrettyPrint()`: 创建一个用户输出美观格式的OutputFormat对象
* `void setEncoding(String encoding)`: 设置字符集
* `org.dom4j.io.XMLWriter`类: 用于向XML中写入数据的输出流
* 构造方法:
* `XMLWriter(OutputStream out)`: 使用字节流创建一个XMLWriter对象
* 成员方法:
* `void write(Document doc)`: 将Document对象中的数据写入到XML文件
* `org.dom4j.Document`接口: 代表一个XML文档
* `Element getRootElement()`: 获取根元素对象. 需要用根元素获取其他子元素
* `org.dom4j.Element`接口: 代表一个元素
* 获取元素信息
* `String getName()`: 获取元素名
* 元素增删改查
* 获取元素
* `List<Element> elements()`: 获取调用元素对象的所有子元素
* `List<Element> elements(String name)`: 获取所有的指定名称的子元素
* `Element element(String name)`: 获取第一个该名称的子元素对象
* `Element getParent()`: 获取当前元素的父元素对象
* 添加元素
* `Element addElement(String elementName)`: 在当前元素下添加指定名称的子元素
* 删除元素
* `boolean remove(Element child)`: 删除子元素
* 操作元素中的文本
* `String elementText(String name)`: 获取第一个该名称的子元素的文本内容
* `String getText()`: 获取当前元素对象的文本
* `void setText(String text)`: 设置当前元素对象的文本
* 操作元素的属性
* `String attributeValue(String name)`: 根据指定的属性名获取属性值
* `Element addAttribute(String name, String value)`: 添加或修改指定属性名的属性值
* `org.dom4j.DocumentHelper`类:
* `static Element createElement(String name)`: 使用指定名称作为元素名, 创建一个元素对象
* 注意:
* 要删除一个节点, 必须通过其父节点来删除
* 在内存中对Document进行修改后, 需要写回文档
|
|