实现多线程

并发和并行

  • 并行:在同一时刻,有多个指令在多个CPU上同时执行。

  • 并发:在同一时刻,有多个指令在单个CPU上交替执行。

进程和线程

  • 进程:是正在运行的程序

    独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
    动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
    并发性:任何进程都可以同其他进程一起并发执行

  • 线程:是进程中的单个顺序控制流,是一条执行路径

    ​ 单线程:一个进程如果只有一条执行路径,则称为单线程程序

    ​ 多线程:一个进程如果有多条执行路径,则称为多线程程序

实现多线程方式一:继承Thread类

  • 方法介绍

    方法名 说明
    void run() 在线程开启后,此方法将被调用执行
    void start() 使此线程开始执行,Java虚拟机会调用run方法()
  • 实现步骤

    • 定义一个类MyThread继承Thread类
    • 在MyThread类中重写run()方法
    • 创建MyThread类的对象
    • 启动线程
  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class MyThread extends Thread {
    @Override
    public void run() {
    for(int i=0; i<100; i++) {
    System.out.println(i);
    }
    }
    }
    public class MyThreadDemo {
    public static void main(String[] args) {
    MyThread my1 = new MyThread();
    MyThread my2 = new MyThread();

    // my1.run();
    // my2.run();

    //void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法
    my1.start();
    my2.start();
    }
    }
  • 两个小问题

    • 为什么要重写run()方法?

      因为run()是用来封装被线程执行的代码

    • run()方法和start()方法的区别?

      run():封装线程执行的代码,直接调用,相当于普通方法的调用

      start():启动线程;然后由JVM调用此线程的run()方法

实现多线程方式二:实现Runnable接口

  • Thread构造方法

    方法名 说明
    Thread(Runnable target) 分配一个新的Thread对象
    Thread(Runnable target, String name) 分配一个新的Thread对象
  • 实现步骤

    • 定义一个类MyRunnable实现Runnable接口
    • 在MyRunnable类中重写run()方法
    • 创建MyRunnable类的对象
    • 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
    • 启动线程
  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class MyRunnable implements Runnable {
    @Override
    public void run() {
    for(int i=0; i<100; i++) {
    System.out.println(Thread.currentThread().getName()+":"+i);
    }
    }
    }
    public class MyRunnableDemo {
    public static void main(String[] args) {
    //创建MyRunnable类的对象
    MyRunnable my = new MyRunnable();

    //创建Thread类的对象,把MyRunnable对象作为构造方法的参数
    //Thread(Runnable target)
    // Thread t1 = new Thread(my);
    // Thread t2 = new Thread(my);
    //Thread(Runnable target, String name)
    Thread t1 = new Thread(my,"坦克");
    Thread t2 = new Thread(my,"飞机");

    //启动线程
    t1.start();
    t2.start();
    }
    }

实现多线程方式三: 实现Callable接口

  • 方法介绍

    方法名 说明
    V call() 计算结果,如果无法计算结果,则抛出一个异常
    FutureTask(Callable callable) 创建一个 FutureTask,一旦运行就执行给定的 Callable
    V get() 如有必要,等待计算完成,然后获取其结果
  • 实现步骤

    • 定义一个类MyCallable实现Callable接口
    • 在MyCallable类中重写call()方法
    • 创建MyCallable类的对象
    • 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
    • 创建Thread类的对象,把FutureTask对象作为构造方法的参数
    • 启动线程
    • 再调用get方法,就可以获取线程结束之后的结果。
  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
    for (int i = 0; i < 100; i++) {
    System.out.println("跟女孩表白" + i);
    }
    //返回值就表示线程运行完毕之后的结果
    return "答应";
    }
    }
    public class Demo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    //线程开启之后需要执行里面的call方法
    MyCallable mc = new MyCallable();

    //Thread t1 = new Thread(mc);

    //可以获取线程执行完毕之后的结果.也可以作为参数传递给Thread对象
    FutureTask<String> ft = new FutureTask<>(mc);

    //创建线程对象
    Thread t1 = new Thread(ft);

    String s = ft.get();
    //开启线程
    t1.start();

    //String s = ft.get();
    System.out.println(s);
    }
    }
  • 三种实现方式的对比

    • 实现Runnable、Callable接口
      • 好处: 扩展性强,实现该接口的同时还可以继承其他的类
      • 缺点: 编程相对复杂,不能直接使用Thread类中的方法
    • 继承Thread类
      • 好处: 编程比较简单,可以直接使用Thread类中的方法
      • 缺点: 可以扩展性较差,不能再继承其他的类

设置和获取线程名称

  • 方法介绍

    方法名 说明
    void setName(String name) 将此线程的名称更改为等于参数name
    String getName() 返回此线程的名称
    Thread currentThread() 返回对当前正在执行的线程对象的引用
  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    public class MyThread extends Thread {
    public MyThread() {}
    public MyThread(String name) {
    super(name);
    }

    @Override
    public void run() {
    for (int i = 0; i < 100; i++) {
    System.out.println(getName()+":"+i);
    }
    }
    }
    public class MyThreadDemo {
    public static void main(String[] args) {
    MyThread my1 = new MyThread();
    MyThread my2 = new MyThread();

    //void setName(String name):将此线程的名称更改为等于参数 name
    my1.setName("高铁");
    my2.setName("飞机");

    //Thread(String name)
    MyThread my1 = new MyThread("高铁");
    MyThread my2 = new MyThread("飞机");

    my1.start();
    my2.start();

    //static Thread currentThread() 返回对当前正在执行的线程对象的引用
    System.out.println(Thread.currentThread().getName());
    }
    }

线程休眠

  • 相关方法

    方法名 说明
    static void sleep(long millis) 使当前正在执行的线程停留(暂停执行)指定的毫秒数
  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class MyRunnable implements Runnable {
    @Override
    public void run() {
    for (int i = 0; i < 100; i++) {
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    System.out.println(Thread.currentThread().getName() + "---" + i);
    }
    }
    }
    public class Demo {
    public static void main(String[] args) throws InterruptedException {
    /*System.out.println("睡觉前");
    Thread.sleep(3000);
    System.out.println("睡醒了");*/

    MyRunnable mr = new MyRunnable();

    Thread t1 = new Thread(mr);
    Thread t2 = new Thread(mr);

    t1.start();
    t2.start();
    }
    }

线程优先级

  • 线程调度

    • 两种调度方式

      • 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
      • 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
    • Java使用的是抢占式调度模型

    • 随机性

      假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的

  • 优先级相关方法

    方法名 说明
    final int getPriority() 返回此线程的优先级
    final void setPriority(int newPriority) 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10
  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
    for (int i = 0; i < 100; i++) {
    System.out.println(Thread.currentThread().getName() + "---" + i);
    }
    return "线程执行完毕了";
    }
    }
    public class Demo {
    public static void main(String[] args) {
    //优先级: 1 - 10 默认值:5
    MyCallable mc = new MyCallable();

    FutureTask<String> ft = new FutureTask<>(mc);

    Thread t1 = new Thread(ft);
    t1.setName("飞机");
    t1.setPriority(10);
    //System.out.println(t1.getPriority());//5
    t1.start();

    MyCallable mc2 = new MyCallable();

    FutureTask<String> ft2 = new FutureTask<>(mc2);

    Thread t2 = new Thread(ft2);
    t2.setName("坦克");
    t2.setPriority(1);
    //System.out.println(t2.getPriority());//5
    t2.start();
    }
    }

守护线程

  • 相关方法

    方法名 说明
    void setDaemon(boolean on) 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出
  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class MyThread1 extends Thread {
    @Override
    public void run() {
    for (int i = 0; i < 10; i++) {
    System.out.println(getName() + "---" + i);
    }
    }
    }
    public class MyThread2 extends Thread {
    @Override
    public void run() {
    for (int i = 0; i < 100; i++) {
    System.out.println(getName() + "---" + i);
    }
    }
    }
    public class Demo {
    public static void main(String[] args) {
    MyThread1 t1 = new MyThread1();
    MyThread2 t2 = new MyThread2();

    t1.setName("女神");
    t2.setName("备胎");

    //把第二个线程设置为守护线程
    //当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了.
    t2.setDaemon(true);

    t1.start();
    t2.start();
    }
    }

线程同步

卖票

  • 案例需求

    某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

  • 实现步骤

    • 定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;

    • 在SellTicket类中重写run()方法实现卖票,代码步骤如下

    • 判断票数大于0,就卖票,并告知是哪个窗口卖的

    • 卖了票之后,总票数要减1

    • 票卖没了,线程停止

    • 定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下

    • 创建SellTicket类的对象

    • 创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称

    • 启动线程

  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public class SellTicket implements Runnable {
    private int tickets = 100;
    //在SellTicket类中重写run()方法实现卖票,代码步骤如下
    @Override
    public void run() {
    while (true) {
    if(ticket <= 0){
    //卖完了
    break;
    }else{
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    ticket--;
    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
    }
    }
    }
    }
    public class SellTicketDemo {
    public static void main(String[] args) {
    //创建SellTicket类的对象
    SellTicket st = new SellTicket();

    //创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
    Thread t1 = new Thread(st,"窗口1");
    Thread t2 = new Thread(st,"窗口2");
    Thread t3 = new Thread(st,"窗口3");

    //启动线程
    t1.start();
    t2.start();
    t3.start();
    }
    }

卖票案例的问题

  • 卖票出现了问题

    • 相同的票出现了多次

    • 出现了负数的票

  • 问题产生原因

    线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现问题

