侧边栏壁纸
博主头像
GabrielxD

列車は必ず次の駅へ。では舞台は?私たちは?

  • 累计撰写 674 篇文章
  • 累计创建 128 个标签
  • 累计收到 20 条评论

目 录CONTENT

文章目录

【学习笔记】Java(下)

GabrielxD
2022-03-16 / 0 评论 / 0 点赞 / 257 阅读 / 21,897 字
温馨提示:
本文最后更新于 2022-09-08,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

学习笔记: Java(上)

异常处理

概述

异常是程序在“编译”或者“执行”的过程中可能出现的问题(注意:语法错误不算在异常体系中)。
比如:数组索引越界、空指针异常、日期格式化异常,等…

为什么需要异常处理

  • 异常一旦出现如果没有提前处理,程序就会退出JVM虚拟机而终止。
  • 研究异常并且避免异常,然后提前处理异常,体现的是程序的安全,健壮性。

异常体系

image-20220306173914645

Error:系统级别问题:JVM退出等,代码无法控制。
Exception:java.lang 包下,称为异常类,它标识程序本身可以处理的问题。

  • 运行时异常:RuntimeException 及其子类
  • 编译时异常:除 RuntimeException 外所有继承或间接继承了 Exception 的异常

image-20220306174357159

运行时异常

直接继承自 RuntimeException 或者其子类,编译阶段不会报错,运行时可能出现的错误。

运行时异常示例

  • 数组索引越界异常:ArraylndexOutOfBoundsException
  • 空指针异常:NullPointerException
  • 数学操作异常:ArithmeticException
  • 类型转换异常:ClassCastException
  • 数字转换异常:NumberFormatException

运行时异常一般是程序员业务没有考虑好或者是编程逻辑不严谨引起的程序错误。

编译时异常

不是 RuntimeException 或者其子类的异常,编译阶段就报错,必须处理,否则代码不通过。

编译时异常示例

String date = "2015-01-02 10:23:21";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date d = sdf.parse(date); // ParseException
System.out.println(d);

异常的默认处理机制

public class ExceptionDemo {
	public static void main(String[] args) {
        System.out.println("===== start =====");
        chu(10, 0);
        System.out.println("===== end =====");
    }
    
    public static void chu(int a, int b) {
        System.out.println(a);
        System.out.println(b);
        int c = a / b; // 出现运行时异常时,自动创建异常对象:ArithmeticException
        System.out.println("result=" + c);
    }
}
  1. 默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException。
  2. 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给 JVM 虚拟机。
  3. 虚拟机接收到异常对象后,先在控制台直接输出异常栈信息数据。
  4. 直接从当前执行的异常点干掉当前程序。
  5. 后续代码没有机会执行了,因为程序已经死亡。

编译时异常的处理机制

1、throws

throws : 用在方法上,可以将方法内部出现的异常抛出去给本方法的调用者处理。
这种方式并不好,发生异常的方法自己不处理异常,如果异常最终抛出去给虚拟机将引起程序死亡。

抛出异常格式
方法 throws 异常1, 异常2, 异常3, ... {
    // do sth.
}
规范做法
方法 throws Exception { }

2、try…catch…

监视捕获异常,用在方法内部,可以将方法内部出现的异常直接捕获处理。
使用这种方式时,发生异常的方法自己独立完成异常的处理,程序可以继续往下执行。

格式
try {
    // 监视异常可能出现的代码
} catch(异常类型1 变量) {
    // 处理异常
} catch(异常类型2 变量) {
    // 处理异常
} // ...
建议格式
try {
    // 监视异常可能出现的代码
} catch(Exception e) {
    e.printStackTrace(); // 打印异常栈信息
}

3、前两种方式结合

方法直接将异常通过 throws 抛出给调用者。
调用者收到异常后再进行捕获处理。

实例
public class ExceptionHandleDemo {
	public static void main(String[] args) {
        System.out.println("===== start =====");
        try {
            parseTime("2015-01-02 10:23:21");
            System.out.println("Success");
        } catch(Exception e) {
            e.printStackTrace();
            System.out.println("Fail");
        }
        System.out.println("===== end =====");
    }
    
    public static void parseTime(String date) throws Exception {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date d = sdf.parse(date);
        System.out.println(d);
        
        InputStream is = new FileInputStream("D:/test.txt");
    }
}

运行时异常的处理机制

运行时异常在编译阶段不会出错,运行时才可能出错,所以在编译阶段不处理也可以。
但是按照规范还是建议处理:在最外层调用处集中捕获处理即可。

实例
public class ExceptionDemo {
	public static void main(String[] args) {
        System.out.println("===== start =====");
        try {
            chu(10, 0);
        } catch(Exception e) {
            e.printStackTrace();
        }
        System.out.println("===== end =====");
    }
    
    public static void chu(int a, int b) {
        System.out.println(a);
        System.out.println(b);
        int c = a / b;
        System.out.println("result=" + c);
    }
}

自定义异常

编译时异常

  1. 定义一个异常类继承 Exception。
  2. 重写构造器。
  3. 在出现异常的地方使用 throw new 自定义对象 的方式抛出。
实例

ageIllegalException.java

public class ageIllegalException extends Exception {
    public ageIllegalException() {
    }

    public ageIllegalException(String message) {
        super(message);
    }
}

Test.java

