0X JavaSE-- Annotation、Reflection、网络编程

Annotation 注解

注解 是在 J5 引入的概念。
同 class 和 interface 一样,注解也属于一种类型

注解提供了一系列数据用来装饰程序代码(类、方法、字段等),但是注解并不是所装饰代码的一部分,它对代码的运行效果没有直接影响:

// Autowired 注解本来是用于 Spring 的自动注入,但添加也并不影响代码的运行
public cl.ass AutowiredTest {
    @Autowired
    private String name;

    public static void main(String[] args) {
        System.out.println("Not SpringFramework");
    }
}
  • 有些注解只适用于方法,有些只适用于成员变量,有些只适用于类,有些则都适用。
  • 注解的生命周期有 3 种策略,三种策略的生命长度依次增加
    • SOURCE:在源文件中有效,被编译器丢弃。
    • CLASS:在编译器生成的字节码文件中有效,但在运行时会被处理类文件的 JVM 丢弃。
    • RUNTIME:在运行时有效。这也是注解生命周期中最常用的一种策略,它允许程序通过反射的方式访问注解,并根据注解的定义执行相应的代码。
  • 注解的生命周期在定义该注解时就已经确定了。
// Override 元注解的定义代码。指明了生命周期、适用范围。
package java.lang;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface Override {
}
  • 可以自定义注解。

自定义注解 Demo

需求:定义一个字段注解,该注解标注的字段必须被序列化,没有被标注的字段不被序列化。

@Retention(RetentionPolicy.RUNTIME)// 生命周期是 RUNTIME,也就是运行时有效
@Target(ElementType.FIELD) // 注解装饰的目标是 FIELD,也就是针对字段的。

public @interface JsonField { // 指定创建该注解时必须用 @interface 关键字。
    public String value() default ""; 
}

关于 public String value() default ""; 的进一步说明:

  • JsonField 注解有一个参数,参数名为 value,参数类型为 String,参数默认值为一个空字符
    • value 允许注解的使用者提供一个无需指定名字的参数。举个例子,我们可以在一个字段上使用 @JsonField(value = "anthony"),也可以把 value = 省略,变成 @JsonField("value")
      default 它允许我们在一个字段上直接使用 @JsonField,而无需指定参数的名和值。”我回答说。

测试:在 Writer 对象中,name 和 bookName 必须被序列化,age 字段不被序列化。

public class Writer {
    private int age;

    @JsonField("writerName")
    private String name;

    @JsonField
    private String bookName;

    public Writer(int age, String name, String bookName) {
        this.age = age;
        this.name = name;
        this.bookName = bookName;
    }

    // 省略 getter、setter、toString
}
public class JsonSerializer {
    public static String serialize(Object object) throws IllegalAccessException {
        Class<?> objectClass = object.getClass();
        Map<String, String> jsonElements = new HashMap<>();
        for (Field field : objectClass.getDeclaredFields()) {
            field.setAccessible(true);
            if (field.isAnnotationPresent(JsonField.class)) {
                jsonElements.put(getSerializedKey(field), (String) field.get(object));
            }
        }
        return toJsonString(jsonElements);
    }

    private static String getSerializedKey(Field field) {
        String annotationValue = field.getAnnotation(JsonField.class).value();
        if (annotationValue.isEmpty()) {
            return field.getName();
        } else {
            return annotationValue;
        }
    }

    private static String toJsonString(Map<String, String> jsonMap) {
        String elementsString = jsonMap.entrySet()
                .stream()
                .map(entry -> "\"" + entry.getKey() + "\":\"" + entry.getValue() + "\"")
                .collect(Collectors.joining(","));
        return "{" + elementsString + "}";
    }
}

