Java基础【上】韩顺平(反射、类加载、final接口、抽象类、内部类)

涵盖知识点:反射、类加载、单例模式、final、抽象类、接口、内部类(局部内部类、匿名内部类、成员内部类、静态内部类)

 P711 反射机制原理

创建如下目录结构,在模块下创建src文件夹,文件夹要设置为Sources文件夹,在src下创建re.properties,写入如下代码:

classfullpath=com.hspedu.Cat
method=hi

在src/com/hspedu/reflection/question下创建ReflectionQuestion类,写入如下代码:

public class ReflectionQuestion {
    public static void main(String[] args) throws IOException {
        Properties properties = new Properties();
        properties.load(new FileInputStream("testtest\\src\\re.properties"));
        String classfullpath = properties.get("classfullpath").toString();
        String methodName = properties.get("method").toString();
        System.out.println("classfullpath="+classfullpath);
        System.out.println("method="+method);
    }
}

在src/com/hspedu下创建Cat类,写入如下代码:

public class Cat {
    private String name = "招财猫";
    public void hi(){
        System.out.println("hi"+name);
    }
}

P712 反射快速入门

可以看到调用newInstance方法之后返回的便是classfullpath类全限定名对应的类的实例对象:

在ReflectionQuestion中写入如下代码:

public class ReflectionQuestion {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Properties properties = new Properties();
        properties.load(new FileInputStream("testtest\\src\\re.properties"));
        String classfullpath = properties.get("classfullpath").toString();
        String methodName = properties.get("method").toString();
        System.out.println("classfullpath="+classfullpath);
        System.out.println("method="+methodName);
        //加载类,返回一个Class类型的对象
        Class cls = Class.forName(classfullpath);
        //通过cls得到你加载的类com.hspedu.Cat的对象实例
        Object o = cls.newInstance(); //创建的是Cat类的实例对象
        //通过cls得到加载的类com.hspedu.Cat的methodName"hi"的方法对象
        //即:在反射中,可以把方法视为对象(万物皆对象)
        Method method = cls.getMethod(methodName);
        //通过method调用方法:即通过方法对象来实现调用方法
        method.invoke(o);//传统方法是对象.方法(),反射机制是方法.invoke(对象)
    }
}

 在Cat类中新增cry方法:

public class Cat {
    private String name = "招财猫";
    public void hi(){
        System.out.println("hi"+name);
    }
    public void cry(){
        System.out.println(name+"喵喵叫");
    }
}

改re.properties配置文件代码如下:

classfullpath=com.hspedu.Cat
method=cry

 可以发现通过外部的配置文件,能在不修改源码的情况下, 通过修改配置文件的配置信息,来控制程序输出,符合设计模式的开闭原则。

P713 反射原理图

反射机制允许程序在执行期间借助于Reflection API取得任何类的内部信息(比如成员变量,构造器,成员方法等等),并能操作对象的属性及方法。反射在设计模式和框架底层都会用到。

加载完类之后,在堆中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息。通过这个对象可以得到类的结构。这个Class对象就像一面镜子,透过镜子能够看到类的结构,所以形象的称之为:反射。

源代码会被编译(javac)成字节码文件(Cat.class),字节码文件里面包含:属性、构造器、成员变量、泛型、异常、成员方法。

在Class类加载的阶段,类加载器会把字节码文件加载到内存的堆中,生成一个Class类对象,对象中包含成员变量、构造器、成员方法。

在运行阶段生成的Cat对象是存放在堆中,该对象知道他所属的Class对象,在得到Class对象之后能够创建对象,调用对象方法,也能操作属性等。

P714 反射相关类

反射机制可以完成:

1.在运行时判断任意一个对象所属的类

2.在运行时构造任意一个类的对象

3.在运行时得到任意一个类所具有的成员变量和方法。

4.在运行时调用任意一个对象的成员变量和方法

5.生成动态代理

获得类的成员变量用getField,但getField不能得到私有的属性。

//java.lang.reflect.Field:代表类的成员变量,Field对象表示某个类的成员变量
//getField不能得到私有的属性
Field nameField = cls.getField("age");
System.out.println(nameField.get(o));

获得类的构造器可以用getConstructor方法。 

//java.lang.reflect.Constructor:代表类的构造方法,Constructor对象表示构造器
Constructor constructor = cls.getConstructor();
System.out.println(constructor);
Constructor constructor1 = cls.getConstructor(String.class);
System.out.println(constructor1);

此时Cat类的代码如下:

public class Cat {
    private String name = "招财猫";
    public int age=12;
    public String s;
    public Cat(){}
    public Cat(String s){
        this.s = s;
    }
    public void hi(){
        System.out.println("hi"+name);
    }
    public void cry(){
        System.out.println(name+"喵喵叫");
    }
}

P715 反射调用优化

反射的优点和缺点:

优点:可以动态的创建和使用对象,使用灵活,没有反射机制,框架技术将失去底层支撑。

缺点:使用反射基本是解释执行,对执行速度有影响。

在src/com/hspedu/reflection下面创建Reflection01,写入如下代码:

public class Reflection01 {
    public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
        m1();
        m2();
    }
    //传统方法来调用hi
    public static void m1(){
        Cat cat = new Cat();
        long start = System.currentTimeMillis();
        for(int i=0;i<2000000000;i++){
            cat.hi();
        }
        long end = System.currentTimeMillis();
        System.out.println("传统方法调用hi 耗时="+(end-start));
    }
    //反射方法来调用hi
    public static void m2() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Class cls = Class.forName("com.hspedu.Cat");
        Object o = cls.newInstance();
        Method hi = cls.getMethod("hi");
        long start = System.currentTimeMillis();
        for(int i=0;i<2000000000;i++){
            hi.invoke(o);
        }
        long end = System.currentTimeMillis();
        System.out.println("传统方法调用hi 耗时="+(end-start));
    }
}

反射调用优化:关闭访问检查。

