javaSE从入门到精通的二十万字总结(三)

48 篇文章 109 订阅 ¥29.90 ¥99.00

前言

在看这篇文章之前先预习java基础

这部分知识一共有4个文档
第四个是当前这个文档

  1. java零基础从入门到精通(全)
  2. javaSE从入门到精通的二十万字总结(一)
  3. javaSE从入门到精通的二十万字总结(二)
  4. javaSE从入门到精通的二十万字总结(三)

关于这部分的源码如下
javase从入门到精通的学习代码.rar

8. 线程

关于这部分知识可看我之前的文章

  1. 【操作系统】线程与进程的深入剖析(全)
  2. 【操作系统】守护线程和守护进程的区别
  3. JUC高并发编程从入门到精通(全)
  4. java之TimeUnit.SECONDS.sleep()详细分析(全)
  5. java并发之synchronized详细分析(全)
  6. java之Thread类详细分析(全)
  7. java之Thread类实战模板(全)

8.1 线程与进程的概念

关于这个概念上面给出了链接
【操作系统】线程与进程的深入剖析(全)

进程是一个应用程序(1个进程是一个软件)
线程是一个进程中的执行场景/执行单元

一个进程可以启动多个线程,进程之间是独立的,不共享资源
线程之间是堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈

为此提出问题
1. 使用了多线程机制之后,main方法结束,是不是有可能程序也不会结束??

main方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在压栈弹栈

2. 启动一个java程序代码的代码过程??
会先启动JVM,而JVM就是一个进程。
JVM再启动一个主线程调用main方法,同时再启动一个垃圾回收线程负责看护,回收垃圾。最起码,现在的java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。

3.对于单核的CPU,可以做到真正的多线程并发吗??
不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。
对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行做

4.分析程序中有多少个线程??

public class ThreadTest01 {
    public static void main(String[] args) {
        System.out.println("main begin");
        m1();
        System.out.println("main over");
    }

    private static void m1() {
        System.out.println("m1 begin");
        m2();
        System.out.println("m1 over");
    }

    private static void m2() {
        System.out.println("m2 begin");
        m3();
        System.out.println("m2 over");
    }

    private static void m3() {
        System.out.println("m3 execute!");
    }
}

结果是只有一个
因为程序中只有一个主栈,没有创建一个分栈,都是在主栈中调用其线程
最后执行的结果是

main begin
m1 begin
m2 begin
m3 execute!
m2 over
m1 over
main over

8.2 线程创建方式

线程的创建方式在上面已经给出了连接
是这两个文档

  1. java之Thread类详细分析(全)
  2. java之Thread类实战模板(全)

第一种方式
直接继承java.lang.Thread,重写run方法

start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)

public class ThreadTest02 {
    public static void main(String[] args) {
        // 这里是main方法,这里的代码属于主线程,在主栈中运行。
        // 新建一个分支线程对象
        MyThread t = new MyThread();
      
        // 启动线程     
        t.start();
        // 这里的代码还是运行在主线程中。
        for(int i = 0; i < 1000; i++){
            System.out.println("主线程--->" + i);
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        // 编写程序,这段程序运行在分支线程中(分支栈)。
        for(int i = 0; i < 1000; i++){
            System.out.println("分支线程--->" + i);
        }
    }
}

第二种方式
编写一个类实现java.lang.Runnable接口

public class ThreadTest03 {
    public static void main(String[] args) {
        // 创建一个可运行的对象
        //MyRunnable r = new MyRunnable();
        // 将可运行的对象封装成一个线程对象
        //Thread t = new Thread(r);
        Thread t = new Thread(new MyRunnable()); // 合并代码
        // 启动线程
        t.start();

        for(int i = 0; i < 100; i++){
            System.out.println("主线程--->" + i);
        }
    }
}

// 这并不是一个线程类,是一个可运行的类。它还不是一个线程。
class MyRunnable implements Runnable {

    @Override
    public void run() {
        for(int i = 0; i < 100; i++){
            System.out.println("分支线程--->" + i);
        }
    }
}

第三种方式
将其上面两种方式结合在一起
使用匿名内部类结合在一起

public class ThreadTest04 {
    public static void main(String[] args) {
        // 创建线程对象,采用匿名内部类方式。
        // 这是通过一个没有名字的类,new出来的对象。
        Thread t = new Thread(new Runnable(){
            @Override
            public void run() {
                for(int i = 0; i < 100; i++){
                    System.out.println("t线程---> " + i);
                }
            }
        });

        // 启动线程
        t.start();

        for(int i = 0; i < 100; i++){
            System.out.println("main线程---> " + i);
        }
    }
}

总结一下上面的方式大致如下
第一种方式

// 定义线程类
public class MyThread extends Thread{
	public void run(){
			
	}
}

// 创建线程对象
MyThread t = new MyThread();
// 启动线程。
t.start();

第二种方式

// 定义一个可运行的类
public class MyRunnable implements Runnable {
	public void run(){
			
	}
}

// 创建线程对象
Thread t = new Thread(new MyRunnable());
// 启动线程
t.start();

涉及到为什么要用start()方法(系统自动调用run方法)而不是直接让对象直接调用run方法的原因
可看我之前的文章
多线程中run()和start()的异同详细分析(全)

8.3 线程生命周期

线程的生命周期里面分为五个阶段

  • 新建状态
  • 就绪状态
  • 运行状态
  • 阻塞状态
  • 死亡状态
    在这里插入图片描述

8.4 线程方法

方法功能
Thread.currentThread()获取当前线程对象
线程对象.getName()获取线程对象名字
线程对象.setName(“线程名字”)修改线程对象名字
static void sleep(long millis)参数是毫秒,让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用
线程对象.interrupt();终止线程睡眠不终止线程执行
线程对象.stop()终止线程执行,已过时(不建议使用。),主要是因为不会保存信息

以上方法的示列代码如下

public class ThreadTest05 {
    public static void main(String[] args) {

        //currentThread就是当前线程对象
        // 这个代码出现在main方法当中,所以当前线程就是主线程。
        Thread currentThread = Thread.currentThread();
        System.out.println(currentThread.getName()); //main

        // 创建线程对象
        MyThread2 t = new MyThread2();
        // 设置线程的名字
        t.setName("t1");
        // 获取线程的名字
        String tName = t.getName();
        System.out.println(tName); //Thread-0

        MyThread2 t2 = new MyThread2();
        t2.setName("t2");
        System.out.println(t2.getName()); //Thread-1\
        t2.start();

        // 启动线程
        t.start();
    }
}

class MyThread2 extends Thread {
    public void run(){
        for(int i = 0; i < 100; i++){
            // currentThread就是当前线程对象。当前线程是谁呢?
            // 当t1线程执行run方法,那么这个当前线程就是t1
            // 当t2线程执行run方法,那么这个当前线程就是t2
            Thread currentThread = Thread.currentThread();
            System.out.println(currentThread.getName() + "-->" + i);
        }
    }
}

使用sleep方法的代码示列如下
此处补充另外一个相关的函数
java之TimeUnit.SECONDS.sleep()详细分析(全)

public class ThreadTest06 {
    public static void main(String[] args) {

        for(int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);

            // 睡眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

此处补充sleep方法的一个面试题
以下代码会让线程t进入休眠状态吗?分支线程会有延迟嘛?

public class ThreadTest07 {
    public static void main(String[] args) {
        // 创建线程对象
        Thread t = new MyThread3();
        t.setName("t");
        t.start();

        // 调用sleep方法
        try {       
            t.sleep(1000 * 5); 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 5秒之后这里才会执行。
        System.out.println("hello World!");
    }
}

class MyThread3 extends Thread {
    public void run(){
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

答案如下:

在执行的时候还是会转换成:Thread.sleep(1000 * 5);,这行代码的作用是:让当前线程进入休眠,也就是说main线程进入休眠。但是分支的线程不会被延迟,start方法也就是开启了分支栈之后还会继续执行下面的代码

特别注意在run方法中调用某些方法需要不可以抛出异常,子类不可比父类多异常,所以只可以使用try catch

==interrupt()方法终止睡眠的示列代码如下 ==
终止睡眠而不终止线程的执行

  • 这种终断睡眠的方式依靠了java的异常处理机制
public class ThreadTest08 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable2());
        t.start();

        // 希望5秒之后,t线程醒来(5秒之后主线程手里的活儿干完了。)
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 终断t线程的睡眠
        t.interrupt(); // 干扰
    }
}

class MyRunnable2 implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "---> begin");
        try {
            // 睡眠1年
            Thread.sleep(1000 * 60 * 60 * 24 * 365);
        } catch (InterruptedException e) {
            // 打印异常信息
            e.printStackTrace();
        }
        //1年之后才会执行这里
        System.out.println(Thread.currentThread().getName() + "---> end");

    }

}