public class JsonFieldTest {
    public static void main(String[] args) throws IllegalAccessException {
        Writer cmower = new Writer(18,"anthony,"A JouneyThrough Time");
        System.out.println(JsonSerializer.serialize(cmower));
    }
}

1)serialize() 方法是用来序列化对象的,它接收一个 Object 类型的参数。objectClass.getDeclaredFields() 通过反射的方式获取对象声明的所有字段,然后进行 for 循环遍历。在 for 循环中,先通过 field.setAccessible(true) 将反射对象的可访问性设置为 true,供序列化使用(如果没有这个步骤的话,private 字段是无法获取的,会抛出 IllegalAccessException 异常);再通过 isAnnotationPresent() 判断字段是否装饰了 JsonField 注解,如果是的话,调用 getSerializedKey() 方法,以及获取该对象上由此字段表示的值,并放入 jsonElements 中。

2)getSerializedKey() 方法用来获取字段上注解的值,如果注解的值是空的,则返回字段名。

3)toJsonString() 方法借助 Stream 流的方式返回格式化后的 JSON 字符串。Stream 流后面详细叙述。

输出结果:

{"bookName":"A Jouney,Through Time","writerName":"anthony"}
  • age 字段没有装饰 @JsonField 注解,所以没有序列化。
  • name 字段装饰了 @JsonField 注解,并且显示指定了字符串“writerName”,所以序列化后变成了 writerName。
  • bookName 字段装饰了 @JsonField 注解,但没有显式指定值,所以序列化后仍然是 bookName。

Reflection 反射

  • 什么是反射

    • Class 对象是一种特殊的 Java 对象,代表了程序中的类和接口。Java 中的每个类型(包括类、接口、数组以及基础类型)在 JVM 中都有一个唯一的 Class 对象与之对应。这个 Class 对象被创建的时机是在 JVM 加载类时,由 JVM 自动完成。
    • Class 对象中包含了与类相关的很多信息,如类的名称、类的父类、类实现的接口、类的构造方法、类的方法、类的字段等等。这些信息通常被称为元数据(metadata)。
    • 通过 Class 对象来获取类的元数据,甚至动态地创建类的实例、调用类的方法、访问类的字段等,就是Java 的反射机制。
  • 反射的缺点

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

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

类加载的过程

在这里插入图片描述

  • 装载(loading):类加载器负责将类的 class 文件读入内存,并创建一个 java.lang.Clas s 对象
  • 链接(linking):。
  • 验证(Verification):确保类的字节码符合 JVM 规范。
  • 准备(Preparation):为类的静态变量分配内存,并初始化为默认值。
  • 解析(Resolution):将常量池中的符号引用转换为直接引用(地址),如将方法名解析为具体的方法引用。
  • 初始化(initialization):静态变量赋值,静态代码块执行

引子

要想知道什么是反射,就需要先了解什么是‘正射’。

一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。(静态方法除外)

        File file = new File("a.txt");
        boolean exists = file.exists();

那反射就好理解了:只知道这个类的一些基本信息,但不知道要初始化的类到底是什么,也就没法直接使用 new 关键字创建对象了。
例如警察会问一些目击证人,根据这些证人提供的信息,找专家把犯罪嫌疑人的样貌给画出来——这个过程,就可以称之为反射。

			// 获取反射类的 Class 对象
            Class<?> clazz = Class.forName("java.io.File");
            // 通过 Class 对象获取 File 类的构造器对象
            // 进而通过构造器对象初始化反射类对象
            Constructor<?> constructor = clazz.getConstructor(String.class);
            Object fileInstance = constructor.newInstance("example.txt");
            // 获取 exists 方法,并调用该方法
            Method existsMethod = clazz.getMethod("exists");
            boolean exists = (boolean) existsMethod.invoke(fileInstance);         

上面两段代码的执行结果,其实是完全一样的。
但是其思路完全不一样,第一段代码在未运行时就已经确定了要运行的类(File);而第二段代码则是在运行时通过字符串值才得知要运行的类(java.io.File)。

所以说什么是反射?
反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

反射总共有四步。

  1. 获取反射类的 Class 对象
  2. 通过 Class 对象获取反射类的构造器
  3. 通过构造器实例化反射类
  4. 通过第一第二步获取的对象,调用方法/获取字段属性