1.Method和Field、Constructor对象都有setAccessible()方法。

2.setAccessible作用是启动和禁用访问安全检查的开关。

3.参数为true时表示反射的对象在使用时取消访问检查,提高反射效率。参数值为false表示反射的对象执行访问检查。

//反射调用优化+关闭访问检查来调用hi
public static void m3() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    Class cls = Class.forName("com.hspedu.Cat");
    Object o = cls.newInstance();
    Method hi = cls.getMethod("hi");
    hi.setAccessible(true);
    long start = System.currentTimeMillis();
    for(int i=0;i<2000000000;i++){
        hi.invoke(o);
    }
    long end = System.currentTimeMillis();
    System.out.println("反射方法调用hi 耗时="+(end-start));
}

 

P716 Class类分析

1.Class也是类,因此也继承Object类。

2.Class类对象不是new出来的,而是系统创建的。

3.对于某个类的Class类对象,在内存中只有一份,因为类只加载一次。

4.每个类的实例都会记得自己是由哪个Class实例所生成。

5.通过Class对象可以完整地得到一个类的完整结构,通过一系列API。

6.Class对象是存放在堆的。

7.类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据(包括:方法代码、变量名、方法名、访问权限等等。)

P717 Class常用方法

public class Class02 {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        String classAllPath="com.hspedu.Car";
        //<?>表示不确定的Java类型
        Class<?> cls = Class.forName(classAllPath);
        //1.显示cls对象,是哪个类的Class对象,com.hspedu.Car
        System.out.println(cls);
        //2.输出cls运行类型,java.lang.Class
        System.out.println(cls.getClass());
        //3.得到包名
        System.out.println(cls.getPackage().getName());
        //4.得到全类名
        System.out.println(cls.getName());
        //5.通过cls创建对象实例
        Car car = (Car) cls.newInstance();
        System.out.println(car);
        //6.通过反射获取属性
        Field brand = cls.getField("brand");
        System.out.println(brand.get(car));
        //7.通过反射给属性赋值
        brand.set(car,"奔驰");
        System.out.println(brand.get(car));
        //8.遍历得到所以的属性
        Field[] fields = cls.getFields();
        for(Field f:fields){
            System.out.println(f.getName());
        }
    }
}

P718 获取Class对象六种方式

1.如果已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException。

实例:Class cls1 = Class.forName("java.lang.Cat")

应用场景:多用于配置文件,读取类全路径,加载类。

//1.Class.forName
String classAllPath="com.hspedu.Car";
Class<?> cls1 = Class.forName(classAllPath);
System.out.println(cls1);

2.如果已知具体的类,通过类的clss获取,该方法最为安全可靠,程序性能最高。

实例:Class cls2 = Cat.class

应用场景:多用于参数传递,比如通过反射得到对应构造器对象。

//2.类名.class,适用于参数传递
Class cls2 = Car.class;
System.out.println(cls2);

3.如果已知某个类的实例,调用该实例的getClass()方法获取Class对象,实例:

Class clazz = 对象.getClass();

//3.对象.getClass(),应用场景:有对象实例
Car car = new Car();
Class cls3 = car.getClass();
System.out.println(cls3);

4.其它方式(比如类加载器方式)

ClassLoader cl = 对象.getClass().getClassLoader();

Class clazz4 = cl.loadClass("类的全类名");

//4.通过类加载器【4种】来获取到类的Class对象
//(1)先得到类加载器car
ClassLoader classLoader = car.getClass().getClassLoader();
//(2)通过类加载器得到Class对象
Class cls4 = classLoader.loadClass(classAllPath);
System.out.println(cls4);

5.基本数据按如下方式得到Class类对象

Class cls = 基本数据类型.class

//5.基本数据按如下方式得到Class对象
Class<Integer> integerClass = int.class;
Class<Character> characterClass = char.class;
Class<Boolean> booleanClass = boolean.class;
System.out.println(integerClass);

6.基本数据类型对应的包装类,可以通过.TYPE得到Class类对象

Class cls = 包装类.TYPE

//6.基本数据类型对应的包装类,可以通过.TYPE得到Class类对象
Class<Integer> type = Integer.TYPE;
System.out.println(type);

P719 哪些类型有Class对象

P720 动态和静态加载

静态加载:编译时加载相关的类(编译时马上加载),如果没有则报错,依赖项太想。

动态加载:运行时加载需要的类(运行时才会加载),如果运行时不用该类,则不报错,降低了依赖项。

P721 类加载流程图

一、加载流程:

1.加载

2.连接(分为3小步):

验证:安全校验,文件格式,元数据,字节码,符号引用是否正确。

准备:针对静态变量进行默认初始化。

解析:虚拟机把常量池中的符号引用替换为直接引用。

3.初始化:执行类中定义的初始代码。

二、类加载后内存布局:

在方法区中保存有类的字节码二进制数据。

在堆区中有类的Class对象。

加载:是由类加载器把类的class文件读入内存,在堆中创建一个Class对象。

连接:把类的二进制数据合并到JRE中。

初始化:JVM负责对类进行初始化,主要是对静态成员进行初始化。

P722 类加载五个阶段(1)

加载阶段:

将来源于不同的数据源(class文件、jar包、网络)的字节码转化为二进制字节流加载到内存中(把类的字节码二进制数据加载到方法区,同时在堆中生成java.lang.Class对象)。

连接阶段-验证:

确保Class文件的字节流文件中包含的信息符合当前虚拟机要求。

包括:文件格式验证(魔数是否以cafebabe开头)、元数据验证、字节码验证和符号引用验证。

可以使用-Xverify:none来关闭类验证措施。

连接阶段-准备:

JVM会在该阶段对静态变量分配内存并默认初始化(对应数据类型的默认初始值,如0、0L、null、false等)。这些变量所实验的内存都在方法区中进行分配。

连接阶段-解析:

虚拟机将常量池中的符号引用替换为直接引用的过程。

