Java - 基础知识整理

编译型语言与解释型语言

计算机是不能理解高级语言的,更不能直接执行高级语言,所以任何的高级语言编写的程序,要想被计算机执行,都必须将其转换成计算机可以识别的机器语言,也就是机器码。
这种转换方式有两种:

  • 编译
  • 解释
    据此,高级语言可以被划分为解释性语言和编译型语言。
    主要区别在于:
    编译型语言:源程序编译之后即可在该平台运行,所以运行速度相对较快。
    解释性语言:在运行期间编译,所以其跨平台性较好。

编译型语言

使用专门的编译器,针对特定的平台,将高级语言的源代码一次性的编译成平台硬件可以执行的机器码,并包装成该平台所能识别的可执行性程序的格式。
特点:
在程序执行之前需要一个专门的编译过程,将源代码编译成硬件平台可以识别的机器码,譬如exe格式的文件,之后再运行时,直接使用编译结果即可,如直接运行exe文件。因此,只需要编译一次,所以其执行效率高。

  • 一次性的编译成平台相关的机器语言文件,运行是脱离开发环境,运行效率高
  • 与特定平台有关,一般无法移植到其他平台。(C,C++,Objective等)

解释型语言

使用专门的解释器对源代码逐行解释成特定平台的机器码,并立即执行。注意:是代码在执行是才被解释器逐行动态翻译和执行,而不是在执行之前就完成翻译的。
特点:
解释型语言不需要提前编译,是直接动态的将源代码解释成机器码并立即执行,因此只有某一平太提供了相应的解释器即可运行该程序。

  • 解释型语言每次运行都需要将源代码解释成机器码并执行,所以效率低下。
  • 只要某一平台提供了相应的解释器,就可以运行该程序,方便移植。(Python等)

JAVA是解释型语言还是编译型语言?

个人认为:java兼具两种语言的特性:既需要一次编译,又需要一个特定的运行环境(JVM),在任何一个平台上,只有运行的有JVM,即可运行Java编译后的字节码(.class)文件

按值传递与按引用传递

按值传递

按值传递类似与把值复制一份过去,不会对原有的变量值产生影响。
基本数据类型都是按值传递的。

按引用传递

按引用传递其实就是按引用值传递,传递的是变量的引用地址,给参数传递的是同一个引用地址,传递后参数和变量指向的是同一个引用地址,因此在方法中改变了变量内容,将会影响到变量本身。
引用类型的变量传递都是按引用传递的。

示例:

package com.example.test;

public class DemoJavaSe {
    public static void main(String[] args) {
        //测试按值传递和按引用传递
        testDiliverWay();
    }

    /**
     * 测试按值传递和按引用传递
     */
    public static void testDiliverWay(){
        int a = 1;
        long b = 2l;
        int[] intArr = new int[]{1,2,3};
        System.out.println("old value for a:"+a);
        System.out.println("old value for b:"+b);
        System.out.println("old value for intArr:"+intArr[1]);
        testDili(a,b,intArr);
        System.out.println("new value for a:"+a);
        System.out.println("new value for b:"+b);
        System.out.println("new value for intArr:"+intArr[1]);
    }
    private static void testDili(int aa, long bb, int[] intArr){
        aa = aa+10;
        bb = bb+10l;
        intArr[1] = 10;
    }
}

/*
输出结果:
old value for a:1
old value for b:2
old value for intArr:2
new value for a:1
new value for b:2
new value for intArr:10
*/

java编译命令

待续。。。

Java数据类型

Java中数据类型主要分为两类:

  • 基本数据类型
  • 引用数据类型

Java基本数据类型

Java引用数据类型

  • 引用类型执行一个对象,执行对象的变量成为应用变量。
  • 对象、数组都是引用数据类型。
  • 所有引用类型的默认值都是null
  • 一个引用变量可以用来引用任何与之兼容的类型。

Java修饰符

Java语言提供了很多修饰符,修饰符主要用来定义类、方法、变量等,通常放在语句的最前端。
Java的修饰符主要分为两个大类:

  • 访问控制修饰符
  • 非访问修饰符

访问控制修饰符