stop()方法终止线程执行的示列代码如下
这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了
线程没有保存的数据将会丢失。不建议使用

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

        // 模拟5秒
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 5秒之后强行终止t线程
        t.stop(); // 已过时(不建议使用。)
    }
}

class MyRunnable3 implements Runnable {

    @Override
    public void run() {
        for(int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

为了克服这种缺点,使得保全终止前信息还保留着
应该设置一个标志临界值来保存信息

public class ThreadTest10 {
    public static void main(String[] args) {
    
        MyRunable4 r = new MyRunable4();
        Thread t = new Thread(r);

        t.start();

        // 模拟5秒
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 终止线程
        // 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。
        r.run = false;
    }
}

class MyRunable4 implements Runnable {

    // 布尔标记
    boolean run = true;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++){
            if(run){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else{

                // 在这里可以保存呀。
                //save....
                //终止当前线程
                return;
            }
        }
    }
}

这里补充一下线程调度的知识点
线程调度的模型有两种主要

  • 抢占式调度模型:那个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些,java采用的就是抢占式调度模型
  • 均分式调度模型:平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样

java中又提供了如下的方法和线程调度有关
以下只列出一些常用的方法

方法功能
实例方法:void setPriority(int newPriority)设置线程的优先级
实例方法:int getPriority()获取线程优先级
实例方法:void join()合并方法
静态方法:static void yield()暂停当前正在执行的线程对象,并执行其他线程

讲解以上方法的时候先补充一些知识点

  • 最低优先级1,默认优先级是5,最高优先级10,优先级比较高的获取CPU时间片可能会多一些
  • yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。注意:在回到就绪之后,有可能还会再次抢到
  • join() 当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续(感觉这个才是让位,腾出线程地方)

线程优先级的示列代码如下

public class ThreadTest11 {
    public static void main(String[] args) {


        System.out.println("最高优先级" + Thread.MAX_PRIORITY);
        System.out.println("最低优先级" + Thread.MIN_PRIORITY);
        System.out.println("默认优先级" + Thread.NORM_PRIORITY);

        // 设置主线程的优先级为1
        Thread.currentThread().setPriority(1);

        // 获取当前线程对象,获取当前线程的优先级
        Thread currentThread = Thread.currentThread();
        // main线程的默认优先级是:5
        //System.out.println(currentThread.getName() + "线程的默认优先级是:" + currentThread.getPriority());

        Thread t = new Thread(new MyRunnable5());
        t.setPriority(10);
        t.setName("t");
        t.start();

        // 优先级较高的,只是抢到的CPU时间片相对多一些。
        // 大概率方向更偏向于优先级比较高的。
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }


    }
}

class MyRunnable5 implements Runnable {