属性==成员变量==字段

非静态变量==实例属性,不与类相关。

举例1:public static int n2 =20; 在初始化阶段n2的值才会变成20,在连接的准备阶段n2会被赋初值0,同时被分配内存。

举例2:public static final int n3 = 30; 在连接的准备阶段n3就会被赋初值30,因为加上final是常量,不是静态变量,一旦被赋值便不可改变。

P723 类加载五个阶段(2)

初始化阶段:

1.真正开始执行类中定义的Java程序代码,执行初始化方法。

2.初始化方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,进行合并。

3.虚拟机保证一个类的初始化方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的初始化方法,其它线程都要阻塞等待。

P724 获取类结构信息(1)

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

    }
    @Test
    //第一组方法API
    public void api_01() throws ClassNotFoundException, NoSuchMethodException {
        //1.得到Class对象
        Class<?> personCls = Class.forName("com.hspedu.reflection.classfile.Person");
        //2.getName:获取全类名
        System.out.println(personCls.getName());
        //3.getSimpleName:获取简单类名
        System.out.println(personCls.getSimpleName());
        //4.getField:获取所有public修饰的属性,包含本类以及父类的
        Field[] fields = personCls.getFields();
        for(Field field:fields){
            System.out.println("本类以及父类的属性="+field.getName());
        }
        //5.getDeclaredFields:获取本类中所有属性
        Field[] declaredFields = personCls.getDeclaredFields();
        for (Field declaredFiled : declaredFields){
            System.out.println("本类中所有属性="+declaredFiled.getName());
        }
        //6.getMethods:获取所有public修饰的方法,包含本类以及父类的
        Method[] methods = personCls.getMethods();
        for(Method method:methods){
            System.out.println("本类及父类的方法的方法="+method.getName());
        }
        //7.getDeclaredMethods:获取本类中所有方法
        Method[] declaredMethods = personCls.getDeclaredMethods();
        for(Method declaredMethod : declaredMethods){
            System.out.println("本类中所有方法="+declaredMethod.getName());
        }
        //8.getConstructors:获取本类所有public修饰的构造器
        Constructor<?>[] constructors = personCls.getConstructors();
        for(Constructor<?> constructor:constructors){
            System.out.println("本类以及父类的构造器="+constructor.getName());
        }
        //9.getDeclaredConstructors:获取本类中所有构造器
        Constructor<?>[] declaredConstructors = personCls.getDeclaredConstructors();
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println("本类中所有构造器="+declaredConstructor);
        }
        //10.getPackage:以Package形式返回包信息
        System.out.println(personCls.getPackage());
        //11,getSuperClass:以Class形式返回父类信息
        Class<?> superclass = personCls.getSuperclass();
        System.out.println("父类的class对象="+superclass);
        //12.getInterfaces:以Class[]形式返回接口信息
        Class<?>[] interfaces = personCls.getInterfaces();
        for (Class<?> anInterface : interfaces) {
            System.out.println("接口信息="+anInterface);
        }
        //13。getAnnotations:以Annotation[]形式返回注解信息
        Annotation[] annotations = personCls.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println("注解信息="+annotation);
        }
    }
}

class A {
    public String hobby;
    public void hi(){}
    public A(){}
}
interface IA{ }
interface IB{ }
@Deprecated
class Person extends  A implements IA, IB {
    public String name;
    protected int age;
    String job;
    private double sal;
    //构造器
    public Person(){}
    public Person(String s){}
    public void m1(){}
    protected void m2(){}
    void m3(){}
    private void m4(){}
}

P725 获取类结构信息(2)

public class Class02 {
    @Test
    //第一组方法API
    public void api_02() throws ClassNotFoundException, NoSuchMethodException {
        //得到Class对象
        Class<?> personCls = Class.forName("com.hspedu.reflection.classfile.Person");
        //getDeclaredFields:获取本类中所有属性
        Field[] declaredFields = personCls.getDeclaredFields();
        for (Field declaredFiled : declaredFields) {
            System.out.println("本类中所有属性=" + declaredFiled.getName()
                    + "  该属性的修饰符值=" + declaredFiled.getModifiers()
                    + "  该属性的类型=" + declaredFiled.getType());
        }
        //getDeclaredMethods:获取本类中所有方法
        Method[] declaredMethods = personCls.getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            System.out.println("本类中所有方法=" + declaredMethod.getName()
                    + "  该方法的访问修饰符值=" + declaredMethod.getModifiers()
                    + "  该方法返回类型" + declaredMethod.getReturnType());
            //输出当前这个方法的形参数组情况
            Class<?>[] parameterTypes = declaredMethod.getParameterTypes();
            for (Class<?> parameterType : parameterTypes) {
                System.out.println("该方法的形参类型=" + parameterType);
            }
        }
        //getDeclaredConstructors:获取本类中所有构造器
        Constructor<?>[] declaredConstructors = personCls.getDeclaredConstructors();
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println("本类中所有构造器=" + declaredConstructor.getName());
            Class<?>[] parameterTypes = declaredConstructor.getParameterTypes();
            for (Class<?> parameterType : parameterTypes) {
                System.out.println("该构造器的形参类型="+parameterType);
            }
        }
    }
}


class A {
    public String hobby;
    public void hi(){}
    public A(){}
}
interface IA{ }
interface IB{ }
@Deprecated
class Person extends  A implements IA, IB {
    public String name;
    protected static int age;
    String job;
    private double sal;
    //构造器
    public Person(){}
    public Person(String s){}
    public void m1(String name,int age,double sal){}
    protected void m2(){}
    void m3(){}
    private void m4(){}
}

P726 反射爆破创建实例

加入constructor是私有的构造器对象,调用下面这段代码:

constructor.setAccessible(true)

可以使得反射访问private构造器,这就是爆破,理解为暴力破解,把门拆了然后进入。