反射相关类

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

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

从 Class 对象说起

什么是 Class 对象

  • 每一个类,不管它最终生成了多少个对象,这些对象只会对应一个 Class 对象,这个 Class 对象是由 Java 虚拟机生成的,由它来获悉整个类的结构信息。
    也就是说,java.lang.Class 是所有反射 API 的入口
  • Class 对象是一种特殊的对象,它代表了程序中的类和接口。
  • Java 中的每个类型(包括类、接口、数组以及基础类型)在 JVM 中都有一个唯一的 Class 对象与之对应。这个 Class 对象被创建的时机是:在 JVM 加载类时,由 JVM 自动完成。
  • Class 对象中包含了与类相关的很多信息,如类的名称、类的父类、类实现的接口、类的构造方法、类的方法、类的字段等等。这些信息通常被称为元数据(metadata)。

第一步:获取 Class 对象的四种方法

  1. 使用 Class.forName 静态方法
    最常用的方法.。在已经知道类的全限定名时采用。此方法在运行时动态加载类,适合在类名在编译时未知的情况下使用。依赖注入就是采用这种方法。
Class<?> clazz = Class.forName("com.example.MyClass");
  1. 使用 ClassName.class
    常用的方法。如果已有一个类的字面量(类本身),即已知类名,可以用类名调用 class 来获取 Class 对象。适用于静态加载类
Class cls = String.class;
  1. 使用 引用.getClass 实例方法
    如果已经有实例,可以通过现有对象来获取其 Class 对象。
MyClass myObject = new MyClass();
Class<?> clazz = myObject.getClass();
  1. 基本类型和数组的 TYPE 属性
    对于基本数据类型,可以通过其包装类的 TYPE 属性来获取 Class 对象。数组类型的 Class 对象也可以通过 ClassName[].class 形式获取。
Class<?> intClass = int.class; // 基本类型
Class<?> integerClass = Integer.TYPE; // 基本类型包装类的 TYPE 属性

Class<?> intArrayClass = int[].class; // 数组类型
Class<?> stringArrayClass = String[].class; // 数组类型
  1. 通过类加载器
    可以通过类加载器来加载类并获取 Class 对象。这适用于一些高级场景,如自定义类加载器。
ClassLoader classLoader = MyClass.class.getClassLoader();
Class<?> clazz = classLoader.loadClass("com.example.MyClass");

第二步:获取构造器对象的四种方法