修饰符 当前类 同一个包内 子孙类(同一个包) 子孙类(不同包) 其他包
public Y Y Y Y Y
protected Y Y Y Y/N N
default Y Y Y N N
private Y N N N N
protected的说明:
  • 子类与基类在同一包中:被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问;
  • 子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的protected方法。

非访问控制修饰符

static

final

abstract

synchronized

transient

volatile

继承和多态

抽象类

接口

数组

用来存储同一类型数据的容器。

数组的声明方式

//方式一
int[] ints = new int[4];//初始话长度为4,每个默认值都是0
//方式二
int[] ints1 = new int[]{1,2};//长度为2,默认给了初始值
//方式三
int[] ints2 = {1,2};//初始化长度2,默认给了初始化值

//定义二维数组
int[][] ints3 = {{1,2,3},{3,4},{5,6}};
/*
* [1,2,3
* 3,4
* 5,6]
* */

数组的特点:

  • 长度是确定的,数组一旦声明,其长度就确定了。
  • 同一个数组中的元素必须相同。
  • 获取数组中的元素只能通过脚标获取。

java集合

存放某一类数据的集合。不同的数据结构用来实现不同的集合,数组结构用于List集合,hash结构可以用于Map集合,链表结构用于LinkedList,LinkedMap

Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为二叉树,元素排好序

Map 接口 键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全-
│—————–├ LinkedHashMap 双向链表和哈希表实现
│—————–└ WeakHashMap
├ ——–TreeMap 红黑树对所有的key进行排序
└———IdentifyHashMap

这里涉及到数据结构。

线程

线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程包含以下内容:

  • 一个指向当前被执行指令的指令指针;
  • 一个栈;
  • 一个寄存器值的集合,定义了一部分描述正在执行线程的处理器状态的值
  • 一个私有的数据区。
    在Java中,每次程序运行至少启动2个线程:一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM实际上就是在操作系统中启动了一个进程。

Java线程的声明方式

1.继承Thread类,实现线程

run()为线程类的核心方法,相当于主线程的main方法,是每个线程的入口,是启动线程后程序执行的入口

  • 一个线程调用 两次start()方法将会抛出线程状态异常,也就是的start()只可以被调用一次
  • native生明的方法只有方法名,没有方法体。是本地方法,不是抽象方法,而是调用c语言方法registerNative()方法包含了所有与线程相关的操作系统方法
  • run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)
package com.example.test.thread;

public class CustomThreadByExtendsThread extends Thread{