public class Class02 {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        //1.先获取到User类的Class对象
            Class<?> userClass = Class.forName("com.hspedu.reflection.classfile.User");
        //2.通过public的无参构造器创建实例
            Object o = userClass.newInstance();
            System.out.println(o);
        //3.通过public的有参构造器创建实例
            //constructor对象就是User类中带一个参数的构造器
            //先得到对应构造器
            Constructor<?> constructor = userClass.getConstructor(String.class);
            //创建实例并传入参数
            Object hsp = constructor.newInstance("hsp");
            System.out.println(hsp);
        //4.通过非public的有参构造器创建实例
            //先得到私有的构造器对象
            Constructor<?> constructor1 = userClass.getDeclaredConstructor(int.class, String.class);
            //创建实例
            constructor1.setAccessible(true); //爆破【暴力破解】,使用反射可以访问private构造器
            Object user2 = constructor1.newInstance(100, "张三丰");
            System.out.println(user2);
    }
}
class User {
    private int age;
    private String name="韩顺平教育";
    public User() {}
    public User(String name){
        this.name = name;
    }
    private User(int age, String name) {
        this.age = age;
        this.name = name;
    }
    public String toString() {
        return "User age=" + age + ", name=" + name;
    }
}

P727 反射爆破创建属性

public class Class02 {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        //1.得到Student类对应的Class对象
        Class<?> stuClass = Class.forName("com.hspedu.reflection.classfile.Student");
        //2.创建对象
        Object o = stuClass.newInstance(); //o的运行类型是Student
        System.out.println(o.getClass());
        //3.使用反射得到age属性对象
        Field age = stuClass.getField("age");
        age.set(o,88); //通过反射来操作属性
        System.out.println(o);
        System.out.println(age.get(o));
        //4.使用反射操作name属性
        Field name = stuClass.getDeclaredField("name");
        name.setAccessible(true);
        //name.set(o,"hsp");
        name.set(null,"hsp"); //因为name是static属性,因此o也可以写成null
        System.out.println(o);
        System.out.println(name.get(o)); //获取属性值
        System.out.println(name.get(null)); //获取属性值,要求name是static
    }
}
class Student {
    public int age;
    private static String name;
    public Student() {}
    public String toString() {
        return "Student age=" + age + ", name=" + name + "}";
    }
}

P728 反射爆破创建方法

public class Class02 {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        //1.得到Boss类对应的Class对象
        Class<?> bossCls = Class.forName("com.hspedu.reflection.classfile.Boss");
        //2.创建对象
        Object o = bossCls.newInstance();
        //3.调用public的hi方法
        Method hi = bossCls.getMethod("hi",String.class);
        //4.因为say方法是private,所以需要爆破
        hi.invoke(o,"韩顺平教育");
        Method say = bossCls.getDeclaredMethod("say", int.class,String.class, char.class);
        say.setAccessible(true);
        //5.调用并输出
        System.out.println(say.invoke(o, 100, "张三", '男'));
        //6.在反射中如果方法有返回值,统一返回Object,但运行类型和方法定义的返回类型一致
        Object reVal = say.invoke(null, 300, "王五", '男');
        System.out.println("reVal的运行类型="+reVal.getClass());
    }
}
class Boss { //类
    public int age;
    private static String name;
    public Boss() { //构造函数
    }
    private static String say(int n, String s, char c) { //静态方法
        return n + " " + s + " " + c;
    }
    public void hi(String s) {
        System.out.println("hi " + s);
    }
}

P729 反射课后练习

public class Class02 {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        //1.得到privateTest类对应的Class对象
        Class<PrivateTest> privateTestClass = PrivateTest.class;
        //2.创建对象实例
        PrivateTest privateTestObj = privateTestClass.newInstance();
        //3。得到name
        Field name = privateTestClass.getDeclaredField("name");
        //4.爆破name
        name.setAccessible(true);
        name.set(privateTestObj,"天龙八部");
        //5.得到getName方法对象
        Method getName = privateTestClass.getMethod("getName");
        //6.因为getName()是public所以直接调用
        Object invoke = getName.invoke(privateTestObj);
        System.out.println("name属性值="+invoke);
    }
}
class PrivateTest { //类
    private String name="hellokitty";
    public String getName(){
        return name;
    }
}
public class Class02 {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        //1.Class类的forName方法得到File类的class对象
        Class<?> fileCls = Class.forName("java.io.File");
        //2.得到所有的构造器
        Constructor<?>[] declaredConstructors = fileCls.getDeclaredConstructors();
        //遍历输出
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println("File构造器="+declaredConstructor);
        }
        //3.单独得到public java.io.File(java.lang.String)
        Constructor<?> declaredConstructor = fileCls.getDeclaredConstructor(String.class);
        String fileAllPath = "C:\\data\\mynew.txt";
        //file的运行类型就是File
        Object file = declaredConstructor.newInstance(fileAllPath);
        //4.得到createNewFile的方法对象
        Method createNewFile = fileCls.getMethod("createNewFile");
        createNewFile.invoke(file);
        System.out.println(file.getClass());
        System.out.println("创建文件成功");
    }
}
class PrivateTest { //类
    private String name="hellokitty";
    public String getName(){
        return name;
    }
}

P730 反射梳理

P392 单例模式饿汉式

单例模式:采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

饿汉式:简单理解为不论有没有使用类或方法都加载,很着急。

public class Class02 {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        GirlFriend instance = GirlFriend.getInstance();
        System.out.println(instance);
        //GirlFriend xb = new GirlFriend("小白");
    }
}
class GirlFriend {
    private String name;
    private static GirlFriend xhh = new GirlFriend("小红红");
    private GirlFriend(String name){
        this.name=name;
    }
    public static GirlFriend getInstance(){
        return xhh;
    }

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

P393 单例模式懒汉式

懒汉式:只有使用时才会创建实例。

public class Class02 {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        System.out.println(GirlFriend.n1);
        System.out.println(GirlFriend.getInstance());
        System.out.println(GirlFriend.getInstance());
        //GirlFriend xb = new GirlFriend("小白");
    }
}
class GirlFriend {
    private String name;
    public static int n1 = 999;
    private static GirlFriend girlFriend;
    private GirlFriend(String name){
        this.name=name;
        System.out.println("构造器被调用...");
    }
    public static GirlFriend getInstance(){
        if(girlFriend==null){
            girlFriend = new GirlFriend("小可爱");
        }
        return girlFriend;
    }
    @Override
    public String toString() {
        return "GirlFriend{" +
                "name='" + name + '\'' +
                '}';
    }
}