    @Override
    public void run() {
        // 获取线程优先级
        //System.out.println(Thread.currentThread().getName() + "线程的默认优先级:" + Thread.currentThread().getPriority());
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

yield()方法示列代码如下
分支线程每100次数让一次给主线程的抢占
但这种都是大概率问题而已

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

        for(int i = 1; i <= 10000; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

class MyRunnable6 implements Runnable {

    @Override
    public void run() {
        for(int i = 1; i <= 10000; i++) {
            //每100个让位一次。
            if(i % 100 == 0){
                Thread.yield(); // 当前线程暂停一下,让给主线程。
            }
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

join方法合并示列代码
所谓的合并代码,也就是直接腾出空间,让其先执行

public class ThreadTest13 {
    public static void main(String[] args) {
        System.out.println("main begin");

        Thread t = new Thread(new MyRunnable7());
        t.setName("t");
        t.start();

        //合并线程
        try {
            t.join(); // t合并到当前线程中,当前线程受阻塞,t线程执行直到结束。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("main over");
    }
}

class MyRunnable7 implements Runnable {

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

8.5 数据安全

什么时候数据在多线程并发的环境下会存在安全问题?

  • 多线程并发
  • 有共享数据
  • 共享数据有修改的行为

满足以上3个条件之后,就会存在线程安全问题
可以使用同步模型,但是牺牲了效率有了安全而已
应该在数据安全的前提下,保全数据效率

异步编程模型:多线程并发(效率较高),异步就是并发。线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁
同步编程模型:线程排队执行,同步就是排队。线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系

  • 堆和方法区都是多线程共享的,所以可能存在线程安全问题
  • 局部变量+常量:不会有线程安全问题
  • 成员变量:可能会有线程安全问题

实例变量在堆中,堆只有1个
静态变量在方法区中,方法区只有1个

实例变量:在堆中
静态变量:在方法区
局部变量:在栈中
以上三大变量中:
局部变量永远都不会存在线程安全问题,因为局部变量不共享(一个线程一个栈),局部变量在栈中。所以局部变量永远都不会共享

如果使用局部变量建议使用:StringBuilder。
因为局部变量不存在线程安全问题。选择StringBuilder,StringBuffer效率比较低。

  • ArrayList是非线程安全的。
  • Vector是线程安全的。
  • HashMap HashSet是非线程安全的。
  • Hashtable是线程安全的。

模拟不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题
账户类

public class Account {
    // 账号
    private String actno;
    // 余额
    private double balance;

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款的方法
    public void withdraw(double money){
        // t1和t2并发这个方法。。。。(t1和t2是两个栈。两个栈操作堆中同一个对象。)
        // 取款之前的余额
        double before = this.getBalance(); // 10000
        // 取款之后的余额
        double after = before - money;

        // 在这里模拟一下网络延迟,100%会出现问题
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 更新余额
        // 思考:t1执行到这里了,但还没有来得及执行这行代码,t2线程进来withdraw方法了。此时一定出问题。
        this.setBalance(after);
    }
}

模拟线程机制内的run内容

public class AccountThread extends Thread {

    // 两个线程必须共享同一个账户对象。
    private Account act;

    // 通过构造方法传递过来账户对象
    public AccountThread(Account act) {
        this.act = act;
    }

    public void run(){
        // run方法的执行表示取款操作。
        // 假设取款5000
        double money = 5000;
        // 取款
        // 多线程并发执行这个方法。
        act.withdraw(money);

        System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());
    }
}

创建一个对象两个线程,两个线程争夺都启动

public class Test {
    public static void main(String[] args) {
        // 创建账户对象(只创建1个)
        Account act = new Account("act-001", 10000);
        // 创建两个线程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);
        // 设置name
        t1.setName("t1");
        t2.setName("t2");
        // 启动线程取款
        t1.start();
        t2.start();
    }
}

会出现数据安全的问题
既然不能使用同步机制,所以要用异步机制
应该引入synchronized关键字

8.5.1 synchronized关键字

关于这个可看我之前的文章知识点
java并发之synchronized详细分析(全)

引入这个关键字后,可以避开一些数据安全的不规范
使用的具体规范是

synchronized(){
   // 线程同步代码块。
}

synchronized后面小括号中传的这个“数据”是相当关键的,这个数据必须是多线程共享的数据。才能达到多线程排队

那要看你想让哪些线程同步:假设t1、t2、t3、t4、t5,有5个线程,你只希望t1 t2 t3排队,t4 t5不需要排队。要在()中写一个t1 t2 t3共享的对象。而这个对象对于t4 t5来说不是共享的。

这里的共享对象是:账户对象。账户对象是共享的,不一定是this就是账户对象,只要是多线程共享的那个对象就行

在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁。),100个对象,100把锁。1个对象1把锁。

执行原理

  • 假设t1和t2线程并发,开始执行以下代码的时候
  • 假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把锁才会释放====假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁, t2占有这把锁之后,进入同步代码块执行程序。这样就达到了线程排队执行。
  • 这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队,执行的这些线程对象所共享的。

主要修改账户类的内容

public class Account {
    // 账号
    private String actno;
    // 余额
    private double balance; //实例变量。

    //对象
    Object obj = new Object(); // 实例变量。(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。)

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款的方法
    public void withdraw(double money){
                
         */
        //Object obj2 = new Object();//内部对象,都会进行创建
        synchronized (this){
        //synchronized (obj) {
        //synchronized ("abc") { // "abc"在字符串常量池当中。
        //synchronized (null) { // 报错:空指针。
        //synchronized (obj2) { // 这样编写就不安全了。因为obj2不是共享对象。
            double before = this.getBalance();
            double after = before - money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(after);
        }
    }
}

如果作用在类上
在实例方法上可以使用synchronized。synchronized出现在实例方法上,一定锁的是this。这种方式不灵活。如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式。

缺点:synchronized出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。

优点:代码写的少了。节俭了。

总结使用该关键字的三种方式

第一种:同步代码块,灵活

synchronized(线程共享对象){
 //同步代码块;
}

第二种:在实例方法上使用synchronized,表示共享对象一定是this,并且同步代码块是整个方法体
第三种:在静态方法上使用synchronized,表示找类锁。类锁永远只有1把,就算创建了100个对象,那类锁也只有一把

  • 对象锁:1个对象1把锁,100个对象100把锁
  • 类锁:100个对象,也可能只是1把类锁

关于synchronized的一些面试题

  • 同一个对象两个线程调用两个方法(一个有synchronized,一个没有),不需要等待
  • 同一个对象两个线程调用两个方法(两个都有synchronized),需要等待
  • 两个对象分配不同的线程,调用不一样的方法(都有synchronized),不需要等待,两个对象,两把锁
  • 两个对象分配不同的线程,调用不一样的方法(都有synchronized但是被static修饰了),需要等待,因为静态方法是类锁,不管创建了几个对象,类锁只有1把

举例第一个示列代码如下

public class Exam01 {
    public static void main(String[] args) throws InterruptedException {
        MyClass mc = new MyClass();

        Thread t1 = new MyThread(mc);
        Thread t2 = new MyThread(mc);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。
        t2.start();
    }
}

class MyThread extends Thread {
    private MyClass mc;
    public MyThread(MyClass mc){
        this.mc = mc;
    }
    public void run(){
        if(Thread.currentThread().getName().equals("t1")){
            mc.doSome();
        }
        if(Thread.currentThread().getName().equals("t2")){
            mc.doOther();
        }
    }
}

class MyClass {
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
    public void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

8.5.2 解决方法

不要轻易选择线程同步synchronized,synchronized会让程序的执行效率降低,用户体验不好,系统的用户吞吐量降低。

  • 第一种方案:尽量使用局部变量代替“实例变量和静态变量”。

  • 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样
    实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,
    对象不共享,就没有数据安全问题了。)

  • 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候
    就只能选择synchronized了。线程同步机制。

8.5.3 死锁

对于synchronized,如果轻易嵌套不好,可能会引来死锁问题
具体死锁的问题是互相抢占资源互相等待

死锁的代码要掌握背会
面试的时候可能会让你写死锁的代码

记住代码
背会死锁

背会死锁

记住代码

public class DeadLock {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();

        // t1和t2两个线程共享o1,o2
        Thread t1 = new MyThread1(o1,o2);
        Thread t2 = new MyThread2(o1,o2);

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

class MyThread1 extends Thread{
    Object o1;
    Object o2;
    public MyThread1(Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o1){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2){

            }
        }
    }
}

class MyThread2 extends Thread {
    Object o1;
    Object o2;
    public MyThread2(Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o2){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1){

            }
        }
    }
}

  • 背会了嘛

8.6 守护线程

关于这部分知识点具体可看我之前的文章
【操作系统】守护线程和守护进程的区别

关于java语言中线程分为两大类:

  • 用户线程
  • 守护线程(后台线程)

其中具有代表性的就是:垃圾回收线程(守护线程)。

守护线程的特点:一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。比如之后要讲到的定时器(每天00:00的时候系统数据自动备份,这个需要使用到定时器,并且我们可以将定时器设置为守护线程,所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份)

将其设置为守护线程的主要代码为

线程对象.setDaemon(true);

注意:主线程main方法是一个用户线程
守护线程的代码示列

public class ThreadTest14 {
    public static void main(String[] args) {
        Thread t = new BakDataThread();
        t.setName("备份数据的线程");

        // 启动线程之前,将线程设置为守护线程
        t.setDaemon(true);

        t.start();

        // 主线程:主线程是用户线程
        for(int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class BakDataThread extends Thread {
    public void run(){
        int i = 0;
        // 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
        while(true){
            System.out.println(Thread.currentThread().getName() + "--->" + (++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

8.7 定时器

定时器的作用:间隔特定的时间,执行特定的程序。

有几种方式可以实现定时器:

  • 可以使用sleep方法,睡眠,设置睡眠时间,到时间点执行任务。这种方式是最原始的定时器
  • 在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的。
  • 在实际的开发中,使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务

主要示列代码如下

public class TimerTest {
    public static void main(String[] args) throws Exception {

        // 创建定时器对象
        Timer timer = new Timer();
        //Timer timer = new Timer(true); //守护线程的方式

        // 指定定时任务
        //timer.schedule(定时任务, 第一次执行时间, 间隔多久执行一次);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date firstTime = sdf.parse("2020-03-14 09:34:30");
        //timer.schedule(new LogTimerTask() , firstTime, 1000 * 10);
      

        //匿名内部类方式
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                // code....
            }
        } , firstTime, 1000 * 10);

    }
}

// 编写一个定时任务类
// 假设这是一个记录日志的定时任务
class LogTimerTask extends TimerTask {

    @Override
    public void run() {
        // 编写你需要执行的任务就行了。
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String strTime = sdf.format(new Date());
        System.out.println(strTime + ":成功完成了一次数据备份!");
    }
}

8.8 实现Callable接口

实现线程的第三种方式:实现Callable接口
有返回值,可以返回线程的结果
接口中的call方法相当于run方法

  • 优点:可以获取到线程的执行结果。
  • 缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低
public class ThreadTest15 {
    public static void main(String[] args) throws Exception {

        // 第一步:创建一个“未来任务类”对象。
        // 参数非常重要,需要给一个Callable接口实现类对象。
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception { 

                // 线程执行一个任务,执行之后可能会有一个执行结果
                // 模拟执行
                System.out.println("call method begin");
                Thread.sleep(1000 * 10);
                System.out.println("call method end!");
                int a = 100;
                int b = 200;
                return a + b; //自动装箱(300结果变成Integer)
            }
        });

        // 创建线程对象
        Thread t = new Thread(task);

        // 启动线程
        t.start();

        // 这里是main方法,这是在主线程中。
        // 在主线程中,怎么获取t线程的返回结果?
        // get()方法的执行会导致“当前线程阻塞”
        Object obj = task.get();
        System.out.println("线程执行结果:" + obj);

        // main方法这里的程序要想执行必须等待get()方法的结束
        // 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果
        // 另一个线程执行是需要时间的。
        System.out.println("hello world!");
    }
}

8.9 wait和notify

wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是Object类中自带的。wait方法和notify方法不是通过线程对象调用,
不是这样的:t.wait(),也不是这样的:t.notify().

  • wait()方法作用
    让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。
    o.wait();方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态。
Object o = new Object();
o.wait();
  • notify()方法作用
    唤醒正在o对象上等待的线程
Object o = new Object();
o.notify();
  • o.wait方法会让正在o对象上活动的当前线程进入等待状态并且释放之前占有的o对象的锁
  • o.notify方法只会通知,不会释放之前所占有的o对象锁

还有一个notifyAll()方法:这个方法是唤醒o对象上处于等待的所有线程

使用wait方法和notify方法实现“生产者和消费者模式”

生产线程负责生产,消费线程负责消费
生产线程和消费线程要达到均衡
这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法

  • wait和notify方法不是线程对象的方法,是普通java对象都有的方法。
  • wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库。有线程安全问题。

模拟这样一个需求:

仓库采用List集合,List集合中假设只能存储1个元素,1个元素就表示仓库满了。,如果List集合中元素个数是0,就表示仓库空了,保证List集合中永远都是最多存储1个元素。
必须做到这种效果:生产1个消费1个。

具体的代码示列如下
当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。

public class ThreadTest16 {
    public static void main(String[] args) {
        // 创建1个仓库对象,共享的。
        List list = new ArrayList();
        // 创建两个线程对象
        // 生产者线程
        Thread t1 = new Thread(new Producer(list));
        // 消费者线程
        Thread t2 = new Thread(new Consumer(list));

        t1.setName("生产者线程");
        t2.setName("消费者线程");

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

// 生产线程
class Producer implements Runnable {
    // 仓库
    private List list;

    public Producer(List list) {
        this.list = list;
    }
    @Override
    public void run() {
        // 一直生产(使用死循环来模拟一直生产)
        while(true){
            // 给仓库对象list加锁。
            synchronized (list){
                if(list.size() > 0){ // 大于0,说明仓库中已经有1个元素了。
                    try {
                        // 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序能够执行到这里说明仓库是空的,可以生产
                Object obj = new Object();
                list.add(obj);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                // 唤醒消费者进行消费
                list.notifyAll();
            }
        }
    }
}

// 消费线程
class Consumer implements Runnable {
    // 仓库
    private List list;

    public Consumer(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        // 一直消费
        while(true){
            synchronized (list) {
                if(list.size() == 0){
                    try {
                        // 仓库已经空了。
                        // 消费者线程等待,释放掉list集合的锁
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序能够执行到此处说明仓库中有数据,进行消费。
                Object obj = list.remove(0);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                // 唤醒生产者生产。
                list.notifyAll();
            }
        }
    }
}

9. 反射机制

通过java语言中的反射机制可以操作字节码文件(可以读和修改字节码文件。)
通过反射机制可以操作代码片段(class文件)

  • java.lang.Class:代表整个字节码,代表一个类型,代表整个类
  • java.lang.reflect.Method:代表字节码中的方法字节码。代表类中的方法
  • java.lang.reflect.Constructor:代表字节码中的构造方法字节码。代表类中的构造方法
  • java.lang.reflect.Field:代表字节码中的属性字节码。代表类中的成员变量(静态变量+实例变量)
java.lang.Classpublic class User{
	// Field
	int no;
	// Constructor
	public User(){
	}
	public User(int no){
		this.no = no;
	}
	// Method
	public void setNo(int no){
		this.no = no;
	}
	public int getNo(){
		return no;
	}
}

9.1 获取class方式

要操作一个类的字节码,需要首先获取到这个类的字节码,获取java.lang.Class实例的三种方式

  • 第一种:Class c = Class.forName(“完整类名带包名”);
  • 第二种:Class c = 对象.getClass();
  • 第三种:Class c = 任何类型.class;

关于Class.forName这个函数

  1. 静态方法
  2. 方法的参数是一个字符串
  3. 字符串需要的是一个完整类名
  4. 完整类名必须带有包名。java.lang包也不能省略

以下是三种创建方式的示意代码

Class c1 = null;
Class c2 = null;
try {
    c1 = Class.forName("java.lang.String"); // c1代表String.class文件,或者说c1代表String类型。
    c2 = Class.forName("java.util.Date"); // c2代表Date类型
    Class c3 = Class.forName("java.lang.Integer"); // c3代表Integer类型
    Class c4 = Class.forName("java.lang.System"); // c4代表System类型
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

// java中任何一个对象都有一个方法:getClass()
String s = "abc";
Class x = s.getClass(); // x代表String.class字节码文件,x代表String类型。
System.out.println(c1 == x); // true(==判断的是对象的内存地址。)

Date time = new Date();
Class y = time.getClass();
System.out.println(c2 == y); // true (c2和y两个变量中保存的内存地址都是一样的,都指向方法区中的字节码文件。)

// 第三种方式,java语言中任何一种类型,包括基本数据类型,它都有.class属性。
Class z = String.class; // z代表String类型
Class k = Date.class; // k代表Date类型
Class f = int.class; // f代表int类型
Class e = double.class; // e代表double类型

System.out.println(x == z); // true

使用这种方式获取class方式,如果只想获取类中单独的静态代码块可以使用这种方式
Class.forName("完整类名");这个方法的执行会导致类加载,类加载时,静态代码块执行

public class ReflectTest04 {
    public static void main(String[] args) {
        try {
            // Class.forName()这个方法的执行会导致:类加载。
            Class.forName("comjava.reflect.MyClass");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

具体实体化的MyClass类为

public class MyClass {

    // 静态代码块在类加载时执行,并且只执行一次。
    static {
        System.out.println("MyClass类的静态代码块执行了!");
    }
}

9.2 实例化对象

通过Class的newInstance()方法来实例化对象

注意:newInstance()方法内部实际上调用了无参数构造方法,必须保证无参构造存在才可以

先创建一个实体类

public class User {
    public User(){
        System.out.println("无参数构造方法!");
    }

    // 定义了有参数的构造方法,无参数构造方法就没了。
    public User(String s){

    }
}
public class ReflectTest02 {
    public static void main(String[] args) {

        // 这是不使用反射机制,创建对象
        User user = new User();
        System.out.println(user);

        // 下面这段代码是以反射机制的方式创建对象。
        try {
            // 通过反射机制,获取Class,通过Class来实例化对象
            Class c = Class.forName("com.java.bean.User"); // c代表User类型。

            // newInstance() 这个方法会调用User这个类的无参数构造方法,完成对象的创建。
            // 重点是:newInstance()调用的是无参构造,必须保证无参构造是存在的!
            Object obj = c.newInstance();

            System.out.println(obj); // com.java.bean.User@10f87f48
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

这种实例化对象不改变java源代码的基础之上,可以做到不同对象的实例化(符合OCP开闭原则:对扩展开放,对修改关闭)

9.3 获取配置文件

通过一个外置的文件来获取关键信息,而不修改代码,实现灵活性

9.3.1 相对路径

创建一个properties存储关键信息,来获取文件并且实例化对象
此处通过前面的io+properties来获取
具体代码简写格式为

 FileReader reader = new FileReader(" ");
// 创建属性类对象Map
Properties pro = new Properties(); // key value都是String
// 加载
pro.load(reader);
// 关闭流
reader.close();

具体代码格式为

public class ReflectTest03 {
    public static void main(String[] args) throws Exception{

        // 这种方式代码就写死了。只能创建一个User类型的对象
        //User user = new User();

        // 以下代码是灵活的,代码不需要改动,可以修改配置文件,配置文件修改之后,可以创建出不同的实例对象。
        // 通过IO流读取classinfo.properties文件
        FileReader reader = new FileReader("chapter25/classinfo2.properties");
        // 创建属性类对象Map
        Properties pro = new Properties(); // key value都是String
        // 加载
        pro.load(reader);
        // 关闭流
        reader.close();

        // 通过key获取value
        String className = pro.getProperty("className");
        //System.out.println(className);

        // 通过反射机制实例化对象
        Class c = Class.forName(className);
        Object obj = c.newInstance();
        System.out.println(obj);
    }
}

以上都是获取文件的相对路径

9.3.2 绝对路径

如果获取文件的绝对路径,即使移植到其它系统或者其他位置还可以识别
可以使用以下方法(但前提是:文件需要在类路径下。才能用这种方式)
所谓的类路径是src目录之下
具体获取绝对路径的代码为

Thread.currentThread().getContextClassLoader().getResource("src下的路径名").getPath(); 
  • Thread.currentThread() 当前线程对象
  • getContextClassLoader() 是线程对象的方法,可以获取到当前线程的类加载器对象
  • getResource() 【获取资源】这是类加载器对象的方法,当前线程的类加载器默认从类的根路径下加载资源
public class AboutPath {
    public static void main(String[] args) throws Exception{
      
        String path = Thread.currentThread().getContextClassLoader()
                .getResource("classinfo2.properties").getPath(); // 这种方式获取文件绝对路径是通用的。

        // 采用以上的代码可以拿到一个文件的绝对路径。
   
        System.out.println(path);

        // 获取db.properties文件的绝对路径(从类的根路径下作为起点开始)
        String path2 = Thread.currentThread().getContextClassLoader()
                .getResource("com//java/bean/db.properties").getPath();
        System.out.println(path2);

    }
}

9.3.3 流的方式

将其替换为
因为流的方式可以直接返回而不用多写一行代码

 // 获取一个文件的绝对路径了!!!!!
String path = Thread.currentThread().getContextClassLoader()
                .getResource("classinfo2.properties").getPath();
FileReader reader = new FileReader(path);

//替换为

// 直接以流的形式返回。
InputStream reader = Thread.currentThread().getContextClassLoader()
                .getResourceAsStream("classinfo2.properties");

完整代码如下

public class IoPropertiesTest {
    public static void main(String[] args) throws Exception{

        // 获取一个文件的绝对路径了!!!!!
        /*String path = Thread.currentThread().getContextClassLoader()
                .getResource("classinfo2.properties").getPath();
        FileReader reader = new FileReader(path);*/

        // 直接以流的形式返回。
        InputStream reader = Thread.currentThread().getContextClassLoader()
                .getResourceAsStream("classinfo2.properties");

        Properties pro = new Properties();
        pro.load(reader);
        reader.close();
        // 通过key获取value
        String className = pro.getProperty("className");
        System.out.println(className);
    }
}

9.3.4 资源绑定器

关于这个类可看我之前的文章
java之ResourceBundle类详细分析(全)

使用这个类直接获取,便于获取属性配置文件中的内容。
使用以下这种方式的时候,属性配置文件xxx.properties必须放到类路径下

public class ResourceBundleTest {
    public static void main(String[] args) {

        // 资源绑定器,只能绑定xxx.properties文件。并且这个文件必须在类路径下。文件扩展名也必须是properties
        // 并且在写路径的时候,路径后面的扩展名不能写。
        //ResourceBundle bundle = ResourceBundle.getBundle("classinfo2");

        ResourceBundle bundle = ResourceBundle.getBundle("com/java/bean/db");

        String className = bundle.getString("className");
        System.out.println(className);

    }
}

科普一下类加载器
概念:专门负责加载类的命令/工具(ClassLoader)
JDK中自带了3个类加载器

  • 启动类加载器:rt.jar
  • 扩展类加载器:ext/*.jar
  • 应用类加载器:classpath

String s = "abc";运行代码的时候,会将所需要类全部加载到JVM当中。通过类加载器加载,看到以上代码类加载器会找String.class,文件,找到就加载
首先通过“启动类加载器”加载(jdk中的jre\lib\rt.jar,jdk中最核心的类库),如果通过“启动类加载器”加载不到的时候,会通过"扩展类加载器"加载(jre\lib\ext*.jar),如果“扩展类加载器”没有加载到,会通过“应用类加载器”加载(环境变量中的classpath中的类)

java中为了保证类加载的安全,使用了双亲委派机制,也就是按照启动类(父)->扩展类(母)->应用类的顺序进行加载

9.4 field

代表属性

9.4.1 编译(了解)

方法功能
Class.forName()获取整个类
getName()获取完整类名
getSimpleName()获取简单名
getFields()获取类中所有的public修饰的Field,返回的是一个数组类型,之后还要通过getName()
getDeclaredFields()获取所有的Field,返回的是一个数组类型,之后还要通过getName()
getModifiers()获取修饰符代号
Modifier.toString()代号数字转换为字符串,结合在一起也就是Modifier.toString(对象.getModifiers())
getType()获取属性类型

以上方法展示的代码如下
设置一个成员变量的类

// 反射属性Field
public class Student {

    // Field翻译为字段,其实就是属性/成员
    // 4个Field,分别采用了不同的访问控制权限修饰符
    private String name; // Field对象
    protected int age; // Field对象
    boolean sex;
    public int no;
    public static final double MATH_PI = 3.1415926;
}

功能代码展示

public class ReflectTest05 {
    public static void main(String[] args) throws Exception{

        // 获取整个类
        Class studentClass = Class.forName("com.java.bean.Student");

        //com.java.bean.Student
        String className = studentClass.getName();
        System.out.println("完整类名:" + className);

		//student
        String simpleName = studentClass.getSimpleName();
        System.out.println("简类名:" + simpleName);

        // 获取类中所有的public修饰的Field
        Field[] fields = studentClass.getFields();
        System.out.println(fields.length); // 测试数组中只有1个元素
        // 取出这个Field
        Field f = fields[0];
        // 取出这个Field它的名字
        String fieldName = f.getName();
        System.out.println(fieldName);

        // 获取所有的Field
        Field[] fs = studentClass.getDeclaredFields();
        System.out.println(fs.length); // 4

        System.out.println("==================================");
        // 遍历
        for(Field field : fs){
            // 获取属性的修饰符列表
            int i = field.getModifiers(); // 返回的修饰符是一个数字,每个数字是修饰符的代号!!!
            System.out.println(i);
            // 可以将这个“代号”数字转换成“字符串”吗?
            String modifierString = Modifier.toString(i);
            System.out.println(modifierString);
            // 获取属性的类型
            Class fieldType = field.getType();
            //String fName = fieldType.getName();
            String fName = fieldType.getSimpleName();
            System.out.println(fName);
            // 获取属性的名字
            System.out.println(field.getName());
        }
    }
}

9.4.2 反编译(了解)

比如public class xx{ } 或者是public int age
都是
获取所有field属性的值,可以使用getDeclaredFields()

  • Modifier.toString(获取对象.getModifiers()) 获取public
  • field.getType().getSimpleName() 获取int
  • field.getName()获取age
public class ReflectTest06 {
    public static void main(String[] args) throws Exception{

        // 创建这个是为了拼接字符串。
        StringBuilder s = new StringBuilder();

        //Class studentClass = Class.forName("com.java.bean.Student");
        Class studentClass = Class.forName("java.lang.Thread");

        s.append(Modifier.toString(studentClass.getModifiers()) + " class " + studentClass.getSimpleName() + " {\n");

        Field[] fields = studentClass.getDeclaredFields();
        for(Field field : fields){
            s.append("\t");
            s.append(Modifier.toString(field.getModifiers()));
            s.append(" ");
            s.append(field.getType().getSimpleName());
            s.append(" ");
            s.append(field.getName());
            s.append(";\n");
        }

        s.append("}");
        System.out.println(s);

    }
}

9.4.3 设置属性(重要)

通过给field属性设置属性值

  • 获取对象类forName("com.java.bean.Student")
  • 创建一个对象newInstance()
  • 根据属性的名称来获取FieldgetDeclaredField("no")
  • 给对象赋值set(obj, 22222)

以上都是public
如果遇到private还要中间使用权限setAccessible(true);

public class ReflectTest07 {
    public static void main(String[] args) throws Exception{

        // 我们不使用反射机制,怎么去访问一个对象的属性呢?
        Student s = new Student();

        // 给属性赋值
        s.no = 1111; //三要素:给s对象的no属性赋值1111
                    //要素1:对象s
                    //要素2:no属性
                    //要素3:1111

        // 读属性值
        // 两个要素:获取s对象的no属性的值。
        System.out.println(s.no);

        // 使用反射机制,怎么去访问一个对象的属性。(set get)
        Class studentClass = Class.forName("com.java.bean.Student");
        Object obj = studentClass.newInstance(); // obj就是Student对象。(底层调用无参数构造方法)

        // 获取no属性(根据属性的名称来获取Field)
        Field noFiled = studentClass.getDeclaredField("no");

        // 给obj对象(Student对象)的no属性赋值
        /*
        虽然使用了反射机制,但是三要素还是缺一不可:
            要素1:obj对象
            要素2:no属性
            要素3:2222值
        注意:反射机制让代码复杂了,但是为了一个“灵活”,这也是值得的。
         */
        noFiled.set(obj, 22222); // 给obj对象的no属性赋值2222

        // 读取属性的值
        // 两个要素:获取obj对象的no属性的值。
        System.out.println(noFiled.get(obj));

        // 可以访问私有的属性吗?
        Field nameField = studentClass.getDeclaredField("name");

        // 打破封装(反射机制的缺点:打破封装,可能会给不法分子留下机会!!!)
        // 这样设置完之后,在外部也是可以访问private的。
        nameField.setAccessible(true);

        // 给name属性赋值
        nameField.set(obj, "jackson");
        // 获取name属性的值
        System.out.println(nameField.get(obj));
    }
}

9.5 Method

9.5.1 编译(了解)

public void xx{
}
  • 获取类
  • 获取权限都是使用getDeclaredMethods()
  • Modifier.toString(method.getModifiers())获取public
  • method.getReturnType().getSimpleName()获取void

如果有多的参数名还需要获取参数名,因为重载的话还要判定参数名多少个的异同
方法为getParameterTypes().getSimpleName()

public class ReflectTest08 {
    public static void main(String[] args) throws Exception{

        // 获取类了
        Class userServiceClass = Class.forName("com.java.service.UserService");

        // 获取所有的Method(包括私有的!)
        Method[] methods = userServiceClass.getDeclaredMethods();
        //System.out.println(methods.length); // 2

        // 遍历Method
        for(Method method : methods){
            // 获取修饰符列表
            System.out.println(Modifier.toString(method.getModifiers()));
            // 获取方法的返回值类型
            System.out.println(method.getReturnType().getSimpleName());
            // 获取方法名
            System.out.println(method.getName());
            // 方法的修饰符列表(一个方法的参数可能会有多个。)
            Class[] parameterTypes = method.getParameterTypes();
            for(Class parameterType : parameterTypes){
                System.out.println(parameterType.getSimpleName());
            }
        }
    }
}

9.5.2 反编译(了解)

public class ReflectTest09 {
    public static void main(String[] args) throws Exception{
        StringBuilder s = new StringBuilder();
        //Class userServiceClass = Class.forName("com.java.service.UserService");
        Class userServiceClass = Class.forName("java.lang.String");
        s.append(Modifier.toString(userServiceClass.getModifiers()) + " class "+userServiceClass.getSimpleName()+" {\n");

        Method[] methods = userServiceClass.getDeclaredMethods();
        for(Method method : methods){
            //public boolean login(String name,String password){}
            s.append("\t");
            s.append(Modifier.toString(method.getModifiers()));
            s.append(" ");
            s.append(method.getReturnType().getSimpleName());
            s.append(" ");
            s.append(method.getName());
            s.append("(");
            // 参数列表
            Class[] parameterTypes = method.getParameterTypes();
            for(Class parameterType : parameterTypes){
                s.append(parameterType.getSimpleName());
                s.append(",");
            }
            // 删除指定下标位置上的字符
            s.deleteCharAt(s.length() - 1);
            s.append("){}\n");
        }

        s.append("}");
        System.out.println(s);
    }
}

9.5.3 调用方法(重要)

  • 获取对象类forName("com.java.bean.Student")
  • 创建一个对象newInstance()
  • 获取方法getDeclaredMethod("login", String.class, String.class);
  • 进行传值invoke(obj, "admin","123123")
public class ReflectTest10 {
    public static void main(String[] args) throws Exception{
        // 不使用反射机制,怎么调用方法
        // 创建对象
        UserService userService = new UserService();
        // 调用方法
        /*
        要素分析:
            要素1:对象userService
            要素2:login方法名
            要素3:实参列表
            要素4:返回值
         */
        boolean loginSuccess = userService.login("admin","123");
        //System.out.println(loginSuccess);
        System.out.println(loginSuccess ? "登录成功" : "登录失败");

        // 使用反射机制来调用一个对象的方法该怎么做?
        Class userServiceClass = Class.forName("com.java.service.UserService");
        // 创建对象
        Object obj = userServiceClass.newInstance();
        // 获取Method
        Method loginMethod = userServiceClass.getDeclaredMethod("login", String.class, String.class);
        //Method loginMethod = userServiceClass.getDeclaredMethod("login", int.class);
        // 调用方法
        // 调用方法有几个要素? 也需要4要素。
        // 反射机制中最最最最最重要的一个方法,必须记住。
        /*
        四要素:
        loginMethod方法
        obj对象
        "admin","123" 实参
        retValue 返回值
         */
        Object retValue = loginMethod.invoke(obj, "admin","123123");
        System.out.println(retValue);
    }
}

9.5.4 可变参数(重要)

此为科普章节
上面的方法中调用了一个getDeclaredMethod("login", String.class, String.class);
查看其源码可以看到
在这里插入图片描述
所谓的可变参数是Class<?>... parameterTypes
具体定义的规则是类型...(注意:一定是3个点。)

  1. 可变长度参数要求的参数个数是:0~N个。
  2. 可变长度参数在参数列表中必须在最后一个位置上,而且可变长度参数只能有1个。
  3. 可变长度参数可以当做一个数组来看待

具体测试代码展示如下

public class ArgsTest {
    public static void main(String[] args) {
        m();
        m(10);
        m(10, 20);

        // 编译报错
        //m("abc");

        m2(100);
        m2(200, "abc");
        m2(200, "abc", "def");
        m2(200, "abc", "def", "xyz");

        m3("ab", "de", "kk", "ff");

        String[] strs = {"a","b","c"};
        // 也可以传1个数组
        m3(strs);

        // 直接传1个数组
        m3(new String[]{"我","是","中","国", "人"}); //没必要

        m3("我","是","中","国", "人");
    }

    public static void m(int... args){
        System.out.println("m方法执行了!");
    }

    //public static void m2(int... args2, String... args1){}

    // 必须在最后,只能有1个。
    public static void m2(int a, String... args1){

    }

    public static void m3(String... args){
        //args有length属性,说明args是一个数组!
        // 可以将可变长度参数当做一个数组来看。
        for(int i = 0; i < args.length; i++){
            System.out.println(args[i]);
        }
    }

}

9.6 Constructor

9.6.1 反编译(了解)

反编译一个类的构造方法

和上面的方法都大同小异,此处就省略了

public class ReflectTest11 {
    public static void main(String[] args) throws Exception{
        StringBuilder s = new StringBuilder();
        Class vipClass = Class.forName("java.lang.String");
        s.append(Modifier.toString(vipClass.getModifiers()));
        s.append(" class ");
        s.append(vipClass.getSimpleName());
        s.append("{\n");

        // 拼接构造方法
        Constructor[] constructors = vipClass.getDeclaredConstructors();
        for(Constructor constructor : constructors){
            //public Vip(int no, String name, String birth, boolean sex) {
            s.append("\t");
            s.append(Modifier.toString(constructor.getModifiers()));
            s.append(" ");
            s.append(vipClass.getSimpleName());
            s.append("(");
            // 拼接参数
            Class[] parameterTypes = constructor.getParameterTypes();
            for(Class parameterType : parameterTypes){
                s.append(parameterType.getSimpleName());
                s.append(",");
            }
            // 删除最后下标位置上的字符
            if(parameterTypes.length > 0){
                s.deleteCharAt(s.length() - 1);
            }
            s.append("){}\n");
        }

        s.append("}");
        System.out.println(s);
    }
}

9.6.2 调用有参构造(了解)

一个实体类如下
在这里插入图片描述
newInstance();只是创建一个实体类,但是只会调用一个无参构造

如果要调用有参构造

  • 通过使用该函数.getDeclaredConstructor(int.class, String.class, String.class,boolean.class);,class要与属性一一对应
  • 通过newInstance(110, "jackson", "1990-10-11", true);给属性一一赋值即可
public class ReflectTest12 {
    public static void main(String[] args) throws Exception{
        // 不使用反射机制怎么创建对象
        Vip v1 = new Vip();
        Vip v2 = new Vip(110, "zhangsan", "2001-10-11", true);

        // 使用反射机制怎么创建对象呢?
        Class c = Class.forName("com.bjpowernode.java.bean.Vip");
        // 调用无参数构造方法
        Object obj = c.newInstance();
        System.out.println(obj);

        // 调用有参数的构造方法怎么办?
        // 第一步:先获取到这个有参数的构造方法
        Constructor con = c.getDeclaredConstructor(int.class, String.class, String.class,boolean.class);
        // 第二步:调用构造方法new对象
        Object newObj = con.newInstance(110, "jackson", "1990-10-11", true);
        System.out.println(newObj);

        // 获取无参数构造方法
        Constructor con2 = c.getDeclaredConstructor();
        Object newObj2 = con2.newInstance();
        System.out.println(newObj2);
    }
}

9.7 获取父类以及接口(重要)

  • 获取父类getSuperclass()
  • 获取实现接口getInterfaces();
public class ReflectTest13 {
    public static void main(String[] args) throws Exception{

        // String举例
        Class stringClass = Class.forName("java.lang.String");

        // 获取String的父类
        Class superClass = stringClass.getSuperclass();
        System.out.println(superClass.getName());

        // 获取String类实现的所有接口(一个类可以实现多个接口。)
        Class[] interfaces = stringClass.getInterfaces();
        for(Class in : interfaces){
            System.out.println(in.getName());
        }
    }
}

10. 注解

  • 注解Annotation是一种引用数据类型。编译之后也是生成xxx.class文件
  • 可以出现在类上、属性上、方法上、变量上,还可以出现在注解类型上等…
[修饰符列表] @interface 注解类型名{

}

java.lang包下的注解类型:

  • 掌握:Deprecated 用 @Deprecated 注释的程序元素(鼓励程序员使用这样的元素,通常是因为它很危险或存在更好的选择)
  • 掌握:Override 表示一个方法声明打算重写超类中的另一个方法声明
  • 不用掌握:SuppressWarnings 指示应该在注释元素(以及包含在该注释元素中的所有程序元素)中取消显示指定的编译器警告

因为Override比较熟悉,此处就不给出
关于@Deprecated源码
在这里插入图片描述

加上注解,调用其方法的时候,在编译器中该代码会出现一条横杠,代表过时,但是还是可以使用

10.1 自定义注解

自定义的注解要满足上面的规则定义

public @interface MyAnnotation {

    /**
     * 我们通常在注解当中可以定义属性,以下这个是MyAnnotation的name属性。
     * 看着像1个方法,但实际上我们称之为属性name。
     * @return
     */
    String name();

    /*
    颜色属性
     */
    String color();

    /*
    年龄属性
     */
    int age() default 25; //属性指定默认值

}

具体引用注解通过如下方式
如果一个注解当中有属性,那么必须给属性赋值。(除非该属性使用default指定了默认值。)

public class MyAnnotationTest {

    // 报错的原因:如果一个注解当中有属性,那么必须给属性赋值。(除非该属性使用default指定了默认值。)
    /*@MyAnnotation
    public void doSome(){

    }*/

    //@MyAnnotation(属性名=属性值,属性名=属性值,属性名=属性值)
    //指定name属性的值就好了。
    @MyAnnotation(name = "zhangsan", color = "红色")
    public void doSome(){

    }

}

关于注解的注意事项

  • 如果一个注解的属性的名字是value,并且只有一个属性的话,在使用的时候,该属性名可以省略
    可以省略的情况,因为只定义了一个value属性
    在这里插入图片描述
    不可以省略,因为不是value属性,而且如果value属性必须只有一个
    在这里插入图片描述
  • 关于注解的使用类型规范
    属性的类型可以是: byte short int long float double boolean char String Class 枚举类型以及以上每一种的数组形式

以下情况是枚举类型的定义加注解

枚举的代码格式

public enum Season {
    SPRING,SUMMER,AUTUMN,WINTER
}

注解代码格式

public @interface OtherAnnotation {
    /*
    年龄属性
     */
    int age();

    /*
    邮箱地址属性,支持多个
     */
    String[] email();

    /**
     * 季节数组,Season是枚举类型
     * @return
     */
    Season[] seasonArray();
}

具体引用注解时根据不同类型

  • 如果数组中只有1个元素:大括号可以省略
  • 数组是大括号,枚举元素要加对象名
public class OtherAnnotationTest {

    // 数组是大括号
    @OtherAnnotation(age = 25, email = {"zhangsan@123.com", "zhangsan@sohu.com"}, seasonArray = Season.WINTER)
    public void doSome(){

    }

    // 如果数组中只有1个元素:大括号可以省略。
    @OtherAnnotation(age = 25, email = "zhangsan@123.com", seasonArray = {Season.SPRING, Season.SUMMER})
    public void doOther(){

    }

}

10.2 元注解

用来标注“注解类型”的“注解”,称为元注解
常见的元注解有:Target、Retention

关于target具体源码

  • 表示“被标注的注解”只能出现在构造方法上、字段上、局部变量上、方法上、类上…
  • 这个Target注解用来标注“被标注的注解”可以出现在哪些位置上
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

具体枚举的元素有
在这里插入图片描述
关于Retention注解

  • 这个Retention注解用来标注“被标注的注解”最终保存在哪里

其源码为

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

其枚举类型为
在这里插入图片描述

  • @Retention(RetentionPolicy.SOURCE):表示该注解只被保留在java源文件中。
  • @Retention(RetentionPolicy.CLASS):表示该注解被保存在class文件中。
  • @Retention(RetentionPolicy.RUNTIME):表示该注解被保存在class文件中,并且可以被反射机制所读取。

具体引用该注解可以为

//@Retention(value=RetentionPolicy.RUNTIME)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation{}

10.3 反射注解

10.3.1 注解类

运用以上注解以及反射进行加深巩固

自定义一个注解
如果让注解可以识别属性,则添加为ElementType.FIELD

//只允许该注解可以标注类、方法
@Target({ElementType.TYPE, ElementType.METHOD})
// 希望这个注解可以被反射
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {

    /*
    value属性。
     */
    String value() default "北京大兴区";//表示如果属性实体类上没有value则给一个默认值
}

实体类

@MyAnnotation("上海浦东区")
public class MyAnnotationTest {

    //@MyAnnotation 不可定义,因为没有标识属性
    int i;

    //@MyAnnotation  不可定义,因为没有标识构造函数
    public MyAnnotationTest(){

    }

    @MyAnnotation
    public void doSome(){

        //@MyAnnotation  不可定义,因为没有标识局部变量
        int i;
    }

}

写一个测试类通过反射机制查询其注解相关

  • 先获取该类通过forName
  • 判断其该类是否有该注解isAnnotationPresent(MyAnnotation.class),只有这个才可以使用注解,因为注解中用了 @Retention(RetentionPolicy.RUNTIME)
  • 如果有其注解,直接获取注解对象getAnnotation(MyAnnotation.class)
  • 之后直接调用方法即可
public class ReflectAnnotationTest {
    public static void main(String[] args) throws Exception{
        // 获取这个类
        Class c = Class.forName("com.java.annotation5.MyAnnotationTest");
        // 判断类上面是否有@MyAnnotation
        //System.out.println(c.isAnnotationPresent(MyAnnotation.class)); // true
        if(c.isAnnotationPresent(MyAnnotation.class)){
            // 获取该注解对象
            MyAnnotation myAnnotation = (MyAnnotation)c.getAnnotation(MyAnnotation.class);
            //System.out.println("类上面的注解对象" + myAnnotation); // @com.bjpowernode.java.annotation5.MyAnnotation()
            // 获取注解对象的属性怎么办?和调接口没区别。
            String value = myAnnotation.value();
            System.out.println(value);
        }

        // 判断String类上面是否存在这个注解
        Class stringClass = Class.forName("java.lang.String");
        System.out.println(stringClass.isAnnotationPresent(MyAnnotation.class)); // false
    }
}

10.3.2 注解方法

  • 获取注解类对象getAnnotation(MyAnnotation.class)
  • 获取注解方法对象getDeclaredMethod("doSome")

注解类

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

    /*
    username属性
     */
    String username();

    /*
    password属性
     */
    String password();
}

测试类

public class MyAnnotationTest {

    @MyAnnotation(username = "admin", password = "456456")
    public void doSome(){

    }

    public static void main(String[] args) throws Exception{
        // 获取MyAnnotationTest的doSome()方法上面的注解信息。
        Class c = Class.forName("com.java.annotation6.MyAnnotationTest");
        // 获取doSome()方法
        Method doSomeMethod = c.getDeclaredMethod("doSome");
        // 判断该方法上是否存在这个注解
        if(doSomeMethod.isAnnotationPresent(MyAnnotation.class)) {
            MyAnnotation myAnnotation = doSomeMethod.getAnnotation(MyAnnotation.class);
            System.out.println(myAnnotation.username());
            System.out.println(myAnnotation.password());
        }
    }

}

10.4 实战开发

结合注解和反射机制

这个注解@Id用来标注类,被标注的类中必须有一个int类型的id属性,没有就报异常。

注解类

// 表示这个注解只能出现在类上面
@Target(ElementType.TYPE)
// 该注解可以被反射机制读取到
@Retention(RetentionPolicy.RUNTIME)
public @interface Id {

}

实体类

@Id
public class User {
    int id;
    String name;
    String password;
}

自定义一个异常类

public class HasNotIdPropertyException extends RuntimeException {
    public HasNotIdPropertyException(){

    }
    public HasNotIdPropertyException(String s){
        super(s);
    }
}

测试类

public class Test {
    public static void main(String[] args) throws Exception{
        // 获取类
        Class userClass = Class.forName("com.java.annotation7.User");
        // 判断类上是否存在Id注解
        if(userClass.isAnnotationPresent(Id.class)){
            // 当一个类上面有@Id注解的时候,要求类中必须存在int类型的id属性
            // 如果没有int类型的id属性则报异常。
            // 获取类的属性
            Field[] fields = userClass.getDeclaredFields();
            boolean isOk = false; // 给一个默认的标记
            for(Field field : fields){
                if("id".equals(field.getName()) && "int".equals(field.getType().getSimpleName())){
                    // 表示这个类是合法的类。有@Id注解,则这个类中必须有int类型的id
                    isOk = true; // 表示合法
                    break;
                }
            }

            // 判断是否合法
            if(!isOk){
                throw new HasNotIdPropertyException("被@Id注解标注的类中必须要有一个int类型的id属性!");
            }

        }
    }
}

完结撒花
完结撒花
完结撒花


至此
java的基础篇章已经结束
可根据博主之前学过的路线进行学习
java框架零基础从入门到精通的学习路线(超全)

学习的同时
记得
一键三连加关注不迷路
一键三连加关注不迷路
一键三连加关注不迷路
一键三连加关注不迷路

  • 13
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农研究僧

你的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值