Java-高级技术(二)

9、多线程

什么是线程?

  • 线程(thread)是一个程序内部的一条执行路径。

  • 我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。

    public static void main(String[] args) {
        // 代码...
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
        // 代码...
    }
    
  • 程序中如果只有一条执行路径,那么这个程序就是单线程的程序。

多线程是什么?

  • 多线程是指从软硬件上实现多条执行流程的技术。
  • 购片系统、上传下载系统、消息通信、淘宝、京东系统都离不开多线程技术。

9.1、多线程的创建

9.1.1、方式一:继承Thread类

Thread类

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

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

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

代码演示

/**
   目标:多线程的创建方式一:继承Thread类实现。
 */
public class ThreadDemo1 {
    public static void main(String[] args) {
        // 3、new一个新线程对象
        Thread t = new MyThread();
        // 4、调用start方法启动线程(执行的还是run方法)
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程执行输出:" + i);
        }

    }
}

/**
   1、定义一个线程类继承Thread类
 */
class MyThread extends Thread{
    /**
       2、重写run方法,里面是定义线程以后要干啥
     */
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程执行输出:" + i);
        }
    }
}

输出结果

主线程执行输出:0
主线程执行输出:1
子线程执行输出:0
主线程执行输出:2
子线程执行输出:1
主线程执行输出:3
子线程执行输出:2
主线程执行输出:4
子线程执行输出:3
子线程执行输出:4

方式一优缺点

  • 优点:编码简单
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。

注意事项

1、为什么不直接调用了run方法,而是调用start启动线程。

  • 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
  • 只有调用start方法才是启动一个新的线程执行。

2、把主线程任务放在子线程之前了。

  • 这样主线程一直是先跑完的,相当于是一个单线程的效果了。

9.1.2、方式二:实现Runnable接口

多线程的实现方案二:实现Runnable接口

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

Thread的构造器

构造器 说明
public Thread(String name) 可以为当前线程指定名称
public Thread(Runnable target) 封装Runnable对象成为线程对象
public Thread(Runnable target ,String name ) 封装Runnable对象成为线程对象,并指定线程名称

代码演示

/**
   目标:学会线程的创建方式二,理解它的优缺点。
 */
public class ThreadDemo2 {
    public static void main(String[] args) {
        // 3、创建一个任务对象
        Runnable target = new MyRunnable();
        // 4、把任务对象交给Thread处理
        Thread t = new Thread(target);
        // Thread t = new Thread(target, "1号");
        // 5、启动线程
        t.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("主线程执行输出:" + i);
        }
    }
}

/**
   1、定义一个线程任务类 实现Runnable接口
 */
class MyRunnable implements Runnable {
    /**
       2、重写run方法,定义线程的执行任务的
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("子线程执行输出:" + i);
        }
    }
}

输出结果

主线程执行输出:0
主线程执行输出:1
主线程执行输出:2
子线程执行输出:0
主线程执行输出:3
子线程执行输出:1
主线程执行输出:4
子线程执行输出:2
主线程执行输出:5
子线程执行输出:3
主线程执行输出:6
子线程执行输出:4
主线程执行输出:7
子线程执行输出:5
主线程执行输出:8
主线程执行输出:9
子线程执行输出:6
子线程执行输出:7
子线程执行输出:8
子线程执行输出:9

方式二优缺点

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
  • 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。

方式二拓展

实现Runnable接口(匿名内部类形式)

  • 可以创建Runnable的匿名内部类对象。
  • 交给Thread处理。
  • 调用线程对象的start()启动线程。

代码演示

/**
   目标:学会线程的创建方式二(匿名内部类方式实现,语法形式)
 */
public class ThreadDemo2Other {
    public static void main(String[] args) {
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("子线程1执行输出:" + i);
                }
            }
        };
        Thread t = new Thread(target);
        t.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("子线程2执行输出:" + i);
                }
            }
        }).start();

        new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    System.out.println("子线程3执行输出:" + i);
            }
        }).start();

        for (int i = 0; i < 10; i++) {
            System.out.println("主线程执行输出:" + i);
        }
    }
}

输出结果

主线程执行输出:0
子线程2执行输出:0
子线程3执行输出:0
子线程3执行输出:1
子线程1执行输出:0
子线程3执行输出:2
子线程2执行输出:1
主线程执行输出:1
子线程2执行输出:2
子线程3执行输出:3
子线程1执行输出:1
子线程3执行输出:4
子线程2执行输出:3
主线程执行输出:2
子线程2执行输出:4
子线程3执行输出:5
子线程1执行输出:2
子线程3执行输出:6
子线程2执行输出:5
主线程执行输出:3
子线程2执行输出:6
子线程3执行输出:7
子线程1执行输出:3
子线程3执行输出:8
子线程2执行输出:7
主线程执行输出:4
子线程2执行输出:8
子线程3执行输出:9
子线程1执行输出:4
子线程1执行输出:5
子线程2执行输出:9
主线程执行输出:5
主线程执行输出:6
主线程执行输出:7
子线程1执行输出:6
主线程执行输出:8
子线程1执行输出:7
主线程执行输出:9
子线程1执行输出:8
子线程1执行输出:9

9.1.3、方式三:实现Callable接口(JDK 5.0新增)

问题引出

1、前2种线程创建方式都存在一个问题:

  • 他们重写的run方法均不能直接返回结果。
  • 不适合需要返回线程执行结果的业务场景。

2、怎么解决这个问题呢?

  • JDK 5.0提供了Callable和FutureTask来实现。
  • 这种方式的优点是:可以得到线程执行的结果。

多线程的实现方案三:利用Callable、FutureTask接口实现。

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

FutureTask的API

方法名称 说明
public FutureTask<>(Callable call) 把Callable对象封装成FutureTask对象。
public V get() throws Exception 获取线程执行call方法返回的结果。

代码演示

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

/**
   目标:学会线程的创建方式三:实现Callable接口,结合FutureTask完成。
 */