P394 final基本使用

final可以修饰类、方法、属性、局部变量。

1.当不希望类被继承(不希望类的方法被重写),可以用final修饰

2.当不希望父类的某个方法被子类覆盖/重写时可以用final修饰

3.当不希望类的某个属性的值被修改,可以用final修饰

4.当不希望某个局部变量被修改,可以使用final修饰

P395 final使用细节1

1.final修饰的属性在定义时,必须赋初值,并且不能修改。赋值可以在如下位置:a.定义时。b.在构造器中。c.在代码块中。

2.如果final修饰的属性是静态的,则初始化位置只能在:a.定义时。b.静态代码块内。(注意:不能在构造器内赋值)

3.final类不能被继承,但可以有实例化对象。

4.如果类不是final类,但是含有final方法,虽然该方法不能被重写,但可以被继承。

P396 final使用细节2

P397 final课堂练习

比较简单不赘述。

P398 抽象类引出

当父类的某些方法,需要声明,但又不确定如何实现时,可以将其声明为抽象方法,那这个类就是抽象类。

抽象类主要解决的是:父类方法的不确定性问题,将方法设计为抽象(abstract)方法,所谓抽象方法就是没有实现的方法,所谓没有实现就是指没有方法体,当一个类中存在抽象方法时,需要将改类声明为abstract。

P399 抽象类细节1

1.抽象类不能被实例化(不能创建对象)。

2.抽象类不一定要包含abstract方法(抽象类可以没有abstract方法)。

3.一旦类包含了abstract方法,则这个类必须声明为abstract。

4.abstract只能修饰类和方法,不能修饰属性和其它的。

P400 抽象类细节2

1.抽象类可以有任意成员(比如非抽象方法、构造器、静态属性等。抽象类本质还是类)。

2.抽象方法不能有主体,即不能实现。

3.如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非它也是abstract类。

4.抽象方法不能使用private、final和static来修饰(因为这些关键字会阻止重写,抽象类本质需要子类继承去重写方法)。

P401 抽象类课堂练习

比较简单不赘述。

P402 抽象模板模式

在Template中定义job这个抽象方法,然后在AA和BB类中进行了实现,所以不同的类(指AA和BB)调用calculateTime类之后,job会被动态绑定到不同的实现,从而实现模板模式。

创建Template类如下:

abstract public class Template { //抽象类-模板设计模式
    public abstract void job(); //抽象方法
    public void calculateTime(){ //实现方法调用job方法
        //得到开始的时间
        long start = System.currentTimeMillis();
        job();
        //得到结束的时间
        long end = System.currentTimeMillis();
        System.out.println("任务执行时间"+(end-start));
    }
}

创建AA类如下:

public class AA extends Template{
    public void job(){ //这里也去,重写了Template的job方法
        long num = 0;
        for (int i = 0; i <= 30000000 ; i++) {
            num += i;
        }
    }
}

创建BB类如下:

public class BB extends Template{
    public void job(){ //这里也去,重写了Template的job方法
        long num = 0;
        for (int i = 0; i <= 80000000 ; i++) {
            num += i;
        }
    }
}

创建TestTemplate类如下:

public class TestTemplate {
    public static void main(String[] args) {
        AA aa = new AA();
        aa.calculateTime();
        BB bb = new BB();
        bb.calculateTime();
    }
}

P403 接口快速入门

P404 接口基本介绍

接口是给出一些没有实现的方法,封装到一起,到某个类要使用的时候,再根据具体情况把这些方法写出来:

interface 接口名{
        属性
        方法
}

implements是实现的意思,类去实现接口:

class 类名 implements 接口{
    自己属性
    自己方法
    必须实现接口的抽象方法
}

在Jdk7前,接口里的所有方法都没有方法体,都是抽象方法。

在Jdk8后接口里可以有静态方法、默认方法,可以中可以有方法的具体实现。 但一定要用default来修饰方法:

public interface UsbInterface {
    default public void ok(){
        System.out.println("ok....");
    }
}

在接口中,抽象方法可以省略abstract关键字。

P405 接口应用场景

提供一种规范,接口内的方法名是固定的,但可以有不同的实现,方便以后统一调用和管理。

P406 接口使用细节1

1.接口不能被实例化。

2.接口中所有的方法是public方法,接口中抽象方法,可以不用abstract修饰。

3.一个普通类实现接口,就必须要将接口中的所有方法实现。

4.抽象类实现接口,可以不用实现接口的方法。

P407 接口使用细节2

1.一个类可以同时实现多个接口,一个类只能继承一个类(单继承)

2.接口中的属性,只能是final的,而且是被public static final修饰符修饰的。

3.接口中属性的访问形式:接口名.属性名

4.接口不能继承其它的类,但是可以继承多个别的接口

5.接口的修饰符只能是public和默认,这点和类的修饰符是一样的

P408 接口课堂练习

P409 接口VS继承

一个类只能继承一个父类(单继承),一个类可以同时实现多个接口

继承的价值:解决代码复用性和可维护性。

接口的价值:设计好规范,让其它类去实现这些方法

接口比继承更加灵活:继承是满足is-a的关系(比如小猴子是猴子),而接口只需要满足like-a(比如猴子可以像鱼一样游泳)。

接口在一定程度上可以实现代码解耦(接口规范性+动态绑定)