// 返回反射类的特定 public 构造方法,可以传递参数,参数为构造方法参数对应 Class 对象;缺省的时候返回默认构造方法。
public Constructor<T> getConstructor(Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
// 返回类的所有 public 构造方法。
public Constructor<?>[] getConstructors() throws SecurityException

// 以下两个方法能获取的构造方法,不受访问性影响--即可以6获取 private 的构造器.Delared 即所有声明的都要获取
// 返回指定参数类型的构造器,所传参数不可缺省。
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
// 返回类的所有构造方法。
public Constructor<?>[] getDeclaredConstructors() throws SecurityException

// 获取 File 类的所有构造方法
Class c2 = Class.forName("java.io.File");
Constructor constructor = c2.getConstructor();

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

第三步:实例化反射类的两种方法

Class.newInstance() // 此方法已经过时.
Constructor.newInstance() // 推荐此方法.因为此方法可以调用任意构造函数实例化反射类

Class c2 = Class.forName("java.io.File");
File file = (File) c1.newInstance();

Constructor constructor = c2.getConstructor();
Object object = constructor.newInstance();

第四步:获取方法并调用

// 获取指定名称和参数类型的公共方法。
public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException

// 获取指定名称和参数类型的声明的方法,包括私有方法。
public Method getDeclaredMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException

// 返回方法调用的结果。如果方法的返回类型是 void,则返回 null
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException

第四步:获取字段

// 先获得字段的 Field 对象,再通过 Field 对象 get 字段的值
// 同时,访问私有字段时,也要设置访问权限.

One More Thing

// Declared 方法可以获取私有方法,但不意味着可以直接调用,还需要设置访问权限
public void setAccessible(boolean flag) throws SecurityException

// 以下代码中,演示了如何通过 setAccessible 方法访问私有的方法、字段
try {
            Person person = new Person();
            
            Class<?> personClass = person.getClass();  // 获取 Person 类的 Class 对象
           
            Field nameField = personClass.getDeclaredField("name"); // 获取私有字段 name
            nameField.setAccessible(true); // 设置私有字段可访问
            // 获取并修改私有字段的值
            String name = (String) nameField.get(person);
            System.out.println("Original Name: " + name);
            nameField.set(person, "Jane Doe");
            System.out.println("Updated Name: " + nameField.get(person));

            // 获取私有方法 printDetails
            Method printDetailsMethod = personClass.getDeclaredMethod("printDetails");
            printDetailsMethod.setAccessible(true);  // 设置私有方法可访问
            printDetailsMethod.invoke(person); // 调用私有方法

        } catch (Exception e) {
            e.printStackTrace();
        }

Parent Delegation Model 双亲委派机制

在 Java 中,类加载器有一个层次结构,通常由三种主要的类加载器组成:

启动类加载器(Bootstrap ClassLoader):负责加载 Java 核心库(例如 rt.jar),是由 JVM 自身实现的类加载器。
扩展类加载器(Extension ClassLoader):负责加载扩展库(例如 lib/ext 目录下的 JAR 包)。
应用程序类加载器(Application ClassLoader):负责加载应用程序类路径(classpath)下的类和库。
双亲委派机制规定,当一个类加载器需要加载一个类时,它首先会把请求委派给它的父类加载器去尝试加载。如果父类加载器无法加载该类,子类加载器才会尝试自己去加载。

工作原理
双亲委派机制的工作过程可以概括为以下几步:

类加载请求:当一个类加载器(如应用程序类加载器)接收到加载类的请求时,它不会直接加载这个类,而是先委派给它的父类加载器(如扩展类加载器)。
父类加载器加载请求:父类加载器接到请求后,也会按同样的机制,继续向上委派,直到请求到达启动类加载器。
启动类加载器尝试加载:启动类加载器尝试加载类,如果找到并成功加载类,则返回加载的类给子类加载器。
逐级返回:如果启动类加载器没有找到该类,则返回给下一级类加载器(扩展类加载器),扩展类加载器继续尝试,依此类推。
子类加载器尝试加载:如果所有父类加载器都没有找到该类,最终由原始请求的类加载器自己尝试加载类。
优点
避免类重复加载:通过双亲委派机制,类只会被加载一次,这避免了重复加载同一个类的问题。
确保核心类的安全性:核心 Java 类库(如 java.lang.*)由启动类加载器加载,避免了自定义类加载器恶意篡改核心类的风险。
层次结构清晰:不同类加载器有明确的职责分工,层次结构清晰,有助于维护和理解类加载过程。

public class ClassLoaderDemo {
    public static void main(String[] args) {
        // 获取应用程序类加载器
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("Application ClassLoader: " + appClassLoader);

        // 获取扩展类加载器
        ClassLoader extClassLoader = appClassLoader.getParent();
        System.out.println("Extension ClassLoader: " + extClassLoader);

        // 获取启动类加载器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println("Bootstrap ClassLoader: " + bootstrapClassLoader);
        
        try {
            // 尝试使用应用程序类加载器加载一个类
            Class<?> clazz = appClassLoader.loadClass("java.lang.String");
            System.out.println("Class loaded by: " + clazz.getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
Application ClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
Extension ClassLoader: sun.misc.Launcher$ExtClassLoader@1540e19d
Bootstrap ClassLoader: null
Class loaded by: null

总结
双亲委派机制通过委派加载请求给父类加载器,实现了类加载器的层次结构,确保了类的唯一性和核心类库的安全性。这一机制是 Java 类加载系统的重要组成部分,有助于维护类加载过程的清晰和稳定。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值