public class ThreadDemo3 {
    public static void main(String[] args) {
        // 3、创建Callable任务对象
        Callable<String> call = new MyCallable(100);
        // 4、把Callable任务对象 交给 FutureTask 对象
        //  FutureTask对象的作用1: 是Runnable的对象(实现了Runnable接口),可以交给Thread了
        //  FutureTask对象的作用2: 可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果
        FutureTask<String> f1 = new FutureTask<>(call);
        // 5、交给线程处理
        Thread t1 = new Thread(f1);
        // 6、启动线程
        t1.start();

        Callable<String> call2 = new MyCallable(200);
        FutureTask<String> f2 = new FutureTask<>(call2);
        Thread t2 = new Thread(f2);
        t2.start();

        try {
            // 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果。
            String rs1 = f1.get();
            System.out.println("第一个结果:" + rs1);
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            // 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才提取结果。
            String rs2 = f2.get();
            System.out.println("第二个结果:" + rs2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

/**
    1、定义一个任务类 实现Callable接口  应该申明线程任务执行完毕后的结果的数据类型
 */
class MyCallable implements Callable<String>{
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    /**
       2、重写call方法(任务方法)
     */
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n ; i++) {
            sum += i;
        }
        return "子线程执行的结果是:" + sum;
    }
}

输出结果

第一个结果:子线程执行的结果是:5050
第二个结果:子线程执行的结果是:20100

方式三优缺点

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

9.1.4、三种方式对比

方式 优点 缺点
继承Thread类 编程比较简单,可以直接使用Thread类中的方法 扩展性较差,不能再继承其他的类,不能返回线程执行的结果
实现Runnable接口 扩展性强,实现该接口的同时还可以继承其他的类。 编程相对复杂,不能返回线程执行的结果
实现Callable接口 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 编程相对复杂

9.2、Thread 的常用方法

Thread常用API说明

  • Thread常用方法:获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。
  • 至于Thread类提供的诸如:yield、join、interrupt、不推荐的方法 stop 、守护线程、线程优先级等线程的控制方法,在开发中很少使用,这些方法会在高级篇以及后续需要用到的时候再为大家讲解。

构造器

构造器 说明
public Thread(String name) 可以为当前线程指定名称
public Thread(Runnable target) 把Runnable对象交给线程对象
public Thread(Runnable target ,String name ) 把Runnable对象交给线程对象,并指定线程名称

方法

方法名称 说明
String getName() 获取当前线程的名称,默认线程名称是Thread-索引
void setName(String name) 设置线程名称
public static Thread currentThread(): 返回对当前正在执行的线程对象的引用
public static void sleep(long time) 让线程休眠指定的时间,单位为毫秒。
public void run() 线程任务方法
public void start() 线程启动方法

9.2.1、Thread获取和设置线程名称

问题引出

当有很多线程在执行的时候,我们怎么去区分这些线程呢?

  • 此时需要使用Thread的常用方法:getName()、setName()、currentThread()等。

Thread获取和设置线程名称

方法名称 说明
String getName() 获取当前线程的名称,默认线程名称是Thread-索引
void setName(String name) 将此线程的名称更改为指定的名称,通过构造器也可以设置线程名称

Thread类获得当前线程的对象

方法名称 说明
public static Thread currentThread(): 返回对当前正在执行的线程对象的引用

注意:

  • 此方法是Thread类的静态方法,可以直接使用Thread类调用。
  • 这个方法是在哪个线程执行中调用的,就会得到哪个线程对象。

Thread的构造器

方法名称 说明
public Thread(String name) 可以为当前线程指定名称
public Thread(Runnable target) 封装Runnable对象成为线程对象
public Thread(Runnable target ,String name ) 封装Runnable对象成为线程对象,并指定线程名称

MyThread 类

public class MyThread extends Thread{
    public MyThread() {
    }

    public MyThread(String name) {
        // 为当前线程对象设置名称,送给父类的有参数构造器初始化名称
        super(name);
    }

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

测试类

/**
    目标:线程的API
 */
public class ThreadDemo01 {
    // main方法是由主线程负责调度的
    public static void main(String[] args) {
        Thread t1 = new MyThread("1号");
        // t1.setName("1号");
        t1.start();
        System.out.println(t1.getName());

        Thread t2 = new MyThread("2号");
        // t2.setName("2号");
        t2.start();
        System.out.println(t2.getName());

        // 哪个线程执行它,它就得到哪个线程对象(当前线程对象)
        // 主线程的名称就叫main
        Thread m = Thread.currentThread();
        System.out.println(m.getName());
        m.setName("最牛的线程");

        for (int i = 0; i < 5; i++) {
            System.out.println( m.getName() + "输出:" + i);
        }
    }
}

输出结果

1号
2号
main
1号输出:0
2号输出:0
2号输出:1
1号输出:1
1号输出:2
2号输出:2
最牛的线程输出:0
2号输出:3
1号输出:3
2号输出:4
最牛的线程输出:1
1号输出:4
最牛的线程输出:2
最牛的线程输出:3
最牛的线程输出:4

9.2.2、Thread类的线程休眠方法

方法名称 说明
public static void sleep(long time) 让当前线程休眠指定的时间后再继续执行,单位为毫秒。

代码演示

/**
    目标:线程的API
 */
public class ThreadDemo02 {
    // main方法是由主线程负责调度的
    public static void main(String[] args) throws Exception {
        for (int i = 1; i <= 5; i++) {
            System.out.println("输出:" + i);
            if(i == 3){
                // 让当前线程进入休眠状态
                // 段子:项目经理让我加上这行代码,如果用户愿意交钱,我就注释掉。
                Thread.sleep(3000);
            }
        }
    }
}

9.3、线程安全

线程安全问题

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

取钱模型演示

  • 需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元。
  • 如果小明和小红同时来取钱,而且2人都要取钱10万元,可能出现什么问题呢?

Java-高级技术(二)_第1张图片

线程安全问题出现的原因

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

总结:多个线程同时访问同一个共享资源且存在修改该资源。

线程安全问题案例模拟

需求:

  • 小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。

分析:

  1. 需要提供一个账户类,创建一个账户对象代表2个人的共享账户。
  2. 需要定义一个线程类,线程类可以处理账户对象。
  3. 创建2个线程对象,传入同一个账户对象。
  4. 启动2个线程,去同一个账户对象中取钱10万。

账户类

public class Account {
    private String cardId;
    private double money; // 账户的余额

    public Account(){

    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

    /**
       小明 小红
     */
    public void drawMoney(double money) {
        // 0、先获取是谁来取钱,线程的名字就是人名
        String name = Thread.currentThread().getName();
        // 1、判断账户是否够钱
        if(this.money >= money){
            // 2、取钱
            System.out.println(name + "来取钱成功,吐出:" + money);
            // 3、更新余额
            this.money -= money;
            System.out.println(name + "取钱后剩余:" + this.money);
        }else {
            // 4、余额不足
            System.out.println(name +"来取钱,余额不足!");
        }

    }

    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }

}

线程类

/**
   取钱的线程类
 */
public class DrawThread extends Thread {
    // 接收处理的账户对象
    private Account acc;
    public DrawThread(Account acc,String name){
        super(name);
        this.acc = acc;
    }
    @Override
    public void run() {
        // 小明 小红:取钱
        acc.drawMoney(100000);
    }
}

测试类

/**
    需求:模拟取钱案例。
 */
public class ThreadDemo {
    public static void main(String[] args) {
        // 1、定义线程类,创建一个共享的账户对象
        Account acc = new Account("ICBC-111", 100000);

        // 2、创建2个线程对象,代表小明和小红同时进来了。
        new DrawThread(acc, "小明").start();
        new DrawThread(acc, "小红").start();
    }
}

输出结果

小明来取钱成功,吐出:100000.0
小红来取钱成功,吐出:100000.0
小红取钱后剩余:-100000.0
小明取钱后剩余:0.0

9.4、线程同步

为了解决线程安全问题

9.4.1、同步思想概述

问题分析

1、取钱案例出现问题的原因?

  • 多个线程同时执行,发现账户都是够钱的。

2、如何才能保证线程安全呢?

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

线程同步的核心思想

加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。

9.4.2、方式一:同步代码块

作用

把出现线程安全问题的核心代码给上锁。

原理

每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

锁对象要求

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

  • 虽然锁住的可以是任意对象,但是会锁住无关线程

synchronized ("chovy") {
    ...
}

锁对象用任意唯一的对象好不好呢?

  • 不好,会影响其他无关线程的执行。

代码演示

public void drawMoney(double money) {
    // 1、拿到是谁来取钱
    String name = Thread.currentThread().getName();
    // 同步代码块
    // this == acc 共享账户
    synchronized (this) {
        // 2、判断余额是否足够
        if(this.money >= money){
            // 钱够了
            System.out.println(name+"来取钱,吐出:" + money);
            // 更新余额
            this.money -= money;
            System.out.println(name+"取钱后,余额剩余:" + this.money);
        }else{
            // 3、余额不足
            System.out.println(name+"来取钱,余额不足!");
        }
    }
}

this == acc 共享账户原理

Java-高级技术(二)_第2张图片

锁对象的规范要求

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

  • 对于实例方法建议使用this作为锁对象。

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

    public class Account {
    	public static void run(){
    		synchronized (Account.class){
    			...
    		}
    	}
    }
    

9.4.3、方式二:同步方法

作用

把出现线程安全问题的核心代码给上锁。

原理

每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

代码演示

public synchronized void drawMoney(double money) {
        // 1、拿到是谁来取钱
        String name = Thread.currentThread().getName();
        // 2、判断余额是否足够
        // 小明  小红
        if(this.money >= money){
            // 钱够了
            System.out.println(name+"来取钱,吐出:" + money);
            // 更新余额
            this.money -= money;
            System.out.println(name+"取钱后,余额剩余:" + this.money);
        }else{
            // 3、余额不足
            System.out.println(name+"来取钱,余额不足!");
        }
    }

同步方法底层原理

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

同步方法对比同步代码块

同步代码块锁的范围更小,同步方法锁的范围更大。

9.4.4、方式三:Lock锁

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

方法

方法名称 说明
public ReentrantLock() 获得Lock锁的实现类对象
void lock() 获得锁
void unlock() 释放锁

代码演示

// final修饰后:锁对象是唯一和不可替换的,非常专业
private final Lock lock = new ReentrantLock();

public void drawMoney(double money) {
    // 1、拿到是谁来取钱
    String name = Thread.currentThread().getName();
    // 2、判断余额是否足够
    // 小明  小红
    lock.lock(); // 上锁
    try {
        if(this.money >= money){
            // 钱够了
            System.out.println(name+"来取钱,吐出:" + money);
            // 更新余额
            this.money -= money;
            System.out.println(name+"取钱后,余额剩余:" + this.money);
        }else{
            // 3、余额不足
            System.out.println(name+"来取钱,余额不足!");
        }
    } finally {
        lock.unlock(); // 解锁
    }
}

9.5、线程通信[了解]

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

线程通信常见形式

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

线程通信实际应用场景

  • 生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费生产者产生的数据。
  • 要求:生产者线程生产完数据后唤醒消费者,然后等待自己,消费者消费完该数据后唤醒生产者,然后等待自己。

线程通信案例模拟
Java-高级技术(二)_第3张图片
Object类的等待和唤醒方法

方法名称 说明
void wait() 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或 notifyAll()方法
void notify() 唤醒正在等待的单个线程
void notifyAll() 唤醒正在等待的所有线程

**注意:**上述方法应该使用当前同步锁对象进行调用。

CallSystem 类

/**
   呼叫系统。
 */
public class CallSystem {
    // 定义一个变量记录当前呼入进来的电话。
    public static int number = 0; // 最多只接听一个。

    /* 接入电话
     */
    public synchronized static void call() {
        try {
            number++;
            System.out.println("成功接入一个用户,等待分发~~~~");

            // 唤醒别人 : 1个
            CallSystem.class.notify();
            // 让当前线程对象进入等待状态。
            CallSystem.class.wait();

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

    /**
       分发电话
     */
    public synchronized static void receive() {
        try {
            String name = Thread.currentThread().getName();
            if(number == 1){
                System.out.println(name + "此电话已经分发给客服并接听完毕了~~~~~");
                number--;
                // 唤醒别人 : 1个
                CallSystem.class.notify();
                CallSystem.class.wait(); // 让当前线程等待
            }else {
                // 唤醒别人 : 1个
                CallSystem.class.notify();
                CallSystem.class.wait(); // 让当前线程等待
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

CallThread 类

public class CallThread extends Thread{
    @Override
    public void run() {
        // 不断的打入电话
        while (true){
            CallSystem.call();
        }
    }
}

ReceiveThread 类

/**
   接电话线程类
 */
public class ReceiveThread extends Thread{
    @Override
    public void run() {
        // 1号  2号
        while (true){
            CallSystem.receive();
        }
    }
}

测试类

public class TestDemo {
    public static void main(String[] args) {
        // 1、生产者线程:负责不断接收打进来的电话
        CallThread call = new CallThread();
        call.start();

        // 2、消费者线程:客服,每个客服每次接听一个电话
        ReceiveThread r1 = new ReceiveThread();
        r1.start();
    }
}

9.6、线程池[重点]

9.6.1、线程池概述

线程池就是一个可以复用线程的技术。

不使用线程池的问题

如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。

线程池的工作原理

Java-高级技术(二)_第4张图片

9.6.2、线程池实现的API、参数说明

谁代表线程池?

JDK 5.0起提供了代表线程池的接口:ExecutorService

如何得到线程池对象

  • 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象

Java-高级技术(二)_第5张图片

  • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象

ThreadPoolExecutor 构造器的参数说明

public ThreadPoolExecutor(int corePoolSize,
                                 int maximumPoolSize,
                                 long keepAliveTime,
                                 TimeUnit unit,
                                 BlockingQueue<Runnable> workQueue,
                                 ThreadFactory threadFactory,
                                 RejectedExecutionHandler handler)
参数一:指定线程池的线程数量(核心线程): corePoolSize	   --->不能小于0
参数二:指定线程池可支持的最大线程数: maximumPoolSize		--->最大数量 >= 核心线程数量
参数三:指定临时线程的最大存活时间: keepAliveTime		 --->不能小于0
参数四:指定存活时间的单位(秒、分、时、天): unit		   --->时间单位
参数五:指定任务队列: workQueue						 --->不能为null
参数六:指定用哪个线程工厂创建线程: threadFactory		 --->不能为null
参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler	--->不能为null

线程池常见面试题

1、临时线程什么时候创建?

  • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

2、什么时候会开始拒绝任务?

  • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。

9.6.3、线程池处理Runnable任务

ThreadPoolExecutor创建线程池对象示例

ExecutorService pool = new ThreadPoolExecutor(3, 5 ,
                6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5) , Executors.defaultThreadFactory(),
               new ThreadPoolExecutor.AbortPolicy() );

ExecutorService的常用方法

方法名称 说明
void execute(Runnable command) 执行任务/命令,没有返回值,一般用来执行 Runnable 任务
Future submit(Callable task) 执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务
void shutdown() 等任务执行完毕后关闭线程池
List shutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

MyRunnable

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "输出了:HelloWorld ==> "  + i);
        }
        try {
            System.out.println(Thread.currentThread().getName() + "本任务与线程绑定了,线程进入休眠了~~~");
            Thread.sleep(10000000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ThreadPoolDemo

import java.util.concurrent.*;

/**
    目标:自定义一个线程池对象,并测试其特性。
 */
public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 1、创建线程池对象
        /**
         public ThreadPoolExecutor(int corePoolSize,
                                 int maximumPoolSize,
                                 long keepAliveTime,
                                 TimeUnit unit,
                                 BlockingQueue workQueue,
                                 ThreadFactory threadFactory,
                                 RejectedExecutionHandler handler)
         */
        ExecutorService pool = new ThreadPoolExecutor(3, 5 ,
                6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5) , Executors.defaultThreadFactory(),
               new ThreadPoolExecutor.AbortPolicy() );

        // 2、给任务线程池处理。
        Runnable target = new MyRunnable();
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);

        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);

        // 创建临时线程
        pool.execute(target);
        pool.execute(target);
//        // 不创建,拒绝策略被触发!!!
//        pool.execute(target);

        // 关闭线程池(开发中一般不会使用)。
        // pool.shutdownNow(); // 立即关闭,即使任务没有完成,会丢失任务的!
        pool.shutdown(); // 会等待全部任务执行完毕之后再关闭(建议使用的)
    }
}

9.6.4、线程池处理Callable任务

ExecutorService的常用方法

方法名称 说明
void execute(Runnable command) 执行任务/命令,没有返回值,一般用来执行 Runnable 任务
Future submit(Callable task) 执行Callable任务,返回未来任务对象获取线程结果
void shutdown() 等任务执行完毕后关闭线程池
List shutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

MyCallable

import java.util.concurrent.Callable;

/**
    1、定义一个任务类 实现Callable接口  应该申明线程任务执行完毕后的结果的数据类型
 */
public class MyCallable implements Callable<String>{
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    /**
       2、重写call方法(任务方法)
     */
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n ; i++) {
            sum += i;
        }
        return Thread.currentThread().getName()
                + "执行 1-" + n+ "的和,结果是:" + sum;
    }
}

ThreadPoolDemo

import java.util.concurrent.*;

/**
    目标:自定义一个线程池对象,并测试其特性。
 */
public class ThreadPoolDemo {
    public static void main(String[] args) throws Exception {
        // 1、创建线程池对象
        /**
         public ThreadPoolExecutor(int corePoolSize,
                                 int maximumPoolSize,
                                 long keepAliveTime,
                                 TimeUnit unit,
                                 BlockingQueue workQueue,
                                 ThreadFactory threadFactory,
                                 RejectedExecutionHandler handler)
         */
        ExecutorService pool = new ThreadPoolExecutor(3, 5 ,
                6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5) , Executors.defaultThreadFactory(),
               new ThreadPoolExecutor.AbortPolicy() );

        // 2、给任务线程池处理。
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));
        Future<String> f4 = pool.submit(new MyCallable(400));
        Future<String> f5 = pool.submit(new MyCallable(500));

//        String rs = f1.get();
//        System.out.println(rs);

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

输出结果

pool-1-thread-1执行 1-100的和,结果是:5050
pool-1-thread-2执行 1-200的和,结果是:20100
pool-1-thread-3执行 1-300的和,结果是:45150
pool-1-thread-1执行 1-400的和,结果是:80200
pool-1-thread-1执行 1-500的和,结果是:125250

9.6.5、新任务拒绝策略

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

9.6.6、Executors工具类实现线程池

Executors得到线程池对象的常用方法

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

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

测试代码

/**
    目标:使用Executors的工具方法直接得到一个线程池对象。
 */
public class ThreadPoolDemo3 {
    public static void main(String[] args) throws Exception {
        // 1、创建固定线程数据的线程池
        ExecutorService pool = Executors.newFixedThreadPool(3);

        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable()); // 已经没有多余线程了
    }
}

代码分析

Java-高级技术(二)_第6张图片
该方法不会创建临时线程

Executors使用可能存在的陷阱

  • 大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
方法名称 存在问题
public static ExecutorService newFixedThreadPool(int nThreads) 允许请求的任务队列长度是Integer.MAX_VALUE,可能出现OOM错误( java.lang.OutOfMemoryError )
public static ExecutorService newSingleThreadExecutor() 允许请求的任务队列长度是Integer.MAX_VALUE,可能出现OOM错误( java.lang.OutOfMemoryError )
public static ExecutorService newCachedThreadPool() 创建的线程数量最大上限是Integer.MAX_VALUE,线程数可能会随着任务1:1增长,也可能出现OOM错误( java.lang.OutOfMemoryError )
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建的线程数量最大上限是Integer.MAX_VALUE,线程数可能会随着任务1:1增长,也可能出现OOM错误( java.lang.OutOfMemoryError )

Java-高级技术(二)_第7张图片

总结

Executors是否适合做大型互联网场景的线程池方案?

  • 不合适。
  • 建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险。

9.7、定时器

  • 定时器是一种控制任务延时调用,或者周期调用的技术。
  • 作用:闹钟、定时邮件发送。

定时器的实现方式

  • 方式一:Timer
  • 方式二: ScheduledExecutorService

9.7.1、Timer定时器

构造器

构造器 说明
public Timer() 创建Timer定时器对象

方法

方法 说明
public void schedule(TimerTask task, long delay, long period) 开启一个定时器,按照计划处理TimerTask任务

代码演示

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
    目标:Timer定时器的使用和了解。
 */