P410 接口多态特性

接口的多态体现:

接口类型的变量test可以指向实现了IF接口的对象实例

public interface UsbInterface {
    public static void main(String[] args) {
        IF test = new Monster();
        test = new Car();
    }
}
interface IF{}
class Monster implements IF{}
class Car implements IF{}

继承的多态体现(向上转型):

父类类型的变量a可以指向继承AAA的子类对象实例。 

public interface UsbInterface {
    public static void main(String[] args) {
        AAA a = new BBB();
        a = new CCC();
    }
}
class AAA{}
class BBB extends AAA{}
class CCC extends AAA{}

 if(usbs[i] instanceof Phone_)判断的是运行类型。

public class UsbInterface {
    public static void main(String[] args) {
        Usb[] usbs = new Usb[2];
        usbs[0] = new Phone_();
        usbs[1] = new Camera_();
        for (int i = 0; i < usbs.length; i++) {
            usbs[i].work();
            if(usbs[i] instanceof Phone_){
                ((Phone_)usbs[i]).call();
            }
        }
    }

}

interface Usb{
    void work();
}
class Phone_ implements Usb{
    public void call(){
        System.out.println("手机可以打电话...");
    }
    @Override
    public void work() {
        System.out.println("手机工作中...");
    }
}
class Camera_ implements Usb{

    @Override
    public void work() {
        System.out.println("相机工作中...");
    }
}

P411 接口多态传递

IG继承了IH接口,Teacher类实现了IG接口

相当于Teacher类也实现了IH接口

于是可以向下转型

interface Usb{
    IG ig = new Teacher();
    IH ih = new Teacher();
}
interface IH{
}
interface IG extends IH{ }
class Teacher implements IG{
}

P412 接口课堂练习

P413 四种内部类

一个类的内部又完整的嵌套了另一个类结构。被嵌套的类称为内部类(inner class),嵌套其它类的类被称为外部类(outer class)。

类的五大成员:属性、方法、构造器、代码块、内部类。

内部类可以直接访问私有属性。

定义在外部类局部位置上(比如方法内)

1.局部内部类(有类名)

2.匿名内部类(没有类名,重点)

定义在外部类的成员位置上:

1.成员内部类(没用static修饰)

2.静态内部类(使用static修饰)

P416 局部内部类1

局部内部类是定义在外部类的局部位置,比如方法中,并且有类名。

1.局部内部类可以直接访问外部类所所有成员,包含私有的。

2.局部内部类不能添加访问修饰符,因为它的地位就是一个局部变量,但可以使用final修饰。

3.局部内部类作用域仅仅在定义它的方法或代码块内。

4.外部类想访问局部内部类成员需要先创建对象再访问。

P415 局部内部类2

1.外部其它类不能访问局部内部类。

2.如果外部类和局部内部类成员重名时,默认遵循就近原则,如果想访问外部类的成员,可以使用(外部类类名.this.成员)去访问。

public class UsbInterface {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.m1();
        //验证:System.out.println(outer);
    }
}
class Outer{
    private int n1=100;
    public void m1(){
        class Inner{
            private int n1 = 800;
            public void f1() {
                //访问重名变量遵循就近原则,默认访问内部类距离更近的变量
                System.out.println(n1);
                //访问外部类中重名的局部变量:外部类.this.变量。
                //Outer02.this本质就是外部类对象,哪个对象调用m1,Outer02.this就是那个对象
                //System.out.println(Outer.this.n1);
               //验证: System.out.println(Outer.this);
            }
        }
        //外部类想调用内部类方法,通过创建对象。
        Inner inner = new Inner();
        inner.f1();
    }
}

P416 匿名内部类本质

匿名内部类:1.本质是类。2.是内部类。3.该类没有名字。4.是一个对象。

匿名内部类是定义在外部类的局部位置上,比如方法中,并且没有类名。

基本语法:

new 类或接口(参数列表){
    类体
};
基于接口的匿名内部类
1.需求:想使用IA接口,并创建对象
2.传统方式:写一个类,实现该接口,并创建对象
3.现在需求:Tiger只用一次,后面不再使用
4.可以使用匿名内部类来简化开发
5.tiger的编译类型是IA
6.tiger的运行类型是内部类
7.jdk在创建匿名内部类Outer$1之后马上(new)创建了Outer04$1实例,并且把地址返回给tigger
8.匿名内部类使用一次,就不能再使用。
9.tiger是new创建出来的实例可以反复调用。
public class AnonymousInnerClass {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.method();
    }
}
class Outer{
    private int n1 = 10;
    public void method(){
        IA tiger = new IA() { //匿名内部类
            @Override
            public void cry() {
                System.out.println("老虎叫唤...");
            }
        };
        System.out.println(tiger.getClass());
        tiger.cry();
    }
}

interface IA{
    public void cry();
}

P417 匿名内部类使用

P418 匿名内部类细节

匿名内部类从语法上看,既有创建对象的特征,也有创建对象的特征。

匿名内部类既是一个类的定义,其本身也是一个对象。

public class AnonymousInnerClass {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.method();
    }
}
class Outer{
    private int n1 = 10;
    public void method(){
        //下面2种写法效果是一样的
        //写法1:
        IA ia = new IA() {
            @Override
            public void cry() {
                System.out.println("老虎叫唤...");
            }
        };
        ia.cry();
        //写法2:
        new IA() {
            @Override
            public void cry() {
                System.out.println("老虎叫唤...");
            }
        }.cry();
    }
}

P419 匿名内部类实践

把匿名内部类当作实参直接传递,简洁高效。

public class AnonymousInnerClass {
    public static void main(String[] args) {
        //匿名内部类写法
        f1(new IL(){
            @Override
            public void show() {
                System.out.println("这是一副名画~~");
            }
        });
        //传统写法
        f1(new Picture());
    }
    public static void f1(IL il){
        il.show();
    }
}

interface IL{
    void show();
}