public class Test {
    public static void main(String[] args) {
        try {
            checkAge(-20);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void checkAge(int age) throws ageIllegalException {
        if (age < 0 || age >= 200) {
            throw new ageIllegalException("age value: " + age + " is illegal");
        }
        System.out.println("age is legal");
    }
}

运行时异常

  1. 定义一个异常类继承 RuntimeException。
  2. 重写构造器。
  3. 在出现异常的地方使用 throw new 自定义对象 的方式抛出。
实例

ageIllegalRuntimeException.java

public class ageIllegalRuntimeException extends RuntimeException {
    public ageIllegalRuntimeException() {
    }

    public ageIllegalRuntimeException(String message) {
        super(message);
    }
}

Test.java

public class Test {
    public static void main(String[] args) {
        try {
            checkAge(-20);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void checkAge(int age) {
        if (age < 0 || age >= 200) {
            throw new ageIllegalRuntimeException("age value: " + age + " is illegal");
        }
        System.out.println("age is legal");
    }
}

try-catch-finally

概述

finally:在异常处理时提供 finally 块来执行所有清除操作,比如说IO流中的释放资源。
被 finally 控制的语句最终一定会执行,除非 JVM 退出。
异常处理的标准格式:try…catch…finally…

语法

try {
    // ...
} catch (Exception e) {
    // ...
} finally {
    // ...
}

实例

public class Test {
    public static void main(String[] args) {
        InputStream is = null;
        OutputStream os = null;
        try {
            is = new FileInputStream("E\\Desktop\\example.mp4");
            os = new FileOutputStream("E:\\Desktop\\test.mp4");

            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            System.out.println("复制完成");

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                // 防止在 os 还是 null 的时候出错造成此处空指针异常
                if (os != null) os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (is != null) is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

try-catch-resource

概述

finally虽然可以用于释放资源,但是释放资源的代码过于繁琐。
于是在 JDK 7 和 JDK 9 中分别有了简化资源释放操作的语法糖。

原来的做法

try {
    // 可能出现异常的代码;
} catch(异常类名 变量名) {
    // 异常的处理代码;
} finally{
    // 执行所有资源释放操作; (手动释放资源)
}

JDK 7 改进方案

try (定义流对象) {
    // 可能出现异常的代码;
} catch(异常类名 变量名) {
    // 异常的处理代码;
}

// 资源用完最终自动释放
实例
public class Test {
    public static void main(String[] args) {
        try (
                InputStream is = new FileInputStream("E\\Desktop\\example.mp4");
                OutputStream os = new FileOutputStream("E:\\Desktop\\test.mp4");
        ) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            System.out.println("复制完成");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

JDK 9 改进方案

// 定义输入流对象;
// 定义输出流对象;
try (输入流对象; 输出流对象) {
    // 可能出现异常的代码;
} catch(异常类名 变量名) {
    // 异常的处理代码;
}

// 资源用完最终自动释放
实例
public class Test3 {
    public static void main(String[] args) throws Exception { // 要手动抛编译时异常
        InputStream is = new FileInputStream("E\\Desktop\\example.mp4");
        OutputStream os = new FileOutputStream("E:\\Desktop\\test.mp4");
        try (is; os) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            System.out.println("复制完成");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意

被自动关闭的资源需要实现 Closeable 或者 AutoCloseable 接口。
因为只有实现了这两个接口才可以自动调用 close() 方法去自动关闭资源。

日志

概述

Log 日志,主要用于记录程序运行的情况,以便于程序在部署之后的排错调试等,也有利于将这些信息进行持久化(如果不将日志信息保存到文件或数据库,则信息便会丢失),日志记录的详细程度决定系统是否容易维护。

为什么要使用日志技术

输出语句记录程序运行情况:System.out.println(), e.printStackTrace(), …
这种方式的弊端:

  1. 信息只能展示在控制台不能将其记录到其他的位置(文件,数据库)
  2. 想取消记录的信息需要修改代码才可以完成

日志技术具备的优势

  1. 可以将系统执行的信息选择性的记录到指定的位置(控制台、文件中、数据库中)。
  2. 可以随时以配置文件的形式控制是否记录日志,无需修改源代码。
输出语句 日志技术
输出位置 只能是控制台 可以将日志信息写入到文件或者数据库中
取消日志 需要修改代码,灵活性比较差 不需要修改代码,灵活性比较好
多线程 性能较差 性能较好

日志技术体系

image-20220307105540939

**日志规范:**一些接口,提供给日志的实现框架设计的标准。

  • JCL:Apache基金会所属的项目,是一套Java日志接口,之前叫Jakarta Commons Logging,后更名为Commons Logging。
  • SLF4J:是一套简易Java日志门面,本身并无日志的实现

**日志框架:**是日志框架的实现,可以直接拿来使用。

  • Log4j:一个具体的日志实现框架。
  • JUL:JDK中的日志记录工具,也常称为JDKLog、jdk-logging,自Java1.4以来的官方日志实现。
  • Logback:一个具体的日志实现框架,和Slf4j是同一个作者,但其性能更好(推荐使用)。

Logback 日志框架介绍

Logback 是基于 slf4j 的日志规范实现的框架。
官方网站:Logback Home

Logback 主要分为三个技术模块

  • logback-core:logback-core 模块为其他两个模块奠定了基础,必须有。
  • logback-classic:它是 log4j 的一个改良版本,同时它完整实现了slf4j API。
  • logback-access:该模块与 Tomcat 和 Jetty 等 Servlet 容器集成,以提供 HTTP 访问日志功能。

Logback 快速入门

  1. 在项目下新建文件夹 lib,导入 Logback 的相关 jar 包并添加到项目依赖库中。
  2. 在 src 目录下配置 Logback 核心配置文件 logback.xml。
  3. 在代码中获取日志的对象:
    public static final Logger LOGGER = LoggerFactory.getLogger("类对象");
  4. 使用日志对象 LOGGER 调用其方法记录日志信息。

实例

image-20220307110801862

Test.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Test {
    public static final Logger LOGGER = LoggerFactory.getLogger("Test.class");
    public static void main(String[] args) {
        try {
            LOGGER.debug("start");
            LOGGER.info("start division");
            int a = 10;
            int b = 0;
            LOGGER.trace("a=" + a);
            LOGGER.trace("b=" + b);
            System.out.println(a / b);
        } catch (Exception e) {
            e.printStackTrace();
            LOGGER.error("Error: " + e);
        }
    }
}

gabrielxd-data.log

2022-03-07 11:09:05.743 [main] DEBUG Test.class - start
2022-03-07 11:09:05.744 [main] INFO  Test.class - start division
2022-03-07 11:09:05.748 [main] TRACE Test.class - a=10
2022-03-07 11:09:05.748 [main] TRACE Test.class - b=0
2022-03-07 11:09:05.751 [main] ERROR Test.class - Error: java.lang.ArithmeticException: / by zero

文件操作

File

概述

java.io.File 代表操作系统的文件对象(文件、文件夹)

File 构造器

构造器 说明
public File(String pathname) 根据文件路径创建文件对象
public File(String parent, String child) 根据父路径名字符串和子路径名字符串创建文件对象
public File(File parent, String child) 根据父路径文件对象和子路径名字符串创建文件对象
  • File 对象可以定位文件和文件夹。
  • File 封装的对象仅仅是一个路径名,这个路径可以存在,也可以不存在。
  • 相对路径从当前工程的目录下开始定位。

常用 API

方法 说明
long length() 返回此抽象路径名表示的文件的长度,如果其表示目录,则返回 0
boolean isDirectory() 测试此抽象路径名表示的 File 是否为文件夹
boolean isFile() 测试此抽象路径名表示的 File 是否为文件
boolean exists() 测试此抽象路径名表示的 File 是否存在
String getAbsolutePath 返回此抽象路径名的绝对路径字符串
String getPath() 返回此抽象路径名转换为路径名的字符串
String getName() 返回此抽象路径名标识的文件或文件夹的名称
long lastModified() 返回文件最后修改的时间毫秒值
方法 说明
boolean createNewFile() 创建一个新的空文件(touch)
boolean mkdir() 创建一级文件夹(mkdir)
boolean mkdirs() 创建多级文件夹(mkdir -p)
方法 说明
boolean delete() 删除此抽象路径名标识的文件或者空文件夹(rm)
方法 说明
String[] list() 获取当前目录下所有一级文件名称到一个字符串数组中并返回
File[] listFiles() 获取当前目录下所有一级文件对象到一个文件对象数组中并返回
  • 调用者不存在或是一个文件时,返回 null。
  • 调用者是一个空文件夹时,返回长度为 0 的数组。
  • 调用者是有内容的文件夹时,返回一级子目录/文件组成的文件路径数组,包括隐藏文件。

字符集

概述

计算机底层不能直接存储字符,只能存储二进制,但二进制可以与十进制互相转换。
于是便出现了字符集的概念。

ASCII 字符集

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码):包括数字、英文、符号等。
ASCII 使用1个字节存储一个字符,一个字节是8位,共可表示128个字符信息。

image-20220308150806621

GBK 字符集

GBK(Guo Biao Kuo zhan,汉字内码扩展规范):包括简体中文、繁体中文、数字、英文、符号以及部分日韩文字等。
GBK 在 00-7F 范围内是一个字节,与 ASCII 保持一致,之外的范围是双字节。
简体中文版 Windows 系统默认字符集为 GBK。

GBK的编码范围

范围 第1字节 第2字节 编码数 字数
水准GBK/1 A1A9 A1FE 846 717
水准GBK/2 B0F7 A1FE 6,768 6,763
水准GBK/3 81A0 40FE (7F除外) 6,080 6,080
水准GBK/4 AAFE 40A0 (7F除外) 8,160 8,160
水准GBK/5 A8A9 40A0 (7F除外) 192 166
用户定义 AAAF A1FE 564
用户定义 F8FE A1FE 658
用户定义 A1A7 40A0 (7F除外) 672
合计: 23,940 21,886

Unicode 字符集

Unicode(统一码):是计算机科学领域的业界标准,其整理、编码了世界上大部分的文字系统。
常用编码格式:UTF-8。UTF-16、GB18030。
UTF-8 是 ASCII 的超集。
以 UTF-8 编码后,中文一般以三个字节的形式存储。

编码

java.lang.String

方法 说明
byte[] getBytes() 使用平台默认字符集将该字符串编码存入字节数组
byte[] getBytes(String charsetName) 使用指定字符集将该字符串编码存入字节数组

解码

java.lang.String

构造器 说明
String(byte[] bytes) 通过平台默认字符集解码指定字节数组来构造新字符串
String(byte[] bytes, String charsetName) 通过指定字符集解码指定字节数组来构造新字符串
String(byte[] bytes, int off, int len) 解码指定字节数组的指定部分来构造新字符串

IO 流

概述

亦称输入、输出流,其专门用来读写数据。

  • I - Input,输入流,是数据从硬盘文件读入到内存的过程,称之为输入,负责读。
    • 字节输入流:以内存为基准,来自磁盘文件/网络中的数据以字节的形式读入到内存中去的流称为字节输入流。
    • 字符输入流:以内存为基准,来自磁盘文件/网络中的数据以字符的形式读入到内存中去的流称为字符输入流。
  • O - Output,输出流,是内存程序的数据从内存写出到硬盘文件的过程,称之为输出,负责写。
    • 字节输出流:以内存为基准,把内存中的数据以字节写出到磁盘文件或者网络中去的流称为字节输出流。
    • 字符输出流:以内存为基准,把内存中的数据以字符写出到磁盘文件或者网络中去的流称为字符输出流。

image-20220309132422587

文件字节输入流:FileInputStream

构造器
构造器 说明
public FileInputStream(File file) 创建字节输入流管道与源文件对象接通
public FileInputStream(String pathName) 创建字节输入流管道与源文件路径接通
常用 API
方法 说明
int read() 每次读取一个字节返回,如果字节已经没有可读则返回 -1
int read(byte[] buffer) 每次读取一个字节数组,返回读取字节数,如果字节已经没有可读则返回 -1
byte[] readAllBytes() 从输入流中读取所有剩余字节

文件字节输出流:FileOutputStream

构造器
构造器 说明
public FileOutputStream(File file) 创建文件输出流以写入由指定的 File 对象表示的文件
public FileOutputStream(String pathName) 创建文件输出流以写入具有指定名称的文件
public FileOutputStream(File file, boolean append) append 为 true 时开启追加模式
public FileOutputStream(File file, boolean append) append 为 true 时开启追加模式
常用 API
方法 说明
void write(int b) 写一个字节
void write(byte[] buffer) 写一个字节数组
void write(byte[] buffer, int off, int len) 从偏移量 off 开始的指定字节数组中的 len 字节写入此文件输出流
方法 说明
flush() 刷新流,还可以继续写数据
close() 关闭流,释放资源,但在关闭之前会先刷新流,关闭后不能再写数据

优化异常处理结合 try-catch-finally / try-catch-resource

文件字符输入流:FileReader

构造器
构造器 说明
public FileReader(File file) 创建字符输入流管道与源文件对象接通
public FileReader(String pathname) 创建字符输入流管道与源文件路径接通
常用 API
方法 说明
int read() 每次读取一个字符返回,如果字符已经没有可读的返回 -1
int read(char[] cbuf) 每次读取一个字节数组,返回读取字符数,如果字节已经没有可读则返回 -1

文件字符输出流:FileWriter

构造器
构造器 说明
public FileWriter(File file) 创建字符输出流管道与源文件对象接通
public FileWriter(File file,boolean append) 创建字符输出流管道与源文件对象接通,可追加数据
public FileWriter(String filepath) 创建字符输出流管道与源文件路径接通
public FileWriter(String filepath,boolean append) 创建字符输出流管道与源文件路径接通,可追加数据
常用 API
方法名称 说明
void write(int c) 写一个字符
void write(char[] cbuf) 写入一个字符数组
void write(char[] cbuf, int off, int len) 写入字符数组的一部分
void write(String str) 写一个字符串
void write(String str, int off, int len) 写一个字符串的一部分
void write(int c) 写一个字符
方法 说明
flush() 刷新流,还可以继续写数据
close() 关闭流,释放资源,但在关闭之前会先刷新流,关闭后不能再写数据

缓冲流

概述

缓冲流也称为高效流、或者高级流,之前的字节流可以称为原始流。
缓冲流自带缓冲区(默认8192B)、可以提高原始字节流、字符流读写数据的性能。

字节缓冲输入流:BufferedInputStream

构造器
构造器 说明
public BufferedInputStream(InputStream is) 传入一个字节输入流创建具有默认缓冲区大小的缓冲字节输入流管道
public BufferedInputStream(InputStream is, int size) 传入一个字节输入流创建具有指定缓冲区大小的缓冲字节输入流管道

API 与字节输入流相同

字节缓冲输出流:BufferedOutputStream

构造器
构造器 说明
public BufferedOutputStream(OutputStream os) 创建新的缓冲输出流以将数据写入指定的基础输出流
public BufferedOutputStream(OutputStream os, int size) 创建新的缓冲输出流,以使用指定的缓冲区大小将数据写入指定的基础输出流

API 与字节输出流相同

性能测试

拷贝性能测试(字节数组均使用1024长度)

结果:BIS + BOS + 字节数组 > IS + OS + 字节数组 > BIS + BOS + 单字节 > IS + OS + 单字节

字符缓冲输入流:BufferedReader

构造器
构造器 说明
public BufferedReader(Reader in) 创建使用默认大小的输入缓冲区的缓冲字符输入流。
public BufferedReader(Reader in, int sz) 创建使用指定大小的输入缓冲区的缓冲字符输入流。
常用 API
方法 说明
String readLine() 读一行文字。

其他API 与字符输入流相同

字符缓冲输出流:BufferedWriter

构造器
构造器 说明
public BufferedWriter(Writer out) 创建使用默认大小的输出缓冲区的缓冲字符输出流。
public BufferedWriter(Writer out, int sz) 创建使用指定大小的输出缓冲区的缓冲字符输出流。
常用 API
方法 说明
void newLine() 写一个行分隔符

其他API 与字符输出流相同

转换流

使用指定字符集把原始字节流转换成字符流,解决不同编码读取时的乱码问题。

字符输入转换流:InputStreamReader

构造器
构造器 说明
public InputStreamReader(InputStream in) 使用默认字符集把字节输入流转换为字符输入流
public InputStreamReader(InputStream in, String charsetName) 使用指定字符集把字节输入流转换为字符输入流
实例
public class Test {
    public static void main(String[] args) {
        try (
				// BufferedReader br = new BufferedReader(new FileReader("E:\\Desktop\\test.txt")) //会乱码
                InputStream is = new FileInputStream("E:\\Desktop\\test.txt");
                Reader r = new InputStreamReader(is, "GBK");
                BufferedReader br = new BufferedReader(r);
        ) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

字符输出转换流:OutputStreamWriter

构造器
构造器 说明
public OutputStreamWriter(OutputStream os) 使用默认字符集把字节输出流转换为字符输出流
public OutputStreamWriter(OutputStream os, String charsetName) 使用指定字符集把字节输出流转换为字符输出流
实例
public class Test {
    public static void main(String[] args) {
        try (
                OutputStream os = new FileOutputStream("E:\\Desktop\\test.txt");
                Writer w = new OutputStreamWriter(os, "GBK");
                BufferedWriter bw = new BufferedWriter(w);
        ) {
            bw.write("test测试test");
            bw.newLine();
            bw.write("test测试test");
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

对象的序列化与反序列化

对象序列化

以内存为基准,把内存中的对象存储到磁盘文件中去,称为对象序列化。

对象字节输出流:ObjectOutputStream

构造器
构造器 说明
public ObjectOutputStream(OutputStream out) 把字节输出流包装成对象字节输出流
常用 API
方法 说明
void writeObject(Object obj) 把对象写出到对象序列化流的文件中

对象反序列化

以内存为基准,把存储到磁盘文件中去的对象数据恢复成内存中的对象,称为对象反序列化。

对象字节输入流:ObjectInputStream

构造器
构造器 说明
public ObjectInputStream(InputStream out) 把字节输入流包装成对象字节输入流
常用 API
方法 说明
Object readObject() 把存储到磁盘文件中去的对象数据恢复成内存中的对象并返回

注意

  • 只有实现了 Serializable 接口的对象才可以被序列化。
  • 使用 transient 关键字修饰的成员属性不会被序列化。
  • 静态变量不会被序列化。
  • 指定序列化版本号 public static final long serialVersionUID
    序列化版本号和反序列化版本号不一致时会报错。

打印流

概述

打印流可以实现方便、高效的打印数据到文件中去。
打印流一般是指:PrintStream,PrintWriter两个类。

字节打印流:PrintStream

构造器
构造器 说明
public PrintStream(OutputStream os) 根据字节输出流创建打印流
public PrintStream(File f) 根据文件对象创建打印流
public PrintStream(String filePath) 根据文件路径字符串创建打印流
常用 API
方法 说明
void write(int b) 将指定的字节写入此流
void print(Object obj) 格式化打印任意类型数据
void println(Object obj) 换行打印任意类型数据

字符打印流:PrintWriter

构造器
构造器 说明
public PrintWriter(OutputStream os) 根据字节输出流创建打印流
public PrintWriter(Writer w) 根据字符输出流创建打印流
public PrintWriter(File f) 根据文件对象创建打印流
public PrintWriter(String filePath) 根据文件路径字符串创建打印流
常用 API
方法 说明
void write(int b) 将指定的字节写入此流
void write(String s) 写一个字符串
void print(Object obj) 格式化打印任意类型数据
void println(Object obj) 换行打印任意类型数据

输出语句重定向

java.lang.System

方法 说明
static void setOut(PrintStream out) 重新分配“标准”输出流
实例

Test.java

public class Test {
    public static void main(String[] args) {
        PrintStream ps = new PrintStream("E://Desktop//log.txt");
        System.setOut(ps);
        System.out.println("测试");
        System.out.println("Test");
	}
}

log.txt

测试
Test

IO 框架

commons-io 是 apache 开源基金组织提供的一组有关 IO 操作的类库,可以提高 IO 功能开发的效率。
commons-io 工具包提供了很多有关 io 操作的类。有两个主要的类 FileUtils,IOUtils

Overview (Apache Commons IO 2.11.0 API)

多线程

概述

线程(Thread)是一个程序内部的一条执行路径,main 方法的执行其实就是一条单独的执行路径。
程序中如果只有一条执行路径,那么这个程序就是单线程的程序。

1、继承 Thread 类实现多线程

Java是通过 java.lang.Thread 类来代表线程的,按照面向对象的思想,Thread类应该提供了实现多线程的方式。

使用

  1. 定义一个子类 MyThread 继承线程类 java.lang.Thread,重写 run() 方法。
  2. 创建 MyThread 类的对象。
  3. 调用线程对象的 start() 方法启动线程(启动后还是执行 run 方法的)。

实例

public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new ChildThread();
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("[Main Thread] i=" + i);
        }
    }
}

class ChildThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("[Child Thread] i=" + i);
        }
    }
}

优缺点

优点:编码简单

缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。

注意

  • 只有调用start方法才是启动一个新的线程执行,直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
  • 主线程任务一定要放在子线程之后运行,否则主线程会一直优先跑完,达不到多线程的效果。

2、实现 Runnable 接口实现多线程

使用

  1. 定义一个线程任务类 MyRunnable 实现 Runnable 接口,重写 run() 方法。
  2. 创建 MyRunnable 任务对象。
  3. 把 MyRunnable 任务对象交给 Thread 处理。
  4. 调用线程对象的 start() 方法启动线程。

java.lang.Thread

构造器 说明
public Thread(String name) 创建一个线程对象并指定名称
public Thread(Runnable target) 封装 Runnable 的实现对象为线程对象
public Thread(Runnable target, String name) 封装 Runnable 的实现对象为线程对象并指定名称

实例

普通写法
public class RunnableDemo {
    public static void main(String[] args) {
        Runnable target = new MyRunnable(); // 创建任务对象
        new Thread(target).start(); // 封装为线程对象并运行
        
        for (int i = 0; i < 5; i++) {
            System.out.println("[Main Thread] i=" + i);
        }
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("[Child Thread] i=" + i);
        }
    }
}
匿名内部类写法
public class RunnableDemo {
    public static void main(String[] args) {
        Runnable target = () -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("[Child Thread] i=" + i);
            }
        };
        new Thread(target).start();

        for (int i = 0; i < 5; i++) {
            System.out.println("[Main Thread] i=" + i);
        }
    }
}

优缺点

优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。

缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。

3、利用 Callable、FutureTask 接口实现多线程

前两种线程创建方式存在问题:其重写的 run() 不能返回结果,不适合需要多线程执行任务返回结果的业务场景。

使用

  1. 得到任务对象
    1. 定义类实现 Callable 接口,重写 call 方法,封装要做的事情。
    2. 用 FutureTask 把 Callable 对象封装成线程任务对象。
  2. 把线程任务对象交给 Thread 处理。
  3. 调用 Thread 的 start 方法启动线程,执行任务。
  4. 线程执行完毕后、通过 FutureTask 的 get 方法去获取任务执行的结果。

java.util.concurrent.FutureTask

构造器 方法
public FutureTask<>(Callable call) 把 Callable 对象封装成 FutureTask 对象
public V get() throws Exceptions 获取线程执行 call 方法返回的结果

实例

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class CallableDemo {
    public static void main(String[] args) {
        Callable<Integer> call = new MyCallable(100); // 创建 callable 任务对象
        // FutureTask 实现了 Runnable 接口,可以作为 Thread 类的构造器参数传入
        FutureTask<Integer> ft = new FutureTask<>(call);
        new Thread(ft).start();

        Callable<Integer> call2 = new MyCallable(200);
        FutureTask<Integer> ft2 = new FutureTask<>(call2);
        new Thread(ft2).start();

        try {
            Integer res = ft.get();
            System.out.println("[ChildThread(100)] res=" + res);
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            Integer res2 = ft2.get();
            System.out.println("[ChildThread(200)] res=" + res2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyCallable implements Callable<Integer> {
    private int n;

    public MyCallable(int n) {
        this.n = n;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        return sum;
    }
}

优缺点

优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
可以在线程执行完毕后去获取线程执行的结果。

缺点:编码复杂一点。

常用 API

java.lang.Thread

方法 说明
String getName() 获取线程对象的名称,默认线程名称是 Thread-#
void setName(String name) 更改指定线程名称
static Thread currentThread() 获取当前正在执行的线程对象的引用
方法 说明
static void sleep(long time) 让当前线程休眠指定的时间后再继续执行,单位为毫秒
方法 说明
void run() 线程任务方法
void start() 线程启动方法

实例

获取与设置线程名称

public class Example {
    public static void main(String[] args) {
        new MyThread("线程A").start();
        new MyThread("线程B").start();
    }
}

class MyThread extends Thread {
    private String name;

    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("[" + Thread.currentThread().getName()
				+ "] i=" + i);
        }
    }
}

线程安全

多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。

原因

  • 存在多线程并发
  • 同时访问共享资源
  • 存在修改共享资源

线程同步

让多个线程实现先后依次访问共享资源,这样就解决了线程安全问题

1、同步代码块

作用:把出现线程安全问题的核心代码给上锁。
原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

语法
synchronized(同步锁对象) {
    操作共享资源的代码(核心代码)
}
锁对象要求

理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可。

规范上:建议使用共享资源作为锁对象

  • 对于实例方法建议使用this作为锁对象。
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。

2、同步方法

作用:把出现线程安全问题的核心方法给上锁。
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

语法
修饰符 synchronized 返回值类型 方法名称(形参列表) {
    操作共享资源的代码
}
底层原理

同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
如果方法是实例方法:同步方法默认用 this 作为的锁对象。但是代码要高度面向对象。
如果方法是静态方法:同步方法默认用类名 .class 作为的锁对象。

3、ReentrantLock

为了更清晰的表达如何加锁和释放锁,JDK 5 以后提供了一个新的锁对象 Lock,更加灵活、方便。
提供比使用 synchronized 方法和语句可以获得更广泛的锁定操作。
Lock 是接口不能直接实例化,这里采用它的实现类 ReentrantLock 来构建 Lock 锁对象。

ReentrantLock 构造器
构造器 说明
public ReentrantLock() Lock 接口的实现类对象
常用 API

java.util.concurrent.locks.Lock

方法 说明
void lock() 上锁
void unlock() 解锁
示例
class X {
    private final ReentrantLock lock = new ReentrantLock();
    // ...
    public void m() {
        lock.lock(); // block until condition holds
        try {
            // ... method body 
        } finally {
            lock.unlock();
        } 
    } 
}

线程通信

所谓线程通信就是线程间相互发送数据,线程间共享一个资源即可实现线程通信。

常见形式

通过共享一个数据的方式实现。
根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。

常用 API

java.lang.Object

方法 说明
void wait() 使当前线程进入阻塞状态,并释放同步监视器
void notify() 唤醒一个被 wait() 的线程,如果多个线程被 wait() 则唤醒优先级高的
void notifyAll() 唤醒所有被 wait() 的线程
说明
  • 以上三个方法必须使用在同步代码块或同步方法中。
  • 以上三个方法调用者必须是同步代码块或者同步方法中的同步监视器,否则会出现 IllegalMonitorStateExcaption 异常

例:生产者与消费者模型

生产者(Producer)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产:如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

这里可能出现两个问题:

  1. 生产者比消费者快时,消费者会漏掉一些数据没有取到。
  2. 消费者比生产者快时,消费者会取相同的数据。
代码
public class ProducerConsumerProblem {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        new Producer(clerk).start();
        new Consumer(clerk).start();
    }
}

class Clerk {
    private int productNum = 0;

    public synchronized void produceProduct() {
        try {
            if (productNum < 20) {
                Thread.sleep(30);
                productNum++;
                System.out.println("[" + Thread.currentThread().getName() +
                                   "] 开始生产第" + productNum + "个产品");
                notify();
            } else {
                wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public synchronized void consumeProduct() {
        try {
            if (productNum > 0) {
                System.out.println("[" + Thread.currentThread().getName() +
                                   "] 开始消费第" + productNum + "个产品");
                Thread.sleep(50);
                productNum--;
                notify();
            } else {
                wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class Producer extends Thread {
    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        while (true) {
            clerk.produceProduct();
        }
    }
}

class Consumer extends Thread {
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        while (true) {
            clerk.consumeProduct();
        }
    }
}

线程池

线程池就是一个可以复用线程的技术。
JDK 5 起提供了代表线程池的接口:ExecutorService。

1、ThreadPoolExecutor 创建线程池对象

ThreadPoolExecutor 是 ExecutorService 的实现类。

构造器
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize : 指定线程池的线程数量(核心线程)
  • maximumPoolSize : 指定线程池可支持的最大线程数
  • keepAliveTime : 指定临时线程的最大存活时间
  • unit : 指定存活时间的单位(秒、分、时、天)
  • workQueue : 指定任务队列
  • threadFactory : 指定用哪个线程工厂创建线程
  • handler : 指定所有线程忙,任务满的时候新任务的应对策略
    • ThreadPoolExecutor.AbortPolicy : 丢弃任务并抛出RejectedExecutionException异常。是默认的策略
    • ThreadPoolExecutor.DiscardPolicy : 丢弃任务,但是不抛出异常 这是不推荐的做法
    • ThreadPoolExecutor.DiscardOldestPolicy : 抛弃队列中等待最久的任务 然后把当前任务加入队列中
    • ThreadPoolExecutor.CallerRunsPolicy : 由主线程负责调用任务的 run() 方法从而绕过线程池直接执行

注意:

  • 新任务提交时核心线程都在忙,任务队列满,且还可以创建临时线程,此时才会创建临时线程(而且不会一次性全部创建)。
  • 新任务提交时核心线程和临时线程都在忙且任务队列满,才会开始任务拒绝。
常用 API
方法 说明
void execute(Runnable command) 执行任务/命令,没有返回值,一般用来执行 Runnable 任务
Future<T> submit(Callable<T> task) 执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务
void shutdown() 等任务执行完毕后关闭线程池
List<Runnable> shutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务
线程池处理 Runnable 任务
import java.util.concurrent.*;

public class Test {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(6), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        Runnable target = new MyRunnable();
        threadPool.execute(target);
        threadPool.execute(target);
        threadPool.execute(target);
        // 核心线程均忙
        threadPool.execute(target);
        threadPool.execute(target);
        threadPool.execute(target);
        threadPool.execute(target);
        threadPool.execute(target);
        threadPool.execute(target);
        // 任务队列占满
        // 开始创建临时线程
        threadPool.execute(target); // 创建1个临时线程
        threadPool.execute(target); // 创建2个临时线程
        // 线程均忙 任务队列占满
        threadPool.execute(target); // 执行任务拒绝策略
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("[" + Thread.currentThread().getName()
                               + "] i=" + i);
        }
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
线程池处理 Callable 任务
import java.util.concurrent.*;

public class Test {
    public static void main(String[] args) throws Exception {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(6), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        
        Future<String> f1 = threadPool.submit(new MyCallable(100));
        Future<String> f2 = threadPool.submit(new MyCallable(200));
        Future<String> f3 = threadPool.submit(new MyCallable(300));
        Future<String> f4 = threadPool.submit(new MyCallable(400));
        Future<String> f5 = threadPool.submit(new MyCallable(500));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
        System.out.println(f5.get());
    }
}

class MyCallable implements Callable<String> {
    private int n;

    public MyCallable() {
    }

    public MyCallable(int n) {
        this.n = n;
    }

    @Override
    public String call() {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "[" + Thread.currentThread().getName() + "] sum=" + sum;
    }
}

2、Executors 工具类创建线程池

常用 API
方法 说明
static ExecutorService newCachedThreadPool() 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉
static ExecutorService newFixedThreadPool(int nThreads) 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它
static ExecutorService newSingleThreadExecutor() 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务

Executors 的底层其实也是基于线程池的实现类 ThreadPoolExecutor 创建线程池对象的。

问题
  • newCachedThreadPool()newCachedThreadPool() 允许请求的任务队列长度是 Integer.MAX_VALUE,可能出现OOM错误( java.lang.OutOfMemoryError )

  • newSingleThreadExecutor()newScheduledThreadPool() 创建的线程数量最大上限是 Integer.MAX_VALUE,线程数可能会随着任务1:1增长,也可能出现OOM错误( java.lang.OutOfMemoryError )

image-20220313211738233

定时器

定时器是一种控制任务延时调用,或者周期调用的技术。

Timer 定时器

构造器
构造器 说明
public Timer() 创建 Timer 定时器对象
常用 API
方法 说明
void schedule(TimerTask task, long delay, long period) 开启一个定时器,按照计划处理 TimerTask 任务
特点和存在的问题

Timer 是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入。
可能因为其中的某个任务的异常使 Timer 线程死掉,从而影响后续任务执行。

ScheduledExecutorService 定时器

ScheduledExecutorService是 JDK 5 中引入的并发包,目的是为了弥补Timer的缺陷, ScheduledExecutorService内部为线程池。

java.util.concurrent.Executors

方法 说明
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 得到线程池对象

java.util.concurrent.ScheduledExecutorService

方法 说明
ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 周期调度方法
特点

基于线程池,某个任务的执行情况不会影响其他定时任务的执行。

并行与并发

并发:CPU分时轮询的执行线程。

并行:同一个时刻同时在执行。

线程生命周期

线程状态 描述
NEW(新建) 线程刚被创建,但是并未启动
Runnable(可运行) 线程已经调用了start()等待CPU调度
Blocked(锁阻塞) 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态
Waiting(无限等待) 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒
TimeWaiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。带有超时参数的常用方法有Thread.sleep 、Object.wait
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死

网络编程

概述

网络编程可以让程序与网络上的其他设备中的程序进行数据交互。

网络通信基本模式

常见的通信模式有2种形式:Client-Server(CS) 、 Browser/Server(BS)。

网络通信三要素

  1. IP 地址:设备在网络中的唯一标识,通过IP可以找到通信所在机器。
  2. 端口:应用程序在设备中的唯一标识。
  3. 传输协议:通信的规则。

IP地址

IP(Internet Protocol):全称“互联网协议地址”,是分配给上网设备的唯一标志。
常见的IP分类为:IPv4和IPv6

IPv4

网际协议版本4(Internet Protocol version 4),长度32位(4字节)。

  • 使用二进制表示:11000000 10101000 00000001 01100100
  • 使用点分十进制表示法:192.168.1.100
IP 地址的组成

网络号段 + 主机号段

  • A类:第一段为网络号段 + 后三段的主机号段
    一个网络号,可以配 256×256×256 = 16,777,216 台主机
  • B类:前二段为网络号段 + 后二段的主机号段
    一个网络号,可以配 256×256 = 65,536 台主机
  • C类:前三段为网络号段 + 后一段的主机号段
    一个网络号,可以配 256 台主机
IP 地址的分类
  • A类 : 1.0.0.1~127.255.255.254
    • 10.X.X.X 是私有地址(在互联网上不使用,而被用在局域网络中的地址)
    • 127.X.X.X 是保留地址,用做循环测试用的。
  • B类 : 128.0.0.1~191.255.255.254
    • 172.16.0.0~172.31.255.255是私有地址
    • 169.254.X.X 是保留地址。
  • C类 : 192.0.0.1~223.255.255.254
    • 192.168.X.X 是私有地址
  • D类 : 224.0.0.1~239.255.255.254(保留地址)
  • E类 : 240.0.0.1~247.255.255.254(保留地址)
两个 DOS 命令
  1. ipconfig : windows系统下查看本机 IP 地址。
  2. ping <IP> : 测试本机与指定的 IP 地址间的通信。
特殊的 IP 地址
  • 127.0.0.1 : 回环地址(表示本机)
  • X.X.X.255 : 广播地址
  • X.X.X.0 : 网络地址

IPv6

网际协议第6版(Internet Protocol version 6),长度128位(16字节)。
通常使用冒分十六进制表示法:以16位为一组,每组以冒号“:”隔开,可以分为8组,每组以4位十六进制的方式表示。

  • 使用二进制表示:0010000000000001 0000110110111000 1000010110100011 0000000000000000 0000000000000000 1000101000101110 0000001101110000 0111001100110100
  • 使用冒分十六进制表示法:2001:0db8:85a3:0000:0000:8a2e:0370:7334

InetAddress

此类表示Internet协议(IP)地址。

常用 API
方法 说明
static InetAddress getLocalHost() 返回本主机的地址对象
static InetAddress getByName(String host) 得到指定主机的IP地址对象,参数是域名或者IP地址
String getHostName() 获取此IP地址的主机名
String getHostAddress() 返回IP地址字符串
boolean isReachable(int timeout) 在指定毫秒内连通该IP地址对应的主机,连通返回 true

端口号

唯一标识正在计算机设备上运行的进程(程序),被规定为一个 16 位的二进制,范围是 0~65535。

端口类型

  • 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用 80,FTP占用21)。
  • 注册端口:1024~49151,分配给用户进程或某些应用程序。(如:Tomcat 占用8080,MySQL占用3306)。
  • 动态端口:49152到65535,之所以称为动态端口,是因为它 一般不固定分配某种进程,而是动态分配。

通信协议

连接和通信数据的规则被称为网络通信协议。

网络通信协议参考模型

  1. OSI 参考模型:世界互联协议标准,全球通信规范,由于此模型过于理想化,未能在因特网上进行广泛推广。
  2. TCP/IP 参考模型(或TCP/IP协议):事实上的国际标准。
OSI 参考模型 TCP/IP 参考模型 各层对应 面向操作
应用层 应用层 HTTP、FTP、DNS、SMTP… 应用程序需要关注的:
浏览器,邮箱。
程序员一般在这一层开发
表示层
会话层
传输层 传输层 TCP、UDP… 选择使用的TCP , UDP协议
网络层 网络层 IP、ICMP… 封装源和目标IP,进行路径选择
数据链路层 数据链路层+物理 物理寻址、比特流… 物理设备中传输
物理层

传输层常见协议

  • TCP(Transmission Control Protocol) :传输控制协议
  • UDP(User Datagram Protocol):用户数据报协议

UDP 协议

  • UDP是一种无连接、不可靠传输的协议。
  • 将数据源IP、目的地IP和端口封装成数据包,不需要建立连接。
  • 每个数据包的大小限制在64KB内。
  • 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的。
  • 可以广播发送 ,发送数据结束时无需释放资源,开销小,速度快。

应用场景:语音通话,视频会话等。

UDP的三种通信方式

单播:单台主机与单台主机之间的通信。
广播:当前主机与所在网络中的所有主机通信。
组播:当前主机与选定的一组主机的通信。

DatagramPacket 数据包
构造器
public DatagramPacket(byte[] buf,
                      int length,
                      InetAddress address,
                      int port)

创建发送端数据包对象

  • buf : 要发送的内容,字节数组
  • length : 要发送内容的字节长度
  • address : 接收端的IP地址对象
  • port : 接收端的端口号
public DatagramPacket(byte[] buf, int length)

创建接收端的数据包对象

  • buf : 用来存储接收的内容
  • length : 能够接收内容的长度
常用 API
方法 说明
int getLength() 获得实际接收到的字节个数
SocketAddress getSocketAddress() 获取此数据包发送到或来自的远程主机的 SocketAddress
InetAddress getAddress() 返回发送此数据报或从中接收数据报的远程主机的 IP 地址
int getPort() 返回发送此数据报或从中接收数据报的远程主机的端口号
DatagramSocket 数据套接字
构造器
构造器 说明
public DatagramSocket() 创建发送端的Socket对象,系统会随机分配一个端口号
public DatagramSocket(int port) 创建接收端的Socket对象并指定端口号
常用 API
方法 说明
public void send(DatagramPacket dp) 发送数据包
public void receive(DatagramPacket dp) 接收数据包
实例:UDP 实现单播多发多收

Client.java

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        DatagramSocket socket = new DatagramSocket(); // 创建发送端对象,随机端口
        System.out.println("===== Client Running =====");
        while (true) {
            System.out.print("请输入要发送的消息(输入 exit 退出):");
            String data = sc.nextLine();
            byte[] buffer = data.getBytes(); // 准备数据
            // 创建数据包对象封装数据
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length,
                    InetAddress.getLocalHost(), 23333);
            socket.send(packet); // 发送数据
            if ("exit".equals(data)) {
                socket.close(); // 释放资源
                System.out.println("===== Client Terminated =====");
                break;
            }
        }
    }
}

Server.java

import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class Server {
    public static void main(String[] args) throws Exception {
        DatagramSocket socket = new DatagramSocket(23333); // 创建接收端对象,指定端口
        System.out.println("===== Server Running =====");
        byte[] buffer = new byte[1024 * 64]; // 准备数据包对象,大小 64KB
        // 创建数据包对象封装数据
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
        while (true) {
            socket.receive(packet); // 接收数据
            int len = packet.getLength(); // 获取收到数据长度
            String data = new String(buffer, 0, len);
            if ("exit".equals(data)) {
                System.out.println(packet.getSocketAddress() + " disconnected.");
                continue;
            }
            System.out.println("[" + packet.getSocketAddress() + "] " + data);
        }
    }
}
UDP 实现广播

使用广播地址:255.255.255.255
具体操作:

  • 发送端发送的数据包的目的地写的是广播地址、且指定端口(255.255.255.255, 9999)。
  • 本机所在网段的其他主机的程序只要注册对应端口(9999)就可以收到消息了。

实例:

服务端同上不做变动

Client.java

package e03_broadcast;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        DatagramSocket socket = new DatagramSocket(); // 创建发送端对象,随机端口
        System.out.println("===== Client Running =====");
        while (true) {
            System.out.print("请输入要发送的消息(输入 exit 退出):");
            String data = sc.nextLine();
            byte[] buffer = data.getBytes(); // 准备数据
            // 创建数据包对象封装数据
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length,
                    InetAddress.getByName("255.255.255.255"/* 【广播】 */), 23333);
            socket.send(packet); // 发送数据
            if ("exit".equals(data)) {
                socket.close(); // 释放资源
                System.out.println("===== Client Terminated =====");
                break;
            }
        }
    }
}
UDP 实现组播

使用组播地址:224.0.0.0 ~ 239.255.255.255
具体操作:

  • 发送端的数据包的目的地是组播IP(例如:224.0.1.1, 端口:9999)。
  • 接收端必须绑定该组播IP(224.0.1.1),端口还要注册发送端的目的端口9999 ,这样即可接收该组播消息。
    DatagramSocket 的子类 MulticastSocket 可以在接收端绑定组播IP。

实例:

Client.java

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        DatagramSocket socket = new DatagramSocket(); // 创建发送端对象,随机端口
        System.out.println("===== Client Running =====");
        while (true) {
            System.out.print("请输入要发送的消息(输入 exit 退出):");
            String data = sc.nextLine();
            byte[] buffer = data.getBytes(); // 准备数据
            // 创建数据包对象封装数据
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length,
                    InetAddress.getByName("224.0.1.1"/* 【组播】 */), 23333);
            socket.send(packet); // 发送数据
            if ("exit".equals(data)) {
                socket.close(); // 释放资源
                System.out.println("===== Client Terminated =====");
                break;
            }
        }
    }
}

Server.java

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;

public class Server {
    public static void main(String[] args) throws Exception {
        // 创建接收端对象,指定端口
        MulticastSocket socket = new MulticastSocket(23333); 
        socket.joinGroup(InetAddress.getByName("224.0.1.1")); // 【绑定组播IP】
        System.out.println("===== Server Running =====");
        byte[] buffer = new byte[1024 * 64]; // 准备数据包对象,大小 64KB
        // 创建数据包对象封装数据
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
        while (true) {
            socket.receive(packet); // 接收数据
            int len = packet.getLength(); // 获取收到数据长度
            String data = new String(buffer, 0, len);
            if ("exit".equals(data)) {
                System.out.println(packet.getSocketAddress() + " disconnected.");
                continue;
            }
            System.out.println("[" + packet.getSocketAddress() + "] " + data);
        }
    }
}

TCP 协议

  • 使用TCP协议,必须双方先建立连接,它是一种面向连接可靠通信协议。
  • 传输前,采用“三次握手”方式建立连接,所以是可靠的 。
  • 在连接中可进行大数据量的传输 。
  • 连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接,通信效率较低。

应用场景:对信息安全要求较高的场景,例如:文件下载、金融等数据通信。

三次握手建立连接

image-20220314172833303

四次挥手断开连接

img

Java 中的 TCP 通信

image-20220314194606644

Socket
构造器
构造器 说明
public Socket(String host, int port) 创建发送端的 Socket 对象与服务端连接,参数为服务端程序的 IP和端口
常用 API
方法 说明
OutputStream getOutputStream() 获得字节输出流对象
InputStream getInputStream() 获得字节输入流对象
ServerSocket
构造器
构造器 说明
public ServerSocket(int port) 注册服务端端口
常用 API
方法 说明
Socket accept() 等待接收客户端的Socket通信连接,连接成功返回 Socket 对象与客户端建立端到端通信
实例:TCP 实现多发多收

注意:不支持多端连接

Client.java

import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        // 创建 Socket 通信管道,指定接收主机的IP和端口
        Socket socket = new Socket("localhost", 23333);
        System.out.println("===== Client Running =====");
        // 从 Socket 通信管道中得到字节输出流 负责发送数据
        OutputStream os =  socket.getOutputStream();
        // 把低级字节输出流包装成打印输出流
        PrintStream ps = new PrintStream(os);
        while (true) {
            System.out.print("请输入要发送的消息(输入 exit 退出):");
            String msg = sc.nextLine();
            // 发送数据
            ps.println(msg);
            ps.flush();
            if ("exit".equals(msg)) {
                socket.close(); // 释放资源
                System.out.println("===== Client Terminated =====");
                break;
            }
        }
    }
}

