Java基础之反射的基本使用

news2025/4/10 2:11:55

简介

在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意属性和方法;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。反射让Java成为了一门动态的语言,是许多Java框架的基石。

为什么要叫“反射”:加载完类之后,在堆中就产生了一个Class类型的对象,叫做类对象,一个类只有一个类对象,这个对象包含了类的完整结构信息,通过这个对象得到类的结构,这个对象就像一面镜子,透过这个镜子看到类的结构,所以,形象地称之为“反射”

作用:反射可以动态的获取类的信息,是许多Java框架的基础

反射机制的优缺点:

  • 优点:可以在运行期间动态的执行方法,访问属性,增加了java的灵活性
  • 缺点:反射的代码总是慢于直接执行的代码

反射的使用

学习反射相关的API时使用的实体类:

public class Box<T extends Number, V> extends ThreadLocal<Box<T, V>> implements Comparable<Box<T, V>> {
    // 泛型字段
    private T item;

    public Box() { }

    public Box(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }

    // 泛型方法
    public static <T> T genericMethod(T obj) throws Exception {
        System.out.println("obj = " + obj);
        return obj;
    }

    @Override
    public int compareTo(Box o) {
        return this.item.hashCode() - o.item.hashCode();
    }
}
public class Person extends ThreadLocal<Person> implements Comparable<Person> {
    private String name;
    private Integer age;

    public Person() { }

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

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

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

    @Override
    public int compareTo(Person o) {
        return 0;
    }
}

获取类对象

获取类对象的三种方法:

  • 类名.class
  • 对象名.getClass()
  • Class.forName(类的全限定名)

案例:

public static void main(String[] args) throws ClassNotFoundException {
    // 方式1:获取一个类的类对象
    Class<Person> personClass = Person.class;
    System.out.println("personClass = " + personClass);  // personClass = class org.wyj.test.beans.Person

    // 方式2,获取一个实例的类对象,getClass是Object类中的方法
    Person person = new Person();
    Class<? extends Person> personClass1 = person.getClass();
    System.out.println("personClass1 = " + personClass1);  // personClass1 = class org.wyj.test.beans.Person

    // 方式3:加载某个类,根据一个类的全限定名,加载某个类。
    Class<?> personClass2 = Class.forName("org.wyj.test.beans.Person");
    System.out.println("personClass2 = " + personClass2);  // personClass2 = class org.wyj.test.beans.Person
}

Class类中的forName方法:public static Class<?> forName(String className) throws ClassNotFoundException :使用类的全限定名对类进行加载,返回类的类对象,默认会对类进行初始化。它是在运行时根据类名字符串加载类,而不是在编译时直接引用类,这在处理插件、扩展或动态加载驱动程序时非常有用。通过这种方法来获取类对象是实际开发中使用的最多的。

案例2:通过类对象查看类的信息