class Picture implements IL{
    @Override
    public void show() {
        System.out.println("这是一副名画...");
    }
}

 

public class AnonymousInnerClass {
    public static void main(String[] args) {
        CellPhone cellPhone = new CellPhone();
        //1.传递的是实现了Bell接口的匿名内部类
        cellPhone.alarmClock(new Bell(){
            @Override
            public void ring() {
                System.out.println("懒猪起床了");
            }
        });
        cellPhone.alarmClock(new Bell() {
            @Override
            public void ring() {
                System.out.println("小伙伴上课了");
            }
        });
    }
}
interface Bell{
    void ring();
}
class CellPhone{
    public void alarmClock(Bell bell){
        System.out.println(bell.getClass());
        bell.ring();
    }
}

P420 成员内部类1

成员内部类不同于匿名内部类,不是写在方法里面的,而是与局部变量呈现平级关系,末尾无分号。

成员内部类是定义在外部类的成员,并且没有static修饰

1.可以直接访问外部类的所有成员,包含私有的。

2.可以添加任意访问修饰符,因为它的地位就是一个成员。

public class AnonymousInnerClass {
    public static void main(String[] args) {
        CellPhone cellPhone = new CellPhone();
        cellPhone.t1();
    }
}
class CellPhone{
    private int n1=10;
    public String name = "张三";
    class Inner08{
        public void say(){
            //可以直接访问外部类的所有成员,包含私有的
            System.out.println("n1= "+n1+" name= "+name);
        }
    }
    public void t1(){
        Inner08 inner08 = new Inner08();
        inner08.say();
    }
}

P421 成员内部类2

1.成员内部类的作用域为整个外部类范围。

2.成员内部类可以直接访问外部类的属性。

3.外部类想访问成员内部类的属性需要通过创建对象再访问。

4.外部的其他类想访问成员内部类只能通过如下2种特殊方式。

5.外部类和内部类重名时同样遵循就近原则,指定调用外部类重名属性语法在下面代码中。

public class AnonymousInnerClass {
    public static void main(String[] args) {
        Outer08 outer08 = new Outer08();
        outer08.t1();
        //外部其它类使用成员内部类的2种方式
        //第1种方式:
        //outer08.new Inner08();相当于把new Inner08()当做是outer08成员
        Outer08.Inner08 inner08 = new Outer08().new Inner08();
        //第2种方式:在外部类中编写一个方法,可以返回Inner08对象。
        Outer08.Inner08 inner08Instance = outer08.getInner08Instance();
        inner08Instance.say();
    }
}
class Outer08{
    private int n1=10;
    public String name = "张三";
    class Inner08{
        private int n1 = 66;
        public void say(){
            //如果成员内部类成员和外部类成员重名,会遵守就近原则
            //可以通过  外部类名.this.属性  来访问外部类的成员
            System.out.println("n1= "+n1+" name= "+name+" 外部类的n1="+Outer08.this.n1);
        }
    }
    public Inner08 getInner08Instance(){
        return new Inner08();
    }
    public void t1(){
        Inner08 inner08 = new Inner08();
        inner08.say();
    }
}

P422 静态内部类1

静态内部类是定义在外部类的成员位置,并且有static修饰。

1.可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员。

2.可以添加任意的修饰符,因为它的地位是一个成员。

3.作用域:同其他的成员,为类的整体。

P423 静态内部类2