Server.java

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws Exception {
        // 创建 ServerSocket 通信管道,指定注册端口
        ServerSocket serverSocket = new ServerSocket(23333); 
        System.out.println("===== Server Running =====");
        // 等待接收客户端的 Socket 练级请求,建立 Socket 通信管道
        Socket socket = serverSocket.accept();
        // 从 Socket 通信管道中得到字节输入流
        InputStream is = socket.getInputStream();
        // 把低级字节输入流包装成字符缓冲输入流
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String msg;
        while ((msg = br.readLine()) != null) { // 按照行读消息
            System.out.println("[" + socket.getRemoteSocketAddress() + "] " + msg);
        }
    }
}
实例:TCP 配合多线程实现多端多发多收

客户端同上不做变动

Server.java

import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) {
        try {
            // 创建 ServerSocket 通信管道,指定注册端口
            ServerSocket serverSocket = new ServerSocket(23333); 
            System.out.println("===== Server Running =====");
            while (true) {
                // 每收到一个客户端的 Socket 管道都交给一个独立的子线程负责读取消息
                Socket socket = serverSocket.accept();
                System.out.println(socket.getRemoteSocketAddress()
                                   + " connected."); // 上线提醒
                // 开始创建独立线程处理 Socket
                new ServerReaderThread(socket).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ServerReaderThread.java

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;

public class ServerReaderThread extends Thread {
    private Socket socket;

    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 从 Socket 通信管道中得到字节输入流
            InputStream is = socket.getInputStream();
            // 把低级字节输入流包装成字符缓冲输入流
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String msg;
            while ((msg = br.readLine()) != null) { // 按照行读消息
                System.out.println("[" + socket.getRemoteSocketAddress()
                                   + "] "+ msg);
            }
        } catch (Exception e) {
            // 下线提醒
            System.out.println(socket.getRemoteSocketAddress() + " disconnected.");
        }
    }
}

实例:TCP 配合线程池优化多端多发多收

客户端同上不做变动

import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;

public class Server {
    private static ExecutorService threadPool = new ThreadPoolExecutor(3, 5, 3,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        try {
            // 创建 ServerSocket 通信管道,指定注册端口
            ServerSocket serverSocket = new ServerSocket(23333);
            System.out.println("===== Server Running =====");

            while (true) {
                Socket socket = serverSocket.accept();
                System.out.println(socket.getRemoteSocketAddress()
                                   + " connected."); // 上线提醒
                // 把任务提交到线程池处理
                threadPool.execute(new ServerReaderRunnable(socket));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ServerReaderRunnable.java

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;

public class ServerReaderRunnable implements Runnable {
    private Socket socket;

    public ServerReaderRunnable(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 从 Socket 通信管道中得到字节输入流
            InputStream is = socket.getInputStream();
            // 把低级字节输入流包装成字符缓冲输入流
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String msg;
            while ((msg = br.readLine()) != null) { // 按照行读消息
                System.out.println("[" + socket.getRemoteSocketAddress()
                                   + "] "+ msg);
            }
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress()
                               + " disconnected."); // 下线提醒
        }
    }
}
实例:即时通信(消息转发)

Client.java

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) {
        try {
            Scanner sc = new Scanner(System.in);
            // 创建 Socket 通信管道,指定接收主机的IP和端口
            Socket socket = new Socket("localhost", 23333);
            System.out.println("===== Client Running =====");
            // 创建一个独立的线程负责该客户端读消息
            new ClientReaderThread(socket).start();

            // 从 Socket 通信管道中得到字节输出流 负责发送数据
            OutputStream os =  socket.getOutputStream();
            // 把低级字节输出流包装成打印输出流
            PrintStream ps = new PrintStream(os);
            while (true) {
                System.out.print("请输入要发送的消息(输入 exit 退出):");
                String msg = sc.nextLine();
                // 发送数据
                ps.println(msg);
                ps.flush();
                if ("exit".equals(msg)) {
                    socket.close(); // 释放资源
                    System.out.println("===== Client Terminated =====");
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class ClientReaderThread extends Thread {
    private Socket socket;

    public ClientReaderThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 从 Socket 通信管道中得到字节输入流
            InputStream is = socket.getInputStream();
            // 把低级字节输入流包装成字符缓冲输入流
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String msg;
            while ((msg = br.readLine()) != null) { // 按照行读消息
                System.out.println("\n[Message] " + msg);
            }
        } catch (Exception e) {
            System.out.println("\nDisconnected from the server.");
        }
    }
}

Server.java

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

public class Server {
    // 所有在线的管道
    public static final List<Socket> ONLINE_SOCKETS = new ArrayList<>(); 

    public static void main(String[] args) {
        try {
            // 创建 ServerSocket 通信管道,指定注册端口
            ServerSocket serverSocket = new ServerSocket(23333);
            System.out.println("===== Server Running =====");
            while (true) {
                // 每收到一个客户端的 Socket 管道都交给一个独立的子线程负责读取消息
                Socket socket = serverSocket.accept();
                ONLINE_SOCKETS.add(socket);
                System.out.println(socket.getRemoteSocketAddress()
                                   + " connected."); // 上线提醒
                // 开始创建独立线程处理 Socket
                new ServerReaderThread(socket).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class ServerReaderThread extends Thread {
    private Socket socket;

    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 从 Socket 通信管道中得到字节输入流
            InputStream is = socket.getInputStream();
            // 把低级字节输入流包装成字符缓冲输入流
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String line;
            while ((line = br.readLine()) != null) { // 按照行读消息
                System.out.println("[" + socket.getRemoteSocketAddress() + "] " + line);
                sendMsgToAll(line);
            }
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress()
                               + " disconnected."); // 下线提醒
            Server.ONLINE_SOCKETS.remove(socket);
        }
    }

    private void sendMsgToAll(String msg) throws Exception {
        for (Socket socket : Server.ONLINE_SOCKETS) {
            PrintStream ps = new PrintStream(socket.getOutputStream());
            ps.println(msg);
            ps.flush();
        }
    }
}
实例:BS架构简易服务端

BSDemo.java

import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;

public class BSDemo {
    private static ExecutorService threadPool = new ThreadPoolExecutor(3, 5, 3,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        try {
            // 创建 ServerSocket 通信管道,指定注册端口
            ServerSocket serverSocket = new ServerSocket(23333);
            System.out.println("服务已在" + serverSocket.getLocalPort()
                               + "端口上启动");
            while (true) {
                Socket socket = serverSocket.accept();
                // 上线提醒
                System.out.println(socket.getRemoteSocketAddress()
                        + " connected.");
                // 把任务提交到线程池处理
                threadPool.execute(new ServerReaderThread(socket));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class ServerReaderThread implements Runnable {
    private Socket socket;

    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 从 Socket 通信管道中得到字节输出流并包装位打印输出流 发送数据
            PrintStream ps = new PrintStream(socket.getOutputStream());
            ps.println("HTTP/1.1 200 OK");
            ps.println("Content-Type: text/html");
            ps.println();
            ps.print("<h1 style=\"color: red;\">Hello Browser!</h1>");
            ps.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

image-20220314225453768

单元测试

概述

单元测试就是针对最小的功能单元编写测试代码,Java程序最小的功能单元是方法,因此,单元测试就是针对 Java方法的测试,进而检查方法的正确性。

JUnit 单元测试框架

​ JUnit 是使用 Java 语言实现的单元测试框架,它是开源的,Java 开发者都应当学习并使用 JUnit 编写单元测试。
​ 此外,几乎所有的IDE工具都集成了 JUnit ,这样我们就可以直接在 IDE 中编写并运行 JUnit 测试,JUnit 目前最新版本是5。

优点

  • JUnit 可以灵活的选择执行哪些测试方法,可以一键执行全部测试方法。
  • JUnit 可以生成全部方法的测试报告。
  • 单元测试中的某个方法测试时不会影响其他测试方法的测试。

使用

  1. 导入 JUnit 的 jar 包到项目中。
  2. 编写测试方法:该测试方法必须是公共的无参数无返回值的非静态方法。
  3. 在测试方法上使用 @Test 注解:标注该方法是一个测试方法。
  4. 在测试方法中完成被测试方法的预期正确性测试。
  5. 选中测试方法,选择“JUnit运行” ,如果测试良好则是绿色;如果测试失败,则是红色
实例

UserService.java

// 业务功能
public class UserService {
    public String loginName(String loginName , String passWord){
        if("admin".equals(loginName) && "123456".equals(passWord)){
            return "登录成功";
        }else {
            return "用户名或者密码有问题";
        }
    }

    public void selectNames(){
        System.out.println(10 / 0);
        System.out.println("查询全部用户名称成功~~");
    }
}

TestUserService.java

import org.junit.Assert;
import org.junit.Test;

public class TestUserService {
    @Test // 使用@Test注解标记
    public void testLoginName() { // 公开 无参 无返回值
        UserService us = new UserService();
        String res = us.loginName("admin1", "123456");
		// 进行预期结果的正确性测试:断言
        Assert.assertEquals(res, "登录成功", res);
    }

    @Test
    public void testSelectNames() {
        UserService us = new UserService();
        us.selectNames();
    }
}
测试结果

image-20220315104351198

JUnit 4 常用注解

注解 说明
@Test 测试方法
@Before 用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次
@After 用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次
@BeforeClass 用来静态修饰方法,该方法会在所有测试方法之前只执行一次
@AfterClass 用来静态修饰方法,该方法会在所有测试方法之后只执行一次

JUnit 5 常用注解

注解 说明
@Test 测试方法
@BeforeEach 用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次
@AfterEach 用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次
@BeforeAll 用来静态修饰方法,该方法会在所有测试方法之前只执行一次
@AfterAll 用来静态修饰方法,该方法会在所有测试方法之后只执行一次

反射

概述

反射是指对于任何一个Class类,在"运行的时候"都可以直接得到这个类全部成分。
在运行时,可以直接得到这个类的构造器对象:Constructor
在运行时,可以直接得到这个类的成员变量对象:Field
在运行时,可以直接得到这个类的成员方法对象:Method
这种运行时动态获取类信息以及动态调用类中成分的能力称为Java语言的反射机制。

反射的关键:反射的第一步都是先得到编译后的Class类对象,然后就可以得到Class的全部成分。

反射获取类对象

1、Class.forName()

java.lang.Class

方法 说明
staic Class<?> forName(String className) 返回与具有给定字符串名称的类或接口关联的类对象

2、类名.class

public class Test {
    public static void main(String[] args) throws Exception {
        Class c1 = Class.forName("e02_reflect.Person"); // 方法1
        Class c2 = Person.class;						// 方法2
        Class c3 = new Person().getClass();				// 方法3
        System.out.println(c1 == c2); // true
        System.out.println(c2 == c3); // true
    }
}

3、对象.getClass()

java.lang.Object

方法 说明
Class<?> getClass() 返回此 Object 的运行时类

反射获取构造器对象

从类对象中可以获取类的成分对象。

java.lang.Class 用于获取构造器的 API

方法 说明
Constructor<?>[] getConstructors() 返回所有构造器对象的数组(只能拿public的)
Constructor<?>[] getDeclaredConstructors() 返回所有构造器对象的数组,存在就能拿到
Constructor<T> getConstructor(Class<?>... parameterTypes) 返回单个构造器对象(只能拿public的)
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 返回单个构造器对象,存在就能拿到
@Test
public void getConstructor() throws Exception {
    Class c = Student.class;
    Constructor constructor = c.getConstructor(String.class, Integer.class);

    System.out.println(constructor.getName() + ":"
                       + constructor.getParameterCount());
}

构造器对象创建对象

java.lang.reflect.Constructor

方法 说明
T newInstance(Object... initargs) 根据指定的构造器创建对象
public void setAccessible(boolean flag) 设置为true,表示取消访问检查,进行暴力反射
实例
@Test
public void newInstance() throws Exception {
    Class c = Student.class;
    Constructor cons = c.getDeclaredConstructor();
    // 私有构造器可以暴力反射(破坏了封装性)
    cons.setAccessible(true); // 打开权限
    Student s = (Student) cons.newInstance(); // 创建对象
    System.out.println(s);

    Constructor cons1 = c.getConstructor(String.class, int.class);
    Student s1 = (Student) cons1.newInstance("gabrielxd", 18);
    System.out.println(s1);
}

反射获取成员变量对象

方法 说明
Field[] getFields() 返回所有成员变量对象的数组(只能拿public的)
Field[] getDeclaredFields() 返回所有成员变量对象的数组,存在就能拿到
Field getField(String name) 返回单个成员变量对象(只能拿public的)
Field getDeclaredField(String name) 返回单个成员变量对象,存在就能拿到
@Test
public void getDeclaredFields() {
    Class c = Student.class;
    Field[] fields = c.getDeclaredFields();
    for (Field field : fields) {
        System.out.println(field);
    }
}

@Test
public void getDeclaredField() throws Exception {
    Class c = Student.class;
    Field field = c.getDeclaredField("name");
    System.out.println(field);
}

成员变量对象操作成员变量

java.lang.reflect.Field

方法 说明
void set(Object obj, Object value) 赋值
Object get(Object obj) 获取值
实例
@Test
public void operateVars() throws Exception {
    Class c = Student.class;
    Field ageF = c.getDeclaredField("age");
    Student stu = new Student(); // 创建对象
    // 修改对象stu的私有属性age
    ageF.setAccessible(true); // 打开权限
    ageF.set(stu, 18);
    System.out.println(stu);
    System.out.println(ageF.get(stu)); // 获取age属性
}

反射获取方法对象

方法 说明
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) // 运行方法
  • obj : 用obj对象调用该方法
  • args : 调用方法的传递的参数(没有则不写)
  • 返回值 : 方法的返回值(没有结果返回 null)

应用:绕过编译阶段为集合添加数据

反射是作用在运行时的技术,此时集合的泛型将不能产生约束了,此时可以为集合存入其他任意类型的元素。

原因:泛型只是在编译阶段可以约束集合只能操作某种数据类型,在编译成 .class 文件进入运行阶段的时候,其真实类型都是 ArrayList,泛型相当于被擦除了。

public class ReflectDemo {
    public static void main(String[] args) throws Exception {
        List<Integer> list1 = new ArrayList<>();
        List<String> list2 = new ArrayList<>();
        // 泛型擦除现象存在
        System.out.println(list1.getClass() == list2.getClass()); // true

        List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);
		// list3.add("测试"); // 编译时错误:不兼容的类型
        System.out.println(intList); // [1, 2]

        Class intListClass = intList.getClass();
        Method intListAdd = intListClass.getDeclaredMethod("add", Object.class);
        intListAdd.invoke(intList, "测试");
        System.out.println(intList); // [1, 2, 测试]
    }
}

应用:反射做通用框架

给任意一个对象,在不清楚对象字段的情况下把对象的字段名称和对应值存储到文件中。

import java.io.FileOutputStream;
import java.io.PrintStream;
import java.lang.reflect.Field;

public class MybatisUtil {
    public static void save(Object obj) {
        try (
                PrintStream ps = new PrintStream(new FileOutputStream("junit-reflect-annotation-proxy-app/src/data.txt", true));
        ) {
            Class c = obj.getClass();
            ps.println("==========" + c.getSimpleName() + "==========");

            Field[] fields = c.getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                ps.println(field.getName() + "=" + field.get(obj));
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注解

概述

Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。
Java 语言中的类、构造器、方法、成员变量、参数等都可以被注解进行标注。

作用

对 Java 中类、方法、成员变量做标记,然后进行特殊处理,至于到底做何种处理由业务需求来决定。
例如:JUnit 框架中,标记了注解 @Test 的方法就可以被当成测试方法执行,而没有标记的就不能当成测试方法执行。

自定义注解

语法

public @interface 注解名称 {
    public 属性类型 属性名() default 默认值;
}

特殊属性

value 属性,如果只有一个 value 属性的情况下,使用 value 属性的时候可以省略value名称不写。
但是如果有多个属性, 且多个属性没有默认值,那么 value 名称是不能省略的。

实例

AnnotationDemo.java

// 注解类
@MyNote(name = "note1", nums = {1}, flag = false)
public class AnnotationDemo {
    // 注解属性 有默认值的属性可不写
    @MyNote(name = "note2", nums = {1, 2})
    public static final String name = "GabrielxD";

    // 注解方法
    @MyNote(name = "note3", nums = {1, 2, 3})
    public static void main(
            // 注解参数
            @MyNote(name = "note4", nums = {1, 2, 3, 4})
            String[] args
    ) {
        // 注解变量 省略 value 名称
        @MyComment("comment1")
        int a = 1;
    }
}

MyNote.java

public @interface MyNote {
    public String name();
    public int[] nums();
    public boolean flag() default true;
}

MyComment.java

public @interface MyComment {
    public String value();
    public boolean status() default true;
}

元注解

用于注解注解的注解。
元注解有两个。

1、@Target

约束自定义注解只能在哪些地方使用。
Target 中可使用的值定义在 ElementType 枚举类中,常用值如下:

  • TYPE : 类,接口
  • FIELD : 成员变量
  • METHOD : 成员方法
  • PARAMETER : 方法参数
  • CONSTRUCTOR : 构造器
  • LOCAL_VARIABLE : 局部变量

2、@Retention

申明注解的生命周期。
Retention 中可使用的值定义在 RetentionPolicy 枚举类中,常用值如下:

  • SOURCE : 注解只作用在源码阶段,生成的字节码文件中不存在
  • CLASS : 注解作用在源码阶段,字节码文件阶段,运行阶段不存在,默认值.
  • RUNTIME : 注解作用在源码阶段,字节码文件阶段,运行阶段(开发常用)

示例

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 注解该注解只能使用在成员方法和成员变量中
@Target({ElementType.METHOD, ElementType.FIELD})
// 注解该注解生命周期一直到运行阶段都还在
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {

}

注解解析

注解的操作中经常需要进行解析,注解的解析就是判断是否存在注解,存在注解就解析出内容。

与注解解析相关的接口

  • Annotation : 注解的顶级接口,注解都是 Annotation 类型的对象。
  • AnnotatedElement : 该接口定义了与注解解析相关的解析方法。
方法 说明
Annotation[] getDeclaredAnnotations() 获得当前对象上使用的所有注解,返回注解数组
T getDeclaredAnnotation(Class<T> annotationClass) 根据注解类型获得对应注解对象
boolean isAnnotationPresent(Class<Annotation> annotationClass) 判断当前对象是否使用了指定的注解,如果使用了则返回true,否则false

所有的类成分 Class, Method, Field, Constructor,都实现了 AnnotatedElement 接口,所以都拥有解析注解的能力。

实例

AnnotationDemo01.java

import org.junit.Test;

import java.lang.reflect.Method;
import java.util.Arrays;

public class AnnotationDemo01 {
    @Test
    public void parseClass() {
        Class c = BookStore.class;
        if (c.isAnnotationPresent(Book.class)) {
            Book book = (Book) c.getAnnotation(Book.class);
            System.out.println(book.value());
            System.out.println(book.price());
            System.out.println(Arrays.toString(book.authors()));
        }
    }

    @Test
    public void parseMethod() throws Exception {
        Class c = BookStore.class;
        Method methodTest = c.getDeclaredMethod("test");
        if (methodTest.isAnnotationPresent(Book.class)) {
            Book book = (Book) methodTest.getAnnotation(Book.class);
            System.out.println(book.value());
            System.out.println(book.price());
            System.out.println(Arrays.toString(book.authors()));
        }
    }
}

@Book(value = "《Test》", price = 19.9, authors = {"a", "b"})
class BookStore {
    @Book(value = "《Test1》", price = 99.9, authors = {"c", "d"})
    public void test() {
    }
}

Book.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Book {
    public String value();
    public double price() default 100.0;
    public String[] authors();
}

实例:模拟 Junit 框架

定义若干个方法,只要加了 MyTest 注解,就可以在启动时被触发执行

AnnotationTest.java

import java.lang.reflect.Method;

public class AnnotationTest {
    public static void main(String[] args) throws Exception {
        AnnotationTest at = new AnnotationTest();
        // 获取类对象
        Class c = at.getClass();
        // 获取全部方法
        Method[] methods = c.getDeclaredMethods();
        // 遍历方法,如果有 MyTest 注释就调用该方法
        for (Method method : methods) {
            if (method.isAnnotationPresent(MyTest.class)) {
                method.invoke(at);
            }
        }
    }

    @MyTest
    public void Test1() {
        System.out.println("===Test1===");
    }

    public void Test2() {
        System.out.println("===Test2===");
    }

    @MyTest
    public void Test3() {
        System.out.println("===Test3===");
    }
}

MyTest.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {
}

动态代理

概述

代理(Proxy)就是被代理者没有能力或者不愿意去完成某件事情,需要找个人代替自己去完成这件事,动态代理就是用来对业务功能(方法)进行代理的。

优点

  • 非常的灵活,支持任意接口类型的实现类对象做代理,也可以直接为接本身做代理。
  • 可以为被代理对象的所有方法做代理。
  • 可以在不改变方法源码的情况下,实现对方法功能的增强。
  • 不仅简化了编程工作、提高了软件系统的可扩展性,同时也提高了开发效率。

关键步骤

  1. 须有接口,实现类要实现接口(代理通常是基于接口实现的)。
  2. 创建一个实现类的对象,该对象为业务对象,紧接着为业务对象做一个代理对象。

Proxy

java.lang.reflect.Proxy

常用 API

返回指定接口的代理实例,该接口将方法调用分派给指定的调用处理程序。

static Object newProxyInstance(ClassLoader loader,
                               Class<?>[] interfaces,
                               InvocationHandler h)
  • loader : 用于定义代理类的类加载器
  • interfaces : 要实现的代理类的接口列表
  • h : 调度方法调用的调用处理程序

java.lang.Class

方法 说明
ClassLoader getClassLoader() 返回类的类加载器
Class<?> getInterfaces() 返回由此对象表示的类或接口直接实现的接口

实例

模拟某企业用户管理业务,需包含用户登录,用户删除,用户查询功能,并要统计每个功能的耗时

Test.java : 测试功能

public class Test {
    public static void main(String[] args) {
        UserService us = ProxyUtil.getProxy((new UserServiceImpl()));
        us.login("admin", "123456");
        us.deleteUsers();
        us.selectUsers();
    }
}
// 结果
login方法耗时: 0.311s
删除全部用户成功
deleteUsers方法耗时: 0.201s
查询全部用户成功
selectUsers方法耗时: 0.511s

UserService.java : 用户服务接口

public interface UserService {
    String login(String username, String password);
    void selectUsers();
    boolean deleteUsers();
}

UserServiceImpl.java : 用户服务的实现类

public class UserServiceImpl implements UserService {
    @Override
    public String login(String username, String password) {
        try {
            Thread.sleep(300);
            if("admin".equals(username) && "123456".equals(password)){
                return "登录成功";
            }
            return "用户名或者密码有问题";
        } catch(Exception e) {
            e.printStackTrace();
            return "error";
        }
    }

    @Override
    public void selectUsers() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("查询全部用户成功");
    }

    @Override
    public boolean deleteUsers() {
        try {
            Thread.sleep(200);
            System.out.println("删除全部用户成功");
            return true;
        } catch(Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

``

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyUtil {
    public static UserService getProxy(UserServiceImpl obj) {
        // 返回代理对象
        return (UserService) Proxy.newProxyInstance(
                obj.getClass().getClassLoader(),
                obj.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method,
                                         Object[] args) throws Throwable {
                        // proxy : 代理对象本身
                        // method : 这在被代理的方法
                        // args : 被代理方法应该传入的参数列表
                        
                        // 统计时间功能
                        long startTime = System.currentTimeMillis();
                        // 调用业务功能 收到执行结果
                        Object res =  method.invoke(obj, args);
                        long endTime = System.currentTimeMillis();
                        System.out.println(method.getName() + "方法耗时: "
                                           + (endTime - startTime) / 1000.0 + "s");
                        return res; // 把业务功能方法执行的结果返回
                    }
                });
    }
}

XML

概述

XML是可扩展标记语言(eXtensible Markup Language)的缩写,是一种数据表示格式,可以描述非常复杂的数据结构,常用于传输和存储数据。

特点

  1. 纯文本,默认使用UTF-8编码。
  2. 可嵌套。

使用场景

XML内容经常被当成消息进行网络传输,或者作为配置文件用于存储系统的信息。

语法

XML的标签(元素)规则

  1. 标签由一对尖括号和合法标识符组成:<name></name>
  2. 必须存在一个根标签,有且只能有一个。
  3. 标签必须成对出现,有开始,有结束:<name></name>
  4. 特殊的标签可以不成对,但是必须有结束标记,如:<br/>
  5. 标签中可以定义属性,属性和标签名空格隔开,属性值必须用引号引起来<student id = "1"></student>
  6. 标签需要正确的嵌套。

XML的其他组成

  • 注释:<!–- 注释内容 -->
  • 特殊字符(与 HTML 规范相似)。
  • CDATA区:<![CDATA[ …内容… ]]>

文档约束

由于 XML 文件可以自定义标签,导致 XML 文件可以随意定义,程序在解析的时候可能出现问题。
而文档约束就是用来限定 XML 文件中的标签以及属性应该怎么写的。

分类

DTD、schema

DTD

实例

bookDTD.dtd

<!ELEMENT 书架 (书+)>
<!ELEMENT 书 (书名, 作者, 售价)>
<!ELEMENT 书名 (#PCDATA)>
<!ELEMENT 作者 (#PCDATA)>
<!ELEMENT 售价 (#PCDATA)>

books.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE 书架 SYSTEM "bookDTD.dtd">
<书架>
    <书>
        <书名>Test1</书名>
        <作者>Author1</作者>
        <售价>10</售价>
    </书>
	<书>
        <书名>Test2</书名>
        <作者>Author2</作者>
        <售价>101</售价>
    </书>
</书架>
缺点

不能约束具体的数据类型。

schema

schema可以约束具体的数据类型,约束能力上更强大。
schema本身也是一个xml文件,本身也受到其他约束文件的要求,所以编写的更加严谨。

实例

data.schema

<?xml version="1.0" encoding="UTF-8" ?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.itcast.cn"
        elementFormDefault="qualified" >
    <!-- targetNamespace:申明约束文档的地址(命名空间)-->
    <element name='书架'>
        <!-- 写子元素 -->
        <complexType>
            <!-- maxOccurs='unbounded': 书架下的子元素可以有任意多个!-->
            <sequence maxOccurs='unbounded'>
                <element name='书'>
                    <!-- 写子元素 -->
                    <complexType>
                        <sequence>
                            <element name='书名' type='string'/>
                            <element name='作者' type='string'/>
                            <element name='售价' type='double'/>
                        </sequence>
                    </complexType>
                </element>
            </sequence>
        </complexType>
    </element>
</schema>

booksSchema.xml

<?xml version="1.0" encoding="UTF-8" ?>
<书架 xmlns="http://www.itcast.cn"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.itcast.cn data.xsd">
    <!-- xmlns="http://www.itcast.cn"  基本位置
         xsi:schemaLocation="http://www.itcast.cn books02.xsd" 具体的位置 -->
    <书>
        <书名>Test1</书名>
        <作者>Author1</作者>
        <售价>10.9</售价>
    </书>
    <书>
        <书名>Test2</书名>
        <作者>Author2</作者>
        <售价>101.9</售价>
    </书>
</书架>

XML 解析

使用程序读取XML中的数据。

两种常用的解析方式

  • SAX解析
  • DOM解析

Dom 解析的常见工具

名称 说明
JAXP SUN 公司提供的一套XML的解析的API
JDOM JDOM 是一个开源项目,它基于树型结构,利用纯JAVA的技术对XML文档实现解析、生成、序列化以及多种操作
dom4j 是 JDOM 的升级品,用来读写 XML 文件。具有性能优异、功能强大和极其易使用的特点,它的性能超过 SUN 公司官方的 DOM 技术,同时它也是一个开放源代码的软件,Hibernate 也用它来读写配置文件
jsoup 功能强大DOM方式的XML解析开发包,尤其对HTML解析更加方便

image-20220316173945380

dom4j

SAXReader 构造器

org.dom4j.io.SAXReader

构造器 说明
public SAXReader() 创建 Dom4J 的解析器对象
常用 API

org.dom4j.io.SAXReader

方法 说明
Document read(String url) 加载 XML 文件成为Document对象

org.dom4j.Document

方法 说明
Element getRootElement() 获得根元素对象
List<Element> elements() 得到当前元素下所有子元素
List<Element> elements(String name) 得到当前元素下指定名字的子元素返回集合
Element element(String name) 得到当前元素下指定名字的子元素,如果有很多名字相同的返回第一个
String getName() 得到元素名字
String attributeValue(String name) 通过属性名直接得到属性值
String elementText(子元素名) 得到指定名称的子元素的文本
String getText() 得到文本
案例

解析 XML 文档新建对象并存入集合

TestXMLDemo.java

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.junit.Test;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

public class TestXMLDemo {
    @Test
    public void testParseXML() throws Exception {
        SAXReader reader = new SAXReader();
        InputStream is = TestXMLDemo.class.getResourceAsStream("/Contacts.xml");
        Document dom = reader.read(is);
        Element root = dom.getRootElement();
        List<Contact> contacts = new ArrayList<>();
        
        for (Element childElement : root.elements("contact")) {
            Contact contact = new Contact(
                    Integer.valueOf(childElement.attributeValue("id")),
                    childElement.elementTextTrim("name"),
                    Boolean.valueOf(childElement.attributeValue("vip")),
                    childElement.elementTextTrim("gender").charAt(0),
                    childElement.elementTextTrim("name")
            );
            contacts.add(contact);
        }

        for (Contact contact : contacts) {
            System.out.println(contact);
        }
    }
}

Contact.xml

<?xml version="1.0" encoding="UTF-8"?>
<contactList>
    <contact id="1" vip="true">
        <name>   潘金莲  </name>
        <gender>女</gender>
        <email>panpan@itcast.cn</email>
    </contact>
    <contact id="2" vip="false">
        <name>武松</name>
        <gender>男</gender>
        <email>wusong@itcast.cn</email>
    </contact>
    <contact id="3" vip="false">
        <name>武大狼</name>
        <gender>男</gender>
        <email>wuda@itcast.cn</email>
    </contact>
    <user>
    </user>
</contactList>

Contact.java

XML 检索

XPath 在解析 XML 文档方面提供了一独树一帜的路径思想,更加优雅,高效。
XPath 使用路径表达式来定位 XML 文档中的元素节点或属性节点。

jaxen

使用
  1. 导入jar包 dom4j 和 jaxen(Xpath技术依赖Dom4j技术)。
  2. 通过 dom4j 的 SAXReader 获取 Document 对象。
  3. 利用 XPath 提供的 API,结合 XPath 的语法完成选取 XML 文档元素节点进行解析操作。
常用 API
方法名 说明
Node selectSingleNode("表达式") 获取符合表达式的唯一元素
List<Node> selectNodes("表达式") 获取符合表达式的元素集合
示例
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.junit.Test;

import java.util.List;

public class TestJaxen {
    @Test
    public void parse01() throws Exception {
        SAXReader reader = new SAXReader();
        Document dom = reader.read(TestJaxen.class.getResourceAsStream("/Contacts2.xml"));
        // 绝对路径: /根元素/子元素/子元素
        List<Node> nameNodes = dom.selectNodes("/contactList/contact/name");
        for (Node nameNode : nameNodes) {
            Element element = (Element) nameNode;
            System.out.println(element.getTextTrim());
        }
    }

    @Test
    public void parse02() throws Exception {
        SAXReader reader = new SAXReader();
        Document dom = reader.read(TestJaxen.class.getResourceAsStream("/Contacts2.xml"));
        Element root = dom.getRootElement();
        // 相对路径: ./子元素/子元素 (.代表了当前元素,也可以省略./)
        List<Node> nameNodes = root.selectNodes("./contact/name");
        for (Node nameNode : nameNodes) {
            Element element = (Element) nameNode;
            System.out.println(element.getTextTrim());
        }
    }

    @Test
    public void parse03() throws Exception {
        SAXReader reader = new SAXReader();
        Document dom = reader.read(TestJaxen.class.getResourceAsStream("/Contacts2.xml"));
        /* 全文搜索:
        * //元素         在全文找这个元素
        * //元素1/元素2   在全文找元素1下面的一级元素2
        * //元素1//元素2  在全文找元素1下面的全部元素2
        * */
        List<Node> nameNodes = dom.selectNodes("//name");
        for (Node nameNode : nameNodes) {
            Element element = (Element) nameNode;
            System.out.println(element.getTextTrim());
        }
    }

    @Test
    public void parse04() throws Exception {
        SAXReader reader = new SAXReader();
        Document dom = reader.read(TestJaxen.class.getResourceAsStream("/Contacts2.xml"));
        /* 属性查找。
        * //@属性名称  在全文检索属性对象
        * //元素[@属性名称]  在全文检索包含该属性的元素对象
        * //元素[@属性名称=值]  在全文检索包含该属性的元素且属性值为该值的元素对象
        * */
        List<Node> nodes = dom.selectNodes("//@id");
        for (Node node : nodes) {
            Attribute attr = (Attribute) node;
            System.out.println(attr.getName() + "=" + attr.getValue());
        }

//        Node node = document.selectSingleNode("//name[@id]");
        Node node = dom.selectSingleNode("//name[@id=888]");
        Element ele = (Element) node;
        System.out.println(ele.getTextTrim());
    }
}
0

评论区