博客记录-day024-Java泛型、反射

84 阅读8分钟

一、沉默王二-Java重要知识点

1、Java 为什么无法实现真正的泛型?

Java泛型会有类型擦除。 但由于类型擦除的原因,以上代码是不会编译通过的——编译器会提示一个错误:

也就是说,两个 method() 方法经过类型擦除后的方法签名是完全相同的,Java 是不允许这样做的。

针对泛型,兼容性具体表现在什么地方呢?来看下面这段代码。

ArrayList<Integer> ints = new ArrayList<Integer>();
ArrayList<String> strs = new ArrayList<String>();
ArrayList list;
list = ints;
list = strs;

“兼容性表现在上面这段代码必须得能够编译运行。怎么办呢?”

“只能搞类型擦除了!”

编译前进行泛型检测,ArrayList<Integer> 只能放 Integer,ArrayList<String> 只能放 String,取的时候就不用担心类型强转出错了。

编译后的字节码文件里,是没有泛型的,放的都是 Object

Java 神奇就神奇在这,表面上万物皆对象,但为了性能上的考量,又存在 int、double 这种原始类型,但原始类型又没办法和 Object 兼容,于是我们就只能写 ArrayList<Integer> 这样很占用内存空间的代码。

2、Java 反射

反射是 Java 中的一个强大特性,它允许在运行时检查和操作类、接口、字段和方法。反射是 Java 的核心组件,支持各种框架和库的实现,如 Spring、Hibernate 等。使用反射,可以在运行时动态地创建对象、调用方法和访问字段,而无需在编译时了解这些对象的具体实现。

反射的主要类位于 java.lang.reflect 包中,主要包括以下几个关键类:

  • Class:代表一个类或接口,包含了类的结构信息(如名称、构造函数、方法、字段等)。通过 Class 对象,可以获取类的元数据并操作类的实例。
  • Constructor:代表类的构造方法,用于创建类的实例。
  • Method:代表类的方法,可以通过它调用类的实例方法。
  • Field:代表类的字段,可以获取或修改字段的值。
  • Modifier:包含方法、字段和类的访问修饰符(如 public、private 等)。

使用反射时,需要注意以下几点:

  • 性能:反射操作通常比直接操作对象的方法和字段慢,因为涉及到额外的间接调用和动态解析。因此,在关注性能的场景中,慎用反射。
  • 安全性:通过反射,可以访问和操作类的私有字段和方法,这可能导致安全问题。因此,使用反射时要确保代码的安全性。
  • 维护性:反射使代码变得更加复杂,可能导致难以维护。在使用反射时要确保代码的可读性和可维护性。

“一般情况下,我们在使用某个类之前已经确定它到底是个什么类了,拿到手就直接可以使用 new 关键字来调用构造方法进行初始化,之后使用这个类的对象来进行操作。”

Writer writer = new Writer();
writer.setName("沉默王二");

像上面这个例子,就可以理解为“正射”。而反射就意味着一开始我们不知道要初始化的类到底是什么,也就没法直接使用 new 关键字创建对象了。

我们只知道这个类的一些基本信息,就好像我们看电影的时候,为了抓住一个犯罪嫌疑人,警察就会问一些目击证人,根据这些证人提供的信息,找专家把犯罪嫌疑人的样貌给画出来——这个过程,就可以称之为反射

// 使用Class.forName加载指定的类
Class clazz = Class.forName("com.itwanger.s39.Writer");

// 获取 clazz 类中名为 setName 的方法,该方法接受一个 String 类型的参数
Method method = clazz.getMethod("setName", String.class);

// 获取 clazz 类的无参构造方法
Constructor constructor = clazz.getConstructor();

// 使用无参构造方法创建 clazz 类的实例
Object object = constructor.newInstance();

// 调用 object 实例的 setName 方法,传入参数 "沉默王二"
method.invoke(object,"沉默王二");

像上面这个例子,就可以理解为“反射”。

反射的缺点主要有两个。

  • 破坏封装:由于反射允许访问私有字段和私有方法,所以可能会破坏封装而导致安全问题。
  • 性能开销:由于反射涉及到动态解析,因此无法执行 Java 虚拟机优化,再加上反射的写法的确要复杂得多,所以性能要比“正射”差很多,在一些性能敏感的程序中应该避免使用反射。

那反射有哪些好处呢?

反射的主要应用场景有:

  • 开发通用框架:像 Spring,为了保持通用性,通过配置文件来加载不同的对象,调用不同的方法。
  • 动态代理:在面向切面编程中,需要拦截特定的方法,就会选择动态代理的方式,而动态代理的底层技术就是反射。
  • 注解:注解本身只是起到一个标记符的作用,它需要利用发射机制,根据标记符去执行特定的行为。

有以下案例:

Writer 类,有两个字段,然后还有对应的 getter/setter。

@Data
public class Writer {
    private int age;
    private String name;
    //getter,setter....
}

测试类:

public class ReflectionDemo1 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //正射
        Writer writer = new Writer();
        writer.setName("沉默王二");
        System.out.println(writer.getName());

        //反射
        
        // 使用Class.forName加载指定的类
        Class clazz = Class.forName("com.itwanger.s39.Writer");
        // 获取该类的无参构造方法
        Constructor constructor = clazz.getConstructor();
        // 使用构造方法创建对象实例
        Object object = constructor.newInstance();

        // 获取对象实例的setName方法,该方法接受一个String类型的参数
        Method setNameMethod = clazz.getMethod("setName", String.class);
        // 调用setName方法,将对象实例的名称设置为"沉默王二"
        setNameMethod.invoke(object, "沉默王二");
        // 获取对象实例的getName方法,该方法不接受参数
        Method getNameMethod = clazz.getMethod("getName");
        // 调用getName方法,并打印返回的字符串
        System.out.println(getNameMethod.invoke(object));

    }
}

来看一下输出结果:

沉默王二
沉默王二

只不过,反射的过程略显曲折了一些。

第一步,获取反射类的 Class 对象

Class clazz = Class.forName("com.itwanger.s39.Writer");

在 Java 中,Class 对象是一种特殊的对象,它代表了程序中的类和接口。

Java 中的每个类型(包括类、接口、数组以及基础类型)在 JVM 中都有一个唯一的 Class 对象与之对应。这个 Class 对象被创建的时机是在 JVM 加载类时,由 JVM 自动完成。

Class 对象中包含了与类相关的很多信息,如类的名称、类的父类、类实现的接口、类的构造方法、类的方法、类的字段等等。这些信息通常被称为元数据(metadata)。

除了前面提到的,通过类的全名获取 Class 对象,还有以下两种方式:

  • 如果你有一个类的实例,你可以通过调用该实例的getClass()方法获取 Class 对象。例如:String str = "Hello World"; Class cls = str.getClass();
  • 如果你有一个类的字面量(即类本身),你可以直接获取 Class 对象。例如:Class cls = String.class;

第二步,通过 Class 对象获取构造方法 Constructor 对象

Constructor constructor = clazz.getConstructor();

第三步,通过 Constructor 对象初始化反射类对象

Object object = constructor.newInstance();

第四步,获取要调用的方法的 Method 对象

Method setNameMethod = clazz.getMethod("setName", String.class);
Method getNameMethod = clazz.getMethod("getName");

第五步,通过 invoke() 方法执行

setNameMethod.invoke(object, "沉默王二");
getNameMethod.invoke(object)

要想使用反射,首先需要获得反射类的 Class 对象,每一个类,不管它最终生成了多少个对象,这些对象只会对应一个 Class 对象,这个 Class 对象是由 Java 虚拟机生成的,由它来获悉整个类的结构信息。

也就是说,java.lang.Class 是所有反射 API 的入口

而方法的反射调用,最终是由 Method 对象的 invoke() 方法完成的。

1)获取反射类的 Class 对象

Class.forName(),参数为反射类的完全限定名。

Class c1 = Class.forName("com.itwanger.s39.ReflectionDemo3");
System.out.println(c1.getCanonicalName());//com.itwanger.s39.ReflectionDemo3

Class c2 = Class.forName("[D");
System.out.println(c2.getCanonicalName());//double[]

Class c3 = Class.forName("[[Ljava.lang.String;");
System.out.println(c3.getCanonicalName());//java.lang.String[][]

类名 + .class,只适合在编译前就知道操作的 Class。。

Class c1 = ReflectionDemo3.class;
System.out.println(c1.getCanonicalName());//com.itwanger.s39.ReflectionDemo3

Class c2 = String.class;
System.out.println(c2.getCanonicalName());//java.lang.String

Class c3 = int[][][].class;
System.out.println(c3.getCanonicalName());//int[][][]

2)创建反射类的对象

通过反射来创建对象的方式有两种:

  • 用 Class 对象的 newInstance() 方法。
  • 用 Constructor 对象的 newInstance() 方法。
Class c1 = Writer.class;
Writer writer = (Writer) c1.newInstance();

Class c2 = Class.forName("com.itwanger.s39.Writer");
Constructor constructor = c2.getConstructor();
Object object = constructor.newInstance();

3)获取构造方法

Class 对象提供了以下方法来获取构造方法 Constructor 对象:

  • getConstructor():返回反射类的特定 public 构造方法,可以传递参数,参数为构造方法参数对应 Class 对象;缺省的时候返回默认构造方法。
  • getDeclaredConstructor():返回反射类的特定构造方法,不限定于 public 的。
  • getConstructors():返回类的所有 public 构造方法。
  • getDeclaredConstructors():返回类的所有构造方法,不限定于 public 的。
Class c2 = Class.forName("com.itwanger.s39.Writer");
Constructor constructor = c2.getConstructor();

Constructor[] constructors1 = String.class.getDeclaredConstructors();
for (Constructor c : constructors1) {
    System.out.println(c);
}

4)获取字段

大体上和获取构造方法类似,把关键字 Constructor 换成 Field 即可。

Field[] fields1 = System.class.getFields();
Field fields2 = System.class.getField("out");

5)获取方法

大体上和获取构造方法类似,把关键字 Constructor 换成 Method 即可。

Method[] methods1 = System.class.getDeclaredMethods();
Method[] methods2 = System.class.getMethods();

如果你想反射访问私有字段和(构造)方法的话,需要使用 Constructor/Field/Method.setAccessible(true) 来绕开 Java 语言的访问限制。