public class AnonymousInnerClass {
    public static void main(String[] args) {
        Outer10 outer10 = new Outer10();
        outer10.m1();
        Outer10.Inner10 inner10 = new Outer10.Inner10();
        inner10.say();
        Outer10.Inner10 inner101 = outer10.getInner10();
        inner101.say();
    }
}
class Outer10{
    private static int n1 = 101;
    private static String name ="张三";
    static class Inner10{
        private int n1 = 998;
        public void say(){
            System.out.println(Outer10.n1);
        }
    }
    public void m1(){
        Inner10 inner10 = new Inner10();
        inner10.say();
    }
    public Inner10 getInner10(){
        return new Inner10();
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/486236.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

农夫山泉财报公布在即,消费升级的瓶装水市场或将重新洗牌

农夫山泉财报公布在即&#xff0c;消费升级的瓶装水市场或将重新洗牌 新年伊始&#xff0c;2024年对于中国瓶装水行业注定是一个地动山摇的一年&#xff0c;随着农夫山泉董事长钟睒睒上次被媒体集体关注&#xff0c;农夫山泉遭遇上市以来的最大舆论风波。 3月26日农夫山泉的财…

2024 年 8 个最佳 PDF 转 JPG 转换器[免费和付费]

虽然 PDF&#xff08;便携式文档文件&#xff09;是一种流行的文档共享格式&#xff0c;但有时您可能希望将 PDF 文件转换为JPG&#xff0c;然后在网页或社交媒体上共享它们。 在本文中&#xff0c;我们将讨论适用于 Windows 10 和 11 的出色 PDF 到 JPG 转换器的所有特性。 …

精准、快速、便捷:游标尺模式在软件设计中的三大优势

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;并且坚持默默的做事。 &#x1f680; 转载自&#xff1a;探索设计模式的魅力&#xff1a;精准、快速、便捷&#xf…

注册马来西亚商标常见问题

马来西亚商标法于1983年9月1日正式生效。这部商标法废除了马来亚、沙巴和沙捞越三地区各自的商标法规和申请程序&#xff0c;使马来西亚有了一部统一商标法。此外&#xff0c;马来西亚有关商标的法规包括1983年9月1日同时生效的《1983年商标法实施细则》。在马来西亚&#xff0…

matlab ICP配准高阶用法——统计每次迭代的配准误差并可视化

目录 一、概述二、代码实现三、结果展示1、原始点云2、配准结果3、配准误差本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、概述 在进行论文写作时,需要做对比实验,来分析改进算法的性能,期间用到了迭代误差分布统…

Java中的锁:实现并发控制与资源共享

JAVA中的锁 锁的概念锁机制为什么要使用锁锁的种类乐观锁/悲观锁独享锁/共享锁互斥锁/读写锁可重入锁/不可重入锁公平锁/非公平锁分段锁/自旋锁CAS/AQS synchronized概念应用场景四种使用场景效果对比synchronized的特点Lock/ReentrantLock对比 Volatile概念Java内存模型线程可…

多工作空间并存时ROS的环境变量异常问题

今天突然发现当多工作空间并存时&#xff0c;我的ROS的环境变量发生了比较诡异的异常&#xff0c;按照我之前的理解&#xff0c;在.bashrc文件中按顺序设定了ROS的环境变量后&#xff0c;ROS就会按照.bashrc中编写的环境变量来搜寻功能包&#xff0c;例如在.bashrc文件使用sour…

Nuclei Poc开发

1、Poc开发工具介绍 Nuclei&#xff1a;https://nuclei.projectdiscovery.io/ Cloud Platfrom云平台&#xff1a;https://cloud.projectdiscovery.io/ 2、目标站点简介 目标演示站点&#xff1a;http://glkb-jqe1.aqlab.cn/nacos/#/login 指纹&#xff1a;Nacos 已知常用漏洞…

【微服务】Eureka(服务注册,服务发现)

文章目录 1.基本介绍1.学前说明2.当前架构分析1.示意图2.问题分析 3.引出Eureka1.项目架构分析2.上图解读 2.创建单机版的Eureka1.创建 e-commerce-eureka-server-9001 子模块2.检查父子pom.xml1.子 pom.xml2.父 pom.xml 3.pom.xml 引入依赖4.application.yml 配置eureka服务5.…

【Web APIs】事件高级

目录 1.事件对象 1.1获取事件对象 1.2事件对象常用属性 2.事件流 1.1事件流的两个阶段&#xff1a;冒泡和捕获 1.2阻止事件流动 1.3阻止默认行为 1.4两种注册事件的区别 3.事件委托 1.事件对象 1.1获取事件对象 事件对象&#xff1a;也是一个对象&#xff0c;这个对象里…

rapidssl证书通配符证书800元

RapidSSL旗下的DV基础型通配符SSL证书可以同时保护多个域名站点&#xff0c;保护主域名以及主域名下的所有子域名。这款通配符SSL证书可以为网站提供数据加密服务&#xff0c;营造安全的上网环境&#xff0c;确保用户在网站上的数据安全传输。今天就随SSL盾小编了解RapidSSL旗下…

2024年HCIE考试题二

27、以下关于在网络中选择认证点位置的描述中&#xff0c;错误的是哪一项&#xff1f; A.在网络的接入层部署认证&#xff0c;有利于实现权限的细颗粒度管理和网络的高安全性 B.用户认证点从接入层上移到汇聚层之后&#xff0c;可能会导致用户的MAC认证失败 C.当用户认证点从…

DC电源模块的设计与调试技巧

BOSHIDA DC电源模块的设计与调试技巧 DC电源模块的设计与调试是电子工程师在实际项目中常常需要面对的任务。一个稳定可靠的DC电源模块对于电路的正常运行起到至关重要的作用。以下是一些设计与调试的技巧&#xff0c;帮助工程师们更好地完成任务。 第一&#xff0c;正确选择…

vue3.0 + ts + eslint报错:error Parsing error: ‘>‘ expected

eslint报错 这里加上对应的 eslint配置即可&#xff1a; parser: vue-eslint-parser, parserOptions: {parser: "typescript-eslint/parser",ecmaVersion: 2020,sourceType: module, }具体如下&#xff1a; module.exports {parser: vue-eslint-parser,parserOpti…

代码随想录阅读笔记-栈与队列【删除字符串中的所有相邻重复项】

题目 给出由小写字母组成的字符串 S&#xff0c;重复项删除操作会选择两个相邻且相同的字母&#xff0c;并删除它们。 在 S 上反复执行重复项删除操作&#xff0c;直到无法继续删除。 在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。 示例&#xff1a; 输入&am…

如何成功将自己开发的APP上架到应用商店

如何成功将自己开发的APP上架到应用商店 随着移动应用市场的蓬勃发展&#xff0c;开发一款优秀的APP已成为许多企业和个人的首要选择。然而&#xff0c;成功上架并有效推广APP至关重要。本文将逐步介绍完整的上架流程&#xff0c;包括准备所需材料、注册开发者账户、进行APP备…

OpenLayers基础教程——使用WebGLPoints加载海量点数据

1、前言 最近遇到一个问题&#xff1a;如何在OpenLayers中高效加载海量的场强点&#xff1f;由于项目中的一些要求&#xff0c;不能使用聚合的方法加载。一番搜索之后发现&#xff1a;OpenLayers中有一个WebGLPoints类&#xff0c;使用该类可以轻松应对几十万的数据量&#xf…

多目标追踪实现_3.9

目标 利用sort算法完成多目标追踪 在这里主要实现了一个多目标跟踪器&#xff0c;管理多个卡尔曼滤波器对象&#xff0c;主要包括以下内容&#xff1a; 初始化&#xff1a;最大检测数&#xff0c;目标未被检测的最大帧数 目标跟踪结果的更新&#xff0c;即跟踪成功和失败的目…

Android第一行代码——快速入门 Kotlin 编程(4.7 编写界面的最佳实践)

目录 4.7 编写界面的最佳实践 4.7.1 制作 9—Patch 图片 4.7.2 编写精美的聊天界面 4.7 编写界面的最佳实践 既然已经学习了那么多 UI 开发的知识,是时候实战一下了。这次我们要综合运用前面所学的大量内容来编写出一个较为复杂且相当美观的聊天…

【前端寻宝之路】JavaScript初学之旅

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法|MySQL| ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-azUa9yH16cRXQUxE {font-family:"trebuchet ms",verdana,arial,sans-serif;f…