public static void main(String[] args) {
    try {
        Class<?> personClass = Class.forName("org.wyj.reflex.beans.Person");

        // 获取类名
        System.out.println("类名 = " + personClass.getName());  // 类的二进制名称
        System.out.println("类名简写 = " + personClass.getSimpleName());

        // 获取父类
        System.out.println("获取父类 = " + personClass.getSuperclass());  // class java.lang.ThreadLocal

        // 获取类所实现的接口
        System.out.println("获取类所实现的接口 = " +
                Arrays.toString(personClass.getInterfaces())); // [interface java.lang.Comparable]

        // 获取类的加载器
        System.out.println("类的加载器 = " +
                personClass.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
}

这里仅仅介绍如何获取类的父类、类实现的接口,还有许多其他操作,随后再介绍。

创建类的实例

具体步骤是,通过反射,获取类对象,然后获取类的构造方法,最后调用构造方法来创建一个类对象

案例:

public static void main(String[] args)  {
    // 获取一个类的类对象
    Class<Person> personClass = Person.class;

    // 通过类对象提供的API实例化一个类,它默认调用类中的无参构造
    Object person;
    try {
        person = personClass.newInstance();
    } catch (InstantiationException | IllegalAccessException e) {
        throw new RuntimeException(e);
    }
    System.out.println("person = " + person);  // person = Person{name='null', age=0}
}

Class类中的newInstance方法:它默认获取类中的无参构造,通过无参构造来创建类的实例

获取类中的构造方法

通过反射获取类中的构造方法,然后通过构造方法来实例化类

案例:

public static void main(String[] args) {
    // 获取一个类的类对象
    Class<Person> personClass = Person.class;

    try {
        // 方式1:获取类中的无参构造
        Constructor<Person> constructor = personClass.getConstructor();
        Person person1 = constructor.newInstance();
        System.out.println("person1 = " + person1);  // person1 = Person{name='null', age=0}

        // 方式2:获取指定类型的构造器,通过构造器来实例化一个类
        Constructor<Person> con = personClass.getConstructor(String.class, int.class);
        Person person2 = con.newInstance("张三", 18);
        System.out.println("person2 = " + person2);    // person2 = Person{name='张三', age=18}
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

获取构造方法时,通过参数类型来指定需要获取哪个构造方法,如果没有传,表示获取无参构造

获取类中的字段

这里演示如何通过反射来操作类中的字段。

案例:

public class Demo4FieldTest {
    public static int publicStaticField = 10;

    public int publicField = 20;
    private int privateField = 30;

    public static void main(String[] args) {
        try {
            Class<Demo4FieldTest> aClass = Demo4FieldTest.class;

            // 1、获取类中的静态字段
            Field staticField = aClass.getDeclaredField("publicStaticField");
            // 查看字段信息
            System.out.println("字段类型 = " + staticField.getType());  // int
            System.out.println("字段所在的类 = " + staticField.getDeclaringClass());  // class org.wyj.reflex.Demo4FieldTest
            System.out.println("字段名称 = " + staticField.getName()); // publicStaticField

            // 获取字段的值,静态字段不属于任何实例,所以,操作静态字段的值时,对象参数的值传null即可
            System.out.println("获取静态字段的值 = " + staticField.get(null)); // 10
            // 设置字段的值
            staticField.set(null, 20);
            System.out.println("获取静态字段的值 = " + staticField.get(null)); // 20

            // 2、获取类中的成员字段
            Demo4FieldTest demo4FieldTest = aClass.newInstance();
            Field publicField = aClass.getDeclaredField("publicField");
            // 获取字段的值,操作成员字段时,需要传入对象参数,表示操作哪个对象中的字段
            System.out.println("获取成员字段的值 = " + publicField.get(demo4FieldTest)); // 20
            // 设置字段的值
            publicField.set(demo4FieldTest, 30);
            System.out.println("获取成员字段的值 = " + publicField.get(demo4FieldTest)); // 30

            // 3、获取类中的私有字段
            Field privateField = aClass.getDeclaredField("privateField");
            // 设置私有字段为可访问,这里是通过用户API来重写访问权限,jdk1.8中才支持,之后应该不允许这么做了。
            privateField.setAccessible(true);
            // 获取字段的值
            System.out.println("获取私有成员字段的值 = " + privateField.get(demo4FieldTest)); // 30
            // 设置字段的值
            privateField.set(demo4FieldTest, 40);
            System.out.println("获取私有成员字段的值 = " + privateField.get(demo4FieldTest)); // 40
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

总结:

  • 获取静态字段和获取成员字段的方式是一致的,只是操作方式不同,操作静态字段时,对象参数传null即可,操作成员字段时,需要明确指定是操作哪个对象中的字段
  • 操作私有字段时,需要设置为可访问,调用对象的 setAccessible 方法,参数为true

调用类中的方法

案例:

public class Demo5MethodTest {
    public static void main(String[] args) {
        Class<Demo5MethodTest> aClass = Demo5MethodTest.class;

        try {
            // 1、操作类中的静态方法
            Method publicStaticMethod = aClass.getMethod("publicStaticMethod", String.class, int.class);
            // 获取方法信息
            System.out.println("方法名 = " + publicStaticMethod.getName()); // publicStaticMethod
            System.out.println("获取方法所在的类 = " +
                    publicStaticMethod.getDeclaringClass());  // class org.wyj.reflex.Demo5MethodTest
            System.out.println("方法的参数信息,参数类型和参数名 = " +
                    Arrays.toString(publicStaticMethod.getParameters()));  // [java.lang.String name, int age]
            System.out.println("参数个数 = " + publicStaticMethod.getParameterCount());  // 2
            System.out.println("方法的参数类型信息 = " +
                    Arrays.toString(publicStaticMethod.getParameterTypes()));  // [class java.lang.String, int]
            System.out.println("方法上声明的异常 = " +
                    Arrays.toString(publicStaticMethod.getExceptionTypes()));  // [class java.lang.Exception]
            System.out.println("方法的返回值类型 = " + publicStaticMethod.getReturnType());  // void

            // 调用方法
            publicStaticMethod.invoke(null, "张三", 18);   // Public Static Method 张三 18

            // 2、操作类中的成员方法
            Demo5MethodTest demo5MethodTest = aClass.newInstance();
            Method publicMethod = aClass.getMethod("publicMethod", Double.class, Float.class);
            publicMethod.invoke(demo5MethodTest, 1.0D, 1.4F);  // Public Method 1.0 1.4

            // 3、操作类中的私有方法
            Method privateMethod = aClass.getDeclaredMethod("privateMethod");
            privateMethod.setAccessible(true);
            privateMethod.invoke(demo5MethodTest);  // Private Method
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void publicStaticMethod(String name, int age) throws Exception {
        System.out.println("Public Static Method " + name + " " + age);
    }

    public void publicMethod(Double a, Float b) {
        System.out.println("Public Method " + a + " " + b);
    }

    private void privateMethod() {
        System.out.println("Private Method");
    }
}

总结:通过反射操作类中的方法,和通过反射操作类中的字段没有什么不同,操作字段是设置字段的值,操作方法是调用方法。

获取泛型信息

泛型的使用场景:泛型类、泛型接口、泛型方法

获取泛型信息:泛型的底层原理,泛型信息只存在于代码编译阶段,在进入jvm之前,与泛型相关的信息将会被擦除。依据这种原理,获取的泛型信息分为两种:

  • 泛型形参:声明在类或方法上的泛型,是一个泛型形参,在运行时,这个泛型信息会被擦除,通过反射,仅仅可以获得泛型形参,例如 public class Container<T>,通过反射,仅仅可以获得“T”这个字符
  • 泛型实参:使用一个泛型类或泛型方法,传入的参数是泛型实参,这种情况下,在编译时指定了泛型实参,通过反射,可以在运行时获得泛型实参,例如,public class Son implements Father<String, Integer>,通过反射,可以获得String和Integer的类对象

获取到的泛型信息是一个泛型形参还是一个泛型实参,取决于要获取哪个位置的泛型,在那个位置上声明了一个泛型形参还是泛型实参。

  • 获取到的是泛型形参的场景:类或接口或方法声明的泛型
  • 获取到的是泛型实参的场景:成员变量的泛型、方法参数的泛型、方法返回值的泛型、父类的泛型

案例1:获取类上的泛型形参,例如,public class Box<T>,这里T就是泛型形参

public static void main(String[] args) {
    try {
        Class<?> boxClass = Class.forName("org.wyj.reflex.beans.Box");
        TypeVariable<? extends Class<?>>[] typeParameters = boxClass.getTypeParameters();
        System.out.println("类上声明的泛型 = " + Arrays.toString(typeParameters));  // [T, V]
        for (TypeVariable<? extends Class<?>> typeParameter : typeParameters) {
            // 泛型标识的名称 = T, 泛型的上界 = [class java.lang.Number]
            // 泛型标识的名称 = V, 泛型的上界 = [class java.lang.Object]
            System.out.println("泛型标识的名称 = " + typeParameter.getName() +
                    ", 泛型的上界 = " + Arrays.toString(typeParameter.getBounds()));
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

总结:TypeVariable,用于表示类或方法上声明的泛型形参,它可以获取泛型标识的名称、泛型的上界、泛型所在位置

案例2:获取泛型实参,例如 List<String>,这里String,就是一个泛型实参。

ublic static void main(String[] args) {
    try {
        Class<?> boxClass = Class.forName("org.wyj.reflex.beans.Box");

        // 获取类所实现的接口,包括接口上的泛型信息
        System.out.println("获取类所实现的接口 = " +
                Arrays.toString(boxClass.getInterfaces()));  // [interface java.lang.Comparable]
        Type[] genericInterfaces = boxClass.getGenericInterfaces();
        for (Type genericInterface : genericInterfaces) {
            if (genericInterface instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
                // 原始类型 + 泛型实参
                String s = parameterizedType.getRawType() + " " + 
                    Arrays.toString(parameterizedType.getActualTypeArguments());
                // interface java.lang.Comparable [org.wyj.reflex.beans.Box<T, V>]
                System.out.println("获取类所实现的接口,包括接口上的泛型信息 = " + s);  
            }
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

总结:ParameterizedType,用于获取泛型实参,它可以获取到原始类型和传递给类型的泛型实参,例如,List<String>,这里的原始类型是List.class,类型实参是String

public class Demo6GenericTest {
    public static void main(String[] args) {
        try {
            Class<?> boxClass = Class.forName("org.wyj.reflex.beans.Box");
            // 1、操作泛型类
            TypeVariable<? extends Class<?>>[] typeParameters = boxClass.getTypeParameters();
            System.out.println("类上声明的泛型 = " + Arrays.toString(typeParameters));  // [T, V]

            // 2、操作带泛型信息的字段
            Field boxListField = boxClass.getDeclaredField("boxList");
            boxListField.setAccessible(true);
            Type genericType1 = boxListField.getGenericType();
            if (genericType1 instanceof ParameterizedType) {
                System.out.println("字段上的泛型信息(泛型实参) = " + genericType1);
            }

            Field containerListField = boxClass.getDeclaredField("containerList");
            containerListField.setAccessible(true);
            Type genericType = containerListField.getGenericType();
            if (genericType instanceof ParameterizedType) {
                System.out.println("字段上的泛型信息(泛型实参) = " + genericType);
            }

            Field itemField = boxClass.getDeclaredField("item");
            containerListField.setAccessible(true);
            Type itemFieldType = itemField.getGenericType();
            if (itemFieldType instanceof TypeVariable) {
                System.out.println("字段上的泛型信息(泛型形参) = " + itemFieldType);
            }

            // 2、操作泛型方法
            Method genericMethod = boxClass.getMethod("genericMethod", Object.class);

            // 获取方法的参数类型
            Parameter[] parameters = genericMethod.getParameters();
            System.out.println("方法的形参(类型和名称) = " + Arrays.toString(parameters));      // [T obj]
            Class<?>[] parameterTypes = genericMethod.getParameterTypes();
            System.out.println("方法的形参(类型信息) = " + Arrays.toString(parameterTypes));  // [class java.lang.Object]
            Type[] genericParameterTypes = genericMethod.getGenericParameterTypes();
            System.out.println("方法的形参,带泛型信息 = " + Arrays.toString(genericParameterTypes)); // [T]

            // 获取方法的返回值类型
            System.out.println("方法的返回值类型 = " + genericMethod.getReturnType());  // class java.lang.Object
            Type genericReturnType = genericMethod.getGenericReturnType();
            if (genericReturnType instanceof TypeVariable) {
                TypeVariable<?> genericReturnType1 = (TypeVariable<?>) genericReturnType;
                // 方法的返回值类型,带泛型信息:泛型名称T,泛型上界 [class java.lang.Object],泛型的声明位置 = 
                // public static java.lang.Object org.wyj.reflex.beans.Box.genericMethod(java.lang.Object) throws java.lang.Exception
                System.out.println("方法的返回值类型,带泛型信息:泛型名称 " + genericReturnType1.getName() +
                        ",泛型上界 " + Arrays.toString(genericReturnType1.getBounds()) +
                        ",泛型的声明位置 = " + genericReturnType1.getGenericDeclaration());  // T
            }

            // 调用方法
            Box<?, ?> box = (Box<?, ?>) boxClass.newInstance();
            Object returnValue = genericMethod.invoke(box, "aaa");
            System.out.println("returnValue = " + returnValue);  // aaa

            // 3、获取父类,包括父类上的泛型信息
            System.out.println("获取父类 = " + boxClass.getSuperclass());  // class java.lang.ThreadLocal
            Type genericSuperclass = boxClass.getGenericSuperclass();
            System.out.println("获取父类,包括父类上有泛型信息 = " +
                    genericSuperclass);  // java.lang.ThreadLocal<org.wyj.reflex.beans.Box<T, V>>

            // 4、获取类所实现的接口,包括接口上的泛型信息
            System.out.println("获取类所实现的接口 = " +
                    Arrays.toString(boxClass.getInterfaces()));  // [interface java.lang.Comparable]
            Type[] genericInterfaces = boxClass.getGenericInterfaces();
            for (Type genericInterface : genericInterfaces) {
                if (genericInterface instanceof ParameterizedType) {
                    ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
                    String s = parameterizedType.getRawType() + " " + Arrays.toString(parameterizedType.getActualTypeArguments());
                    System.out.println("获取类所实现的接口,包括接口上的泛型信息 = " + s);  // interface java.lang.Comparable [org.wyj.reflex.beans.Box<T, V>]
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

总结:通过反射获取带泛型的元素信息和不带泛型的元素信息,上面的演示都涉及到了,API比较多,这里只做简单介绍,需要的时候可以参考。重点是知道如何获取泛型实参,这是实际开发中用的最多的。

获取注解信息

public static void main(String[] args) {
    try {
        Class<?> aClass = Class.forName("org.wyj.reflex.beans.Son");

        // 1、获取类上的注解
        MyAnno annotation = aClass.getAnnotation(MyAnno.class);
        System.out.println("类上的注解 = " + annotation);  // @org.wyj.reflex.beans.MyAnno(value=bbb)
        // 获取注解中存储的数据
        System.out.println("注解中存储的数据 = " + annotation.value());  // bbb
        // 获取注解上的注解
        MyAnno2 annotation1 = annotation.annotationType().getAnnotation(MyAnno2.class);
        System.out.println("注解上的注解 = " + annotation1);  // @org.wyj.reflex.beans.MyAnno2(value=aaa)

        // 2、获取方法上的注解
        Method annoTestMethod = aClass.getDeclaredMethod("annoTest", String.class, Integer.class);
        System.out.println("方法上的注解 = " +
                Arrays.toString(annoTestMethod.getDeclaredAnnotations()));  // [@org.wyj.reflex.beans.MyAnno(value=方法注解)]
        System.out.println("方法中参数上的注解 = " +
                Arrays.deepToString(annoTestMethod.getParameterAnnotations()));  // [[@org.wyj.reflex.beans.MyAnno(value=参数1)], [@org.wyj.reflex.beans.MyAnno(value=参数2)]]

        // 3、获取字段上的注解
        Field annoField = aClass.getDeclaredField("annoField");
        System.out.println("字段上的注解 = " +
                Arrays.toString(annoField.getAnnotations()));  // [@org.wyj.reflex.beans.MyAnno(value=字段注解)]
        System.out.println("字段上的注解 = " +
                Arrays.toString(annoField.getDeclaredAnnotations()));  // [@org.wyj.reflex.beans.MyAnno(value=字段注解)]
        // getAnnotations、getDeclaredAnnotations,它们的主要区别在于是否考虑注解的继承,不过在这里它们的行为是一致的,因为字段无法继承注解

        // 4、获取父类上的注解
        System.out.println("父类上的注解 = " +
                Arrays.toString(aClass.getSuperclass().getAnnotations()));  // [@org.wyj.reflex.beans.MyAnno(value=父类上的注解)]
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

注解在开发中用于存储配置信息,代替配置文件,熟悉spring的肯定很有认同.通过反射,获取代码中的注解信息,实际开发中使用的很多。这里对相关API做一个演示

获取内部类信息

案例:

public static void main(String[] args) {
    try {
        // 1、通过反射操作静态内部类
        Class<?> staticInnerClass = Class.forName("org.wyj.reflex.beans.OuterClass$StaticInnerClass");
        // 创建静态内部类的实例
        Object staticInnerObj = staticInnerClass.newInstance();
        System.out.println("静态内部类的实例 = " + staticInnerObj);  // org.wyj.reflex.beans.OuterClass$StaticInnerClass@4b67cf4d

        // 2、通过反射操作成员内部类
        Class<?> innerClass = Class.forName("org.wyj.reflex.beans.OuterClass$NonStaticInnerClass");

        // 创建外部类的实例
        Class<?> outerClass = Class.forName("org.wyj.reflex.beans.OuterClass");
        System.out.println("outerClass.getEnclosingClass() = " + outerClass.getEnclosingClass());
        OuterClass outerObj = (OuterClass) outerClass.newInstance();

        // 获取成员内部类的构造方法。注意,内部类的实例依赖外部类的实例,所以成员内部类的无参构造实际上有一个参数,
        // 是外部类的实例
        Constructor<?> constructor = innerClass.getConstructor(OuterClass.class);
        Object innerObj = constructor.newInstance(outerObj);
        System.out.println("成员内部类的实例 = " + innerObj);  // org.wyj.reflex.beans.OuterClass$NonStaticInnerClass@7ea987ac

        // 3、获取内部类的外部类
        Class<?> enclosingClass = innerClass.getEnclosingClass();
        System.out.println("内部类所属的外部类 = " + enclosingClass);  // class org.wyj.reflex.beans.OuterClass
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

通过反射操作内部类,内部类的名称是 OuterClass$InnerClass,通过美元符连接外部类名和内部类名,还有一点,调用成员内部类的构造器,需要传入外部类的实例,

数组

案例:

public static void main(String[] args) throws ClassNotFoundException {
    // 1、通过反射创建数组,Array类中还提供了get、set方法来操作数组
    Object arr = Array.newInstance(int.class, 5);
    System.out.println("通过反射创建的数组= " + arr);  // [I@4b67cf4d

    // 2、操作数组中的元素
    Array.set(arr, 0, 1);
    System.out.println("获取数组中的元素 " + Array.get(arr, 0));    // 1

    // 3、通过反射获取数组中元素的类型
    Class<?> aClass = arr.getClass();
    System.out.println("数组中的元素类型 = " + aClass.getComponentType()); // int
}

通过反射操作数组,主要使用到了Array类中的API

其它API

反射中一些不常见的api,但是很有用,偶尔会使用到。

Class类的 getMethods和getDeclaredMethods

这两个方法的区别主要在于搜索范围和访问权限:

  • getMethods:获取当前类和父类中的公共方法
    • 搜索范围:当前类、父类
    • 访问权限:public方法
  • getDeclaredMethods:获取当前类中的所有方法,包括私有方法,但需要设置 setAccessible(true) 来访问。
    • 搜索范围:当前类
    • 访问权限:所有方法

其它类似的API也一样,例如,字段 getFields、getDeclaredFields,构造器 getConstructors、getDeclaredCongtructors。

通过子类的类对象,是否可以获取到父类中的元素,首先要看该元素是否可以被继承、被外部访问。

Class类的 getName和getCanonicalName

案例:

    public static void main(String[] args) {
        try {
            Class<?> innerClass = Class.forName("org.wyj.reflex.beans.OuterClass$NonStaticInnerClass");

            // 类的二进制名称和规范名称,通常使用的都是二进制名称,这里对于规范名称仅做介绍。
            System.out.println("类的二进制名称 = " + innerClass.getName());  // org.wyj.reflex.beans.OuterClass$NonStaticInnerClass
            System.out.println("类的规范名称 = " + innerClass.getCanonicalName());  // org.wyj.reflex.beans.OuterClass.NonStaticInnerClass
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

getName方法获取的是类的二进制名称,通常用户使用的也是这个名称,getCanonicalName获取的是类的规范名称,它会对二进制名称做一些修改

getName:返回类的二进制名称,包括包名。对于内部类,返回包含 $ 的字符串。对于数组类型,返回带有方括号的字符串。更适合用于需要类的二进制名称的场合,尤其是在反射和字节码操作中。

getCanonicalName:返回类的规范名称,包括包名。对于内部类,返回包含外部类名的字符串。对于匿名类和局部类,返回 null。更适合用于需要类的完整信息的场合,尤其是代码生成和文档生成工具。

Class类的 isAssignableFrom

判断当前类是否是指定类对象的父类,a is assignable from b ,a可以由b赋值,所以a一定是b的父类或b本身

格式:父类.isAssignableFrom(子类)

案例:

public void test1() {
    boolean assignableFrom = Object.class.isAssignableFrom(String.class);  // 判断Object是不是可以由String赋值
    System.out.println("assignableFrom = " + assignableFrom);              // true
    boolean assignableFrom1 = Object.class.isAssignableFrom(Object.class);  
    System.out.println("assignableFrom1 = " + assignableFrom1);            // true
}

isAssignableFrom方法是Class类中的一个native方法:

// Class类
public native boolean isAssignableFrom(Class<?> cls);

Modifier 访问修饰符

类的访问修饰符。

相关源码:

public class Modifier {
  
    // 每一个访问修饰符都使用一个数字来表示,这里的数字是16进制的。
    public static final int PUBLIC           = 0x00000001;  // 000001
    public static final int PRIVATE          = 0x00000002;  // 000010
    public static final int PROTECTED        = 0x00000004;  // 000100
    public static final int STATIC           = 0x00000008;  // 001000
    public static final int FINAL            = 0x00000010;  // 010000
    public static final int SYNCHRONIZED     = 0x00000020;  // 100000
    public static final int VOLATILE         = 0x00000040;  // 以此类推
    public static final int TRANSIENT        = 0x00000080;
    public static final int NATIVE           = 0x00000100;
    public static final int INTERFACE        = 0x00000200;
    public static final int ABSTRACT         = 0x00000400;
    public static final int STRICT           = 0x00000800;

    // 访问修饰符的计算,这里计算一个元素是否是public
    public static boolean isPublic(int mod) {
        // 按位与,遇0则0,例如,如果字段的修饰符是 public static,那么它的访问修饰符是 1001,
        // 它和PUBLIC(0001)按位与,结果是0001,不为0,证明字段被public关键字修饰。因为按位与遇0则0,
        // 只有传入的访问修饰符PUBLIC位上不为0,按位与的结果才不为0
        return (mod & PUBLIC) != 0;   
    }
}

通过Modifier类,可以判断一个元素有什么访问修饰符。

它的原理是,每一个访问修饰符使用一个2的倍数来表示,如果一个元素有多个访问修饰符,那么就是多个访问修饰符执行或运算,组合在一起,如果需要判断元素上有没有指定访问修饰符,就是元素的访问修饰符和修饰符常量进行与运算,结果不为0就表示元素上有该修饰符。这应该算是比较简单的位运算,很有参考价值,在设计状态值枚举的时候,尤其是一个实体可以同时拥有多种状态的情况下很有用,否则使用简单的枚举类就可以了。

合成方法 isSynthetic

合成方法是由编译器自动生成的方法,如内部类访问外部类的私有成员时,编译器会自动在外部类中生成一个合成方法,java正是通过这个合成方法来解决内部类无法访问外部类私有成员的问题的。

案例:

// 外部类和内部类
public class OuterClass {
    private int value = 10;

    // 成员内部类,测试合成方法
    public class InnerClass {
        public void accessPrivateMember() {
            System.out.println("value = " + value);
        }
    }
}

// 测试
public static void main(String[] args) {
    try {
        Class<?> aClass = Class.forName("org.wyj.reflex.beans.OuterClass");
        Method[] methods = aClass.getDeclaredMethods();
        for (Method method : methods) {
            if (method.isSynthetic()) {  // 如果方法是合成方法
                // static int org.wyj.reflex.beans.OuterClass.access$000(org.wyj.reflex.beans.OuterClass)
                System.out.println("method = " + method);
                System.out.println("method.getModifiers() = " + method.getModifiers());  // 4104
            }
        }
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
}

使用javap反编译代码,查看字节码中的合成方法:

 static int access$000(org.wyj.reflex.beans.OuterClass);
   descriptor: (Lorg/wyj/reflex/beans/OuterClass;)I
   flags: ACC_STATIC, ACC_SYNTHETIC         # 注意这里方法的修饰符,表示它是一个静态的合成方法
   Code:
     stack=1, locals=1, args_size=1
        0: aload_0
        1: getfield      #1                  // Field value:I   # 在合成方法的内部访问私有变量
        4: ireturn
     LineNumberTable:
       line 3: 0
     LocalVariableTable:
       Start  Length  Slot  Name   Signature
           0       5     0    x0   Lorg/wyj/reflex/beans/OuterClass;

在使用反射获取方法时,有时候会获取到合成方法,需要过滤掉这种方法,在这里介绍一下它的作用。

桥接方法 isBridge

编译器自动生成的方法,作用在方法重写时。方法重写时对返回值的要求分两种情况,一,父子类的返回值完全一致;二,子类返回值可以是父类返回值的子类。如果是第二种情况,jvm会自动生成一个桥接方法,被synthetic bridge修饰,专门供jvm使用,程序员不可见。例如,父类中方法返回Object,子类中重写父类的方法后,返回String,这在语法上是支持的,java内部会生成一个桥接方法,来支持这种重写。

桥接方法的作用:在桥接方法中调用重写的方法,作为中间层,进行过渡

案例:

// 接口
public interface Action {
    Object play(String action);
}

// 子类实现接口中的方法
public class Children implements Action {
    // 测试桥接方法
    @Override
    public String play(String action) {
        return "children play basketball....";
    }
}

// 测试
public static void main(String[] args) {
    try {
        Class<?> aClass = Class.forName("org.wyj.reflex.beans.Children");
        Method[] declaredMethods = aClass.getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            if (declaredMethod.isBridge()) {
                // public java.lang.Object org.wyj.reflex.beans.Children.play(java.lang.String)
                System.out.println("桥接方法 = " + declaredMethod);
            }
        }
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
}

使用javap反编译字节码,查看生成的桥接方法

  public java.lang.Object play(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC    # 桥接方法,同时也是一个合成方法
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: invokevirtual #3                  // Method play:(Ljava/lang/String;)Ljava/lang/String;
         5: areturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lorg/wyj/reflex/beans/Children;
    MethodParameters:
      Name                           Flags
      action                         synthetic

桥接方法也是在使用反射获取方法时可能遇到的,需要过滤掉,这里仅做介绍,

反射相关的源码

相关组件

1、顶层接口

Type:所有类型的顶层接口,包括原始类型、带泛型的类型、数组类型等

public interface Type {
    // 获取类型名称
    default String getTypeName() {
        return toString();
    }
}

AnnotatedElement:代表一个被注解标注的元素

public interface AnnotatedElement {

    // 获取指定类型的注解
    <T extends Annotation> T getAnnotation(Class<T> annotationClass);

    // 获取当前元素上的注解
    Annotation[] getAnnotations();
  
    // 获取当前元素上的注解,不考虑继承
    Annotation[] getDeclaredAnnotations();
}

GenericDeclaration:一个同时有泛型、有注解的元素

public interface GenericDeclaration extends AnnotatedElement {
    // 返回元素上的泛型信息
    public TypeVariable<?>[] getTypeParameters();
}

总结:

  • Type是顶层接口,代表任何类型
  • AnnotatedElement,继承了Type接口,代表一个有注解信息的元素
  • GenericDeclaration,继承了AnnotatedElement接口,代表一个有泛型信息的元素

2、中间层,提供了公共抽象

AccessibleObject:代表一个可访问的对象,Constructor、Field、Method的公共父类

public class AccessibleObject implements AnnotatedElement {
    boolean override;   // 一个标识,表示是否由用户来重写访问权限,setAccessible方法就是操作的这个变量
}

Executable:代表一个可执行的对象,Constructor和Method的抽象父类

public abstract class Executable extends AccessibleObject
    implements Member, GenericDeclaration {
}

Member:代表类中的一个成员,它是Field的父接口

public interface Member {

    // 获取成员所在类
    public Class<?> getDeclaringClass();

    // 获取名称
    public String getName();

    // 获取访问修饰符
    public int getModifiers();

    // 是否合成,表示当前成员是否由编译器引入。
    public boolean isSynthetic();
}

3、实现类,用户使用的API

Class:代表一个类的类对象

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
}

Constructor:代表一个构造器的类对象

public final class Constructor<T> extends Executable {
    private Class<T>            clazz;  // 构造器所属的类
    private Class<?>[]          parameterTypes;  // 形参列表
}

Field:代表一个字段的类对象

public final
class Field extends AccessibleObject implements Member {

    private Class<?>            clazz;  // 字段所属的类
    private String              name;   // 字段名
    private Class<?>            type;   // 字段类型
    private int                 modifiers;  // 字段的修饰符
}

Method:代表一个方法的类对象

public final class Method extends Executable {
    private Class<?>            clazz;   // 方法所属的类对象
    private int                 slot;    // 
    // This is guaranteed to be interned by the VM in the 1.4
    // reflection implementation
    private String              name;    // 方法名称
    private Class<?>            returnType;   // 返回值类型
    private Class<?>[]          parameterTypes;  // 形参列表
    private Class<?>[]          exceptionTypes;  // 方法上声明的异常
    private int                 modifiers;       // 方法的修饰符
}

Parameter:代表方法上的参数

public final class Parameter implements AnnotatedElement {

    private final String name;  // 参数名
    private final int modifiers;
    private final Executable executable;  // 参数所在的方法
    private final int index;   // 参数的下标
}

4、工具类

Array:通过反射操作数组的工具类

public final class Array {
    private Array() {}
}

5、泛型相关

TypeVariable:代表泛型形参,如果java检测到解析的泛型是一个泛型形参,例如,public class List<T>,此时T就是一个泛型形参,java会使用TypeVariable来封装解析好的结果

public interface TypeVariable<D extends GenericDeclaration> extends Type, AnnotatedElement {
    // 泛型的上界,例如 T extents Number,此时Number就是泛型的上界
    Type[] getBounds();

    // 泛型的声明位置
    D getGenericDeclaration();

    // 泛型的名称,例如T、E
    String getName();
}

ParameterizedType:代表泛型实参,如果java检测到解析的泛型是一个泛型实参,例如,List<String>,此时String就是一个泛型实参,java会使用ParameterizedType来封装解析好的结果

public interface ParameterizedType extends Type {
    // 泛型实参,例如List<String>中的String
    Type[] getActualTypeArguments();

    // 声明了泛型的类
    Type getRawType();
}

总结:这里仅仅介绍一些API,主要是学习第三方框架时会看到很多这种API,熟悉一下相关接口。这里不介绍具体流程,主要一些本地方法,以及本地缓存,它的内部会把解析好的结果缓存到成员变量中。

实战案例

获取父接口上的泛型实参

案例:类实现了一个带泛型信息的接口,获取接口上的泛型实参

// 接口1:带泛型的接口
public interface MyInf<T> { }  // T 是泛型形参
// 接口2:不带泛型的接口
public interface MyInf2 { }
// 实现类,注意,接口上声明的 T 是泛型形参,这里实现接口后指定的是泛型实参
public class MyObj implements MyInf<String>, MyInf2 {  }

获取类所实现的所有接口的泛型信息

public static void main(String[] args) {
    // 1. 获取类对象
    Class<MyObj> myObjClass = MyObj.class;

    // 2. 获取类实现的所有接口
    Type[] genericInterfaces = myObjClass.getGenericInterfaces();
    for (Type genericInterface : genericInterfaces) {
        // 3. 判断接口有没有带泛型信息
        if (genericInterface instanceof ParameterizedType) {
            // 4. 获取接口的原始类型和泛型实参
            ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
            Type rawType = parameterizedType.getRawType();  // 去掉泛型信息后的原始类型
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); // 接口的泛型实参
            System.out.println("ParameterizedType = " + rawType + " " + Arrays.toString(actualTypeArguments));
        } else {
            System.out.println("Type = " + genericInterface);
        }
    }
}

总结:这是在学习spring事件时使用到的操作,事件的监听器需要继承ApplicationListener,它是一个泛型接口,实现类需要指定接口的泛型实参,例如,public class MyAppListener implements ApplicationListener<MyEvent>,在这个监听器中,它只可以处理MyEvent类型的事件,spring会获取监听器上ApplicationListener接口的泛型实参,通过它来判断监听器可以处理什么类型的事件。

解析json数据

这里使用gson来解析json数组,要求解析过程中指定泛型信息。

第一步:添加依赖

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.8</version>
</dependency>

第二步:解析数据

public static void main(String[] args) {
    String json = "[{\"id\":1,\"name\":\"Alice\"},{\"id\":2,\"name\":\"Bob\"}]";

    Gson gson = new Gson();
    Type listType = new TypeToken<List<Person>>() { }.getType();  // 泛型类型
    List<Person> personList = gson.fromJson(json, listType);

    for (Person person : personList) {
        System.out.println("ID: " + person.getId() + ", Name: " + person.getName());
    }
}

注意看,解析过程中是如何指定泛型类型的,new TypeToken<List<Person>>() { }.getType(),通过继承了TypeToken的匿名内部类,这里实际上是通过子类,获取父类上的泛型实参,通过这种方式来指定json数组的类型。

反射的优化

缓存反射相关的实例、关闭安全检查

踩坑记录

通过反射,获取方法的参数值

这是不可以的,反射只可以获取到类对象相关的信息,参数值,是实例中的值,所以无法获取到。反射只可以获取到形参相关的数据,获取不到实参相关的数据。

通过反射,无法获取方法的参数名

java 通过Method实例中的method.getParameters(),无法获取参数名称

案例:

public class MethodTest {
    public static void main(String[] args) throws NoSuchMethodException {
        Method testMethod = MethodTest.class.getDeclaredMethod("test", String.class, int.class);
        Parameter[] parameters = testMethod.getParameters();
        for (Parameter parameter : parameters) {
            System.out.println("parameter.getName() = " + parameter.getName());
        }
    }
    public void test(String name, int age) {

    }
}

结果:

parameter.getName() = arg0
parameter.getName() = arg1

原因:java代码编译为字节码后,在字节码中没有保留方法的参数名称,反编译字节码

  public void test(java.lang.String, int);
    descriptor: (Ljava/lang/String;I)V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=3, args_size=3
         0: return
      LineNumberTable:
        line 18: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lorg/wyj/reflex/practice/Practice2MethodParameterTest;
            0       1     1  name   Ljava/lang/String;
            0       1     2   age   I
    MethodParameters:
      Name                           Flags     # 参数名
      name
      age

这是有参数名的情况,这个问题是偶发的,我没有截取到没有参数名的情况,但是在没有参数名的情况下,参数名默认是arg0、arg1等。

解决方案:

  • 编译时加上配置:build -> compiler -> java compiler,在additional command line parameters中加上参数“-parameters”

在这里插入图片描述

  • Maven配置编译插件,编译时保留方法的参数名称
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <compilerArgs>
            <arg>-parameters</arg>
        </compilerArgs>
    </configuration>
</plugin>

这个问题是偶发的,开发中偶尔就会遇到。我在测试环境中也遇到过类似的问题,但是比这更复杂,最终排查,发现打包生成的字节码中有参数名,但是运行时加载到的字节码中没有参数名,最后发现,类被javaAgent重写了,导致失去了参数名称

注解的继承

注解的继承规则:Java 注解本身不支持继承,但可以通过 @Inherited 元注解实现类级别注解的继承。@Inherited 注解只适用于类级别的注解,不会影响方法、构造函数或字段上的注解。注解继承只对直接父类有效,不会跨多个继承层级。

获取注解时要注意注解的继承,它用的不多,但有时候会用到。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大语言模型中的嵌入模型

本教程将拆解什么是嵌入模型、为什么它们在NLP中如此重要,并提供一个简单的Python实战示例。 分词器将原始文本转换为token和ID,而嵌入模型则将这些ID映射为密集向量表示。二者合力为LLMs的语义理解提供动力。图片来源:[https://tzamtzis.gr/2024/coding/tokenization-by-an…

【从零实现Json-Rpc框架】- 项目实现 - 服务端主题实现及整体封装

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;博客仓库&#xff1a;https://gitee.com/JohnKingW/linux_test/tree/master/lesson &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &…

开源的 LLM 应用开发平台Dify的安装和使用

文章目录 前提环境应用安装deocker desktop镜像源配置Dify简介Dify本地docker安装Dify安装ollama插件Dify安装硅基流动插件简单应用练习进阶应用练习数据库图像检索与展示助手echart助手可视化 前提环境 Windows环境 docker desktop魔法环境&#xff1a;访问Dify项目ollama电脑…

从零构建大语言模型全栈开发指南:第五部分:行业应用与前沿探索-5.1.2行业落地挑战:算力成本与数据隐私解决方案

👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 从零构建大语言模型全栈开发指南-第五部分:行业应用与前沿探索5.1.2 行业落地挑战:算力成本与数据隐私解决方案1. 算力成本挑战与优化策略1.1 算力成本的核心问题1.2 算力优化技术方案2. 数据隐私挑战…

NodeJS--NPM介绍使用

1、使用npm install命令安装模块 1.1、本地安装 npm install express 1.2、全局安装 npm install express -g 1.3、本地安装和全局安装的区别

DeepSeek与ChatGPT的优势对比:选择合适的工具来提升工作效率

选DeepSeek还是ChatGPT&#xff1f;这就像问火锅和披萨哪个香&#xff01; "到底该用DeepSeek还是ChatGPT?” 这个问题最近在互联网圈吵翻天!其实这就跟选手机系统-样&#xff0c;安卓党iOS党都能说出一万条理由&#xff0c;但真正重要的是你拿它来干啥&#xff01;&am…

25大唐杯赛道一本科B组知识点大纲(下)

5G/6G网络技术知识点&#xff08;10%&#xff09; 工程概论及通信工程项目实践&#xff08;20%&#xff09; 5G垂直行业应用知识点&#xff08;20%&#xff09; ⭐⭐⭐为重点知识&#xff0c;尽量要过一遍哦 大唐杯赛道一国一备赛思路 大唐杯国一省赛回忆录--有付出就会有收…

Python+Playwright自动化测试-1-环境准备与搭建

1、Playwright 是什么&#xff1f; 微软在 2020 年初开源的新一代自动化测试工具&#xff0c;它的功能类似于 Selenium、Pyppeteer 等&#xff0c;都可以驱动浏览器进行各种自动化操作。它的功能也非常强大&#xff0c;对市面上的主流浏览器都提供了支持&#xff0c;API 功能简…

生产管理系统如何破解汽车零部件行业追溯难痛点

在汽车零部件制造行业中&#xff0c;生产追溯一直是企业面临的核心挑战之一。随着市场竞争的加剧和客户需求的日益复杂&#xff0c;如何确保产品质量、快速定位问题源头、减少批次性返工&#xff0c;成为了每个企业亟待解决的问题。而生产管理系统&#xff0c;作为智能制造的重…

【XTerminal】【树莓派】Linux系统下的函数调用编程

目录 一、XTerminal下的Linux系统调用编程 1.1理解进程和线程的概念并在Linux系统下完成相应操作 (1) 进程 (2)线程 (3) 进程 vs 线程 (4)Linux 下的实践操作 1.2Linux的“虚拟内存管理”和stm32正式物理内存&#xff08;内存映射&#xff09;的区别 (1)Linux虚拟内存管…

umi框架开发移动端h5

1、官网&#xff1a;https://umijs.org/ 2、创建出来的项目 yarn create umi yarn start3、推荐目录结构 . ├── config │ └── config.ts ├── public//静态资源 ├── dist ├── mock │ └── app.ts&#xff5c;tsx ├── src │ ├── .umi │ ├── .um…

3.9/Q2,Charls最新文章解读

文章题目&#xff1a;Association between remnant cholesterol and depression in middle-aged and older Chinese adults: a population-based cohort study DOI&#xff1a;10.3389/fendo.2025.1456370 中文标题&#xff1a;中国中老年人残留胆固醇与抑郁症的关系&#xff1…

Java Lambda 表达式提升效率

lambda 表达式的应用场景 Stream 的应用场景 Lambda/Stream 的进一步封装 自定义函数式接口&#xff08;用 jdk 自带的函数式接口也可以&#xff09; https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html import java.io.Serializable;/*** 可序…

人工智能混合编程实践:C++ ONNX进行图像超分重建

人工智能混合编程实践:C++ ONNX进行图像超分重建 前言相关介绍C++简介ONNX简介ONNX Runtime 简介**核心特点**图像超分辨率重建简介应用场景前提条件实验环境项目结构使用C++ ONNX进行图像超分重建sr_main.cpp参考文献前言 由于本人水平有限,难免出现错漏,敬请批评改正。更多…

K8S学习之基础七十四:部署在线书店bookinfo

部署在线书店bookinfo 在线书店-bookinfo 该应用由四个单独的微服务构成&#xff0c;这个应用模仿在线书店的一个分类&#xff0c;显示一本书的信息&#xff0c;页面上会显示一本书的描述&#xff0c;书籍的细节&#xff08;ISBN、页数等&#xff09;&#xff0c;以及关于这本…

Python不可变数据类型全解析:原理、优势与实战指南

目录 引言&#xff1a;为什么Python要区分可变与不可变&#xff1f; 一、不可变数据类型的核心特性 二、五大不可变数据类型深度解析 三、不可变数据类型的三大核心优势 四、不可变数据类型的典型应用场景 五、不可变 vs 可变&#xff1a;如何选择&#xff1f; 六、实战技…

Apache Doris 2025 Roadmap:构建 GenAI 时代实时高效统一的数据底座

在全球 290 位开发者的协作下&#xff0c;Apache Doris 在 2024 年完成了 7000 次代码提交&#xff0c;并发布了 22 个版本&#xff0c;实现在实时分析、湖仓一体和半结构化数据分析等核心场景的技术突破及创新。 2025 年&#xff0c;Apache Doris 社区将秉承“以场景驱动创新…

二极管正负极区分

二极管正负极区分 二极管是一种具有单向导电性的半导体器件&#xff0c;正确区分正负极对于其使用非常重要。以下是几种常见的二极管正负极区分方法&#xff1a; 1. 外观标识 有标记的二极管 色环或色点&#xff1a;许多二极管在表面会有一个色环或色点&#xff0c;这个标记…

【c++深入系列】:类与对象详解(中)

&#x1f525; 本文专栏&#xff1a;c &#x1f338;作者主页&#xff1a;努力努力再努力wz &#x1f4aa; 今日博客励志语录&#xff1a; 不是因为看到希望才坚持&#xff0c;而是坚持了才能看到希望 那么上一篇博客我讲解了什么是类和对象以及类和对象是怎么定义的&#xff0…

汽车 HMI 设计的发展趋势与设计要点

一、汽车HMI设计的发展历程与现状 汽车人机交互界面&#xff08;HMI&#xff09;设计经历了从简单到复杂、从单一到多元的演变过程。2012年以前&#xff0c;汽车HMI主要依赖物理按键进行操作&#xff0c;交互方式较为单一。随着特斯拉Model S的推出&#xff0c;触控屏逐渐成为…