public class TimerDemo1 {
    public static void main(String[] args) {
        // 1、创建Timer定时器
        Timer timer = new Timer();  // 定时器本身就是一个单线程。
        // 2、调用方法,处理定时任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行AAA~~~" + new Date());
//                try {
//                    Thread.sleep(5000);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
            }
        }, 0, 2000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行BB~~~"+ new Date());
                System.out.println(10/0);
            }
        }, 0, 2000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行CCC~~~"+ new Date());
            }
        }, 0, 3000);
    }
}

Timer定时器的特点和存在的问题

1、Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入。

try {
	Thread.sleep(5000);
} catch (InterruptedException e) {
	e.printStackTrace();
}

2、可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行。

System.out.println(10/0);

9.7.2、ScheduledExecutorService定时器

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

Executors的方法

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

ScheduledExecutorService的方法

ScheduledExecutorService的方法 说明
public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 周期调度方法

代码演示

import java.util.Date;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
    目标:Timer定时器的使用和了解。
 */
public class TimerDemo2 {
    public static void main(String[] args) {
        // 1、创建ScheduledExecutorService线程池,做定时器
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);

        // 2、开启定时任务
        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行输出:AAA  ==》 " + new Date());
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 0, 2, TimeUnit.SECONDS);


        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行输出:BBB  ==》 " + new Date());
                System.out.println(10 / 0);
            }
        }, 0, 2, TimeUnit.SECONDS);


        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行输出:CCC  ==》 " + new Date());
            }
        }, 0, 2, TimeUnit.SECONDS);

    }
}

输出结果

pool-1-thread-1执行输出:AAA  ==》 Wed Apr 13 00:31:17 CST 2022
pool-1-thread-3执行输出:CCC  ==》 Wed Apr 13 00:31:19 CST 2022
pool-1-thread-3执行输出:CCC  ==》 Wed Apr 13 00:31:21 CST 2022
pool-1-thread-3执行输出:CCC  ==》 Wed Apr 13 00:31:23 CST 2022
pool-1-thread-3执行输出:CCC  ==》 Wed Apr 13 00:31:25 CST 2022
pool-1-thread-3执行输出:CCC  ==》 Wed Apr 13 00:31:27 CST 2022
pool-1-thread-1执行输出:AAA  ==》 Wed Apr 13 00:31:27 CST 2022
pool-1-thread-3执行输出:CCC  ==》 Wed Apr 13 00:31:29 CST 2022
pool-1-thread-3执行输出:CCC  ==》 Wed Apr 13 00:31:31 CST 2022
pool-1-thread-3执行输出:CCC  ==》 Wed Apr 13 00:31:33 CST 2022
pool-1-thread-3执行输出:CCC  ==》 Wed Apr 13 00:31:35 CST 2022
pool-1-thread-3执行输出:CCC  ==》 Wed Apr 13 00:31:37 CST 2022
pool-1-thread-1执行输出:AAA  ==》 Wed Apr 13 00:31:37 CST 2022
pool-1-thread-3执行输出:CCC  ==》 Wed Apr 13 00:31:39 CST 2022
pool-1-thread-3执行输出:CCC  ==》 Wed Apr 13 00:31:41 CST 2022
pool-1-thread-3执行输出:CCC  ==》 Wed Apr 13 00:31:43 CST 2022
...

ScheduledExecutorService的优点

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

9.8、并发、并行

正在运行的程序(软件)就是一个独立的进程, 线程是属于进程的,多个线程其实是并发与并行同时进行的。

并发的理解

  • CPU同时处理线程的数量有限。
  • CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

Java-高级技术(二)_第8张图片

并行的理解

  • 在同一个时刻上,同时有多个线程在被CPU处理并执行。

Java-高级技术(二)_第9张图片

9.9、线程的生命周期

人的状态

Java-高级技术(二)_第10张图片

线程的状态

  • 线程的状态:也就是线程从生到死的过程,以及中间经历的各种状态及状态转换。
  • 理解线程的状态有利于提升并发编程的理解能力。

Java线程的状态

  • Java总共定义了6种状态
  • 6种状态都定义在Thread类的内部枚举类中。
public class Thread{
	...
	public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }
	...
}

线程的6种状态

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

线程的6种状态互相转换

Java-高级技术(二)_第11张图片

10、网络编程

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

网络通信基本模式

Java-高级技术(二)_第12张图片
Java-高级技术(二)_第13张图片

10.1、网络通信三要素

  • IP地址:设备在网络中的地址,是唯一的标识。
  • 端口:应用程序在设备中唯一的标识。
  • 协议: 数据在网络中传输的规则,常见的协议有UDP协议和TCP协议。

10.1.1、要素一:IP地址

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

IPv4

Java-高级技术(二)_第14张图片

IPv6

  • IPv6:128位(16个字节),号称可以为地球每一粒沙子编号。
  • IPv6分成8个整数,每个整数用四个十六进制位表示, 数之间用冒号(:)分开。

Java-高级技术(二)_第15张图片

IP地址基本寻路

Java-高级技术(二)_第16张图片

IP地址形式

  • 公网地址、和私有地址(局域网使用)。
  • 192.168. 开头的就是常见的局域网地址,范围即为192.168.0.0–192.168.255.255,专门为组织机构内部使用。

IP地址常用命令

  • ipconfig:查看本机IP地址
  • ping IP地址:检查网络是否连通

特殊IP地址

  • 本机IP: 127.0.0.1或者localhost:称为回送地址也可称本地回环地址,只会寻找当前所在本机。

10.1.2、IP地址操作类-InetAddress

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

InetAddress API

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

测试代码

import java.net.InetAddress;

/**
    目标:InetAddress类概述(了解)
         一个该类的对象就代表一个IP地址对象。

    InetAddress类成员方法:
         static InetAddress getLocalHost()
            * 获得本地主机IP地址对象。
         static InetAddress getByName(String host)
            * 根据IP地址字符串或主机名获得对应的IP地址对象。
         String getHostName()
            * 获得主机名。
         String getHostAddress()
            * 获得IP地址字符串。
 */
public class InetAddressDemo01 {
    public static void main(String[] args) throws Exception {
        // 1.获取本机地址对象。
        InetAddress ip1 = InetAddress.getLocalHost();
        System.out.println(ip1.getHostName());
        System.out.println(ip1.getHostAddress());

        // 2.获取域名ip对象
        InetAddress ip2 = InetAddress.getByName("www.baidu.com");
        System.out.println(ip2.getHostName());
        System.out.println(ip2.getHostAddress());

        // 3.获取公网IP对象。
        InetAddress ip3 = InetAddress.getByName("112.80.248.76");
        System.out.println(ip3.getHostName());
        System.out.println(ip3.getHostAddress());

        // 4.判断是否能通: ping  5s之前测试是否可通
        System.out.println(ip3.isReachable(5000));
    }
}

输出结果

MS-MJPPDXIVHSHA
192.168.31.151
www.baidu.com
14.215.177.39
112.80.248.76
112.80.248.76
true

10.1.3、要素二:端口号

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

端口类型

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

注意:我们自己开发的程序选择注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错。

10.1.4、要素三:协议

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

参考模型

Java-高级技术(二)_第17张图片

传输层的2个常见协议

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

TCP协议特点

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

TCP协议通信场景

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

TCP三次握手确立连接

Java-高级技术(二)_第18张图片
Java-高级技术(二)_第19张图片

TCP四次挥手断开连接

Java-高级技术(二)_第20张图片
Java-高级技术(二)_第21张图片

UDP协议

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

10.2、UDP通信

UDP协议的特点

  • UDP是一种无连接、不可靠传输的协议。
  • 将数据源IP、目的地IP和端口以及数据封装成数据包,大小限制在64KB内,直接发送出去即可。

10.2.1、UDP通信:快速入门

DatagramPacket:数据包对象

构造器

构造器 说明
public DatagramPacket(byte[] buf, int length, InetAddress address, int port) 创建发送端数据包对象buf:要发送的内容,字节数组length:要发送内容的字节长度address:接收端的IP地址对象port:接收端的端口号
public DatagramPacket(byte[] buf, int length) 创建接收端的数据包对象buf:用来存储接收的内容length:能够接收内容的长度

方法

方法 说明
public int getLength() 获得实际接收到的字节个数

DatagramSocket:发送端和接收端对象

构造器

构造器 说明
public DatagramSocket() 创建发送端的Socket对象,系统会随机分配一个端口号。
public DatagramSocket(int port) 创建接收端的Socket对象并指定端口号

方法

方法 说明
public void send(DatagramPacket dp) 发送数据包
public void receive(DatagramPacket p) 接收数据包

使用UDP通信实现:发送消息、接收消息

客户端实现步骤:

  • 创建DatagramSocket对象(发送端对象)
  • 创建DatagramPacket对象封装需要发送的数据(数据包对象)
  • 使用DatagramSocket对象的send方法传入DatagramPacket对象
  • 释放资源

接收端实现步骤:

  • 创建DatagramSocket对象并指定端口(接收端对象)
  • 创建DatagramPacket对象接收数据(数据包对象)
  • 使用DatagramSocket对象的receive方法传入DatagramPacket对象
  • 释放资源

发送端

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

/**
  发送端  一发 一收
 */
public class ClientDemo1 {
    public static void main(String[] args) throws Exception {
        System.out.println("=====客户端启动======");

        // 1、创建发送端对象:发送端自带默认的端口号(人)
        DatagramSocket socket = new DatagramSocket(6666);

        // 2、创建一个数据包对象封装数据(韭菜盘子)
        /**
         public DatagramPacket(byte buf[], int length,
         InetAddress address, int port)
         参数一:封装要发送的数据(韭菜)
         参数二:发送数据的大小
         参数三:服务端的主机IP地址
         参数四:服务端的端口
         */
        byte[] buffer = "我是一颗快乐的韭菜,你愿意吃吗?".getBytes();
        DatagramPacket packet = new DatagramPacket( buffer, buffer.length,
                InetAddress.getLocalHost() , 8888);

        // 3、发送数据出去
        socket.send(packet);

        socket.close();
    }
}

接收端

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

/**
  接收端
 */
public class ServerDemo2 {
    public static void main(String[] args) throws Exception {
        System.out.println("=====服务端启动======");
        // 1、创建接收端对象:注册端口(人)
        DatagramSocket socket = new DatagramSocket(8888);

        // 2、创建一个数据包对象接收数据(韭菜盘子)
        byte[] buffer = new byte[1024 * 64];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

        // 3、等待接收数据。
        socket.receive(packet);

        // 4、取出数据即可
        // 读取多少倒出多少
        int len = packet.getLength();
        String rs = new String(buffer,0, len);
        System.out.println("收到了:" + rs);

        // 获取发送端的ip和端口
        String ip  =packet.getSocketAddress().toString();
        System.out.println("对方地址:" + ip);

        int port  = packet.getPort();
        System.out.println("对方端口:" + port);

        socket.close();
    }
}

10.2.2、UDP通信:多发多收

使用UDP通信实现:多发多收消息

需求:

  • 使用UDP通信方式开发接收端和发送端。

分析:

  • 发送端可以一直发送消息。
  • 接收端可以不断的接收多个发送端的消息展示。
  • 发送端输入了exit则结束发送端程序。

客户端实现步骤:

  • 创建DatagramSocket对象(发送端对象)
  • 使用while死循环不断的接收用户的数据输入,如果用户输入的exit则退出程序
  • 如果用户输入的不是exit, 把数据封装成DatagramPacket
  • 使用DatagramSocket对象的send方法将数据包对象进行发送
  • 释放资源

接收端实现步骤:

  • 创建DatagramSocket对象并指定端口(接收端对象)
  • 创建DatagramPacket对象接收数据(数据包对象)
  • 使用while死循环不断的进行第4步
  • 使用DatagramSocket对象的receive方法传入DatagramPacket对象

代码演示

发送端:

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

/**
  发送端  多发 多收
 */
public class ClientDemo1 {
    public static void main(String[] args) throws Exception {
        System.out.println("=====客户端启动======");

        // 1、创建发送端对象:发送端自带默认的端口号(人)
        DatagramSocket socket = new DatagramSocket(7777);
        

        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.println("请说:");
            String msg = sc.nextLine();

            if("exit".equals(msg)){
                System.out.println("离线成功!");
                socket.close();
                break;
            }

            // 2、创建一个数据包对象封装数据(韭菜盘子)
            byte[] buffer = msg.getBytes();
            DatagramPacket packet = new DatagramPacket( buffer, buffer.length,
                    InetAddress.getLocalHost() , 8888);

            // 3、发送数据出去
            socket.send(packet);
        }

    }
}

接收端:

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

/**
  接收端
 */
public class ServerDemo2 {
    public static void main(String[] args) throws Exception {
        System.out.println("=====服务端启动======");
        // 1、创建接收端对象:注册端口(人)
        DatagramSocket socket = new DatagramSocket(8888);

        // 2、创建一个数据包对象接收数据(韭菜盘子)
        byte[] buffer = new byte[1024 * 64];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

        while (true) {
            // 3、等待接收数据。
            socket.receive(packet);
            // 4、取出数据即可
            // 读取多少倒出多少
            int len = packet.getLength();
            String rs = new String(buffer,0, len);
            System.out.println("收到了来自:" + packet.getAddress() +", 对方端口是" + packet.getPort() +"的消息:" + rs);
        }
    }
}

注意事项

UDP的接收端可以接收很多发送端的消息,因为接收端只负责接收数据包,无所谓是哪个发送端的数据包。

10.2.3、UDP通信:广播、组播

UDP的三种通信方式

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

广播

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

代码演示