同步代码块解决数据安全问题

  • 安全问题出现的条件

    • 是多线程环境

    • 有共享数据

    • 有多条语句操作共享数据

  • 如何解决多线程安全问题呢?

    • 基本思想:让程序没有安全问题的环境
  • 怎么实现呢?

    • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可

    • Java提供了同步代码块的方式来解决

  • 同步代码块格式:

    1
    2
    3
    synchronized(任意对象) { 
    多条语句操作共享数据的代码
    }

    synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁

  • 同步的好处和弊端

    • 好处:解决了多线程的数据安全问题

    • 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public class SellTicket implements Runnable {
    private int tickets = 100;
    private Object obj = new Object();

    @Override
    public void run() {
    while (true) {
    synchronized (obj) { // 对可能有安全问题的代码加锁,多个线程必须使用同一把锁
    //t1进来后,就会把这段代码给锁起来
    if (tickets > 0) {
    try {
    Thread.sleep(100);
    //t1休息100毫秒
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    //窗口1正在出售第100张票
    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
    tickets--; //tickets = 99;
    }
    }
    //t1出来了,这段代码的锁就被释放了
    }
    }
    }

    public class SellTicketDemo {
    public static void main(String[] args) {
    SellTicket st = new SellTicket();

    Thread t1 = new Thread(st, "窗口1");
    Thread t2 = new Thread(st, "窗口2");
    Thread t3 = new Thread(st, "窗口3");

    t1.start();
    t2.start();
    t3.start();
    }
    }

同步方法解决数据安全问题

  • 同步方法的格式

    同步方法:就是把synchronized关键字加到方法上

    1
    2
    3
    修饰符 synchronized 返回值类型 方法名(方法参数) { 
    方法体;
    }

    同步方法的锁对象是什么呢?

    ​ this

  • 静态同步方法

    同步静态方法:就是把synchronized关键字加到静态方法上

    1
    2
    3
    修饰符 static synchronized 返回值类型 方法名(方法参数) { 
    方法体;
    }

    同步静态方法的锁对象是什么呢?

    ​ 类名.class

  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    public class MyRunnable implements Runnable {
    private static int ticketCount = 100;

    @Override
    public void run() {
    while(true){
    if("窗口一".equals(Thread.currentThread().getName())){
    //同步方法
    boolean result = synchronizedMthod();
    if(result){
    break;
    }
    }

    if("窗口二".equals(Thread.currentThread().getName())){
    //同步代码块
    synchronized (MyRunnable.class){
    if(ticketCount == 0){
    break;
    }else{
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    ticketCount--;
    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
    }
    }
    }

    }
    }

    private static synchronized boolean synchronizedMthod() {
    if(ticketCount == 0){
    return true;
    }else{
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    ticketCount--;
    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
    return false;
    }
    }
    }

    public class Demo {
    public static void main(String[] args) {
    MyRunnable mr = new MyRunnable();

    Thread t1 = new Thread(mr);
    Thread t2 = new Thread(mr);

    t1.setName(“窗口一”);
    t2.setName(“窗口二”);

    t1.start();
    t2.start();
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71


    ### Lock锁

    虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

    Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化

    - ReentrantLock构造方法

    | 方法名 | 说明 |
    | --------------- | -------------------- |
    | ReentrantLock() | 创建一个ReentrantLock的实例 |

    - 加锁解锁方法

    | 方法名 | 说明 |
    | ------------- | ---- |
    | void lock() | 获得锁 |
    | void unlock() | 释放锁 |

    - 代码演示

    ```java
    public class Ticket implements Runnable {
    //票的数量
    private int ticket = 100;
    private Object obj = new Object();
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
    while (true) {
    //synchronized (obj){//多个线程必须使用同一把锁.
    try {
    lock.lock();
    if (ticket <= 0) {
    //卖完了
    break;
    } else {
    Thread.sleep(100);
    ticket--;
    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
    }
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    lock.unlock();
    }
    // }
    }
    }
    }

    public class Demo {
    public static void main(String[] args) {
    Ticket ticket = new Ticket();

    Thread t1 = new Thread(ticket);
    Thread t2 = new Thread(ticket);
    Thread t3 = new Thread(ticket);

    t1.setName("窗口一");
    t2.setName("窗口二");
    t3.setName("窗口三");

    t1.start();
    t2.start();
    t3.start();
    }
    }

死锁

  • 概述

    线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行

  • 什么情况下会产生死锁

    1. 资源有限
    2. 同步嵌套
  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public class Demo {
    public static void main(String[] args) {
    Object objA = new Object();
    Object objB = new Object();

    new Thread(()->{
    while(true){
    synchronized (objA){
    //线程一
    synchronized (objB){
    System.out.println("小康同学正在走路");
    }
    }
    }
    }).start();

    new Thread(()->{
    while(true){
    synchronized (objB){
    //线程二
    synchronized (objA){
    System.out.println("小薇同学正在走路");
    }
    }
    }
    }).start();
    }
    }

生产者消费者

生产者和消费者模式概述

  • 概述

    生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。

    所谓生产者消费者问题,实际上主要是包含了两类线程:

    ​ 一类是生产者线程用于生产数据

    ​ 一类是消费者线程用于消费数据

    为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

    生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为

    消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

  • Object类的等待和唤醒方法

    方法名 说明
    void wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
    void notify() 唤醒正在等待对象监视器的单个线程
    void notifyAll() 唤醒正在等待对象监视器的所有线程

阻塞队列基本使用

  • 阻塞队列继承结构

  • 常见BlockingQueue:

    ArrayBlockingQueue: 底层是数组,有界

    LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值

  • BlockingQueue的核心方法:

    put(anObject): 将参数放入队列,如果放不进去会阻塞

    take(): 取出第一个数据,取不到会阻塞

  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Demo02 {
    public static void main(String[] args) throws Exception {
    // 创建阻塞队列的对象,容量为 1
    ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);

    // 存储元素
    arrayBlockingQueue.put("汉堡包");

    // 取元素
    System.out.println(arrayBlockingQueue.take());
    System.out.println(arrayBlockingQueue.take()); // 取不到会阻塞

    System.out.println("程序结束了");
    }
    }

阻塞队列实现等待唤醒机制

  • 案例需求

    • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务

      1.构造方法中接收一个阻塞队列对象

      2.在run方法中循环向阻塞队列中添加包子

      3.打印添加结果

    • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务

      1.构造方法中接收一个阻塞队列对象

      2.在run方法中循环获取阻塞队列中的包子

      3.打印获取结果

    • 测试类(Demo):里面有main方法,main方法中的代码步骤如下

      创建阻塞队列对象

      创建生产者线程和消费者线程对象,构造方法中传入阻塞队列对象

      分别开启两个线程

  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    public class Cooker extends Thread {

    private ArrayBlockingQueue<String> bd;

    public Cooker(ArrayBlockingQueue<String> bd) {
    this.bd = bd;
    }
    // 生产者步骤:
    // 1,判断桌子上是否有汉堡包
    // 如果有就等待,如果没有才生产。
    // 2,把汉堡包放在桌子上。
    // 3,叫醒等待的消费者开吃。

    @Override
    public void run() {
    while (true) {
    try {
    bd.put("汉堡包");
    System.out.println("厨师放入一个汉堡包");
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }

    public class Foodie extends Thread {
    private ArrayBlockingQueue<String> bd;

    public Foodie(ArrayBlockingQueue<String> bd) {
    this.bd = bd;
    }

    @Override
    public void run() {
    // 1,判断桌子上是否有汉堡包。
    // 2,如果没有就等待。
    // 3,如果有就开吃
    // 4,吃完之后,桌子上的汉堡包就没有了
    // 叫醒等待的生产者继续生产
    // 汉堡包的总数量减一

    //套路:
    //1. while(true)死循环
    //2. synchronized 锁,锁对象要唯一
    //3. 判断,共享数据是否结束. 结束
    //4. 判断,共享数据是否结束. 没有结束
    while (true) {
    try {
    String take = bd.take();
    System.out.println("吃货将" + take + "拿出来吃了");
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }

    }
    }

    public class Demo {
    public static void main(String[] args) {
    ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1);

    Foodie f = new Foodie(bd);
    Cooker c = new Cooker(bd);

    f.start();
    c.start();
    }
    }

线程池

线程状态介绍

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么Java中的线程存在哪几种状态呢?

Java中的线程状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Thread {

public enum State {

/* 新建 */
NEW ,

/* 可运行状态 */
RUNNABLE ,

/* 阻塞状态 */
BLOCKED ,

/* 无限等待状态 */
WAITING ,

/* 计时等待 */
TIMED_WAITING ,

/* 终止 */
TERMINATED;

}

// 获取当前线程的状态
public State getState() {
return jdk.internal.misc.VM.toThreadState(threadStatus);
}

}

通过源码我们可以看到Java中的线程存在6种状态,每种线程状态的含义如下

线程状态 具体含义
NEW 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。
RUNNABLE 当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。
BLOCKED 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
WAITING 一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。
TIMED_WAITING 一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。
TERMINATED 一个完全运行完成的线程的状态。也称之为终止状态、结束状态

线程池-基本原理

概述 :

​ 提到池,大家应该能想到的就是水池。水池就是一个容器,在该容器中存储了很多的水。那么什么是线程池呢?线程池也是可以看做成一个池子,在该池子中存储很多个线程。

线程池存在的意义:

​ 系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理是对系

​ 统资源的消耗,这样就有点”舍本逐末”了。针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就

​ 会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。

线程池的设计思路 :

  1. 准备一个任务容器
  2. 一次性启动多个(2个)消费者线程
  3. 刚开始任务容器是空的,所以线程都在wait
  4. 直到一个外部线程向这个任务容器中扔了一个”任务”,就会有一个消费者线程被唤醒
  5. 这个消费者线程取出”任务”,并且执行这个任务,执行完毕后,继续等待下一次任务的到来

线程池-Executors默认线程池

概述 : JDK对线程池也进行了相关的实现,在真实企业开发中我们也很少去自定义线程池,而是使用JDK中自带的线程池。

我们可以使用Executors中所提供的静态方法来创建线程池

​ static ExecutorService newCachedThreadPool() 创建一个默认的线程池
static newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池

代码实现 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.itheima.mythreadpool;


//static ExecutorService newCachedThreadPool() 创建一个默认的线程池
//static newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {

//1,创建一个默认的线程池对象.池子中默认是空的.默认最多可以容纳int类型的最大值.
ExecutorService executorService = Executors.newCachedThreadPool();
//Executors --- 可以帮助我们创建线程池对象
//ExecutorService --- 可以帮助我们控制线程池

executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});

//Thread.sleep(2000);

executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});

executorService.shutdown();
}
}

线程池-Executors创建指定上限的线程池

使用Executors中所提供的静态方法来创建线程池

​ static ExecutorService newFixedThreadPool(int nThreads) : 创建一个指定最多线程数量的线程池

代码实现 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.itheima.mythreadpool;

//static ExecutorService newFixedThreadPool(int nThreads)
//创建一个指定最多线程数量的线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class MyThreadPoolDemo2 {
public static void main(String[] args) {
//参数不是初始值而是最大值
ExecutorService executorService = Executors.newFixedThreadPool(10);

ThreadPoolExecutor pool = (ThreadPoolExecutor) executorService;
System.out.println(pool.getPoolSize());//0

executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});

executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});

System.out.println(pool.getPoolSize());//2
// executorService.shutdown();
}
}

线程池-ThreadPoolExecutor

创建线程池对象 :

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);

代码实现 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.itheima.mythreadpool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyThreadPoolDemo3 {
// 参数一:核心线程数量
// 参数二:最大线程数
// 参数三:空闲线程最大存活时间
// 参数四:时间单位
// 参数五:任务队列
// 参数六:创建线程工厂
// 参数七:任务的拒绝策略
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(2,5,2,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());

pool.shutdown();
}
}

线程池-参数详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

corePoolSize: 核心线程的最大值,不能小于0
maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
keepAliveTime: 空闲线程最大存活时间,不能小于0
unit: 时间单位
workQueue: 任务队列,不能为null
threadFactory: 创建线程工厂,不能为null
handler: 任务的拒绝策略,不能为null

线程池-非默认任务拒绝策略

RejectedExecutionHandler是jdk提供的一个任务拒绝策略接口,它下面存在4个子类。

1
2
3
4
ThreadPoolExecutor.AbortPolicy: 		    丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。

注:明确线程池对多可执行的任务数 = 队列容量 + 最大线程数

案例演示1:演示ThreadPoolExecutor.AbortPolicy任务处理策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ThreadPoolExecutorDemo01 {

public static void main(String[] args) {

/**
* 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy()) ;

// 提交5个任务,而该线程池最多可以处理4个任务,当我们使用AbortPolicy这个任务处理策略的时候,就会抛出异常
for(int x = 0 ; x < 5 ; x++) {
threadPoolExecutor.submit(() -> {
System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
});
}
}
}

控制台输出结果

1
2
3
4
pool-1-thread-1---->> 执行了任务
pool-1-thread-3---->> 执行了任务
pool-1-thread-2---->> 执行了任务
pool-1-thread-3---->> 执行了任务

控制台报错,仅仅执行了4个任务,有一个任务被丢弃了

案例演示2:演示ThreadPoolExecutor.DiscardPolicy任务处理策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ThreadPoolExecutorDemo02 {
public static void main(String[] args) {
/**
* 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.DiscardPolicy()) ;

// 提交5个任务,而该线程池最多可以处理4个任务,当我们使用DiscardPolicy这个任务处理策略的时候,控制台不会报错
for(int x = 0 ; x < 5 ; x++) {
threadPoolExecutor.submit(() -> {
System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
});
}
}
}

控制台输出结果

1
2
3
4
pool-1-thread-1---->> 执行了任务
pool-1-thread-1---->> 执行了任务
pool-1-thread-3---->> 执行了任务
pool-1-thread-2---->> 执行了任务

控制台没有报错,仅仅执行了4个任务,有一个任务被丢弃了

案例演示3:演示ThreadPoolExecutor.DiscardOldestPolicy任务处理策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ThreadPoolExecutorDemo02 {
public static void main(String[] args) {
/**
* 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
*/
ThreadPoolExecutor threadPoolExecutor;
threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.DiscardOldestPolicy());
// 提交5个任务
for(int x = 0 ; x < 5 ; x++) {
// 定义一个变量,来指定指定当前执行的任务;这个变量需要被final修饰
final int y = x ;
threadPoolExecutor.submit(() -> {
System.out.println(Thread.currentThread().getName() + "---->> 执行了任务" + y);
});
}
}
}

控制台输出结果

1
2
3
4
pool-1-thread-2---->> 执行了任务2
pool-1-thread-1---->> 执行了任务0
pool-1-thread-3---->> 执行了任务3
pool-1-thread-1---->> 执行了任务4

由于任务1在线程池中等待时间最长,因此任务1被丢弃。

案例演示4:演示ThreadPoolExecutor.CallerRunsPolicy任务处理策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ThreadPoolExecutorDemo04 {
public static void main(String[] args) {

/**
* 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
*/
ThreadPoolExecutor threadPoolExecutor;
threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.CallerRunsPolicy());

// 提交5个任务
for(int x = 0 ; x < 5 ; x++) {
threadPoolExecutor.submit(() -> {
System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
});
}
}
}

控制台输出结果

1
2
3
4
5
pool-1-thread-1---->> 执行了任务
pool-1-thread-3---->> 执行了任务
pool-1-thread-2---->> 执行了任务
pool-1-thread-1---->> 执行了任务
main---->> 执行了任务

通过控制台的输出,我们可以看到次策略没有通过线程池中的线程执行任务,而是直接调用任务的run()方法绕过线程池直接执行。

原子性

volatile-问题

代码分析 :

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.itheima.myvolatile;

public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
t1.setName("小路同学");
t1.start();

MyThread2 t2 = new MyThread2();
t2.setName("小皮同学");
t2.start();
}
}
1
2
3
4
5
package com.itheima.myvolatile;

public class Money {
public static int money = 100000;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.itheima.myvolatile;

public class MyThread1 extends Thread {
@Override
public void run() {
while(Money.money == 100000){

}

System.out.println("结婚基金已经不是十万了");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.itheima.myvolatile;

public class MyThread2 extends Thread {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}

Money.money = 90000;
}
}

程序问题 : 女孩虽然知道结婚基金是十万,但是当基金的余额发生变化的时候,女孩无法知道最新的余额。

volatile解决

以上案例出现的问题 :

​ 当A线程修改了共享数据时,B线程没有及时获取到最新的值,如果还在使用原先的值,就会出现问题

​ 1,堆内存是唯一的,每一个线程都有自己的线程栈。

​ 2 ,每一个线程在使用堆里面变量的时候,都会先拷贝一份到变量的副本中。

​ 3 ,在线程中,每一次使用是从变量的副本中获取的。

Volatile关键字 : 强制线程每次在使用的时候,都会看一下共享区域最新的值

代码实现 : 使用volatile关键字解决

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.itheima.myvolatile;

public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
t1.setName("小路同学");
t1.start();

MyThread2 t2 = new MyThread2();
t2.setName("小皮同学");
t2.start();
}
}
1
2
3
4
5
package com.itheima.myvolatile;

public class Money {
public static volatile int money = 100000;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.itheima.myvolatile;

public class MyThread1 extends Thread {
@Override
public void run() {
while(Money.money == 100000){

}

System.out.println("结婚基金已经不是十万了");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.itheima.myvolatile;

public class MyThread2 extends Thread {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}

Money.money = 90000;
}
}

synchronized解决

synchronized解决 :

​ 1 ,线程获得锁

​ 2 ,清空变量副本

​ 3 ,拷贝共享变量最新的值到变量副本中

​ 4 ,执行代码

​ 5 ,将修改后变量副本中的值赋值给共享数据

​ 6 ,释放锁

代码实现 :

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.itheima.myvolatile2;

public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
t1.setName("小路同学");
t1.start();

MyThread2 t2 = new MyThread2();
t2.setName("小皮同学");
t2.start();
}
}
1
2
3
4
5
6
package com.itheima.myvolatile2;

public class Money {
public static Object lock = new Object();
public static volatile int money = 100000;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.itheima.myvolatile2;

public class MyThread1 extends Thread {
@Override
public void run() {
while(true){
synchronized (Money.lock){
if(Money.money != 100000){
System.out.println("结婚基金已经不是十万了");
break;
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.itheima.myvolatile2;

public class MyThread2 extends Thread {
@Override
public void run() {
synchronized (Money.lock) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}

Money.money = 90000;
}
}
}

原子性

概述 : 所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。

代码实现 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.itheima.threadatom;

public class AtomDemo {
public static void main(String[] args) {
MyAtomThread atom = new MyAtomThread();

for (int i = 0; i < 100; i++) {
new Thread(atom).start();
}
}
}
class MyAtomThread implements Runnable {
private volatile int count = 0; //送冰淇淋的数量

@Override
public void run() {
for (int i = 0; i < 100; i++) {
//1,从共享数据中读取数据到本线程栈中.
//2,修改本线程栈中变量副本的值
//3,会把本线程栈中变量副本的值赋值给共享数据.
count++;
System.out.println("已经送了" + count + "个冰淇淋");
}
}
}

代码总结 : count++ 不是一个原子性操作, 他在执行的过程中,有可能被其他线程打断

volatile关键字不能保证原子性

解决方案 : 我们可以给count++操作添加锁,那么count++操作就是临界区中的代码,临界区中的代码一次只能被一个线程去执行,所以count++就变成了原子操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.itheima.threadatom2;

public class AtomDemo {
public static void main(String[] args) {
MyAtomThread atom = new MyAtomThread();

for (int i = 0; i < 100; i++) {
new Thread(atom).start();
}
}
}
class MyAtomThread implements Runnable {
private volatile int count = 0; //送冰淇淋的数量
private Object lock = new Object();

@Override
public void run() {
for (int i = 0; i < 100; i++) {
//1,从共享数据中读取数据到本线程栈中.
//2,修改本线程栈中变量副本的值
//3,会把本线程栈中变量副本的值赋值给共享数据.
synchronized (lock) {
count++;
System.out.println("已经送了" + count + "个冰淇淋");
}
}
}
}

原子性_AtomicInteger

概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。因为变

量的类型有很多种,所以在Atomic包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)。本次我们只讲解

使用原子的方式更新基本类型,使用原子的方式更新基本类型Atomic包提供了以下3个类:

AtomicBoolean: 原子更新布尔类型

AtomicInteger: 原子更新整型

AtomicLong: 原子更新长整型

以上3个类提供的方法几乎一模一样,所以本节仅以AtomicInteger为例进行讲解,AtomicInteger的常用方法如下:

1
2
3
4
5
6
7
8
public AtomicInteger():	   			    初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer

int get(): 获取值
int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值。

代码实现 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.itheima.threadatom3;

import java.util.concurrent.atomic.AtomicInteger;

public class MyAtomIntergerDemo1 {
// public AtomicInteger(): 初始化一个默认值为0的原子型Integer
// public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer
public static void main(String[] args) {
AtomicInteger ac = new AtomicInteger();
System.out.println(ac);

AtomicInteger ac2 = new AtomicInteger(10);
System.out.println(ac2);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.itheima.threadatom3;

import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger;

public class MyAtomIntergerDemo2 {
// int get(): 获取值
// int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。
// int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。
// int addAndGet(int data): 以原子方式将参数与对象中的值相加,并返回结果。
// int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值。
public static void main(String[] args) {
// AtomicInteger ac1 = new AtomicInteger(10);
// System.out.println(ac1.get());

// AtomicInteger ac2 = new AtomicInteger(10);
// int andIncrement = ac2.getAndIncrement();
// System.out.println(andIncrement);
// System.out.println(ac2.get());

// AtomicInteger ac3 = new AtomicInteger(10);
// int i = ac3.incrementAndGet();
// System.out.println(i);//自增后的值
// System.out.println(ac3.get());

// AtomicInteger ac4 = new AtomicInteger(10);
// int i = ac4.addAndGet(20);
// System.out.println(i);
// System.out.println(ac4.get());

AtomicInteger ac5 = new AtomicInteger(100);
int andSet = ac5.getAndSet(20);
System.out.println(andSet);
System.out.println(ac5.get());
}
}

AtomicInteger-内存解析

AtomicInteger原理 : 自旋锁 + CAS 算法

CAS算法:

​ 有3个操作数(内存值V, 旧的预期值A,要修改的值B)

​ 当旧的预期值A == 内存值 此时修改成功,将V改为B

​ 当旧的预期值A!=内存值 此时修改失败,不做任何操作

​ 并重新获取现在的最新值(这个重新获取的动作就是自旋)

AtomicInteger-源码解析

代码实现 :

1
2
3
4
5
6
7
8
9
10
11
package com.itheima.threadatom4;

public class AtomDemo {
public static void main(String[] args) {
MyAtomThread atom = new MyAtomThread();

for (int i = 0; i < 100; i++) {
new Thread(atom).start();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.itheima.threadatom4;

import java.util.concurrent.atomic.AtomicInteger;

public class MyAtomThread implements Runnable {
//private volatile int count = 0; //送冰淇淋的数量
//private Object lock = new Object();
AtomicInteger ac = new AtomicInteger(0);

@Override
public void run() {
for (int i = 0; i < 100; i++) {
//1,从共享数据中读取数据到本线程栈中.
//2,修改本线程栈中变量副本的值
//3,会把本线程栈中变量副本的值赋值给共享数据.
//synchronized (lock) {
// count++;
// ac++;
int count = ac.incrementAndGet();
System.out.println("已经送了" + count + "个冰淇淋");
// }
}
}
}

源码解析 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

//先自增,然后获取自增后的结果
public final int incrementAndGet() {
//+ 1 自增后的结果
//this 就表示当前的atomicInteger(值)
//1 自增一次
return U.getAndAddInt(this, VALUE, 1) + 1;
}

public final int getAndAddInt(Object o, long offset, int delta) {
//v 旧值
int v;
//自旋的过程
do {
//不断的获取旧值
v = getIntVolatile(o, offset);
//如果这个方法的返回值为false,那么继续自旋
//如果这个方法的返回值为true,那么自旋结束
//o 表示的就是内存值
//v 旧值
//v + delta 修改后的值
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
//作用:比较内存中的值,旧值是否相等,如果相等就把修改后的值写到内存中,返回true。表示修改成功。
// 如果不相等,无法把修改后的值写到内存中,返回false。表示修改失败。
//如果修改失败,那么继续自旋。
return v;
}

悲观锁和乐观锁

synchronized和CAS的区别 :

相同点:在多线程情况下,都可以保证共享数据的安全性。

不同点:synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每 次操作共享数据之前,都会上锁。(悲观锁)

​ cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。

​ 如果别人修改过,那么我再次获取现在最新的值。

​ 如果别人没有修改过,那么我现在直接修改共享数据的值.(乐观锁)

并发工具类

并发工具类-Hashtable

Hashtable出现的原因 : 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。

代码实现 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.itheima.mymap;

import java.util.HashMap;
import java.util.Hashtable;

public class MyHashtableDemo {
public static void main(String[] args) throws InterruptedException {
Hashtable<String, String> hm = new Hashtable<>();

Thread t1 = new Thread(() -> {
for (int i = 0; i < 25; i++) {
hm.put(i + "", i + "");
}
});


Thread t2 = new Thread(() -> {
for (int i = 25; i < 51; i++) {
hm.put(i + "", i + "");
}
});

t1.start();
t2.start();

System.out.println("----------------------------");
//为了t1和t2能把数据全部添加完毕
Thread.sleep(1000);

//0-0 1-1 ..... 50- 50

for (int i = 0; i < 51; i++) {
System.out.println(hm.get(i + ""));
}//0 1 2 3 .... 50


}
}

并发工具类-ConcurrentHashMap基本使用

ConcurrentHashMap出现的原因 : 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。

基于以上两个原因我们可以使用JDK1.5以后所提供的ConcurrentHashMap。

总结 :

​ 1 ,HashMap是线程不安全的。多线程环境下会有数据安全问题

​ 2 ,Hashtable是线程安全的,但是会将整张表锁起来,效率低下

​ 3,ConcurrentHashMap也是线程安全的,效率较高。 在JDK7和JDK8中,底层原理不一样。

代码实现 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.itheima.mymap;

import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;

public class MyConcurrentHashMapDemo {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, String> hm = new ConcurrentHashMap<>(100);

Thread t1 = new Thread(() -> {
for (int i = 0; i < 25; i++) {
hm.put(i + "", i + "");
}
});


Thread t2 = new Thread(() -> {
for (int i = 25; i < 51; i++) {
hm.put(i + "", i + "");
}
});

t1.start();
t2.start();

System.out.println("----------------------------");
//为了t1和t2能把数据全部添加完毕
Thread.sleep(1000);

//0-0 1-1 ..... 50- 50

for (int i = 0; i < 51; i++) {
System.out.println(hm.get(i + ""));
}//0 1 2 3 .... 50
}
}

并发工具类-ConcurrentHashMap

总结 :

​ 1,如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。 在第一次添加元素的时候创建哈希表

​ 2,计算当前元素应存入的索引。

​ 3,如果该索引位置为null,则利用cas算法,将本结点添加到数组中。

​ 4,如果该索引位置不为null,则利用volatile关键字获得当前位置最新的结点地址,挂在他下面,变成链表。

​ 5,当链表的长度大于等于8时,自动转换成红黑树6,以链表或者红黑树头结点为锁对象,配合悲观锁保证多线程操作集合时数据的安全性

并发工具类-CountDownLatch

CountDownLatch类 :

方法 解释
public CountDownLatch(int count) 参数传递线程数,表示等待线程数量
public void await() 让线程等待
public void countDown() 当前线程执行完毕

使用场景: 让某一条线程等待其他线程执行完毕之后再执行

代码实现 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.itheima.mycountdownlatch;

import java.util.concurrent.CountDownLatch;

public class ChileThread1 extends Thread {

private CountDownLatch countDownLatch;
public ChileThread1(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}

@Override
public void run() {
//1.吃饺子
for (int i = 1; i <= 10; i++) {
System.out.println(getName() + "在吃第" + i + "个饺子");
}
//2.吃完说一声
//每一次countDown方法的时候,就让计数器-1
countDownLatch.countDown();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.itheima.mycountdownlatch;

import java.util.concurrent.CountDownLatch;

public class ChileThread2 extends Thread {

private CountDownLatch countDownLatch;
public ChileThread2(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
//1.吃饺子
for (int i = 1; i <= 15; i++) {
System.out.println(getName() + "在吃第" + i + "个饺子");
}
//2.吃完说一声
//每一次countDown方法的时候,就让计数器-1
countDownLatch.countDown();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.itheima.mycountdownlatch;

import java.util.concurrent.CountDownLatch;

public class ChileThread3 extends Thread {

private CountDownLatch countDownLatch;
public ChileThread3(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
//1.吃饺子
for (int i = 1; i <= 20; i++) {
System.out.println(getName() + "在吃第" + i + "个饺子");
}
//2.吃完说一声
//每一次countDown方法的时候,就让计数器-1
countDownLatch.countDown();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.itheima.mycountdownlatch;

import java.util.concurrent.CountDownLatch;

public class MotherThread extends Thread {
private CountDownLatch countDownLatch;
public MotherThread(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}

@Override
public void run() {
//1.等待
try {
//当计数器变成0的时候,会自动唤醒这里等待的线程。
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.收拾碗筷
System.out.println("妈妈在收拾碗筷");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.itheima.mycountdownlatch;

import java.util.concurrent.CountDownLatch;

public class MyCountDownLatchDemo {
public static void main(String[] args) {
//1.创建CountDownLatch的对象,需要传递给四个线程。
//在底层就定义了一个计数器,此时计数器的值就是3
CountDownLatch countDownLatch = new CountDownLatch(3);
//2.创建四个线程对象并开启他们。
MotherThread motherThread = new MotherThread(countDownLatch);
motherThread.start();

ChileThread1 t1 = new ChileThread1(countDownLatch);
t1.setName("小明");

ChileThread2 t2 = new ChileThread2(countDownLatch);
t2.setName("小红");

ChileThread3 t3 = new ChileThread3(countDownLatch);
t3.setName("小刚");

t1.start();
t2.start();
t3.start();
}
}

总结 :

​ 1. CountDownLatch(int count):参数写等待线程的数量。并定义了一个计数器。

​ 2. await():让线程等待,当计数器为0时,会唤醒等待的线程

​ 3. countDown(): 线程执行完毕时调用,会将计数器-1。

并发工具类-Semaphore

使用场景 :

​ 可以控制访问特定资源的线程数量。

实现步骤 :

​ 1,需要有人管理这个通道

​ 2,当有车进来了,发通行许可证

​ 3,当车出去了,收回通行许可证

​ 4,如果通行许可证发完了,那么其他车辆只能等着

代码实现 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.itheima.mysemaphore;

import java.util.concurrent.Semaphore;

public class MyRunnable implements Runnable {
//1.获得管理员对象,
private Semaphore semaphore = new Semaphore(2);
@Override
public void run() {
//2.获得通行证
try {
semaphore.acquire();
//3.开始行驶
System.out.println("获得了通行证开始行驶");
Thread.sleep(2000);
System.out.println("归还通行证");
//4.归还通行证
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

1
2
3
4
5
6
7
8
9
10
11
package com.itheima.mysemaphore;

public class MySemaphoreDemo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();

for (int i = 0; i < 100; i++) {
new Thread(mr).start();
}
}
}

网络编程

网络编程概述

  • 计算机网络

    是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统

  • 网络编程

    在网络通信协议下,不同计算机上运行的程序,可以进行数据传输

网络编程三要素

  • IP地址

    要想让网络中的计算机能够互相通信,必须为每台计算机指定一个标识号,通过这个标识号来指定要接收数据的计算机和识别发送的计算机,而IP地址就是这个标识号。也就是设备的标识

  • 端口

    网络的通信,本质上是两个应用程序的通信。每台计算机都有很多的应用程序,那么在网络通信时,如何区分这些应用程序呢?如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的应用程序了。也就是应用程序的标识

  • 协议

    通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。常见的协议有UDP协议和TCP协议

IP地址

IP地址:是网络中设备的唯一标识

  • IP地址分为两大类
    • IPv4:是给每个连接在网络上的主机分配一个32bit地址。按照TCP/IP规定,IP地址用二进制来表示,每个IP地址长32bit,也就是4个字节。例如一个采用二进制形式的IP地址是“11000000 10101000 00000001 01000010”,这么长的地址,处理起来也太费劲了。为了方便使用,IP地址经常被写成十进制的形式,中间使用符号“.”分隔不同的字节。于是,上面的IP地址可以表示为“192.168.1.66”。IP地址的这种表示法叫做“点分十进制表示法”,这显然比1和0容易记忆得多
    • IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。为了扩大地址空间,通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,这样就解决了网络地址资源数量不够的问题
  • DOS常用命令:
    • ipconfig:查看本机IP地址
    • ping IP地址:检查网络是否连通
  • 特殊IP地址:
    • 127.0.0.1:是回送地址,可以代表本机地址,一般用来测试使用

InetAddress

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

  • 相关方法

    方法名 说明
    static InetAddress getByName(String host) 确定主机名称的IP地址。主机名称可以是机器名称,也可以是IP地址
    String getHostName() 获取此IP地址的主机名
    String getHostAddress() 返回文本显示中的IP地址字符串
  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class InetAddressDemo {
    public static void main(String[] args) throws UnknownHostException {
    //InetAddress address = InetAddress.getByName("itheima");
    InetAddress address = InetAddress.getByName("192.168.1.66");

    //public String getHostName():获取此IP地址的主机名
    String name = address.getHostName();
    //public String getHostAddress():返回文本显示中的IP地址字符串
    String ip = address.getHostAddress();

    System.out.println("主机名:" + name);
    System.out.println("IP地址:" + ip);
    }
    }

端口和协议

  • 端口

    • 设备上应用程序的唯一标识
  • 端口号

    • 用两个字节表示的整数,它的取值范围是065535。其中,01023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败
  • 协议

    • 计算机网络中,连接和通信的规则被称为网络通信协议
  • UDP协议

    • 用户数据报协议(User Datagram Protocol)
    • UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
    • 由于使用UDP协议消耗系统资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输
    • 例如视频会议通常采用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议
  • TCP协议

    • 传输控制协议 (Transmission Control Protocol)

    • TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”

    • 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠

      第一次握手,客户端向服务器端发出连接请求,等待服务器确认

      第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求

      第三次握手,客户端再次向服务器端发送确认信息,确认连接

    • 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛。例如上传文件、下载文件、浏览网页等

UDP通信程序

UDP发送数据

  • Java中的UDP通信

    • UDP协议是一种不可靠的网络协议,它在通信的两端各建立一个Socket对象,但是这两个Socket只是发送,接收数据的对象,因此对于基于UDP协议的通信双方而言,没有所谓的客户端和服务器的概念
    • Java提供了DatagramSocket类作为基于UDP协议的Socket
  • 构造方法

    方法名 说明
    DatagramSocket() 创建数据报套接字并将其绑定到本机地址上的任何可用端口
    DatagramPacket(byte[] buf,int len,InetAddress add,int port) 创建数据包,发送长度为len的数据包到指定主机的指定端口
  • 相关方法

    方法名 说明
    void send(DatagramPacket p) 发送数据报包
    void close() 关闭数据报套接字
    void receive(DatagramPacket p) 从此套接字接受数据报包
  • 发送数据的步骤

    • 创建发送端的Socket对象(DatagramSocket)
    • 创建数据,并把数据打包
    • 调用DatagramSocket对象的方法发送数据
    • 关闭发送端
  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class SendDemo {
    public static void main(String[] args) throws IOException {
    //创建发送端的Socket对象(DatagramSocket)
    // DatagramSocket() 构造数据报套接字并将其绑定到本地主机上的任何可用端口
    DatagramSocket ds = new DatagramSocket();

    //创建数据,并把数据打包
    //DatagramPacket(byte[] buf, int length, InetAddress address, int port)
    //构造一个数据包,发送长度为 length的数据包到指定主机上的指定端口号。
    byte[] bys = "hello,udp,我来了".getBytes();

    DatagramPacket dp = new DatagramPacket(bys,bys.length,InetAddress.getByName("127.0.0.1"),10086);

    //调用DatagramSocket对象的方法发送数据
    //void send(DatagramPacket p) 从此套接字发送数据报包
    ds.send(dp);

    //关闭发送端
    //void close() 关闭此数据报套接字
    ds.close();
    }
    }

UDP接收数据

  • 接收数据的步骤

    • 创建接收端的Socket对象(DatagramSocket)
    • 创建一个数据包,用于接收数据
    • 调用DatagramSocket对象的方法接收数据
    • 解析数据包,并把数据在控制台显示
    • 关闭接收端
  • 构造方法

    方法名 说明
    DatagramPacket(byte[] buf, int len) 创建一个DatagramPacket用于接收长度为len的数据包
  • 相关方法

    方法名 说明
    byte[] getData() 返回数据缓冲区
    int getLength() 返回要发送的数据的长度或接收的数据的长度
  • 示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class ReceiveDemo {
    public static void main(String[] args) throws IOException {
    //创建接收端的Socket对象(DatagramSocket)
    DatagramSocket ds = new DatagramSocket(12345);

    //创建一个数据包,用于接收数据
    byte[] bys = new byte[1024];
    DatagramPacket dp = new DatagramPacket(bys, bys.length);

    //调用DatagramSocket对象的方法接收数据
    ds.receive(dp);

    //解析数据包,并把数据在控制台显示
    System.out.println("数据是:" + new String(dp.getData(), 0, dp.getLength()));
    }
    }
    }

UDP三种通讯方式

  • 单播

    单播用于两个主机之间的端对端通信

  • 组播

    组播用于对一组特定的主机进行通信

  • 广播

    广播用于一个主机对整个局域网上所有主机上的数据通信

UDP组播实现

  • 实现步骤

    • 发送端
      1. 创建发送端的Socket对象(DatagramSocket)
      2. 创建数据,并把数据打包(DatagramPacket)
      3. 调用DatagramSocket对象的方法发送数据(在单播中,这里是发给指定IP的电脑但是在组播当中,这里是发给组播地址)
      4. 释放资源
    • 接收端
      1. 创建接收端Socket对象(MulticastSocket)
      2. 创建一个箱子,用于接收数据
      3. 把当前计算机绑定一个组播地址
      4. 将数据接收到箱子中
      5. 解析数据包,并打印数据
      6. 释放资源
  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // 发送端
    public class ClinetDemo {
    public static void main(String[] args) throws IOException {
    // 1. 创建发送端的Socket对象(DatagramSocket)
    DatagramSocket ds = new DatagramSocket();
    String s = "hello 组播";
    byte[] bytes = s.getBytes();
    InetAddress address = InetAddress.getByName("224.0.1.0");
    int port = 10000;
    // 2. 创建数据,并把数据打包(DatagramPacket)
    DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port);
    // 3. 调用DatagramSocket对象的方法发送数据(在单播中,这里是发给指定IP的电脑但是在组播当中,这里是发给组播地址)
    ds.send(dp);
    // 4. 释放资源
    ds.close();
    }
    }
    // 接收端
    public class ServerDemo {
    public static void main(String[] args) throws IOException {
    // 1. 创建接收端Socket对象(MulticastSocket)
    MulticastSocket ms = new MulticastSocket(10000);
    // 2. 创建一个箱子,用于接收数据
    DatagramPacket dp = new DatagramPacket(new byte[1024],1024);
    // 3. 把当前计算机绑定一个组播地址,表示添加到这一组中.
    ms.joinGroup(InetAddress.getByName("224.0.1.0"));
    // 4. 将数据接收到箱子中
    ms.receive(dp);
    // 5. 解析数据包,并打印数据
    byte[] data = dp.getData();
    int length = dp.getLength();
    System.out.println(new String(data,0,length));
    // 6. 释放资源
    ms.close();
    }
    }

UDP广播实现

  • 实现步骤

    • 发送端
      1. 创建发送端Socket对象(DatagramSocket)
      2. 创建存储数据的箱子,将广播地址封装进去
      3. 发送数据
      4. 释放资源
    • 接收端
      1. 创建接收端的Socket对象(DatagramSocket)
      2. 创建一个数据包,用于接收数据
      3. 调用DatagramSocket对象的方法接收数据
      4. 解析数据包,并把数据在控制台显示
      5. 关闭接收端
  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // 发送端
    public class ClientDemo {
    public static void main(String[] args) throws IOException {
    // 1. 创建发送端Socket对象(DatagramSocket)
    DatagramSocket ds = new DatagramSocket();
    // 2. 创建存储数据的箱子,将广播地址封装进去
    String s = "广播 hello";
    byte[] bytes = s.getBytes();
    InetAddress address = InetAddress.getByName("255.255.255.255");
    int port = 10000;
    DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port);
    // 3. 发送数据
    ds.send(dp);
    // 4. 释放资源
    ds.close();
    }
    }

    // 接收端
    public class ServerDemo {
    public static void main(String[] args) throws IOException {
    // 1. 创建接收端的Socket对象(DatagramSocket)
    DatagramSocket ds = new DatagramSocket(10000);
    // 2. 创建一个数据包,用于接收数据
    DatagramPacket dp = new DatagramPacket(new byte[1024],1024);
    // 3. 调用DatagramSocket对象的方法接收数据
    ds.receive(dp);
    // 4. 解析数据包,并把数据在控制台显示
    byte[] data = dp.getData();
    int length = dp.getLength();
    System.out.println(new String(data,0,length));
    // 5. 关闭接收端
    ds.close();
    }
    }

TCP通信程序

TCP发送数据

  • Java中的TCP通信

    • Java对基于TCP协议的的网络提供了良好的封装,使用Socket对象来代表两端的通信端口,并通过Socket产生IO流来进行网络通信。
    • Java为客户端提供了Socket类,为服务器端提供了ServerSocket类
  • 构造方法

    方法名 说明
    Socket(InetAddress address,int port) 创建流套接字并将其连接到指定IP指定端口号
    Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号
  • 相关方法

    方法名 说明
    InputStream getInputStream() 返回此套接字的输入流
    OutputStream getOutputStream() 返回此套接字的输出流
  • 示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class Client {
    public static void main(String[] args) throws IOException {
    //TCP协议,发送数据

    //1.创建Socket对象
    //细节:在创建对象的同时会连接服务端
    // 如果连接不上,代码会报错
    Socket socket = new Socket("127.0.0.1",10000);

    //2.可以从连接通道中获取输出流
    OutputStream os = socket.getOutputStream();
    //写出数据
    os.write("aaa".getBytes());

    //3.释放资源
    os.close();
    socket.close();
    }
    }

TCP接收数据

  • 构造方法

    方法名 说明
    ServletSocket(int port) 创建绑定到指定端口的服务器套接字
  • 相关方法

    方法名 说明
    Socket accept() 监听要连接到此的套接字并接受它
  • 注意事项

    1. accept方法是阻塞的,作用就是等待客户端连接
    2. 客户端创建对象并连接服务器,此时是通过三次握手协议,保证跟服务器之间的连接
    3. 针对客户端来讲,是往外写的,所以是输出流
      针对服务器来讲,是往里读的,所以是输入流
    4. read方法也是阻塞的
    5. 客户端在关流的时候,还多了一个往服务器写结束标记的动作
    6. 最后一步断开连接,通过四次挥手协议保证连接终止
  • 三次握手和四次挥手

  • 示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class Server {
    public static void main(String[] args) throws IOException {
    //TCP协议,接收数据

    //1.创建对象ServerSocker
    ServerSocket ss = new ServerSocket(10000);

    //2.监听客户端的链接
    Socket socket = ss.accept();

    //3.从连接通道中获取输入流读取数据
    InputStream is = socket.getInputStream();
    int b;
    while ((b = is.read()) != -1){
    System.out.println((char) b);
    }

    //4.释放资源
    socket.close();
    ss.close();
    }
    }

TCP程序练习(传输中文)

发送端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Client {
public static void main(String[] args) throws IOException {
//TCP协议,发送数据

//1.创建Socket对象
//细节:在创建对象的同时会连接服务端
// 如果连接不上,代码会报错
Socket socket = new Socket("127.0.0.1",10000);


//2.可以从连接通道中获取输出流
OutputStream os = socket.getOutputStream();
//写出数据
os.write("你好你好".getBytes());//12字节

//3.释放资源
os.close();
socket.close();

}
}

接收端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Server {
public static void main(String[] args) throws IOException {
//TCP协议,接收数据

//1.创建对象ServerSocker
ServerSocket ss = new ServerSocket(10000);

//2.监听客户端的链接
Socket socket = ss.accept();

//3.从连接通道中获取输入流读取数据
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);

// BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));

int b;
while ((b = br.read()) != -1){
System.out.print((char) b);
}

//4.释放资源
socket.close();
ss.close();

}
}

反射

反射的概述:

专业的解释(了解一下):

​ 是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;

​ 对于任意一个对象,都能够调用它的任意属性和方法;

​ 这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。

通俗的理解:(掌握)

  • 利用反射创建的对象可以无视修饰符调用类里面的内容

  • 可以跟配置文件结合起来使用,把要创建的对象信息和方法写在配置文件中。

    读取到什么类,就创建什么类的对象

    读取到什么方法,就调用什么方法

    此时当需求变更的时候不需要修改代码,只要修改配置文件即可。

学习反射到底学什么?

反射都是从class字节码文件中获取的内容。

  • 如何获取class字节码文件的对象
  • 利用反射如何获取构造方法(创建对象)
  • 利用反射如何获取成员变量(赋值,获取值)
  • 利用反射如何获取成员方法(运行)

1.3 获取字节码文件对象的三种方式

  • Class这个类里面的静态方法forName(“全类名”)(最常用)
  • 通过class属性获取
  • 通过对象获取字节码文件对象

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//1.Class这个类里面的静态方法forName
//Class.forName("类的全类名"): 全类名 = 包名 + 类名
Class clazz1 = Class.forName("com.itheima.reflectdemo.Student");
//源代码阶段获取 --- 先把Student加载到内存中,再获取字节码文件的对象
//clazz 就表示Student这个类的字节码文件对象。
//就是当Student.class这个文件加载到内存之后,产生的字节码文件对象


//2.通过class属性获取
//类名.class
Class clazz2 = Student.class;

//因为class文件在硬盘中是唯一的,所以,当这个文件加载到内存之后产生的对象也是唯一的
System.out.println(clazz1 == clazz2);//true


//3.通过Student对象获取字节码文件对象
Student s = new Student();
Class clazz3 = s.getClass();
System.out.println(clazz1 == clazz2);//true
System.out.println(clazz2 == clazz3);//true

字节码文件和字节码文件对象

java文件:就是我们自己编写的java代码。

字节码文件:就是通过java文件编译之后的class文件(是在硬盘上真实存在的,用眼睛能看到的)

字节码文件对象:当class文件加载到内存之后,虚拟机自动创建出来的对象。

​ 这个对象里面至少包含了:构造方法,成员变量,成员方法。

而我们的反射获取的是什么?字节码文件对象,这个对象在内存中是唯一的。

获取构造方法

规则:

​ get表示获取

​ Declared表示私有

​ 最后的s表示所有,复数形式

​ 如果当前获取到的是私有的,必须要临时修改访问权限,否则无法使用

方法名 说明
Constructor<?>[] getConstructors() 获得所有的构造(只能public修饰)
Constructor<?>[] getDeclaredConstructors() 获得所有的构造(包含private修饰)
Constructor getConstructor(Class<?>… parameterTypes) 获取指定构造(只能public修饰)
Constructor getDeclaredConstructor(Class<?>… parameterTypes) 获取指定构造(包含private修饰)

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class ReflectDemo2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
//1.获得整体(class字节码文件对象)
Class clazz = Class.forName("com.itheima.reflectdemo.Student");


//2.获取构造方法对象
//获取所有构造方法(public)
Constructor[] constructors1 = clazz.getConstructors();
for (Constructor constructor : constructors1) {
System.out.println(constructor);
}

System.out.println("=======================");

//获取所有构造(带私有的)
Constructor[] constructors2 = clazz.getDeclaredConstructors();

for (Constructor constructor : constructors2) {
System.out.println(constructor);
}
System.out.println("=======================");

//获取指定的空参构造
Constructor con1 = clazz.getConstructor();
System.out.println(con1);

Constructor con2 = clazz.getConstructor(String.class,int.class);
System.out.println(con2);

System.out.println("=======================");
//获取指定的构造(所有构造都可以获取到,包括public包括private)
Constructor con3 = clazz.getDeclaredConstructor();
System.out.println(con3);
//了解 System.out.println(con3 == con1);
//每一次获取构造方法对象的时候,都会新new一个。

Constructor con4 = clazz.getDeclaredConstructor(String.class);
System.out.println(con4);
}
}

获取构造方法并创建对象

涉及到的方法:newInstance

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
//首先要有一个javabean类
public class Student {
private String name;

private int age;


public Student() {

}

public Student(String name) {
this.name = name;
}

private Student(String name, int age) {
this.name = name;
this.age = age;
}


/**
* 获取
* @return name
*/
public String getName() {
return name;
}

/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}

/**
* 获取
* @return age
*/
public int getAge() {
return age;
}

/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}

public String toString() {
return "Student{name = " + name + ", age = " + age + "}";
}
}



//测试类中的代码:
//需求1:
//获取空参,并创建对象

//1.获取整体的字节码文件对象
Class clazz = Class.forName("com.itheima.a02reflectdemo1.Student");
//2.获取空参的构造方法
Constructor con = clazz.getConstructor();
//3.利用空参构造方法创建对象
Student stu = (Student) con.newInstance();
System.out.println(stu);


System.out.println("=============================================");


//测试类中的代码:
//需求2:
//获取带参构造,并创建对象
//1.获取整体的字节码文件对象
Class clazz = Class.forName("com.itheima.a02reflectdemo1.Student");
//2.获取有参构造方法
Constructor con = clazz.getDeclaredConstructor(String.class, int.class);
//3.临时修改构造方法的访问权限(暴力反射)
con.setAccessible(true);
//4.直接创建对象
Student stu = (Student) con.newInstance("zhangsan", 23);
System.out.println(stu);

获取成员变量

规则:

​ get表示获取

​ Declared表示私有

​ 最后的s表示所有,复数形式

​ 如果当前获取到的是私有的,必须要临时修改访问权限,否则无法使用

方法名:

方法名 说明
Field[] getFields() 返回所有成员变量对象的数组(只能拿public的)
Field[] getDeclaredFields() 返回所有成员变量对象的数组,存在就能拿到
Field getField(String name) 返回单个成员变量对象(只能拿public的)
Field getDeclaredField(String name) 返回单个成员变量对象,存在就能拿到

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
public class ReflectDemo4 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
//获取成员变量对象

//1.获取class对象
Class clazz = Class.forName("com.itheima.reflectdemo.Student");

//2.获取成员变量的对象(Field对象)只能获取public修饰的
Field[] fields1 = clazz.getFields();
for (Field field : fields1) {
System.out.println(field);
}

System.out.println("===============================");

//获取成员变量的对象(public + private)
Field[] fields2 = clazz.getDeclaredFields();
for (Field field : fields2) {
System.out.println(field);
}

System.out.println("===============================");
//获得单个成员变量对象
//如果获取的属性是不存在的,那么会报异常
//Field field3 = clazz.getField("aaa");
//System.out.println(field3);//NoSuchFieldException

Field field4 = clazz.getField("gender");
System.out.println(field4);

System.out.println("===============================");
//获取单个成员变量(私有)
Field field5 = clazz.getDeclaredField("name");
System.out.println(field5);

}
}



public class Student {
private String name;

private int age;

public String gender;

public String address;


public Student() {
}

public Student(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}


public Student(String name, int age, String gender, String address) {
this.name = name;
this.age = age;
this.gender = gender;
this.address = address;
}

/**
* 获取
* @return name
*/
public String getName() {
return name;
}

/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}

/**
* 获取
* @return age
*/
public int getAge() {
return age;
}

/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}

/**
* 获取
* @return gender
*/
public String getGender() {
return gender;
}

/**
* 设置
* @param gender
*/
public void setGender(String gender) {
this.gender = gender;
}

/**
* 获取
* @return address
*/
public String getAddress() {
return address;
}

/**
* 设置
* @param address
*/
public void setAddress(String address) {
this.address = address;
}

public String toString() {
return "Student{name = " + name + ", age = " + age + ", gender = " + gender + ", address = " + address + "}";
}
}

获取成员变量并获取值和修改值

方法 说明
void set(Object obj, Object value) 赋值
Object get(Object obj) 获取值

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
public class ReflectDemo5 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Student s = new Student("zhangsan",23,"广州");
Student ss = new Student("lisi",24,"北京");

//需求:
//利用反射获取成员变量并获取值和修改值

//1.获取class对象
Class clazz = Class.forName("com.itheima.reflectdemo.Student");

//2.获取name成员变量
//field就表示name这个属性的对象
Field field = clazz.getDeclaredField("name");
//临时修饰他的访问权限
field.setAccessible(true);

//3.设置(修改)name的值
//参数一:表示要修改哪个对象的name?
//参数二:表示要修改为多少?
field.set(s,"wangwu");

//3.获取name的值
//表示我要获取这个对象的name的值
String result = (String)field.get(s);

//4.打印结果
System.out.println(result);

System.out.println(s);
System.out.println(ss);

}
}


public class Student {
private String name;
private int age;
public String gender;
public String address;


public Student() {
}

public Student(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}


public Student(String name, int age, String gender, String address) {
this.name = name;
this.age = age;
this.gender = gender;
this.address = address;
}

/**
* 获取
* @return name
*/
public String getName() {
return name;
}

/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}

/**
* 获取
* @return age
*/
public int getAge() {
return age;
}

/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}

/**
* 获取
* @return gender
*/
public String getGender() {
return gender;
}

/**
* 设置
* @param gender
*/
public void setGender(String gender) {
this.gender = gender;
}

/**
* 获取
* @return address
*/
public String getAddress() {
return address;
}

/**
* 设置
* @param address
*/
public void setAddress(String address) {
this.address = address;
}

public String toString() {
return "Student{name = " + name + ", age = " + age + ", gender = " + gender + ", address = " + address + "}";
}
}

获取成员方法

规则:

​ get表示获取

​ Declared表示私有

​ 最后的s表示所有,复数形式

​ 如果当前获取到的是私有的,必须要临时修改访问权限,否则无法使用

方法名 说明
Method[] getMethods() 返回所有成员方法对象的数组(只能拿public的)
Method[] getDeclaredMethods() 返回所有成员方法对象的数组,存在就能拿到
Method getMethod(String name, Class<?>… parameterTypes) 返回单个成员方法对象(只能拿public的)
Method getDeclaredMethod(String name, Class<?>… parameterTypes) 返回单个成员方法对象,存在就能拿到

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ReflectDemo6 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
//1.获取class对象
Class<?> clazz = Class.forName("com.itheima.reflectdemo.Student");


//2.获取方法
//getMethods可以获取父类中public修饰的方法
Method[] methods1 = clazz.getMethods();
for (Method method : methods1) {
System.out.println(method);
}

System.out.println("===========================");
//获取所有的方法(包含私有)
//但是只能获取自己类中的方法
Method[] methods2 = clazz.getDeclaredMethods();
for (Method method : methods2) {
System.out.println(method);
}

System.out.println("===========================");
//获取指定的方法(空参)
Method method3 = clazz.getMethod("sleep");
System.out.println(method3);

Method method4 = clazz.getMethod("eat",String.class);
System.out.println(method4);

//获取指定的私有方法
Method method5 = clazz.getDeclaredMethod("playGame");
System.out.println(method5);
}
}

获取成员方法并运行

方法

Object invoke(Object obj, Object… args) :运行方法

参数一:用obj对象调用该方法

参数二:调用方法的传递的参数(如果没有就不写)

返回值:方法的返回值(如果没有就不写)

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package com.itheima.a02reflectdemo1;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectDemo6 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//1.获取字节码文件对象
Class clazz = Class.forName("com.itheima.a02reflectdemo1.Student");

//2.获取一个对象
//需要用这个对象去调用方法
Student s = new Student();

//3.获取一个指定的方法
//参数一:方法名
//参数二:参数列表,如果没有可以不写
Method eatMethod = clazz.getMethod("eat",String.class);

//运行
//参数一:表示方法的调用对象
//参数二:方法在运行时需要的实际参数
//注意点:如果方法有返回值,那么需要接收invoke的结果
//如果方法没有返回值,则不需要接收
String result = (String) eatMethod.invoke(s, "重庆小面");
System.out.println(result);

}
}



public class Student {
private String name;
private int age;
public String gender;
public String address;


public Student() {

}

public Student(String name) {
this.name = name;
}

private Student(String name, int age) {
this.name = name;
this.age = age;
}

/**
* 获取
* @return name
*/
public String getName() {
return name;
}

/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}

/**
* 获取
* @return age
*/
public int getAge() {
return age;
}

/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}

public String toString() {
return "Student{name = " + name + ", age = " + age + "}";
}

private void study(){
System.out.println("学生在学习");
}

private void sleep(){
System.out.println("学生在睡觉");
}

public String eat(String something){
System.out.println("学生在吃" + something);
return "学生已经吃完了,非常happy";
}
}

面试题:

​ 你觉得反射好不好?好,有两个方向

​ 第一个方向:无视修饰符访问类中的内容。但是这种操作在开发中一般不用,都是框架底层来用的。

​ 第二个方向:反射可以跟配置文件结合起来使用,动态的创建对象,动态的调用方法。

练习泛型擦除(掌握概念,了解代码)

理解:(掌握)

​ 集合中的泛型只在java文件中存在,当编译成class文件之后,就没有泛型了。

代码示例:(了解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.itheima.reflectdemo;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;

public class ReflectDemo8 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//1.创建集合对象
ArrayList<Integer> list = new ArrayList<>();
list.add(123);
// list.add("aaa");

//2.利用反射运行add方法去添加字符串
//因为反射使用的是class字节码文件

//获取class对象
Class clazz = list.getClass();

//获取add方法对象
Method method = clazz.getMethod("add", Object.class);

//运行方法
method.invoke(list,"aaa");

//打印集合
System.out.println(list);
}
}

练习:修改字符串的内容

在这个练习中,我需要你掌握的是字符串不能修改的真正原因。

字符串,在底层是一个byte类型的字节数组,名字叫做value

1
private final byte[] value;

真正不能被修改的原因:final和private

final修饰value表示value记录的地址值不能修改。

private修饰value而且没有对外提供getvalue和setvalue的方法。所以,在外界不能获取或修改value记录的地址值。

如果要强行修改可以用反射:

代码示例:(了解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
String s = "abc";
String ss = "abc";
// private final byte[] value= {97,98,99};
// 没有对外提供getvalue和setvalue的方法,不能修改value记录的地址值
// 如果我们利用反射获取了value的地址值。
// 也是可以修改的,final修饰的value
// 真正不可变的value数组的地址值,里面的内容利用反射还是可以修改的,比较危险

//1.获取class对象
Class clazz = s.getClass();

//2.获取value成员变量(private)
Field field = clazz.getDeclaredField("value");
//但是这种操作非常危险
//JDK高版本已经屏蔽了这种操作,低版本还是可以的
//临时修改权限
field.setAccessible(true);

//3.获取value记录的地址值
byte[] bytes = (byte[]) field.get(s);
bytes[0] = 100;

System.out.println(s);//dbc
System.out.println(ss);//dbc

练习,反射和配置文件结合动态获取的练习

需求: 利用反射根据文件中的不同类名和方法名,创建不同的对象并调用方法。

分析:

①通过Properties加载配置文件

②得到类名和方法名

③通过类名反射得到Class对象

④通过Class对象创建一个对象

⑤通过Class对象得到方法

⑥调用方法

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ReflectDemo9 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//1.读取配置文件的信息
Properties prop = new Properties();
FileInputStream fis = new FileInputStream("day14-code\\prop.properties");
prop.load(fis);
fis.close();
System.out.println(prop);

String classname = prop.get("classname") + "";
String methodname = prop.get("methodname") + "";

//2.获取字节码文件对象
Class clazz = Class.forName(classname);

//3.要先创建这个类的对象
Constructor con = clazz.getDeclaredConstructor();
con.setAccessible(true);
Object o = con.newInstance();
System.out.println(o);

//4.获取方法的对象
Method method = clazz.getDeclaredMethod(methodname);
method.setAccessible(true);

//5.运行方法
method.invoke(o);


}
}

配置文件中的信息:
classname=com.itheima.a02reflectdemo1.Student
methodname=sleep

利用发射保存对象中的信息(重点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class MyReflectDemo {
public static void main(String[] args) throws IllegalAccessException, IOException {
/*
对于任意一个对象,都可以把对象所有的字段名和值,保存到文件中去
*/
Student s = new Student("小A",23,'女',167.5,"睡觉");
Teacher t = new Teacher("播妞",10000);
saveObject(s);
}

//把对象里面所有的成员变量名和值保存到本地文件中
public static void saveObject(Object obj) throws IllegalAccessException, IOException {
//1.获取字节码文件的对象
Class clazz = obj.getClass();
//2. 创建IO流
BufferedWriter bw = new BufferedWriter(new FileWriter("myreflect\\a.txt"));
//3. 获取所有的成员变量
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
//获取成员变量的名字
String name = field.getName();
//获取成员变量的值
Object value = field.get(obj);
//写出数据
bw.write(name + "=" + value);
bw.newLine();
}

bw.close();

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public class Student {
private String name;
private int age;
private char gender;
private double height;
private String hobby;

public Student() {
}

public Student(String name, int age, char gender, double height, String hobby) {
this.name = name;
this.age = age;
this.gender = gender;
this.height = height;
this.hobby = hobby;
}

/**
* 获取
* @return name
*/
public String getName() {
return name;
}

/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}

/**
* 获取
* @return age
*/
public int getAge() {
return age;
}

/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}

/**
* 获取
* @return gender
*/
public char getGender() {
return gender;
}

/**
* 设置
* @param gender
*/
public void setGender(char gender) {
this.gender = gender;
}

/**
* 获取
* @return height
*/
public double getHeight() {
return height;
}

/**
* 设置
* @param height
*/
public void setHeight(double height) {
this.height = height;
}

/**
* 获取
* @return hobby
*/
public String getHobby() {
return hobby;
}

/**
* 设置
* @param hobby
*/
public void setHobby(String hobby) {
this.hobby = hobby;
}

public String toString() {
return "Student{name = " + name + ", age = " + age + ", gender = " + gender + ", height = " + height + ", hobby = " + hobby + "}";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Teacher {
private String name;
private double salary;

public Teacher() {
}

public Teacher(String name, double salary) {
this.name = name;
this.salary = salary;
}

/**
* 获取
* @return name
*/
public String getName() {
return name;
}

/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}

/**
* 获取
* @return salary
*/
public double getSalary() {
return salary;
}

/**
* 设置
* @param salary
*/
public void setSalary(double salary) {
this.salary = salary;
}

public String toString() {
return "Teacher{name = " + name + ", salary = " + salary + "}";
}
}

动态代理

好处:

​ 无侵入式的给方法增强功能

动态代理三要素:

1,真正干活的对象

2,代理对象

3,利用代理调用方法

切记一点:代理可以增强或者拦截的方法都在接口中,接口需要写在newProxyInstance的第二个参数里。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
public static void main(String[] args) {
/*
需求:
外面的人想要大明星唱一首歌
1. 获取代理的对象
代理对象 = ProxyUtil.createProxy(大明星的对象);
2. 再调用代理的唱歌方法
代理对象.唱歌的方法("只因你太美");
*/
//1. 获取代理的对象
BigStar bigStar = new BigStar("鸡哥");
Star proxy = ProxyUtil.createProxy(bigStar);

//2. 调用唱歌的方法
String result = proxy.sing("只因你太美");
System.out.println(result);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/*
*
* 类的作用:
* 创建一个代理
*
* */
public class ProxyUtil {
/*
*
* 方法的作用:
* 给一个明星的对象,创建一个代理
*
* 形参:
* 被代理的明星对象
*
* 返回值:
* 给明星创建的代理
*
*
*
* 需求:
* 外面的人想要大明星唱一首歌
* 1. 获取代理的对象
* 代理对象 = ProxyUtil.createProxy(大明星的对象);
* 2. 再调用代理的唱歌方法
* 代理对象.唱歌的方法("只因你太美");
* */
public static Star createProxy(BigStar bigStar){
/* java.lang.reflect.Proxy类:提供了为对象产生代理对象的方法:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
参数一:用于指定用哪个类加载器,去加载生成的代理类
参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法
参数三:用来指定生成的代理对象要干什么事情*/
Star star = (Star) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),//参数一:用于指定用哪个类加载器,去加载生成的代理类
new Class[]{Star.class},//参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法
//参数三:用来指定生成的代理对象要干什么事情
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*
* 参数一:代理的对象
* 参数二:要运行的方法 sing
* 参数三:调用sing方法时,传递的实参
* */
if("sing".equals(method.getName())){
System.out.println("准备话筒,收钱");
}else if("dance".equals(method.getName())){
System.out.println("准备场地,收钱");
}
//去找大明星开始唱歌或者跳舞
//代码的表现形式:调用大明星里面唱歌或者跳舞的方法
return method.invoke(bigStar,args);
}
}
);
return star;
}
}
1
2
3
4
5
6
7
public interface Star {
//我们可以把所有想要被代理的方法定义在接口当中
//唱歌
public abstract String sing(String name);
//跳舞
public abstract void dance();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class BigStar implements Star {
private String name;


public BigStar() {
}

public BigStar(String name) {
this.name = name;
}

//唱歌
@Override
public String sing(String name){
System.out.println(this.name + "正在唱" + name);
return "谢谢";
}

//跳舞
@Override
public void dance(){
System.out.println(this.name + "正在跳舞");
}

/**
* 获取
* @return name
*/
public String getName() {
return name;
}

/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}

public String toString() {
return "BigStar{name = " + name + "}";
}
}

额外扩展

动态代理,还可以拦截方法

比如:

​ 在这个故事中,经济人作为代理,如果别人让邀请大明星去唱歌,打篮球,经纪人就增强功能。

​ 但是如果别人让大明星去扫厕所,经纪人就要拦截,不会去调用大明星的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
* 类的作用:
* 创建一个代理
* */
public class ProxyUtil {
public static Star createProxy(BigStar bigStar){
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
Star star = (Star) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),
new Class[]{Star.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("cleanWC".equals(method.getName())){
System.out.println("拦截,不调用大明星的方法");
return null;
}
//如果是其他方法,正常执行
return method.invoke(bigStar,args);
}
}
);
return star;
}
}

动态代理的练习

​ 对add方法进行增强,对remove方法进行拦截,对其他方法不拦截也不增强

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class MyProxyDemo1 {
public static void main(String[] args) {
//动态代码可以增强也可以拦截
//1.创建真正干活的人
ArrayList<String> list = new ArrayList<>();

//2.创建代理对象
//参数一:类加载器。当前类名.class.getClassLoader()
// 找到是谁,把当前的类,加载到内存中了,我再麻烦他帮我干一件事情,把后面的代理类,也加载到内存

//参数二:是一个数组,在数组里面写接口的字节码文件对象。
// 如果写了List,那么表示代理,可以代理List接口里面所有的方法,对这些方法可以增强或者拦截
// 但是,一定要写ArrayList真实实现的接口
// 假设在第二个参数中,写了MyInter接口,那么是错误的。
// 因为ArrayList并没有实现这个接口,那么就无法对这个接口里面的方法,进行增强或拦截
//参数三:用来创建代理对象的匿名内部类
List proxyList = (List) Proxy.newProxyInstance(
//参数一:类加载器
MyProxyDemo1.class.getClassLoader(),
//参数二:是一个数组,表示代理对象能代理的方法范围
new Class[]{List.class},
//参数三:本质就是代理对象
new InvocationHandler() {
@Override
//invoke方法参数的意义
//参数一:表示代理对象,一般不用(了解)
//参数二:就是方法名,我们可以对方法名进行判断,是增强还是拦截
//参数三:就是下面第三步调用方法时,传递的参数。
//举例1:
//list.add("阿玮好帅");
//此时参数二就是add这个方法名
//此时参数三 args[0] 就是 阿玮好帅
//举例2:
//list.set(1, "aaa");
//此时参数二就是set这个方法名
//此时参数三 args[0] 就是 1 args[1]"aaa"
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//对add方法做一个增强,统计耗时时间
if (method.getName().equals("add")) {
long start = System.currentTimeMillis();
//调用集合的方法,真正的添加数据
method.invoke(list, args);
long end = System.currentTimeMillis();
System.out.println("耗时时间:" + (end - start));
//需要进行返回,返回值要跟真正增强或者拦截的方法保持一致
return true;
}else if(method.getName().equals("remove") && args[0] instanceof Integer){
System.out.println("拦截了按照索引删除的方法");
return null;
}else if(method.getName().equals("remove")){
System.out.println("拦截了按照对象删除的方法");
return false;
}else{
//如果当前调用的是其他方法,我们既不增强,也不拦截
method.invoke(list,args);
return null;
}
}
}
);

//3.调用方法
//如果调用者是list,就好比绕过了第二步的代码,直接添加元素
//如果调用者是代理对象,此时代理才能帮我们增强或者拦截

//每次调用方法的时候,都不会直接操作集合
//而是先调用代理里面的invoke,在invoke方法中进行判断,可以增强或者拦截
proxyList.add("aaa");
proxyList.add("bbb");
proxyList.add("ccc");
proxyList.add("ddd");

proxyList.remove(0);
proxyList.remove("aaa");


//打印集合
System.out.println(list);
}
}

日志

作用:

​ 跟输出语句一样,可以把程序在运行过程中的详细信息都打印在控制台上。

​ 利用log日志还可以把这些详细信息保存到文件和数据库中。

使用步骤:

​ 不是java的,也不是自己写的,是第三方提供的代码,所以我们要导入jar包。

  • 把第三方的代码导入到当前的项目当中

    新建lib文件夹,把jar粘贴到lib文件夹当中,全选后右键点击选择add as a ….

    检测导入成功:导入成功后jar包可以展开。在项目重构界面可以看到导入的内容

  • 把配置文件粘贴到src文件夹下

  • 在代码中获取日志对象

  • 调用方法打印日志

日志级别

1
TRACE, DEBUG, INFO, WARN, ERROR

还有两个特殊的:

​ ALL:输出所有日志

​ OFF:关闭所有日志

日志级别从小到大的关系:

​ TRACE < DEBUG < INFO < WARN < ERROR

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--
CONSOLE :表示当前的日志信息是可以输出到控制台的。
-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--输出流对象 默认 System.out 改为 System.err-->
<target>System.out</target>
<encoder>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度
%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %c [%thread] : %msg%n</pattern>
</encoder>
</appender>

<!-- File是输出的方向通向文件的 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!--日志输出路径-->
<file>C:/code/itheima-data.log</file>
<!--指定日志文件拆分和压缩规则-->
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--通过指定压缩文件名称,来确定分割文件方式-->
<fileNamePattern>C:/code/itheima-data2-%d{yyyy-MMdd}.log%i.gz</fileNamePattern>
<!--文件拆分大小-->
<maxFileSize>1MB</maxFileSize>
</rollingPolicy>
</appender>

<!--

level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF
, 默认debug
<root>可以包含零个或多个<appender-ref>元素,标识这个输出位置将会被本日志级别控制。
-->
<root level="info">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE" />
</root>
</configuration>

写在前面的话:

基础加强包含了:

反射,动态代理,类加载器,xml,注解,日志,单元测试等知识点

其中最难的是反射和动态代理,其他知识点都非常简单

由于B站P数限制,xml,注解等知识点,阿玮写了详细文档供大家学习

类加载器

类加载器

类加载的完整过程

  • 类加载时机

    简单理解:字节码文件什么时候会被加载到内存中?

    有以下的几种情况:

    • 创建类的实例(对象)
    • 调用类的类方法
    • 访问类或者接口的类变量,或者为该类变量赋值
    • 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
    • 初始化某个类的子类
    • 直接使用java.exe命令来运行某个主类

    总结而言:用到了就加载,不用不加载

  • 类加载过程

    1. 加载

      • 通过包名 + 类名,获取这个类,准备用流进行传输
      • 在这个类加载到内存中
      • 加载完毕创建一个class对象
    2. 链接

      • 验证

        确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全

        (文件中的信息是否符合虚拟机规范有没有安全隐患)

      • 准备

        负责为类的类变量(被static修饰的变量)分配内存,并设置默认初始化值

        (初始化静态变量)

      • 解析

        将类的二进制数据流中的符号引用替换为直接引用

        (本类中如果用到了其他类,此时就需要找到对应的类)

    3. 初始化

      根据程序员通过程序制定的主观计划去初始化类变量和其他资源

      (静态变量赋值以及初始化其他资源)

  • 小结

    • 当一个类被使用的时候,才会加载到内存
    • 类加载的过程: 加载、验证、准备、解析、初始化

类加载的分类

  • 分类

    • Bootstrap class loader:虚拟机的内置类加载器,通常表示为null ,并且没有父null
    • Platform class loader:平台类加载器,负责加载JDK中一些特殊的模块
    • System class loader:系统类加载器,负责加载用户类路径上所指定的类库
  • 类加载器的继承关系

    • System的父加载器为Platform
    • Platform的父加载器为Bootstrap
  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class ClassLoaderDemo1 {
    public static void main(String[] args) {
    //获取系统类加载器
    ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

    //获取系统类加载器的父加载器 --- 平台类加载器
    ClassLoader classLoader1 = systemClassLoader.getParent();

    //获取平台类加载器的父加载器 --- 启动类加载器
    ClassLoader classLoader2 = classLoader1.getParent();

    System.out.println("系统类加载器" + systemClassLoader);
    System.out.println("平台类加载器" + classLoader1);
    System.out.println("启动类加载器" + classLoader2);

    }
    }

双亲委派模型

  • 介绍

    如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

ClassLoader 中的两个方法

  • 方法介绍

    方法名 说明
    public static ClassLoader getSystemClassLoader() 获取系统类加载器
    public InputStream getResourceAsStream(String name) 加载某一个资源文件
  • 示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class ClassLoaderDemo2 {
    public static void main(String[] args) throws IOException {
    //static ClassLoader getSystemClassLoader() 获取系统类加载器
    //InputStream getResourceAsStream(String name) 加载某一个资源文件

    //获取系统类加载器
    ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

    //利用加载器去加载一个指定的文件
    //参数:文件的路径(放在src的根目录下,默认去那里加载)
    //返回值:字节流。
    InputStream is = systemClassLoader.getResourceAsStream("prop.properties");

    Properties prop = new Properties();
    prop.load(is);

    System.out.println(prop);

    is.close();
    }
    }

xml

概述

  • 万维网联盟(W3C)

    万维网联盟(W3C)创建于1994年,又称W3C理事会。1994年10月在麻省理工学院计算机科学实验室成立。
    建立者: Tim Berners-Lee (蒂姆·伯纳斯·李)。
    是Web技术领域最具权威和影响力的国际中立性技术标准机构。
    到目前为止,W3C已发布了200多项影响深远的Web技术标准及实施指南,

    • 如广为业界采用的超文本标记语言HTML(标准通用标记语言下的一个应用)、

    • 可扩展标记语言XML(标准通用标记语言下的一个子集)

    • 以及帮助残障人士有效获得Web信息的无障碍指南(WCAG)等

  • xml概述

    XML的全称为(EXtensible Markup Language),是一种可扩展的标记语言
    标记语言: 通过标签来描述数据的一门语言(标签有时我们也将其称之为元素)
    可扩展:标签的名字是可以自定义的,XML文件是由很多标签组成的,而标签名是可以自定义的

  • 作用

    • 用于进行存储数据和传输数据
    • 作为软件的配置文件
  • 作为配置文件的优势

    • 可读性好
    • 可维护性高

标签的规则

  • 标签由一对尖括号和合法标识符组成

    1
    <student>
  • 标签必须成对出现

    1
    2
    <student> </student>
    前边的是开始标签,后边的是结束标签
  • 特殊的标签可以不成对,但是必须有结束标记

    1
    <address/>
  • 标签中可以定义属性,属性和标签名空格隔开,属性值必须用引号引起来

    1
    <student id="1"> </student>
  • 标签需要正确的嵌套

    1
    2
    这是正确的: <student id="1"> <name>张三</name> </student>
    这是错误的: <student id="1"><name>张三</student></name>

语法规则

  • 语法规则

    • XML文件的后缀名为:xml

    • 文档声明必须是第一行第一列

      version:该属性是必须存在的
      encoding:该属性不是必须的

      ​ 打开当前xml文件的时候应该是使用什么字符编码表(一般取值都是UTF-8)

      standalone: 该属性不是必须的,描述XML文件是否依赖其他的xml文件,取值为yes/no

    • 必须存在一个根标签,有且只能有一个

    • XML文件中可以定义注释信息

    • XML文件中可以存在以下特殊字符

      1
      2
      3
      4
      5
      &lt; < 小于
      &gt; > 大于
      &amp; & 和号
      &apos; ' 单引号
      &quot; " 引号
    • XML文件中可以存在CDATA区

  • 示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <?xml version="1.0" encoding="UTF-8" ?>
    <!--注释的内容-->
    <!--本xml文件用来描述多个学生信息-->
    <students>

    <!--第一个学生信息-->
    <student id="1">
    <name>张三</name>
    <age>23</age>
    <info>学生&lt; &gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;的信息</info>
    <message> <![CDATA[内容 <<<<<< >>>>>> ]]]></message>
    </student>

    <!--第二个学生信息-->
    <student id="2">
    <name>李四</name>
    <age>24</age>
    </student>

    </students>

xml解析

  • 概述

    xml解析就是从xml中获取到数据

  • 常见的解析思想

    DOM(Document Object Model)文档对象模型:就是把文档的各个组成部分看做成对应的对象。
    会把xml文件全部加载到内存,在内存中形成一个树形结构,再获取对应的值

  • 常见的解析工具

    • JAXP: SUN公司提供的一套XML的解析的API
    • JDOM: 开源组织提供了一套XML的解析的API-jdom
    • DOM4J: 开源组织提供了一套XML的解析的API-dom4j,全称:Dom For Java
    • pull: 主要应用在Android手机端解析XML
  • 解析的准备工作

    1. 我们可以通过网站:https://dom4j.github.io/ 去下载dom4j

      今天的资料中已经提供,我们不用再单独下载了,直接使用即可

    2. 将提供好的dom4j-1.6.1.zip解压,找到里面的dom4j-1.6.1.jar

    3. 在idea中当前模块下新建一个libs文件夹,将jar包复制到文件夹中

    4. 选中jar包 -> 右键 -> 选择add as library即可

  • 需求

    • 解析提供好的xml文件
    • 将解析到的数据封装到学生对象中
    • 并将学生对象存储到ArrayList集合中
    • 遍历集合
  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    <?xml version="1.0" encoding="UTF-8" ?>
    <!--注释的内容-->
    <!--本xml文件用来描述多个学生信息-->
    <students>

    <!--第一个学生信息-->
    <student id="1">
    <name>张三</name>
    <age>23</age>
    </student>

    <!--第二个学生信息-->
    <student id="2">
    <name>李四</name>
    <age>24</age>
    </student>

    </students>

    // 上边是已经准备好的student.xml文件
    public class Student {
    private String id;
    private String name;
    private int age;

    public Student() {
    }

    public Student(String id, String name, int age) {
    this.id = id;
    this.name = name;
    this.age = age;
    }

    public String getId() {
    return id;
    }

    public void setId(String id) {
    this.id = id;
    }

    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;
    }

    @Override
    public String toString() {
    return "Student{" +
    "id='" + id + '\'' +
    ", name='" + name + '\'' +
    ", age=" + age +
    '}';
    }
    }

    /**
    * 利用dom4j解析xml文件
    */
    public class XmlParse {
    public static void main(String[] args) throws DocumentException {
    //1.获取一个解析器对象
    SAXReader saxReader = new SAXReader();
    //2.利用解析器把xml文件加载到内存中,并返回一个文档对象
    Document document = saxReader.read(new File("myxml\\xml\\student.xml"));
    //3.获取到根标签
    Element rootElement = document.getRootElement();
    //4.通过根标签来获取student标签
    //elements():可以获取调用者所有的子标签.会把这些子标签放到一个集合中返回.
    //elements("标签名"):可以获取调用者所有的指定的子标签,会把这些子标签放到一个集合中并返回
    //List list = rootElement.elements();
    List<Element> studentElements = rootElement.elements("student");
    //System.out.println(list.size());

    //用来装学生对象
    ArrayList<Student> list = new ArrayList<>();

    //5.遍历集合,得到每一个student标签
    for (Element element : studentElements) {
    //element依次表示每一个student标签

    //获取id这个属性
    Attribute attribute = element.attribute("id");
    //获取id的属性值
    String id = attribute.getValue();

    //获取name标签
    //element("标签名"):获取调用者指定的子标签
    Element nameElement = element.element("name");
    //获取这个标签的标签体内容
    String name = nameElement.getText();

    //获取age标签
    Element ageElement = element.element("age");
    //获取age标签的标签体内容
    String age = ageElement.getText();

    // System.out.println(id);
    // System.out.println(name);
    // System.out.println(age);

    Student s = new Student(id,name,Integer.parseInt(age));
    list.add(s);
    }
    //遍历操作
    for (Student student : list) {
    System.out.println(student);
    }
    }
    }

DTD约束

  • 什么是约束

    用来限定xml文件中可使用的标签以及属性

  • 约束的分类

    • DTD
    • schema
  • 编写DTD约束

    • 步骤

      1. 创建一个文件,这个文件的后缀名为.dtd

      2. 看xml文件中使用了哪些元素

        可以定义元素

      3. 判断元素是简单元素还是复杂元素

        简单元素:没有子元素。
        复杂元素:有子元素的元素;

    • 代码实现

      1
      2
      3
      4
      <!ELEMENT persons (person)>
      <!ELEMENT person (name,age)>
      <!ELEMENT name (#PCDATA)>
      <!ELEMENT age (#PCDATA)>
    1

  • 引入DTD约束

    • 引入DTD约束的三种方法

      • 引入本地dtd

      • 在xml文件内部引入

      • 引入网络dtd

    • 代码实现

      • 引入本地DTD约束

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        // 这是persondtd.dtd文件中的内容,已经提前写好
        <!ELEMENT persons (person)>
        <!ELEMENT person (name,age)>
        <!ELEMENT name (#PCDATA)>
        <!ELEMENT age (#PCDATA)>

        // 在person1.xml文件中引入persondtd.dtd约束
        <?xml version="1.0" encoding="UTF-8" ?>
        <!DOCTYPE persons SYSTEM 'persondtd.dtd'>

        <persons>
        <person>
        <name>张三</name>
        <age>23</age>
        </person>

        </persons>
      • 在xml文件内部引入

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        <?xml version="1.0" encoding="UTF-8" ?>
        <!DOCTYPE persons [
        <!ELEMENT persons (person)>
        <!ELEMENT person (name,age)>
        <!ELEMENT name (#PCDATA)>
        <!ELEMENT age (#PCDATA)>
        ]>

        <persons>
        <person>
        <name>张三</name>
        <age>23</age>
        </person>

        </persons>
      • 引入网络dtd

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        <?xml version="1.0" encoding="UTF-8" ?>
        <!DOCTYPE persons PUBLIC "dtd文件的名称" "dtd文档的URL">

        <persons>
        <person>
        <name>张三</name>
        <age>23</age>
        </person>

        </persons>
  • DTD语法

    • 定义元素

      定义一个元素的格式为:
      简单元素:

      ​ EMPTY: 表示标签体为空

      ​ ANY: 表示标签体可以为空也可以不为空

      ​ PCDATA: 表示该元素的内容部分为字符串

      复杂元素:
      ​ 直接写子元素名称. 多个子元素可以使用”,”或者”|”隔开;
      ​ “,”表示定义子元素的顺序 ; “|”: 表示子元素只能出现任意一个
      ​ “?”零次或一次, “+”一次或多次, “*”零次或多次;如果不写则表示出现一次

    • 定义属性

      格式

      定义一个属性的格式为:
      属性的类型:
      ​ CDATA类型:普通的字符串

      属性的约束:

      ​ // #REQUIRED: 必须的
      ​ // #IMPLIED: 属性不是必需的
      ​ // #FIXED value:属性值是固定的

    • 代码实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      <!ELEMENT persons (person+)>
      <!ELEMENT person (name,age)>
      <!ELEMENT name (#PCDATA)>
      <!ELEMENT age (#PCDATA)>
      <!ATTLIST person id CDATA #REQUIRED>

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE persons SYSTEM 'persondtd.dtd'>

      <persons>
      <person id="001">
      <name>张三</name>
      <age>23</age>
      </person>

      <person id = "002">
      <name>张三</name>
      <age>23</age>
      </person>

      </persons>
      ​```

schema约束

  • schema和dtd的区别

    1. schema约束文件也是一个xml文件,符合xml的语法,这个文件的后缀名.xsd
    2. 一个xml中可以引用多个schema约束文件,多个schema使用名称空间区分(名称空间类似于java包名)
    3. dtd里面元素类型的取值比较单一常见的是PCDATA类型,但是在schema里面可以支持很多个数据类型
    4. schema 语法更加的复杂
  • 编写schema约束

    • 步骤

      1,创建一个文件,这个文件的后缀名为.xsd。
      2,定义文档声明
      3,schema文件的根标签为:
      4,在中定义属性:
      ​ xmlns=http://www.w3.org/2001/XMLSchema
      5,在中定义属性 :
      ​ targetNamespace =唯一的url地址,指定当前这个schema文件的名称空间。
      6,在中定义属性 :
      ​ elementFormDefault=”qualified“,表示当前schema文件是一个质量良好的文件。
      7,通过element定义元素
      8,判断当前元素是简单元素还是复杂元素

    • 代码实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      <?xml version="1.0" encoding="UTF-8" ?>
      <schema
      xmlns="http://www.w3.org/2001/XMLSchema"
      targetNamespace="http://www.itheima.cn/javase"
      elementFormDefault="qualified"
      >

      <!--定义persons复杂元素-->
      <element name="persons">
      <complexType>
      <sequence>
      <!--定义person复杂元素-->
      <element name = "person">
      <complexType>
      <sequence>
      <!--定义name和age简单元素-->
      <element name = "name" type = "string"></element>
      <element name = "age" type = "string"></element>
      </sequence>

      </complexType>
      </element>
      </sequence>
      </complexType>

      </element>

      </schema>

  • 引入schema约束

    • 步骤

      1,在根标签上定义属性xmlns=”http://www.w3.org/2001/XMLSchema-instance
      2,通过xmlns引入约束文件的名称空间
      3,给某一个xmlns属性添加一个标识,用于区分不同的名称空间
      ​ 格式为: xmlns:标识=“名称空间地址” ,标识可以是任意的,但是一般取值都是xsi
      4,通过xsi:schemaLocation指定名称空间所对应的约束文件路径
      ​ 格式为:xsi:schemaLocation = “名称空间url 文件路径“

    • 代码实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <?xml version="1.0" encoding="UTF-8" ?>

      <persons
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns="http://www.itheima.cn/javase"
      xsi:schemaLocation="http://www.itheima.cn/javase person.xsd"
      >
      <person>
      <name>张三</name>
      <age>23</age>
      </person>

      </persons>
      ​```
  • schema约束定义属性

    • 代码示例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      <?xml version="1.0" encoding="UTF-8" ?>
      <schema
      xmlns="http://www.w3.org/2001/XMLSchema"
      targetNamespace="http://www.itheima.cn/javase"
      elementFormDefault="qualified"
      >

      <!--定义persons复杂元素-->
      <element name="persons">
      <complexType>
      <sequence>
      <!--定义person复杂元素-->
      <element name = "person">
      <complexType>
      <sequence>
      <!--定义name和age简单元素-->
      <element name = "name" type = "string"></element>
      <element name = "age" type = "string"></element>
      </sequence>

      <!--定义属性,required( 必须的)/optional( 可选的)-->
      <attribute name="id" type="string" use="required"></attribute>
      </complexType>

      </element>
      </sequence>
      </complexType>
      </element>

      </schema>

      <?xml version="1.0" encoding="UTF-8" ?>
      <persons
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns="http://www.itheima.cn/javase"
      xsi:schemaLocation="http://www.itheima.cn/javase person.xsd"
      >
      <person id="001">
      <name>张三</name>
      <age>23</age>
      </person>

      </persons>
      ​```

单元测试Junit

什么是单元测试?

对部分代码进行测试。

Junit的特点?

  • 是一个第三方的工具。(把别人写的代码导入项目中)(专业叫法:导jar包)

  • 如果运行结果显示绿色,表示运行结果是正确的。

    如果运行结果显示红色,表示运行结果是错误的。

基本用法:

1,一定要先写一个方法。

2,在这个方法的上面写@Test

3,鼠标点一下@Test 按alt + 回车,点击Junit4

​ 此时就可以自动导包。

​ 如果自动导包失败(连接外网,或者自己手动导包)

​ 如果导包成功在左下角就会出现Junit4的相关jar包

手动导包

1,在当前模块下,右键新建一个文件夹(lib)

2,把今天资料里面的两个jar包,拷贝到lib文件夹里面

3,选中两个jar右键点击add as a lib….

4,到代码中,找到@Test,按alt + 回车,再来导入。

运行测试代码

  • 只能直接运行无参无返回值的非静态方法
  • 想要运行谁,就右键点击哪个方法。如果想要运行一个类里面所有的测试方法,选择类名,有点点击即可。

Junit正确的打开方式(正确的使用方式)

注意点:并不是直接在要测试的方法上面直接加@Test

原因:因为要测试的方法有可能是有参数的,有返回值,或者是静态的。

正确的使用方式:

1,新建测试类

2,新建测试方法(要测试的方法名 + Test) methodTest

3,在这个方法中直接调用要测试的方法

4,在测试方法的上面写@Test

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//真正用来测试的类
//测试用例(测试类)
public class JunitTest {

//在这个类里面再写无参无返回值的非静态方法
//在方法中调用想要测试的方法

@Test
public void method2Test(){
//调用要测试的方法
JunitDemo1 jd = new JunitDemo1();
jd.method2(10);
}
}

实际开发中单元测试的使用方式(掌握)

需求:测试File中的delete方法,写的是否正确(掌握)

开发中的测试原则:

不污染原数据。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class JunitDemo3 {
//在实际开发中,真正完整的单元测试该怎么写?
//前提:
//以后在工作的时候,测试代码不能污染原数据。(修改,篡改)
//1.利用Before去对数据做一个初始化的动作
//2.利用Test真正的去测试方法
//3.利用After去还原数据

//需求:测试File类中的delete方法是否书写正确???
@Before
public void beforemethod() throws IOException {
//先备份
File src = new File("C:\\Users\\moon\\Desktop\\a.txt");
File dest = new File("C:\\Users\\moon\\Desktop\\copy.txt");

FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest);
int b;
while((b = fis.read()) != -1){
fos.write(b);
}
fos.close();
fis.close();
}

//作为一个标准的测试人员,运行完单元测试之后,不能污染原数据
//需要达到下面两个要求:
//1.得到结果
//2.a.txt还在而且其他的备份文件消失
@Test
public void method(){
File file = new File("C:\\Users\\moon\\Desktop\\a.txt");
boolean delete = file.delete();

//检查a.txt是否存在
boolean exists = file.exists();

//只有同时满足了下面所有的断言,才表示delete方法编写正确
Assert.assertEquals("delete方法出错了",delete,true);
Assert.assertEquals("delete方法出错了",exists,false);
}


@After
public void aftermethod() throws IOException {
//还要对a.txt做一个还原
File src = new File("C:\\Users\\moon\\Desktop\\copy.txt");
File dest = new File("C:\\Users\\moon\\Desktop\\a.txt");

FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest);
int b;
while((b = fis.read()) != -1){
fos.write(b);
}
fos.close();
fis.close();

//备份数据要删除
src.delete();

}
}

扩展点:

在单元测试中,相对路径是相对当前模块而言的。

代码示例:

1
2
3
File file = new File("aweihaoshuai.txt");
file.createNewFile();
//此时是把aweihaoshuai.txt这个文件新建到模块中了。

注解

注释和注解的区别?

共同点:都可以对程序进行解释说明。

不同点:注释,是给程序员看的。只在Java中有效。在class文件中不存在注释的。

​ 当编译之后,会进行注释擦除。

​ 注解,是给虚拟机看的。当虚拟机看到注解之后,就知道要做什么事情了。

如何使用注解(掌握)

在以前看过注解@Override。

当子类重写父类方法的时候,在重写的方法上面写@Override。

当虚拟机看到@Override的时候,就知道下面的方法是重写的父类的。检查语法,如果语法正确编译正常,如果语法错误,就会报错。

Java中已经存在的注解

@Override:表示方法的重写

@Deprecated:表示修饰的方法已过时

@SuppressWarnings(“all”):压制警告

除此之外,还需要掌握第三方框架中提供的注解:

比如:Junit

@Test 表示运行测试方法

@Before 表示在Test之前运行,进行数据的初始化

@After 表示在Test之后运行,进行数据的还原

自定义注解

自定义注解单独存在是没有什么意义的,一般会跟反射结合起来使用,会用发射去解析注解。

针对于注解,只要掌握会使用别人已经写好的注解即可。

关于注解的解析,一般是在框架的底层已经写好了。

特殊属性

value:

​ 当注解中只有”一个属性”,并且属性名是”value”,使用注解时,可以省略value属性名

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//注解的定义
public @interface Anno2 {
public String value();

public int age() default 23;
}

//注解的使用
@Anno2("123")
public class AnnoDemo2 {

@Anno2("123")
public void method(){

}
}

元注解

可以写在注解上面的注解

@Target :指定注解能在哪里使用

@Retention :可以理解为保留时间(生命周期)

Target:

​ 作用:用来标识注解使用的位置,如果没有使用该注解标识,则自定义的注解可以使用在任意位置。

​ 可使用的值定义在ElementType枚举类中,常用值如下

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

Retention:

​ 作用:用来标识注解的生命周期(有效范围)

​ 可使用的值定义在RetentionPolicy枚举类中,常用值如下

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

注解的解析:

模拟JUnit自带的@Test注解

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {
}

public class MyTestMethod {

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

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

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

public class MyTestDemo {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException {
//1,获取class对象
Class clazz = Class.forName("com.itheima.test2.MyTestMethod");

//获取对象
Object o = clazz.newInstance();

//2.获取所有方法
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
//method依次表示类里面的每一个方法
method.setAccessible(true);
//判断当前方法有没有MyTest注解
if(method.isAnnotationPresent(MyTest.class)){
method.invoke(o);
}
}
}
}

注解小结:

掌握如何使用已经存在的注解即可。

@Override:表示方法的重写

@Deprecated:表示修饰的方法已过时

@SuppressWarnings(“all”):压制警告

@Test:表示要运行的方法

在以后的实际开发中,注解是使用框架已经提供好的注解。

自定义注解+解析注解(很难的,了解),一般会出现在框架的底层。当以后我们要自己写一个框架的时候,才会用到自定义注解+解析注解。