    public void run() {
        for(int i=0;i<5;i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
    public static void main(String[] args) {
        CustomThreadByExtendsThread mThread1=new CustomThreadByExtendsThread();
        CustomThreadByExtendsThread mThread2=new CustomThreadByExtendsThread();
        CustomThreadByExtendsThread myThread3=new CustomThreadByExtendsThread();
        mThread1.start();
        mThread2.start();
        myThread3.start();
    }
}
/*结果:
Thread-1:0
Thread-1:1
Thread-1:2
Thread-1:3
Thread-1:4
Thread-2:0
Thread-2:1
Thread-0:0
Thread-2:2
Thread-2:3
Thread-2:4
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
*/

Thread类也是实现了Runnable接口

Runnable接口中只定义了一个抽象方法

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface {@code Runnable} is used
     * to create a thread, starting the thread causes the object's
     * {@code run} method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method {@code run} is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

2.实现Runnable接口,覆写run()方法(推荐)

因为实现Runnable接口,可以避免Java单继承的局限性,因此推荐这种方式。

package com.example.test.thread;

public class CustomeThreadByImplRunnable implements Runnable{

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

    public static void main(String[] args) {
        CustomeThreadByImplRunnable customeThreadByImplRunnable=new CustomeThreadByImplRunnable();
        Thread mThread1=new Thread(customeThreadByImplRunnable,"线程1");
        Thread mThread2=new Thread(customeThreadByImplRunnable,"线程2");
        Thread mThread3=new Thread(customeThreadByImplRunnable,"线程3");
        mThread1.start();
        mThread2.start();
        mThread3.start();
    }
}
/*
线程1:0
线程3:0
线程2:0
线程2:1
线程2:2
线程3:1
线程1:1
线程3:2
线程2:3
线程1:2
线程2:4
线程3:3
线程1:3
线程3:4
线程1:4
*/

实现Callable接口

  • 覆写call()方法
  • 有返回值*
package com.example.test.thread;

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

public class CustomeThreadByImplCallable implements Callable<String> {
    private static int count = 20;
    @Override
    public String call() throws Exception {
        for(int i=count;i>0;i--) {
            Thread.yield();
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
        return "test callable implements";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> callable = new CustomeThreadByImplCallable();
        FutureTask<String> futureTask = new FutureTask<>(callable);
        Thread mThread=new Thread(futureTask);
        mThread.setName("线程3");
        Thread mThread2=new Thread(futureTask);
        mThread2.setName("线程2");
        Thread mThread3=new Thread(futureTask);
        mThread3.setName("线程1");
//        for(int i =0;i<5;i++){
            mThread.start();
            mThread2.start();
            mThread3.start();
//        }
        System.out.println(futureTask.get());
    }
}

注意:
为什么运行的时候只打印了一个线程的结果?
为什么运行三次,得到的分别是线程1、线程2、线程3 这样的结果?

线程2:20
线程2:19
线程2:18
线程2:17
线程2:16
线程2:15
线程2:14
线程2:13
线程2:12
线程2:11
线程2:10
线程2:9
线程2:8
线程2:7
线程2:6
线程2:5
线程2:4
线程2:3
线程2:2
线程2:1
test callable implements

通过线程池启动多线程

通过Executor 的工具类可以创建三种类型的普通线程池:

FixThreadPool(int n); 固定大小的线程池

使用于为了满足资源管理需求而需要限制当前线程数量的场合。使用于负载比较重的服务器。

package com.example.test.thread;

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

public class FixedThreadPoolTest {

    public static void main(String[] args) {
        //创建一个固定大小的线程池
        ExecutorService ex = Executors.newFixedThreadPool(5);
        /**
         * 这里设置成小于5的数值,就只会有小于5的固定的线程执行
         * 设置的大于5的数值,就会是有5个线程一起随机执行
         * 不会出现大于5个线程运行的情况
         */
        for(int i=0;i<30;i++) {
            ex.submit(new Runnable() {
                @Override
                public void run() {
                    for(int j=0;j<5;j++) {
                        System.out.println("线程"+Thread.currentThread().getName()+"执行第"+j+"次");
                    }
                }
            });
        }
        //获取线程返回数据
        ex.shutdown();
    }
}

执行结果:

线程pool-1-thread-1执行第0次
线程pool-1-thread-5执行第0次
线程pool-1-thread-2执行第0次
线程pool-1-thread-3执行第0次
线程pool-1-thread-3执行第1次
线程pool-1-thread-4执行第0次
线程pool-1-thread-3执行第2次
线程pool-1-thread-2执行第1次
线程pool-1-thread-5执行第1次
线程pool-1-thread-1执行第1次
线程pool-1-thread-5执行第2次
线程pool-1-thread-2执行第2次
线程pool-1-thread-3执行第3次
线程pool-1-thread-4执行第1次
线程pool-1-thread-3执行第4次
线程pool-1-thread-2执行第3次
线程pool-1-thread-5执行第3次
线程pool-1-thread-1执行第2次
线程pool-1-thread-5执行第4次
线程pool-1-thread-2执行第4次
线程pool-1-thread-4执行第2次
线程pool-1-thread-1执行第3次
线程pool-1-thread-4执行第3次
线程pool-1-thread-1执行第4次
线程pool-1-thread-5执行第0次
线程pool-1-thread-4执行第4次
线程pool-1-thread-5执行第1次
线程pool-1-thread-5执行第2次
线程pool-1-thread-5执行第3次
线程pool-1-thread-5执行第4次

SingleThreadPoolExecutor :单线程池

需要保证顺序执行各个人物的场景

package com.example.test.thread;

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

public class SingleThreadPoolExecuor {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for(int i=0;i<6;i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    for(int j=0;j<5;j++) {
                        System.out.println("线程"+Thread.currentThread().getName()+"执行第"+j+"次");
                    }
                }
            });
        }
        //获取线程返回数据
        executorService.shutdown();
    }
}

运行结果:

线程pool-1-thread-1执行第0次
线程pool-1-thread-1执行第1次
线程pool-1-thread-1执行第2次
线程pool-1-thread-1执行第3次
线程pool-1-thread-1执行第4次
线程pool-1-thread-1执行第0次
线程pool-1-thread-1执行第1次
线程pool-1-thread-1执行第2次
线程pool-1-thread-1执行第3次
线程pool-1-thread-1执行第4次
线程pool-1-thread-1执行第0次
线程pool-1-thread-1执行第1次
线程pool-1-thread-1执行第2次
线程pool-1-thread-1执行第3次
线程pool-1-thread-1执行第4次
线程pool-1-thread-1执行第0次
线程pool-1-thread-1执行第1次
线程pool-1-thread-1执行第2次
线程pool-1-thread-1执行第3次
线程pool-1-thread-1执行第4次
线程pool-1-thread-1执行第0次
线程pool-1-thread-1执行第1次
线程pool-1-thread-1执行第2次
线程pool-1-thread-1执行第3次
线程pool-1-thread-1执行第4次
线程pool-1-thread-1执行第0次
线程pool-1-thread-1执行第1次
线程pool-1-thread-1执行第2次
线程pool-1-thread-1执行第3次
线程pool-1-thread-1执行第4次

CashedThreadPool(); 缓存线程池

当提交任务速度高于线程池中任务处理速度时,缓存线程池会不断的创建线程
适用于提交短期的异步小程序,以及负载较轻的服务器

package com.example.test.thread;

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

public class CacheThreadPoolTest {

    public static void main(String[] args) {
        //创建一个缓存线程下池
        ExecutorService executorService = Executors.newCachedThreadPool();
        for(int i=0;i<6;i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    for(int j=0;j<5;j++) {
                        System.out.println("线程"+Thread.currentThread().getName()+"执行第"+j+"次");
                    }
                }
            });
        }
        //获取线程返回数据
        executorService.shutdown();
    }
}

运行结果:

线程pool-1-thread-4执行第0次
线程pool-1-thread-2执行第0次
线程pool-1-thread-1执行第0次
线程pool-1-thread-3执行第0次
线程pool-1-thread-6执行第0次
线程pool-1-thread-6执行第1次
线程pool-1-thread-5执行第0次
线程pool-1-thread-6执行第2次
线程pool-1-thread-5执行第1次
线程pool-1-thread-3执行第1次
线程pool-1-thread-1执行第1次
线程pool-1-thread-2执行第1次
线程pool-1-thread-1执行第2次
线程pool-1-thread-4执行第1次
线程pool-1-thread-1执行第3次
线程pool-1-thread-2执行第2次
线程pool-1-thread-3执行第2次
线程pool-1-thread-5执行第2次
线程pool-1-thread-6执行第3次
线程pool-1-thread-6执行第4次
线程pool-1-thread-5执行第3次
线程pool-1-thread-3执行第3次
线程pool-1-thread-2执行第3次
线程pool-1-thread-1执行第4次
线程pool-1-thread-4执行第2次
线程pool-1-thread-2执行第4次
线程pool-1-thread-3执行第4次
线程pool-1-thread-5执行第4次
线程pool-1-thread-4执行第3次
线程pool-1-thread-4执行第4次

线程的状态

image
image
image

线程运行的几个方法

  • Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
  • Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
  • t.join()/t.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程进入就绪状态。
  • obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
  • obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。

反射

注解

元注解

在java.lang.annotation包下,定义了6个注解类

  • Documented
    说明该注解将被包含在javadoc中
  • Retention
    定义注解的保留策略
@Retention(RetentionPolicy.SOURCE)   //注解仅存在于源码中,在class字节码文件中不包含
@Retention(RetentionPolicy.CLASS)     // 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得,
@Retention(RetentionPolicy.RUNTIME)  // 注解会在class字节码文件中存在,在运行时可以通过反射获取到
首 先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。
  • Target
    定义注解的作用目标