// 发送端
DatagramPacket packet = new DatagramPacket( buffer, buffer.length, InetAddress.getByName("255.255.255.255") , 8888);

// 接收端
DatagramSocket socket = new DatagramSocket(8888);

UDP实现广播发送端是随机指定端口的

收到了来自:/192.168.31.151, 对方端口是51301的消息:1

组播

  • 使用组播地址:224.0.0.0 ~ 239.255.255.255
  • 具体操作:
    1. 发送端的数据包的目的地是组播IP (例如:224.0.1.1, 端口:9999)
    2. 接收端必须绑定该组播IP(224.0.1.1),端口还要注册发送端的目的端口9999 ,这样即可接收该组播消息。
    3. DatagramSocket的子类MulticastSocket可以在接收端绑定组播IP。

代码演示

// 发送端
DatagramPacket packet = new DatagramPacket( buffer, buffer.length, InetAddress.getByName("224.0.1.1") , 9898);

// 接收端
MulticastSocket socket = new MulticastSocket(9898);
// 绑定组播地址
socket.joinGroup(new InetSocketAddress(InetAddress.getByName("224.0.1.1") , 9898), etworkInterface.getByInetAddress(InetAddress.getLocalHost()));

UDP实现组播发送端是随机指定端口的

收到了来自:/192.168.31.151, 对方端口是58786的消息:1

10.3、TCP通信

10.3.1、TCP通信-快速入门

  • TCP是一种面向连接,安全、可靠的传输数据的协议
  • 传输前,采用“三次握手”方式,点对点通信,是可靠的
  • 在连接中可进行大数据量的传输

TCP通信模式

Java-高级技术(二)_第22张图片

Socket

构造器

构造器 说明
public Socket(String host , int port) 创建发送端的Socket对象与服务端连接,参数为服务端程序的ip和端口。

方法

方法 说明
OutputStream getOutputStream() 获得字节输出流对象
InputStream getInputStream() 获得字节输入流对象

客户端实现步骤

  1. 创建客户端的Socket对象,请求与服务端的连接。
  2. 使用socket对象调用getOutputStream()方法得到字节输出流。
  3. 使用字节输出流完成数据的发送。
  4. 释放资源:关闭socket管道。

代码演示

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

/**
   目标:完成Socket网络编程入门案例的客户端开发,实现1发1收。
 */
public class ClientDemo1 {
    public static void main(String[] args) {
        try {
            System.out.println("====客户端启动===");
            // 1、创建Socket通信管道请求有服务端的连接
            // public Socket(String host, int port)
            // 参数一:服务端的IP地址
            // 参数二:服务端的端口
            Socket socket = new Socket("127.0.0.1", 7777);

            // 2、从socket通信管道中得到一个字节输出流 负责发送数据
            OutputStream os = socket.getOutputStream();

            // 3、把低级的字节流包装成打印流
            PrintStream ps = new PrintStream(os);

            // 4、发送消息
            ps.println("我是TCP的客户端,我已经与你对接,并发出邀请:约吗?");
            ps.flush();

            // 关闭资源。
            // socket.close();

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

ServerSocket(服务端)

构造器

构造器 说明
public ServerSocket(int port) 注册服务端端口

方法

方法 说明
public Socket accept() 等待接收客户端的Socket通信连接连接成功返回Socket对象与客户端建立端到端通信

服务端实现步骤

  1. 创建ServerSocket对象,注册服务端端口。
  2. 调用ServerSocket对象的accept()方法,等待客户端的连接,并得到Socket管道对象。
  3. 通过Socket对象调用getInputStream()方法得到字节输入流、完成数据的接收。
  4. 释放资源:关闭socket管道

代码演示

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

/**
   目标:开发Socket网络编程入门代码的服务端,实现接收消息
 */
public class ServerDemo2 {
    public static void main(String[] args) {
        try {
            System.out.println("===服务端启动成功===");
            // 1、注册端口
            ServerSocket serverSocket = new ServerSocket(7777);
            // 2、必须调用accept方法:等待接收客户端的Socket连接请求,建立Socket通信管道
            Socket socket = serverSocket.accept();
            // 3、从socket通信管道中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 5、按照行读取消息
            String msg;
            if ((msg = br.readLine()) != null){
                System.out.println(socket.getRemoteSocketAddress() + "说了:: " + msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

TCP通信的基本原理

  • 客户端怎么发,服务端就应该怎么收。
  • 客户端如果没有消息,服务端会进入阻塞等待。
  • Socket一方关闭或者出现异常、对方Socket也会失效或者出错。

10.3.2、TCP通信-多发多收消息

使用TCP通信方式实现:多发多收消息

具体要求:

  • 可以使用死循环控制服务端收完消息继续等待接收下一个消息。
  • 客户端也可以使用死循环等待用户不断输入消息。
  • 客户端一旦输入了exit,则关闭客户端程序,并释放资源。

对客户端代码进行改进

while (true) {
    System.out.println("请说:");
    String msg = sc.nextLine();
    // 4、发送消息
    ps.println(msg);
    if (msg.equals("exit")){
        return;
    }
    ps.flush();
}

对服务端代码进行改进

String msg;
while ((msg = br.readLine()) != null) {
    System.out.println(socket.getRemoteSocketAddress() + "说了:: " + msg);
}

总结

1、本次多发多收是如何实现的

  • 客户端使用循环反复地发送消息。
  • 服务端使用循环反复地接收消息。

2、现在服务端为什么不可以同时接收多个客户端的消息。

  • 目前服务端是单线程的,每次只能处理一个客户端的消息。

10.3.3、TCP通信-同时接受多个客户端消息

1、之前我们的通信是否可以同时与多个客户端通信,为什么?

  • 不可以的
  • 单线程每次只能处理一个客户端的Socket通信

2、如何才可以让服务端可以处理多个客户端的通信需求?

  • 引入多线程。

Java-高级技术(二)_第23张图片

添加线程类

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

public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 3、从socket通信管道中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 5、按照行读取消息
            String msg;
            while ((msg = br.readLine()) != null){
                System.out.println(socket.getRemoteSocketAddress() + "说了:: " + msg);
            }
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
        }
    }
}

对服务端代码进行改进

while (true) {
    // 2、每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取消息
    Socket socket = serverSocket.accept();
    System.out.println(socket.getRemoteSocketAddress()+ "它来了,上线了!");
    // 3、开始创建独立线程处理socket
    new ServerReaderThread(socket).start();
}

总结

目前的通信架构存在什么问题?

  • 客户端与服务端的线程模型是: N-N的关系。
  • 客户端并发越多,系统瘫痪的越快。

10.3.4、TCP通信-使用线程池优化

Java-高级技术(二)_第24张图片

ServerReaderRunnable

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

public class ServerReaderRunnable implements Runnable{
    private Socket socket;
    public ServerReaderRunnable(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 3、从socket通信管道中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 5、按照行读取消息
            String msg;
            while ((msg = br.readLine()) != null){
                System.out.println(socket.getRemoteSocketAddress() + "说了:: " + msg);
            }
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
        }
    }
}

服务端

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

/**
   目标:实现服务端可以同时处理多个客户端的消息。
 */
public class ServerDemo2 {

    // 使用静态变量记住一个线程池对象
    private static ExecutorService pool = new ThreadPoolExecutor(300,
            1500, 6, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(2)
    , Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        try {
            System.out.println("===服务端启动成功===");
            // 1、注册端口
            ServerSocket serverSocket = new ServerSocket(6666);
            // a.定义一个死循环由主线程负责不断的接收客户端的Socket管道连接。
            while (true) {
                // 2、每接收到一个客户端的Socket管道,
                Socket socket = serverSocket.accept();
                System.out.println(socket.getRemoteSocketAddress()+ "它来了,上线了!");

                // 任务对象负责读取消息。
                Runnable target = new ServerReaderRunnable(socket);
                pool.execute(target);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

总结

本次使用线程池的优势在哪里?

  • 服务端可以复用线程处理多个客户端,可以避免系统瘫痪。
  • 适合客户端通信时长较短的场景。

10.3.5、TCP通信实战案例-即时通信

即时通信是什么含义,要实现怎么样的设计?

  • 即时通信,是指一个客户端的消息发出去,其他客户端可以接收到。
  • 之前我们的消息都是发给服务端的。
  • 即时通信需要进行端口转发的设计思想。

即时通信-端口转发

Java-高级技术(二)_第25张图片

ClientReaderThread

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

public class ClientReaderThread extends Thread{
    private Socket socket;
    public ClientReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 3、从socket通信管道中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 5、按照行读取消息
            String msg;
            while ((msg = br.readLine()) != null){
                System.out.println(socket.getRemoteSocketAddress() + "收到了: " + msg);
            }
        } catch (Exception e) {
            System.out.println("服务端把你踢出去了~~");
        }
    }

}

Client 端测试类

/**
    拓展:即时通信

    客户端:发消息的同时,随时有人发消息过来。
    服务端:接收消息后,推送给其他所有的在线socket
 */
public class ClientDemo {
    public static void main(String[] args) {
        try {
            System.out.println("====客户端启动===");
            // 1、创建Socket通信管道请求有服务端的连接
            // public Socket(String host, int port)
            // 参数一:服务端的IP地址
            // 参数二:服务端的端口
            Socket socket = new Socket("127.0.0.1", 6868);

            // 马上为客户端分配一个独立的线程负责读取它收到的消息
            new ClientReaderThread(socket).start();

            // 2、从socket通信管道中得到一个字节输出流 负责发送数据
            OutputStream os = socket.getOutputStream();

            // 3、把低级的字节流包装成打印流
            PrintStream ps = new PrintStream(os);

            Scanner sc =  new Scanner(System.in);
            while (true) {
                System.out.println("请说:");
                String msg = sc.nextLine();
                // 4、发送消息
                ps.println(msg);
                ps.flush();
            }
            // 关闭资源。
            // socket.close();

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

ServerReaderThread

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

public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 3、从socket通信管道中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 5、按照行读取消息
            String msg;
            while ((msg = br.readLine()) != null){
                System.out.println(socket.getRemoteSocketAddress() + "说了:: " + msg);
                // 把这个消息发给当前所有在线socket
                sendMsgToAll(msg);
            }
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
            // 从在线集合中抹掉本客户端socket
            ServerDemo2.onLineSockets.remove(socket);
        }
    }

    private void sendMsgToAll(String msg) {
        try {
            // 遍历全部的在线 socket给他们发消息
            for (Socket onLineSocket : ServerDemo2.onLineSockets) {
                // 除了自己的socket,其他socket我都发!!
                if(onLineSocket != socket){
                    PrintStream ps = new PrintStream(socket.getOutputStream());
                    ps.println(msg);
                    ps.flush();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Server 端测试类

import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

/**
   目标: 即时通信
 */
public class ServerDemo {

    public static List<Socket> onLineSockets = new ArrayList<>();

    public static void main(String[] args) {
        try {
            System.out.println("===服务端启动成功===");
            // 1、注册端口
            ServerSocket serverSocket = new ServerSocket(6868);
            // a.定义一个死循环由主线程负责不断的接收客户端的Socket管道连接。
            while (true) {
                // 2、每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取消息
                Socket socket = serverSocket.accept();
                System.out.println(socket.getRemoteSocketAddress()+ "它来了,上线了!");
                // 把当前客户端管道Socket加入到在线集合中去
                onLineSockets.add(socket);

                // 3、开始创建独立线程处理socket
                new ServerReaderThread(socket).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

10.3.6、TCP通信实战案例-模拟BS系统

1、之前的客户端都是什么样的?

  • 其实就是CS架构,客户端实需要我们自己开发实现的。

2、BS结构是什么样的,需要开发客户端吗?

  • 浏览器访问服务端,不需要开发客户端。

实现BS开发

Java-高级技术(二)_第26张图片

HTTP响应数据的协议格式

Java-高级技术(二)_第27张图片

ServerReaderRunnable

import java.io.PrintStream;
import java.net.Socket;

public class ServerReaderRunnable implements Runnable{
    private Socket socket;
    public ServerReaderRunnable(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 浏览器 已经与本线程建立了Socket管道
            // 响应消息给浏览器显示
            PrintStream ps = new PrintStream(socket.getOutputStream());
            // 必须响应HTTP协议格式数据,否则浏览器不认识消息
            ps.println("HTTP/1.1 200 OK"); // 协议类型和版本 响应成功的消息!
            ps.println("Content-Type:text/html;charset=UTF-8"); // 响应的数据类型:文本/网页

            ps.println(); // 必须发送一个空行

            // 才可以响应数据回去给浏览器
            ps.println("《少女的祈祷》 ");
            ps.close();
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
        }
    }
}

测试类

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

/**
    了解:BS-浏览器-服务器基本了解。

    引入:
        之前客户端和服务端都需要自己开发。也就是CS架构。
        接下来模拟一下BS架构。

    客户端:浏览器。(无需开发)
    服务端:自己开发。
    需求:在浏览器中请求本程序,响应一个网页文字给浏览器显示


 */
public class BSserverDemo {
    // 使用静态变量记住一个线程池对象
    private static ExecutorService pool = new ThreadPoolExecutor(3,
            5, 6, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(2)
            , Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        try {
            // 1.注册端口
            ServerSocket ss = new ServerSocket(8080);
            // 2.创建一个循环接收多个客户端的请求。
            while(true){
                Socket socket = ss.accept();
                // 3.交给一个独立的线程来处理!
                pool.execute(new ServerReaderRunnable(socket));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

测试结果

Java-高级技术(二)_第28张图片

11、单元测试

11.1、单元测试概述

单元测试

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

目前测试方法是怎么进行的,存在什么问题?

  • 只有一个main方法,如果一个方法的测试失败了,其他方法测试会受到影响。
  • 无法得到测试的结果报告,需要程序员自己去观察测试是否成功。
  • 无法实现自动化测试。

Junit单元测试框架

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

JUnit优点

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

11.2、单元测试快速入门

使用单元测试进行业务方法预期结果、正确性测试的快速入门

  • 将JUnit的jar包导入到项目中

    • IDEA通常整合好了Junit框架,一般不需要导入。
    • 如果IDEA没有整合好,需要自己手工导入如下2个JUnit的jar包到模块

在这里插入图片描述

  • 编写测试方法:该测试方法必须是公共的无参数无返回值的非静态方法。

  • 在测试方法上使用@Test注解:标注该方法是一个测试方法

  • 在测试方法中完成被测试方法的预期正确性测试。

  • 选中测试方法,选择“JUnit运行” ,如果测试良好则是绿色;如果测试失败,则是红色

业务方法

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

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

测试方法

/**
    测试方法
    注意点:
    	1、必须是公开的,无参数 无返回值的方法
		2、测试方法必须使用@Test注解标记。
*/
@Test
public void testLoginName(){
    UserService userService = new UserService();
    String rs = userService.loginName("admin","123456");

    // 进行预期结果的正确性测试:断言。
    Assert.assertEquals("您的登录业务可能出现问题", "登录成功", rs );
}

@Test
public void testSelectNames(){
    UserService userService = new UserService();
    userService.selectNames();
}

11.3、单元测试常用注解

Junit常用注解 (Junit 4.xx版本)

注解 说明
@Test 测试方法
@Before 用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次。
@After 用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次。
@BeforeClass 用来静态修饰方法,该方法会在所有测试方法之前只执行一次。
@AfterClass 用来静态修饰方法,该方法会在所有测试方法之后只执行一次。
  • 开始执行的方法:初始化资源。
  • 执行完之后的方法:释放资源。

Junit常用注解 (Junit 5.xx版本)

注解 说明
@Test 测试方法
@BeforeEach 用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次。
@AfterEach 用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次。
@BeforeAll 用来静态修饰方法,该方法会在所有测试方法之前只执行一次。
@AfterAll 用来静态修饰方法,该方法会在所有测试方法之后只执行一次。
  • 开始执行的方法:初始化资源。
  • 执行完之后的方法:释放资源。

测试方法

import org.junit.*;

/**
   测试类
 */
public class TestUserService {

    // 修饰实例方法的
    @Before
    public void before(){
        System.out.println("===before方法执行一次===");
    }

    @After
    public void after(){
        System.out.println("===after方法执行一次===");
    }

    // 修饰静态方法
    @BeforeClass
    public static void beforeClass(){
        System.out.println("===beforeClass方法执行一次===");
    }

    @AfterClass
    public static void afterClass(){
        System.out.println("===afterClass方法执行一次===");
    }


    /**
       测试方法
       注意点:
            1、必须是公开的,无参数 无返回值的方法
            2、测试方法必须使用@Test注解标记。
     */
    @Test
    public void testLoginName(){
        UserService userService = new UserService();
        String rs = userService.loginName("admin","123456");

        // 进行预期结果的正确性测试:断言。
        Assert.assertEquals("您的登录业务可能出现问题", "登录成功", rs );
    }

    @Test
    public void testSelectNames(){
        UserService userService = new UserService();
        userService.selectNames();
    }

}

输出结果

===beforeClass方法执行一次===
===before方法执行一次===
===after方法执行一次===
===before方法执行一次===
5
查询全部用户名称成功~~
===after方法执行一次===
===afterClass方法执行一次===

12、反射

12.1、反射概述

反射概述

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

反射的关键

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

HelloWorld.java -> javac -> HelloWorld.class
Class c = HelloWorld.class;

12.2、反射获取类对象

Java-高级技术(二)_第29张图片

获取Class类的对象的三种方式

  • 方式一:Class c1 = Class.forName(“全类名”)
  • 方式二:Class c2 = 类名.class
  • 方式三:Class c3 = 对象.getClass()

Student 类

package com.itheima.d2_reflect_class;

public class Student {
}

测试类

/**
   目标:反射的第一步:获取Class对象
 */
public class Test {
    public static void main(String[] args) throws Exception {
        // 1、Class类中的一个静态方法:forName(全限名:包名 + 类名)
        Class c = Class.forName("com.itheima.d2_reflect_class.Student");
        System.out.println(c); // Student.class

        // 2、类名.class
        Class c1 = Student.class;
        System.out.println(c1);

        // 3、对象.getClass() 获取对象对应类的Class对象。
        Student s = new Student();
        Class c2 = s.getClass();
        System.out.println(c2);
    }
}

输出结果

class com.itheima.d2_reflect_class.Student
class com.itheima.d2_reflect_class.Student
class com.itheima.d2_reflect_class.Student

12.3、反射获取构造器对象

Java-高级技术(二)_第30张图片

使用反射技术获取构造器对象并使用

  • 反射的第一步是先得到类对象,然后从类对象中获取类的成分对象。
  • Class类中用于获取构造器的方法
方法 说明
Constructor[] getConstructors() 返回所有构造器对象的数组(只能拿public的)
Constructor[] getDeclaredConstructors() 返回所有构造器对象的数组,存在就能拿到
Constructor getConstructor(Class… parameterTypes) 返回单个构造器对象(只能拿public的)
Constructor getDeclaredConstructor(Class… parameterTypes) 返回单个构造器对象,存在就能拿到
  • 获取构造器的作用依然是初始化一个对象返回。

Constructor类中用于创建对象的方法

符号 说明
T newInstance(Object… initargs) 根据指定的构造器创建对象
public void setAccessible(boolean flag) 设置为true, 表示取消访问检查,进行暴力反射

Student 类

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

    private Student(){
        System.out.println("无参数构造器执行!");
    }

    public Student(String name, int age) {
        System.out.println("有参数构造器执行!");
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

测试代码

import org.junit.Test;

import java.lang.reflect.Constructor;

/**
    目标:反射_获取Constructor构造器对象.

    反射的第一步是先得到Class类对象。(Class文件)

    反射中Class类型获取构造器提供了很多的API:
         1. Constructor getConstructor(Class... parameterTypes)
            根据参数匹配获取某个构造器,只能拿public修饰的构造器,几乎不用!
         2. Constructor getDeclaredConstructor(Class... parameterTypes)
            根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符,建议使用!
         3. Constructor[] getConstructors()
            获取所有的构造器,只能拿public修饰的构造器。几乎不用!!太弱了!
         4. Constructor[] getDeclaredConstructors()
            获取所有申明的构造器,只要你写我就能拿到,无所谓权限。建议使用!!
    小结:
        获取类的全部构造器对象: Constructor[] getDeclaredConstructors()
            -- 获取所有申明的构造器,只要你写我就能拿到,无所谓权限。建议使用!!
        获取类的某个构造器对象:Constructor getDeclaredConstructor(Class... parameterTypes)
            -- 根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符,建议使用!

 */
public class TestStudent01 {
    // 1. getConstructors:
    // 获取全部的构造器:只能获取public修饰的构造器。
    // Constructor[] getConstructors()
    @Test
    public void getConstructors(){
        // a.第一步:获取类对象
        Class c = Student.class;
        // b.提取类中的全部的构造器对象(这里只能拿public修饰)
        Constructor[] constructors = c.getConstructors();
        // c.遍历构造器
        for (Constructor constructor : constructors) {
            System.out.println(constructor.getName() + "===>" + constructor.getParameterCount());
        }
    }


    // 2.getDeclaredConstructors():
    // 获取全部的构造器:只要你敢写,这里就能拿到,无所谓权限是否可及。
    @Test
    public void getDeclaredConstructors(){
        // a.第一步:获取类对象
        Class c = Student.class;
        // b.提取类中的全部的构造器对象
        Constructor[] constructors = c.getDeclaredConstructors();
        // c.遍历构造器
        for (Constructor constructor : constructors) {
            System.out.println(constructor.getName() + "===>" + constructor.getParameterCount());
        }
    }

    // 3.getConstructor(Class... parameterTypes)
    // 获取某个构造器:只能拿public修饰的某个构造器
    @Test
    public void getConstructor() throws Exception {
        // a.第一步:获取类对象
        Class c = Student.class;
        // b.定位单个构造器对象 (按照参数定位无参数构造器 只能拿public修饰的某个构造器)
        Constructor cons = c.getConstructor();
        System.out.println(cons.getName() + "===>" + cons.getParameterCount());
    }


    // 4.getConstructor(Class... parameterTypes)
    // 获取某个构造器:只要你敢写,这里就能拿到,无所谓权限是否可及。
    @Test
    public void getDeclaredConstructor() throws Exception {
        // a.第一步:获取类对象
        Class c = Student.class;
        // b.定位单个构造器对象 (按照参数定位无参数构造器)
        Constructor cons = c.getDeclaredConstructor();
        System.out.println(cons.getName() + "===>" + cons.getParameterCount());

        // c.定位某个有参构造器
        Constructor cons1 = c.getDeclaredConstructor(String.class, int.class);
        System.out.println(cons1.getName() + "===>" + cons1.getParameterCount());

    }

}

调用构造器得到一个类的对象

测试代码

import org.junit.Test;

import java.lang.reflect.Constructor;

/**
    目标: 反射_获取Constructor构造器然后通过这个构造器初始化对象。

    反射获取Class中的构造器对象Constructor作用:
            也是初始化并得到类的一个对象返回。

    Constructor的API:
         1. T newInstance(Object... initargs)
                创建对象,注入构造器需要的数据。
         2. void setAccessible(true)
                修改访问权限,true代表暴力攻破权限,false表示保留不可访问权限(暴力反射)
    小结:
        可以通过定位类的构造器对象。
        如果构造器对象没有访问权限可以通过:void setAccessible(true)打开权限
        构造器可以通过T newInstance(Object... initargs)调用自己,传入参数!
 */
public class TestStudent02 {
    // 1.调用构造器得到一个类的对象返回。
    @Test
    public void getDeclaredConstructor() throws Exception {
        // a.第一步:获取类对象
        Class c = Student.class;
        // b.定位单个构造器对象 (按照参数定位无参数构造器)
        Constructor cons = c.getDeclaredConstructor();
        System.out.println(cons.getName() + "===>" + cons.getParameterCount());

        // 如果遇到了私有的构造器,可以暴力反射
        cons.setAccessible(true); // 权限被打开

        Student s = (Student) cons.newInstance();
        System.out.println(s);

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

        // c.定位某个有参构造器
        Constructor cons1 = c.getDeclaredConstructor(String.class, int.class);
        System.out.println(cons1.getName() + "===>" + cons1.getParameterCount());

        Student s1 = (Student) cons1.newInstance("孙悟空", 1000);
        System.out.println(s1);
    }


}

输出结果

com.itheima.d3_reflect_constructor.Student===>0
无参数构造器执行!
Student{name='null', age=0}
-------------------
com.itheima.d3_reflect_constructor.Student===>2
有参数构造器执行!
Student{name='孙悟空', age=1000}

反射得到的构造器可以做什么?

  • 依然是创建对象的
    • public newInstance(Object… initargs)
  • 如果是非public的构造器,需要打开权限(暴力反射),然后再创建对象
    • setAccessible(boolean)
    • 反射可以破坏封装性,私有的也可以执行了。

12.4、反射获取成员变量对象

Java-高级技术(二)_第31张图片

使用反射技术获取成员变量对象并使用

  • 反射的第一步是先得到类对象,然后从类对象中获取类的成分对象。
  • Class类中用于获取成员变量的方法
方法 说明
Field[] getFields() 返回所有成员变量对象的数组(只能拿public的)
Field[] getDeclaredFields() 返回所有成员变量对象的数组,存在就能拿到
Field getField(String name) 返回单个成员变量对象(只能拿public的)
Field getDeclaredField(String name) 返回单个成员变量对象,存在就能拿到
  • 获取成员变量的作用依然是在某个对象中取值、赋值

Field类中用于取值、赋值的方法

符号 说明
void set(Object obj, Object value): 赋值
Object get(Object obj) 获取值。

代码演示

Student类

public class Student {
    private String name;
    private int age;
    public static String schoolName;
    public static final String  COUNTTRY = "中国";

    public Student(){
        System.out.println("无参数构造器执行!");
    }

    public Student(String name, int age) {
        System.out.println("有参数构造器执行!");
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

测试类

import org.junit.Test;

import java.lang.reflect.Field;

/**
     目标:反射_获取Field成员变量对象。

     反射的第一步是先得到Class类对象。

     1、Field getField(String name);
            根据成员变量名获得对应Field对象,只能获得public修饰
     2.Field getDeclaredField(String name);
            根据成员变量名获得对应Field对象,只要申明了就可以得到
     3.Field[] getFields();
            获得所有的成员变量对应的Field对象,只能获得public的
     4.Field[] getDeclaredFields();
            获得所有的成员变量对应的Field对象,只要申明了就可以得到
     小结:
        获取全部成员变量:getDeclaredFields
        获取某个成员变量:getDeclaredField
 */
public class FieldDemo01 {
    /**
     * 1.获取全部的成员变量。
     * Field[] getDeclaredFields();
     *  获得所有的成员变量对应的Field对象,只要申明了就可以得到
     */
    @Test
    public void getDeclaredFields(){
        // a.定位Class对象
        Class c = Student.class;
        // b.定位全部成员变量
        Field[] fields = c.getDeclaredFields();
        // c.遍历一下
        for (Field field : fields) {
            System.out.println(field.getName() + "==>" + field.getType());
        }
    }

    /**
        2.获取某个成员变量对象 Field getDeclaredField(String name);
     */
    @Test
    public void getDeclaredField() throws Exception {
        // a.定位Class对象
        Class c = Student.class;
        // b.根据名称定位某个成员变量
        Field f = c.getDeclaredField("age");
        System.out.println(f.getName() +"===>" + f.getType());
    }

}

取值、赋值

import org.junit.Test;

import java.lang.reflect.Field;

/**
    目标:反射获取成员变量: 取值和赋值。

    Field的方法:给成员变量赋值和取值
        void set(Object obj, Object value):给对象注入某个成员变量数据
        Object get(Object obj):获取对象的成员变量的值。
        void setAccessible(true);暴力反射,设置为可以直接访问私有类型的属性。
        Class getType(); 获取属性的类型,返回Class对象。
        String getName(); 获取属性的名称。
 */
public class FieldDemo02 {
    @Test
    public void setField() throws Exception {
        // a.反射第一步,获取类对象
        Class c = Student.class;
        // b.提取某个成员变量
        Field ageF = c.getDeclaredField("age");

        ageF.setAccessible(true); // 暴力打开权限

        // c.赋值
        Student s = new Student();
        ageF.set(s , 18);  // s.setAge(18);
        System.out.println(s);

        // d、取值
        int age = (int) ageF.get(s);
        System.out.println(age);

    }
}

取值、赋值输出结果

无参数构造器执行!
Student{name='null', age=18}
18

12.5、反射获取方法对象

Java-高级技术(二)_第32张图片

使用反射技术获取方法对象并使用

  • 反射的第一步是先得到类对象,然后从类对象中获取类的成分对象。
  • Class类中用于获取成员方法的方法
方法 说明
Method[] getMethods() 返回所有成员方法对象的数组(只能拿public的)
Method[] getDeclaredMethods() 返回所有成员方法对象的数组,存在就能拿到
Method getMethod(String name, Class… parameterTypes) 返回单个成员方法对象(只能拿public的)
Method getDeclaredMethod(String name, Class… parameterTypes) 返回单个成员方法对象,存在就能拿到
  • 获取成员方法的作用依然是在某个对象中进行执行此方法

Method类中用于触发执行的方法

Java-高级技术(二)_第33张图片

测试代码

Dog 类

public class Dog {
    private String name ;
    public Dog(){
    }

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

    public void run(){
        System.out.println("狗跑的贼快~~");
    }

    private void eat(){
        System.out.println("狗吃骨头");
    }

    private String eat(String name){
        System.out.println("狗吃" + name);
        return "吃的很开心!";
    }

    public static void inAddr(){
        System.out.println("在黑马学习Java!");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

获得方法

/**
	* 1.获得类中的所有成员方法对象
*/
@Test
public void getDeclaredMethods(){
    // a.获取类对象
    Class c = Dog.class;
    // b.提取全部方法;包括私有的
    Method[] methods = c.getDeclaredMethods();
    // c.遍历全部方法
    for (Method method : methods) {
        System.out.println(method.getName() +" 返回值类型:" + method.getReturnType() + " 参数个数:" + method.getParameterCount());
    }
}

输出结果

getName 返回值类型:class java.lang.String 参数个数:0
run 返回值类型:void 参数个数:0
setName 返回值类型:void 参数个数:1
eat 返回值类型:void 参数个数:0
eat 返回值类型:class java.lang.String 参数个数:1
inAddr 返回值类型:void 参数个数:0

使用方法

/**
	* 2. 获取某个方法对象
*/
@Test
public void getDeclardMethod() throws Exception {
    // a.获取类对象
    Class c = Dog.class;
    // b.提取单个方法对象
    Method m = c.getDeclaredMethod("eat");
    Method m2 = c.getDeclaredMethod("eat", String.class);
    Method m3 = c.getDeclaredMethod("inAddr");

    // 暴力打开权限了
    m.setAccessible(true);
    m2.setAccessible(true);
    m3.setAccessible(true);

    // c.触发方法的执行
    Dog d = new Dog();
    // 注意:方法如果是没有结果回来的,那么返回的是null.
    Object result = m.invoke(d);
    System.out.println(result);

    Object result2 = m2.invoke(d, "骨头");
    System.out.println(result2);

    Object result3 = m3.invoke(d);
    System.out.println(result3);
}

输出结果

狗吃骨头
null
狗吃骨头
吃的很开心!
在黑马学习Java!
null

静态方法也是可以通过反射调用的

Method m3 = c.getDeclaredMethod("inAddr");

12.6、反射的作用-绕过编译阶段为集合添加数据

  • 反射是作用在运行时的技术,此时集合的泛型将不能产生约束了,此时是可以为集合存入其他任意类型的元素
  • 泛型只是在编译阶段可以约束集合只能操作某种数据类型,在编译成Class文件进入运行阶段的时候,其真实类型都是ArrayList了,泛型相当于被擦除了。

代码演示

测试代码

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

public class ReflectDemo {
    public static void main(String[] args) throws Exception {
        // 需求:反射实现泛型擦除后,加入其他类型的元素
        ArrayList<String> lists1 = new ArrayList<>();
        ArrayList<Integer> lists2 = new ArrayList<>();

        System.out.println(lists1.getClass());
        System.out.println(lists2.getClass());

        System.out.println(lists1.getClass() ==  lists2.getClass());  // ArrayList.class

        System.out.println("---------------------------");
        ArrayList<Integer> lists3 = new ArrayList<>();
        lists3.add(23);
        lists3.add(22);
        // lists3.add("黑马");

        Class c = lists3.getClass(); // ArrayList.class  ===> public boolean add(E e)
        // 定位c类中的add方法
        Method add = c.getDeclaredMethod("add", Object.class);
        boolean rs = (boolean) add.invoke(lists3, "黑马");
        System.out.println(rs);

        System.out.println(lists3);

        // 其实也可以不使用反射
        ArrayList list4 = lists3;
        list4.add("白马");
        list4.add(false);
        System.out.println(lists3);
    }
}

输出结果

class java.util.ArrayList
class java.util.ArrayList
true
---------------------------
true
[23, 22, 黑马]
[23, 22, 黑马, 白马, false]

其实不适用反射也可以

// 其实也可以不使用反射
ArrayList list4 = lists3;
list4.add("白马");
list4.add(false);
System.out.println(lists3);

12.7、反射的作用-通用框架的底层原理

反射做通用框架

需求:

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

分析:

  • 定义一个方法,可以接收任意类的对象。
  • 每次收到一个对象后,需要解析这个对象的全部成员变量名称。
  • 这个对象可能是任意的,那么怎么样才可以知道这个对象的全部成员变量名称呢?
  • 使用反射获取对象的Class类对象,然后获取全部成员变量信息。
  • 遍历成员变量信息,然后提取本成员变量在对象中的具体值
  • 存入成员变量名称和值到文件中去即可。

代码演示

学生类

public class Student {
    private String name;
    private char sex;
    private int age;
    private String className;
    private String hobby;

    public Student(){

    }

    public Student(String name, char sex, int age, String className, String hobby) {
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.className = className;
        this.hobby = hobby;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public char getSex() {
        return sex;
    }

    public void setSex(char sex) {
        this.sex = sex;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public String getHobby() {
        return hobby;
    }

    public void setHobby(String hobby) {
        this.hobby = hobby;
    }
}

老师类

public class Teacher {
    private String name;
    private char sex;
    private double salary;

    public Teacher(){

    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public char getSex() {
        return sex;
    }

    public void setSex(char sex) {
        this.sex = sex;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }
}

工具

public class MybatisUtil {
    /**
     保存任意类型的对象
     * @param obj
     */
    public static void save(Object obj){
        try (
                PrintStream ps = new PrintStream(new FileOutputStream("junit-reflect-annotation-proxy-app/src/data.txt", true));
        ){
            // 1、提取这个对象的全部成员变量:只有反射可以解决
            Class c = obj.getClass();  //   c.getSimpleName()获取当前类名   c.getName获取全限名:包名+类名
            ps.println("================" + c.getSimpleName() + "================");

            // 2、提取它的全部成员变量
            Field[] fields = c.getDeclaredFields();
            // 3、获取成员变量的信息
            for (Field field : fields) {
                String name = field.getName();
                // 提取本成员变量在obj对象中的值(取值)
                field.setAccessible(true);
                String value = field.get(obj) + "";
                ps.println(name  + "=" + value);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

测试类

/**
   目标:提供一个通用框架,支持保存所有对象的具体信息。
 */
public class ReflectDemo {
    public static void main(String[] args) throws Exception {
        Student s = new Student();
        s.setName("猪八戒");
        s.setClassName("西天跑路1班");
        s.setAge(1000);
        s.setHobby("吃,睡");
        s.setSex('男');
        MybatisUtil.save(s);

        Teacher t = new Teacher();
        t.setName("波仔");
        t.setSex('男');
        t.setSalary(6000);
        MybatisUtil.save(t);
    }
}

data.txt文件信息

================Student================
name=猪八戒
sex=男
age=1000
className=西天跑路1班
hobby=吃,睡
================Teacher================
name=波仔
sex=男
salary=6000.0

12.8、反射的作用

  • 可以在运行时得到一个类的全部成分然后操作。
  • 可以破坏封装性。(很突出)
  • 也可以破坏泛型的约束性。(很突出)
  • 更重要的用途是适合:做Java高级框架
  • 基本上主流框架都会基于反射设计一些通用技术功能。

13、注解

13.1、注解概述

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

注解的作用

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

13.2、自定义注解

格式

public @interface MyBook {
    String name();
    String[] authors();
    double price();
}

特殊属性

  • value属性,如果只有一个value属性的情况下,使用value属性的时候可以省略value名称不写。
  • 但是如果有多个属性, 且多个属性没有默认值,那么value名称是不能省略的。
public @interface Book {
    String value(); // 特殊属性
    double price() ;
    //double price() default 9.9;
}

//@Book("/delete")
@Book(value = "/delete", price = 23.5)

13.3、元注解

  • 注解注解的注解。

  • @Target: 约束自定义注解只能在哪些地方使用

  • @Target中可使用的值定义在ElementType枚举类中,常用值如下

    • TYPE,类,接口
    • FIELD, 成员变量
    • METHOD, 成员方法
    • PARAMETER, 方法参数
    • CONSTRUCTOR, 构造器
    • LOCAL_VARIABLE, 局部变量
  • @Retention:申明注解的生命周期

  • @Retention中可使用的值定义在RetentionPolicy枚举类中,常用值如下

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

代码演示

@Target({ElementType.METHOD,ElementType.FIELD}) // 元注解
@Retention(RetentionPolicy.RUNTIME) // 一直活着,在运行阶段这个注解也不消失
public @interface MyTest {
}

13.4、注解解析

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

注解解析相关接口

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

解析注解的技巧

  • 注解在哪个成分上,我们就先拿哪个成分对象。
  • 比如注解作用成员方法,则要获得该成员方法对应的Method对象,再来拿上面的注解
  • 比如注解作用在类上,则要该类的Class对象,再来拿上面的注解
  • 比如注解作用在成员变量上,则要获得该成员变量对应的Field对象,再来拿上面的注解

代码演示

Bookk注解

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

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

测试类

import org.junit.Test;

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

/**
   目标:完成注解的解析
 */
public class AnnotationDemo3 {
    @Test
    public void parseClass(){
        // a.先得到类对象
        Class c = BookStore.class;
        // b.判断这个类上面是否存在这个注解
        if(c.isAnnotationPresent(Bookk.class)){
            //c.直接获取该注解对象
            Bookk book = (Bookk) c.getDeclaredAnnotation(Bookk.class);
            System.out.println(book.value());
            System.out.println(book.price());
            System.out.println(Arrays.toString(book.author()));
        }
    }

    @Test
    public void parseMethod() throws NoSuchMethodException {
        // a.先得到类对象
        Class c = BookStore.class;

        Method m = c.getDeclaredMethod("test");

        // b.判断这个类上面是否存在这个注解
        if(m.isAnnotationPresent(Bookk.class)){
            //c.直接获取该注解对象
            Bookk book = (Bookk) m.getDeclaredAnnotation(Bookk.class);
            System.out.println(book.value());
            System.out.println(book.price());
            System.out.println(Arrays.toString(book.author()));
        }
    }
}

@Bookk(value = "《情深深雨濛濛》", price = 99.9, author = {"琼瑶", "dlei"})
class BookStore{

    @Bookk(value = "《三少爷的剑》", price = 399.9, author = {"古龙", "熊耀华"})
    public void test(){
    }
}

输出结果

《情深深雨濛濛》
99.9
[琼瑶, dlei]
《三少爷的剑》
399.9
[古龙, 熊耀华]

13.5、注解的应用场景:junit框架

模拟Junit框架

需求:

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

分析:

  • 定义一个自定义注解MyTest,只能注解方法,存活范围是一直都在。
  • 定义若干个方法,只要有@MyTest注解的方法就能在启动时被触发执行,没有这个注解的方法不能执行。

代码演示

MyTest 注解

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

@Target({ElementType.METHOD,ElementType.FIELD}) // 元注解
@Retention(RetentionPolicy.RUNTIME) // 一直活着,在运行阶段这个注解也不消失
public @interface MyTest {
}

测试类

import java.lang.reflect.Method;

public class AnnotationDemo4 {
    public void test1(){
        System.out.println("===test1===");
    }

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

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

    /**
      启动菜单:有注解的才被调用。
     */
    public static void main(String[] args) throws Exception {
        AnnotationDemo4 t = new AnnotationDemo4();
        // a.获取类对象
        Class c = AnnotationDemo4.class;
        // b.提取全部方法
        Method[] methods = c.getDeclaredMethods();
        // c.遍历方法,看是否有MyTest注解,有就跑它
        for (Method method : methods) {
            if(method.isAnnotationPresent(MyTest.class)){
                // 跑它
                method.invoke(t);
            }
        }
    }
}

输出结果

===test2===
===test3===

14、动态代理

14.1、准备案例、提出问题

模拟企业业务功能开发,并完成每个功能的性能统计

需求:

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

分析:

  • 定义一个UserService表示用户业务接口,规定必须完成用户登录,用户删除,用户查询功能。
  • 定义一个实现类UserServiceImpl实现UserService,并完成相关功能,且统计每个功能的耗时。
  • 定义测试类,创建实现类对象,调用方法。

代码演示

本案例存在哪些问题?

业务对象的的每个方法都要进行性能统计,存在大量重复的代码。

14.2、使用动态代理解决问题

动态代理

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

关键步骤

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

Java-高级技术(二)_第34张图片

代码演示

UserService 接口

/**
   模拟用户业务功能
 */
public interface UserService {
    String login(String loginName , String passWord) ;
    void selectUsers();
    boolean deleteUsers();
    void updateUsers();
}

UserServiceImpl

public class UserServiceImpl implements UserService{
    @Override
    public String login(String loginName, String passWord)  {
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if("admin".equals(loginName) && "1234".equals(passWord)) {
            return "success";
        }
        return "登录名和密码可能有毛病";

    }

    @Override
    public void selectUsers() {
        System.out.println("查询了100个用户数据!");
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public boolean deleteUsers() {
        try {
            System.out.println("删除100个用户数据!");
            Thread.sleep(500);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    @Override
    public void updateUsers() {
        try {
            System.out.println("修改100个用户数据!");
            Thread.sleep(2500);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

代理对象

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
    public static Object newProxyInstance(ClassLoader loader,  Class[] interfaces, InvocationHandler h)
    参数一:类加载器,负责加载代理类到内存中使用。
    参数二:获取被代理对象实现的全部接口。代理要为全部接口的全部方法进行代理
    参数三:代理的核心处理逻辑
 */
public class ProxyUtil {
    /**
      生成业务对象的代理对象。
     * @param obj
     * @return
     */
    public static <T> T  getProxy(T obj) {
        // 返回了一个代理对象了
        return (T)Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                obj.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 参数一:代理对象本身。一般不管
                        // 参数二:正在被代理的方法
                        // 参数三:被代理方法,应该传入的参数
                       long startTimer = System .currentTimeMillis();
                        // 马上触发方法的真正执行。(触发真正的业务功能)
                        Object result = method.invoke(obj, args);

                        long endTimer = System.currentTimeMillis();
                        System.out.println(method.getName() + "方法耗时:" + (endTimer - startTimer) / 1000.0 + "s");

                        // 把业务功能方法执行的结果返回给调用者
                        return result;
                    }
                });
    }
}

测试类

public class Test {
    public static void main(String[] args) {
        // 1、把业务对象,直接做成一个代理对象返回,代理对象的类型也是 UserService类型
        UserService userService = ProxyUtil.getProxy(new UserServiceImpl());

        System.out.println(userService.login("admin", "1234"));
        System.out.println(userService.deleteUsers());
        userService.selectUsers();
        userService.updateUsers(); // 走代理
    }
}

测试结果

login方法耗时:1.01s
success
删除100个用户数据!
deleteUsers方法耗时:0.502s
true
查询了100个用户数据!
selectUsers方法耗时:2.001s
修改100个用户数据!
updateUsers方法耗时:2.501s

动态代理的优点

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

15、XML

15.1、XML概述

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

	<data>
        <sender>张三sender>
        <receiver>李四receiver>
        <src>
			<addr>北京addr>
            <date>2022-11-11 11:11:11date>
            src>
        <current>武汉current>
        <dest>广州dest>
data>

XML的几个特点和使用场景

  • 一是纯文本,默认使用UTF-8编码;二是可嵌套。
  • 如果把XML内容存为文件,那么它就是一个XML文件。
  • XML的使用场景:XML内容经常被当成消息进行网络传输,或者作为配置文件用于存储系统的信息。

Java-高级技术(二)_第35张图片

15.2、XML的创建、语法规则

XML的创建

就是创建一个XML类型的文件,要求文件的后缀必须使用xml,如hello_world.xml

XML的语法规则

  • XML文件的后缀名为:xml
  • 文档声明必须是第一行


XML的标签(元素)规则

  • 标签由一对尖括号和合法标识符组成: ,必须存在一个根标签,有且只能有一个。
  • 标签必须成对出现,有开始,有结束:
  • 特殊的标签可以不成对,但是必须有结束标记,如:
  • 标签中可以定义属性,属性和标签名空格隔开,属性值必须用引号引起来
  • 标签需要正确的嵌套

XML的其他组成

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

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

    <    <  小于
    >    >  大于
    &   &  和号
    '  '  单引号
    "  "  引号
    
  • XML文件中可以存在CDATA区:

15.3、XML文档约束方式一-DTD约束

文档约束

  • 问题:由于XML文件可以自定义标签,导致XML文件可以随意定义,程序在解析的时候可能出现问题。
  • 文档约束:是用来限定xml文件中的标签以及属性应该怎么写。
  • 此强制约束程序员必须按照文档约束的规定来编写xml文件。

文档约束的分类

  • DTD
  • schema

XML文档约束-DTD的使用

需求:

  • 利用DTD文档约束,约束一个XML文件的编写。

分析:

  1. 编写DTD约束文档,后缀必须是.dtd

    
    
    
    
    
    
  2. 在需要编写的XML文件中导入该DTD约束文档

    
    DOCTYPE 书架 SYSTEM "data.dtd">
    <书架>
        <>
            <书名>精通JavaSE加强书名>
            <作者>dlei作者>
            <售价>很贵售价>
        >
        <>
            <书名>书名>
            <作者>作者>
            <售价>售价>
        >
        <>
            <书名>书名>
            <作者>作者>
            <售价>售价>
        >
    书架>
    
  3. 按照约束的规定编写XML文件的内容

MyBatis框架使用到DTD约束


DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

DTD约束的问题

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

15.4、XML文档约束方式二-schema约束

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

Java-高级技术(二)_第36张图片

XML文档约束-schema的使用

需求:

  • 利用schema文档约束,约束一个XML文件的编写。

分析:

  1. 编写schema约束文档,后缀必须是.xsd,具体的形式到代码中观看。

    
    <schema xmlns="http://www.w3.org/2001/XMLSchema"
            targetNamespace="http://www.itcast.cn"
            elementFormDefault="qualified" >
        
        <element name='书架'>
            
            <complexType>
                
                <sequence maxOccurs='unbounded'>
                    <element name=''>
                        
                        <complexType>
                            <sequence>
                                <element name='书名' type='string'/>
                                <element name='作者' type='string'/>
                                <element name='售价' type='double'/>
                            sequence>
                        complexType>
                    element>
                sequence>
            complexType>
        element>
    schema>
    
  2. 在需要编写的XML文件中导入该schema约束文档

    
    <书架 xmlns="http://www.itcast.cn"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.itcast.cn data.xsd">
        
        <>
            <书名>神雕侠侣书名>
            <作者>金庸作者>
            <售价>399.9售价>
        >
        <>
            <书名>神雕侠侣书名>
            <作者>金庸作者>
            <售价>19.5售价>
        >
    
    书架>
    
  3. 按照约束内容编写XML文件的标签。

Spring框架使用到schema约束


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
		 http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/mvc
          http://www.springframework.org/schema/mvc/spring-mvc.xsd
          http://code.alibabatech.com/schema/dubbo
          http://code.alibabatech.com/schema/dubbo/dubbo.xsd
          http://www.springframework.org/schema/context
          http://www.springframework.org/schema/context/spring-context.xsd">
    
beans>

XML的文档约束-schema的优点

  • 可以约束XML文件的标签内容格式,以及具体的数据类型。
  • 本身也是xml文件,格式更严谨。

15.5、XML解析技术

15.5.1、XML解析技术概述

XML的数据的作用是什么,最终需要怎么处理?

  • 存储数据、做配置信息、进行数据传输。
  • 最终需要被程序进行读取,解析里面的信息。

什么是XML解析?

  • 使用程序读取XML中的数据
    Java-高级技术(二)_第37张图片

XML解析方式

  • SAX解析
  • DOM解析

Dom常见的解析工具

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

DOM解析解析文档对象模型

Java-高级技术(二)_第38张图片

15.5.2、Dom4J解析XML文件

Dom4j解析XML-得到Document对象

SAXReader类

构造器/方法 说明
public SAXReader() 创建Dom4J的解析器对象
Document read(String url) 加载XML文件成为Document对象

Document类

方法名 说明
Element getRootElement() 获得根元素对象

代码演示

Contacts.xml


<contactList>
    <contact id="1" vip="true">
        <name>   潘金莲  name>
        <gender>gender>
        <email>[email protected]email>
    contact>
    <contact id="2" vip="false">
        <name>武松name>
        <gender>gender>
        <email>[email protected]email>
    contact>
    <contact id="3" vip="false">
        <name>武大狼name>
        <gender>gender>
        <email>[email protected]email>
    contact>
    <user>
    user>
contactList>

测试代码

SAXReader saxReader = new SAXReader();

InputStream is = Dom4JHelloWorldDemo1.class.getResourceAsStream("/Contacts.xml");
Document document = saxReader.read(is);

Element root = document.getRootElement();
System.out.println(root.getName());

输出结果

contactList

15.5.3、Dom4J解析XML文件中的各种节点

Dom4j解析XML的元素、属性、文本

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

代码演示

测试代码

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

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.List;

/**
   目标:学会使用dom4j解析XML文件中的数据。
    1、导入dom4j框架。
    2、准备一个XML文件。
 */
public class Dom4JHelloWorldDemo1 {
    @Test
    public void parseXMLData() throws Exception {
        // 1、创建一个Dom4j的解析器对象,代表了整个dom4j框架
        SAXReader saxReader = new SAXReader();

        // 2、把XML文件加载到内存中成为一个Document文档对象
        // Document document = saxReader.read(new File("xml-app\\src\\Contacts.xml")); // 需要通过模块名去定位
        // Document document = saxReader.read(new FileInputStream("xml-app\\src\\Contacts.xml"));

        // 注意: getResourceAsStream中的/是直接去src下寻找的文件
        InputStream is = Dom4JHelloWorldDemo1.class.getResourceAsStream("/Contacts.xml");
        Document document = saxReader.read(is);

        // 3、获取根元素对象
        Element root = document.getRootElement();
        System.out.println(root.getName());

        // 4、拿根元素下的全部子元素对象(一级)
        // List sonEles =  root.elements();
        List<Element> sonEles =  root.elements("contact");
        for (Element sonEle : sonEles) {
            System.out.println(sonEle.getName());
        }

        // 拿某个子元素
        Element userEle = root.element("user");
        System.out.println(userEle.getName());

        // 默认提取第一个子元素对象 (Java语言。)
        Element contact = root.element("contact");

        // 获取子元素文本
        // Element name = contact.element("name");
        // System.out.println(name.getText());
        System.out.println(contact.elementText("name"));
        // 去掉前后空格
        System.out.println(contact.elementTextTrim("name"));
        // 获取当前元素下的子元素对象
        Element email = contact.element("email");
        System.out.println(email.getText());
        // 去掉前后空格
        System.out.println(email.getTextTrim());

        // 根据元素获取属性值
        Attribute idAttr = contact.attribute("id");
        System.out.println(idAttr.getName() + "-->" + idAttr.getValue());
        // 直接提取属性值
        System.out.println(contact.attributeValue("id"));
        System.out.println(contact.attributeValue("vip"));
    }
}

输出结果

contactList
contact
contact
contact
user
   潘金莲  
潘金莲
[email protected]
[email protected]
id-->1
1
true

Dom4J的解析思想

得到文档对象Document,从中获取元素对象和内容。

15.5.4、Dom4J解析XML文件-案例实战

  • 利用Dom4J的知识,将Contact.xml文件中的联系人数据封装成List集合,其中每个元素是实体类Contact。
  • 打印输出 List 中的每个元素。

代码演示

Contacts.xml


<contactList>
    <contact id="1" vip="true">
        <name>   潘金莲  name>
        <gender>gender>
        <email>[email protected]email>
    contact>
    <contact id="2" vip="false">
        <name>武松name>
        <gender>gender>
        <email>[email protected]email>
    contact>
    <contact id="3" vip="false">
        <name>武大狼name>
        <gender>gender>
        <email>[email protected]email>
    contact>
    <user>
    user>
contactList>

Contact 类

/**
 
    潘金莲  
 
 [email protected]
 
 */
public class Contact {
    private String name;
    private int id;
    private boolean vip;
    private char gender;
    private String email;

    public Contact() {
    }

    public Contact(String name, int id, boolean vip, char gendar, String email) {
        this.name = name;
        this.id = id;
        this.vip = vip;
        this.gender = gendar;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

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

    public boolean isVip() {
        return vip;
    }

    public void setVip(boolean vip) {
        this.vip = vip;
    }

    public char getGender() {
        return gender;
    }

    public void setGender(char gender) {
        this.gender = gender;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return "Contact{" +
                "name='" + name + '\'' +
                ", id=" + id +
                ", vip=" + vip +
                ", gendar=" + gender +
                ", email='" + email + '\'' +
                '}';
    }
}

测试代码

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

import java.util.ArrayList;
import java.util.List;

public class Dom4JTest2 {
    @Test
    public void parseToList() throws Exception {
        // 需求:解析XML中的数据成为一个List集合对象。
        // 1、导入框架(做过)
        // 2、创建SaxReader对象
        SAXReader saxReader = new SAXReader();
        // 3、加载XML文件成为文档对象Document对象。
        Document document = saxReader.read(Dom4JTest2.class.getResourceAsStream("/Contacts.xml"));
        // 4、先拿根元素
        Element root = document.getRootElement();
        // 5、提取contact子元素
        List<Element> contactEles = root.elements("contact");
        // 6、准备一个ArrayList集合封装联系人信息
        List<Contact> contacts = new ArrayList<>();
        // 7、遍历Contact子元素
        for (Element contactEle : contactEles) {
            // 8、每个子元素都是一个联系人对象
            Contact contact = new Contact();
            contact.setId(Integer.valueOf(contactEle.attributeValue("id")));
            contact.setVip(Boolean.valueOf(contactEle.attributeValue("vip")));
            contact.setName(contactEle.elementTextTrim("name"));
            contact.setGender(contactEle.elementTextTrim("gender").charAt(0));
            contact.setEmail(contactEle.elementText("email"));
            // 9、把联系人对象数据加入到List集合
            contacts.add(contact);
        }
        // 10、遍历List集合
        for (Contact contact : contacts) {
            System.out.println(contact);
        }
    }
}

输出结果

Contact{name='潘金莲', id=1, vip=true, gendar=女, email='[email protected]'}
Contact{name='武松', id=2, vip=false, gendar=男, email='[email protected]'}
Contact{name='武大狼', id=3, vip=false, gendar=男, email='[email protected]'}

Contacts.xml

通常数据会封装成Java的对象,如单个对象,或者集合对象形式。

15.6、XML检索技术:Xpath

如果需要从XML文件中检索需要的某个信息(如name)怎么解决?

  • Dom4j需要进行文件的全部解析,然后再寻找数据。
  • Xpath技术更加适合做信息检索。

XPath介绍

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

使用Xpath检索出XML文件

需求:

  • 使用Dom4J把一个XML文件的数据进行解析。

分析:

  1. 导入jar包(dom4j和jaxen-1.1.2.jar),Xpath技术依赖Dom4j技术
  2. 通过dom4j的SAXReader获取Document对象
  3. 利用XPath提供的API,结合XPath的语法完成选取XML文档元素节点进行解析操作。
  4. Document中与Xpath相关的API如下:
方法名 说明
Node selectSingleNode(“表达式”) 获取符合表达式的唯一元素
List selectNodes(“表达式”) 获取符合表达式的元素集合

Xpath的四大检索方案

  • 绝对路径
  • 相对路径
  • 全文检索
  • 属性查找

Contacts2.xml


<contactList>
    <contact id="1" vip="true">
        <name>   潘金莲  name>
        <gender>gender>
        <email>[email protected]email>
    contact>
    <contact id="2" vip="false">
        <name>武松name>
        <gender>gender>
        <email>[email protected]email>
    contact>
    <contact id="3" vip="false">
        <name>武大狼name>
        <gender>gender>
        <email>[email protected]email>
    contact>
    <user>
        <contact>
            <info>
                <name id="888">我是西门庆name>
            info>
         contact>
    user>
contactList>

XPath:绝对路径

采用绝对路径获取从根节点开始逐层的查找/contactList/contact/name节点列表并打印信息

方法名 说明
/根元素/子元素/孙元素 从根元素开始,一级一级向下查找,不能跨级

代码演示

/**
    1.绝对路径: /根元素/子元素/子元素。
*/
@Test
public void parse01() throws Exception {
    // a、创建解析器对象
    SAXReader saxReader = new SAXReader();
    // b、把XML加载成Document文档对象
    Document document =
        saxReader.read(XPathDemo.class.getResourceAsStream("/Contacts2.xml"));
    // c、检索全部的名称
    List<Node> nameNodes = document.selectNodes("/contactList/contact/name");
    for (Node nameNode : nameNodes) {
        Element nameEle = (Element) nameNode;
        System.out.println(nameEle.getTextTrim());
    }
}

输出结果

潘金莲
武松
武大狼

XPath:相对路径

  • 先得到根节点contactList
  • 再采用相对路径获取下一级contact 节点的name子节点并打印信息
方法名 说明
./子元素/孙元素 从当前元素开始,一级一级向下查找,不能跨级

代码演示

/**
	2.相对路径: ./子元素/子元素。 (.代表了当前元素)
*/
@Test
public void parse02() throws Exception {
    // a、创建解析器对象
    SAXReader saxReader = new SAXReader();
    // b、把XML加载成Document文档对象
    Document document =
        saxReader.read(XPathDemo.class.getResourceAsStream("/Contacts2.xml"));
    Element root = document.getRootElement();
    // c、检索全部的名称
    List<Node> nameNodes = root.selectNodes("./contact/name");
    for (Node nameNode : nameNodes) {
        Element  nameEle = (Element) nameNode;
        System.out.println(nameEle.getTextTrim());
    }
}

输出结果

潘金莲
武松
武大狼

XPath:全文搜索

直接全文搜索所有的name元素并打印

方法名 说明
//contact 找contact元素,无论元素在哪里
//contact/name 找contact,无论在哪一级,但name一定是contact的子节点
//contact//name contact无论在哪一种,name只要是contact的子孙元素都可以找到

代码演示

/**
	3.全文搜索:
	//元素  在全文找这个元素
	//元素1/元素2  在全文找元素1下面的一级元素2
	//元素1//元素2  在全文找元素1下面的全部元素2
*/
@Test
public void parse03() throws Exception {
    // a、创建解析器对象
    SAXReader saxReader = new SAXReader();
    // b、把XML加载成Document文档对象
    Document document =
        saxReader.read(XPathDemo.class.getResourceAsStream("/Contacts2.xml"));
    // c、检索数据
    // List nameNodes = document.selectNodes("//name");
    // List nameNodes = document.selectNodes("//contact/name");
    List<Node> nameNodes = document.selectNodes("//contact//name");
    for (Node nameNode : nameNodes) {
        Element  nameEle = (Element) nameNode;
        System.out.println(nameEle.getTextTrim());
    }
}

输出结果

List nameNodes = document.selectNodes("//name");
潘金莲
武松
武大狼
我是西门庆
List nameNodes = document.selectNodes("//contact/name");
潘金莲
武松
武大狼
List nameNodes = document.selectNodes("//contact//name");
潘金莲
武松
武大狼
我是西门庆

XPath:属性查找

在全文中搜索属性,或者带属性的元素

方法名 说明
//@属性名 查找属性对象,无论是哪个元素,只要有这个属性即可。
//元素[@属性名] 查找元素对象,全文搜索指定元素名和属性名。
//元素//[@属性名=‘值’] 查找元素对象,全文搜索指定元素名和属性名,并且属性值相等。

代码演示

/**
    4.属性查找。
    //@属性名称  在全文检索属性对象。
    //元素[@属性名称]  在全文检索包含该属性的元素对象。
    //元素[@属性名称=值]  在全文检索包含该属性的元素且属性值为该值的元素对象。
*/
@Test
public void parse04() throws Exception {
    // a、创建解析器对象
    SAXReader saxReader = new SAXReader();
    // b、把XML加载成Document文档对象
    Document document =
        saxReader.read(XPathDemo.class.getResourceAsStream("/Contacts2.xml"));
    // c、检索数据
    List<Node> nodes = document.selectNodes("//@id");
    for (Node node : nodes) {
        Attribute attr = (Attribute) node;
        System.out.println(attr.getName() + "===>" + attr.getValue());
    }

    // 查询name元素(包含id属性的)
    // Node node = document.selectSingleNode("//name[@id]");
    Node node = document.selectSingleNode("//name[@id=888]");
    Element ele = (Element) node;
    System.out.println(ele.getTextTrim());
}

输出结果

id===>1
id===>2
id===>3
id===>888
我是西门庆

16、设计模式

16.1、设计模式:工厂模式

  • 之前我们创建类对象时, 都是使用new 对象的形式创建,在很多业务场景下也提供了不直接new的方式 。
  • 工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一, 这种类型的设计模式属于创建型模式,它提供了一种获取对象的方式。

工厂模式的作用

  • 工厂的方法可以封装对象的创建细节,比如:为该对象进行加工和数据注入。
  • 可以实现类与类之间的解耦操作(核心思想)。

代码演示

Computer 类

public abstract class Computer {
    private String name;
    private double price;

    public abstract void start();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }
}

Mac 类

public class Mac extends Computer{
    @Override
    public void start() {
        System.out.println(getName() + "以非常优雅的方法启动了,展示了一个苹果logo");
    }
}

Huawei 类

public class Huawei extends Computer{
    @Override
    public void start() {
        System.out.println(getName() + "开机了,展示了华为的菊花图标~~~~");
    }
}

FactoryPattern 类

public class FactoryPattern {
    /**
       定义一个方法,创建对象返回
     */
    public static Computer createComputer(String info){
        switch (info){
            case "huawei":
                Computer c = new Huawei();
                c.setName("huawei pro 16");
                c.setPrice(5999);
                return c;
            case "mac":
                Computer c2 = new Mac();
                c2.setName("MacBook pro");
                c2.setPrice(11999);
                return c2;
            default:
                return null;
        }
    }
}

测试类

public class FactoryDemo {
    public static void main(String[] args) {
        Computer c1 = FactoryPattern.createComputer("huawei");
        c1.start();

        Computer c2 = FactoryPattern.createComputer("mac");
        c2.start();
    }
}

输出结果

huawei pro 16开机了,展示了华为的菊花图标~~~~
MacBook pro以非常优雅的方法启动了,展示了一个苹果logo

16.2、设计模式:装饰模式

创建一个新类,包装原始类,从而在新类中提升原来类的功能。

装饰模式的作用

装饰模式指的是在不改变原类的基础上, 动态地扩展一个类的功能。

装饰模式的实现方式

  1. 定义父类。
  2. 定义原始类,继承父类,定义功能。
  3. 定义装饰类,继承父类,包装原始类,增强功能。
InputStream(抽象父类)
FileInputStream(实现子类,读写性能较差)
BufferedInputStream(实现子类,装饰类,读写性能高)

代码演示

InputStream 类

/**
   共同父类
 */
public abstract class InputStream {
    public abstract int read();
    public abstract int read(byte[] buffer);
}

FileInputStream 类(原始类)

import java.util.Arrays;

/**
   原始类
 */
public class FileInputStream extends InputStream{
    @Override
    public int read() {
        System.out.println("低性能的方式读取了一个字节a");
        return 97;
    }

    @Override
    public int read(byte[] buffer) {
        buffer[0] = 97;
        buffer[1] = 98;
        buffer[2] = 99;
        System.out.println("低性能的方式读取了一个字节数组:" + Arrays.toString(buffer));
        return 3;
    }
}

BufferedInputStream 类(装饰类)

/**
   装饰类:继承InputStream 拓展原始类的功能
 */
public class BufferedInputStream extends InputStream{
    private InputStream is;
    public BufferedInputStream(InputStream is){
        this.is = is;
    }
    @Override
    public int read() {
        System.out.println("提供8KB的缓冲区,提高读数据性能~~~~");
        return is.read();
    }

    @Override
    public int read(byte[] buffer) {
        System.out.println("提供8KB的缓冲区,提高读数据性能~~~~");
        return is.read(buffer);
    }
}

测试类

/**
  装饰模式

    定义父类:InputStream
    定义实现类:FileInputStream 继续父类 定义功能
    定义装饰实现类:BufferedInputStream 继承父类 定义功能 包装原始类,增强功能。
 */
public class DecoratorPattern {
    public static void main(String[] args) {
        InputStream is = new BufferedInputStream(new FileInputStream());
        System.out.println(is.read());
        System.out.println(is.read(new byte[3]));
    }
}

输出结果

提供8KB的缓冲区,提高读数据性能~~~~
低性能的方式读取了一个字节a
97
提供8KB的缓冲区,提高读数据性能~~~~
低性能的方式读取了一个字节数组:[97, 98, 99]
3

本文章参考B站 Java入门基础视频教程,java零基础自学首选黑马程序员Java入门教程(含Java项目和Java真题),仅供个人学习使用,部分内容为本人自己见解,与黑马程序员无关。

你可能感兴趣的:(Java,java)