@Documented  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.ANNOTATION_TYPE)  
public @interface Target {  
    ElementType[] value();  
}  
@Target(ElementType.TYPE)   //接口、类、枚举、注解
@Target(ElementType.FIELD) //字段、枚举的常量
@Target(ElementType.METHOD) //方法
@Target(ElementType.PARAMETER) //方法参数
@Target(ElementType.CONSTRUCTOR)  //构造函数
@Target(ElementType.LOCAL_VARIABLE)//局部变量
@Target(ElementType.ANNOTATION_TYPE)//注解
@Target(ElementType.PACKAGE) ///包  
由于target注解的value是一个ElementType类型的数组,所以这个注解的值可以传数组方式, 如:@Target({ElementType.FIELD})
  • Inherited
    说明子类可以继承父类中的该注解
  • Native
    使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可
  • Repeatable
    允许在相同的程序元素中重复注解,在需要对同一种注解多次使用时,往往需要借助 @Repeatable 注解。Java 8 版本以前,同一个程序元素前最多只能有一个相同类型的注解,如果需要在同一个元素前使用多个相同类型的注解,则必须使用注解“容器”

2个枚举类

  • RetentionPolicy
public enum RetentionPolicy {
    //此注解类型的信息只会记录在源文件中,编译时将被编译器丢弃,也就是说
    //不会保存在编译好的类信息中
    SOURCE,
 
    //编译器将注解记录在类文件中,但不会加载到JVM中。如果一个注解声明没指定范围,则系统
    //默认值就是Class
    CLASS,
 
    //注解信息会保留在源文件、类文件中,在执行的时也加载到Java的JVM中,因此可以反射性的读取。
    RUNTIME
}
  • ElementType
    定义注解的作用范围:类,字段,局部变量

如何获取定义的注解中的值?

使用java的反射机制获取注解上的值

//获取类名的包名地址
            printClass = Class.forName("com.lxp.demo.Schedules.TestThtread");
    //java反射机制获取所有方法名
    Method[] declaredMethods = printClass.getDeclaredMethods();
    //遍历循环方法并获取对应的注解名称
    for (Method declaredMethod : declaredMethods) {
        String isNotNullStr = "";
        // 判断是否方法上存在注解  MethodInterface
        boolean annotationPresent = declaredMethod.isAnnotationPresent(MethodInterface.class);
        if (annotationPresent) {
            // 获取自定义注解对象
            MethodInterface methodAnno = declaredMethod.getAnnotation(MethodInterface.class);
            // 根据对象获取注解值
            isNotNullStr = methodAnno.name();
        }
        list.add(new KeyValueDto(declaredMethod.getName(),isNotNullStr));
    }
//等等

//在切面中获取注解中的描述信息

其他注解

  • @RequestHeader
    SpringMVC提供了@RequestHeader注解用于映射请求头数据到Controller方法的对应参数。
    使用@RequestHeader注解与使用@RequestParam一样,在方法的形参前加上注解即可。
    了解Request Header的内容,你可以访问W3C的网站
+ @RequestBody
@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的);GET方式无请求体,所以使用@RequestBody接收数据时,前端不能使用GET方式提交数据,而是用POST方式进行提交。在后端的同一个接收方法里,@RequestBody与@RequestParam()可以同时使用,@RequestBody最多只能有一个,而@RequestParam()可以有多个。
注:一个请求,只有一个RequestBody;一个请求,可以有多个RequestParam。

枚举

Java 枚举是一个特殊的类,一般表示一组常量
Java 枚举类使用 enum 关键字来定义,各个常量使用逗号 , 来分割

public enum Color {
    RED,BLACK,GREEN;
}

枚举的构造方法

类拥有构造器,枚举是一种特殊的类,所以枚举也可以拥有自己的构造器。但与普通类的不同之处在于枚举的构造器不可以是public的,其原因在于该构造器是提供给枚举对象中的枚举项构造时使用的,它并不需要在枚举对象之外使用。
例如,如果希望枚举MyColor中的每个枚举项包含有相应的中文说明以及其对应的Color信息,则可以为MyColor增加一个包含有两个参数的构造器,并且在声明每一个枚举项时使用这个构造器进行构造

package com.example.test.annotation;

public enum MyColor {
    
    RED("红色",Color.RED);//如果不声明下面的构造函数,这种写法会报错。

    String desc;
    Color color;
    private MyColor(String desc, Color color){
        this.desc = desc;
        this.color = color;
    }
}

欢迎各位看官指正!
特此说明:

  • 该文章只做个人学习只用,不做商用。
  • 该文章有有借鉴其他的文章,若有侵权请联系修正,谢谢!
posted @ 2021-06-24 21:52  依梦维马  阅读(83)  评论(0)    收藏  举报