六. Java反射机制以及动态代理是基于什么原理
6.1 反射机制:
Java语言提供的一种基础功能,赋予程序在运行时自省(introspect)的能力。
通过反射我们可以直接操作类和对象:
比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。
6.2 反射例子:
反射的示例,展示了如何获取类的信息、创建对象和调用方法
通过 Class<?> clazz = MyClass.class
获取了类 MyClass
的定义。然后使用反射获取了类的名称、属性和方法,并输出到控制台。
接下来,我们使用反射创建了 MyClass
类的对象实例,并通过反射调用了 setName()
和 setAge()
方法来设置对象的属性值。
最后,我们通过调用对象的 toString()
方法将对象信息输出到控制台。
import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; public class ReflectionExample { public static void main(String[] args) throws Exception { // 获取类的定义 Class<?> clazz = MyClass.class; // 获取类的名称 String className = clazz.getName(); System.out.println("Class Name: " + className); // 获取类声明的属性 Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { String fieldName = field.getName(); System.out.println("Field Name: " + fieldName); } // 获取类声明的方法 Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { String methodName = method.getName(); System.out.println("Method Name: " + methodName); } // 创建对象实例 Constructor<?> constructor = clazz.getDeclaredConstructor(); Object obj = constructor.newInstance(); // 调用方法 Method setNameMethod = clazz.getDeclaredMethod("setName", String.class); setNameMethod.invoke(obj, "John Doe"); Method setAgeMethod = clazz.getDeclaredMethod("setAge", int.class); setAgeMethod.invoke(obj, 30); // 输出对象信息 System.out.println(obj.toString()); } } class MyClass { private String name; private int age; public MyClass() { } public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Name: " + name + ", Age: " + age; } }
输出
Class Name: MyClass Field Name: name Field Name: age Method Name: setName Method Name: setAge Name: John Doe, Age: 30
展示了如何使用反射获取类的信息、创建对象实例和调用方法。通过反射,我们可以在运行时动态地操作类和对象,灵活应对不同的需求
6.3 动态代理:
是一种方便运行时动态构建代理,动态处理代理方法调用的机制,很多场景都是利用类似机制做到的。
比如用来面向切面的编程(AOP)
6.4 例子:
创建一个实现InvocationHandler
接口的类,用于处理代理方法的调用
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class LogInvocationHandler implements InvocationHandler { private Object target; // 目标对象 public LogInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 在方法执行前输出日志 System.out.println("Before invoking " + method.getName() + "()"); // 调用目标对象的方法 Object result = method.invoke(target, args); // 在方法执行后输出日志 System.out.println("After invoking " + method.getName() + "()"); return result; } }
使用Proxy
类来动态生成代理对象,并将目标对象和LogInvocationHandler
关联起来
import java.lang.reflect.Proxy; public class Main { public static void main(String[] args) { UserService userService = new UserServiceImpl(); // 原始的目标对象 // 创建LogInvocationHandler实例 LogInvocationHandler handler = new LogInvocationHandler(userService); // 使用Proxy类动态生成代理对象 UserService proxy = (UserService) Proxy.newProxyInstance( userService.getClass().getClassLoader(), userService.getClass().getInterfaces(), handler ); // 调用代理对象的方法 proxy.addUser("Alice"); proxy.deleteUser("Bob"); } }
使用了Proxy
类的newProxyInstance()
方法来动态生成代理对象。
newProxyInstance()
方法接受三个参数:
ClassLoader loader
: 指定用于定义代理类的类加载器。我们可以使用目标对象的类加载器,通过userService.getClass().getClassLoader()
获取。Class<?>[] interfaces
: 指定代理类要实现的接口列表。我们可以通过userService.getClass().getInterfaces()
获取目标对象实现的接口数组。InvocationHandler handler
: 指定用于处理代理方法调用的InvocationHandler
实例。在我们的例子中,我们使用了自定义的LogInvocationHandler
作为处理器。
每次调用代理对象的方法时,都会经过LogInvocationHandler的invoke()方法。在方法执行前,会输出相应的日志信息(例如"Before invoking addUser()"),然后调用目标对象的对应方法,最后输出方法执行后的日志信息(例如"After invoking addUser()")。
6.5 总结:
6.5.1 代理模式
通过代理静默地解决一些业务无关的问题,比如远程、安全、事务、日志、资源关
闭......让应用开发者可以只关心他的业务
静态代理:
事先写好代理类,可以手工编写,也可以用工具生成。缺点是每个业务类都要
对应一个代理类,非常不灵活。
6.5.1.1动态代理:
运行时自动生成代理对象。缺点是生成代理代理对象和调用代理方法都要额外
花费时间。
*6.5.1.2 JDK动态代理:
基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生
成代理对象。新版本也开始结合ASM机制。
*6.5.1.3 cglib动态代理:
基于ASM机制实现,通过生成业务类的子类作为代理类。
Java 反射机制的常见应用:动态代理(AOP、RPC)、提供第三方开发者扩展能力(Servlet容
器,JDBC连接)、第三方组件创建对象(DI)
6.5.1.4 反射与动态代理原理
1 关于反射
反射最大的作用之一就在于我们可以不在编译时知道某个对象的类型,而在运行时通过提供
完整的”包名+类名.class”得到。注意:不是在编译时,而是在运行时。
功能:
•在运行时能判断任意一个对象所属的类。
•在运行时能构造任意一个类的对象。
•在运行时判断任意一个类所具有的成员变量和方法。
•在运行时调用任意一个对象的方法。
说大白话就是,利用Java反射机制我们可以加载一个运行时才得知名称的class,获悉其构造
方法,并生成其对象实体,能对其fields设值并唤起其methods。
6.5.2 应用场景:
反射技术常用在各类通用框架开发中。因为为了保证框架的通用性,需要根据配置文件加载
不同的对象或类,并调用不同的方法,这个时候就会用到反射——运行时动态加载需要加载
的对象。
6.5.2.1特点:
由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要
用反射。另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问
题。
6.5.2.2 动态代理
为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不
能直接引用另一个对象,而代理对象可以在两者之间起到中介的作用(可类比房屋中介,房
东委托中介销售房屋、签订合同等)。
所谓动态代理,就是实现阶段不用关心代理谁,而是在运行阶段才指定代理哪个一个对象
(不确定性)。如果是自己写代理类的方式就是静态代理(确定性)。
6.5.2.3 组成要素:
(动态)代理模式主要涉及三个要素:
其一:抽象类接口
其二:被代理类(具体实现抽象接口的类)
其三:动态代理类:实际调用被代理类的方法和属性的类
6.5.2.4 实现方式:
实现动态代理的方式很多,比如 JDK 自身提供的动态代理,就是主要利用了反射机制。还有
其他的实现方式,比如利用字节码操作机制,类似 ASM、CGLIB(基于 ASM)、Javassist
等。
举例,常可采用的JDK提供的动态代理接口InvocationHandler来实现动态代理类。其中invoke
方法是该接口定义必须实现的,它完成对真实方法的调用。通过InvocationHandler接口,所
有方法都由该Handler来进行处理,即所有被代理的方法都由InvocationHandler接管实际的处
理任务。此外,我们常可以在invoke方法实现中增加自定义的逻辑实现,实现对被代理类的
业务逻辑无侵入。
七. int 和 Integer 有什么区别? Integer的值缓存范围?
Java虽然被称为面向对象的语言,但是原始数据类型仍然是重要的组成元素,经常考察原始数据类型和包装类等Java语言特性。
7.1 int:
int是我们是整型数字,是Java的8个原始数据类型(boolean,byte,short,char,int ,float,double,long)之一。
Java语言虽然号称一切都是对象,但原始数据类型是例外。
Integer 是 int 对应的包装类,有一个int 类型的字段存储数据,并且提供了基本操作,如数学运算,int和字符串之间的转换等
Java 5 中,引入了自动装箱和自动拆箱的功能(boxing/unboxing),Java根据上下文,自动进行转换,简化了相关编程。
7.2 Integer的值缓存:
涉及到Java5 中另一个改进,构建Integer对象的传统方式是直接调用构造器,直接进行new一个对象。
根据实践我们发现大部分数据都是集中在有限的,较小的数值范围,因此,在Java5中新增加静态工厂方法valueOf,调用时会利用缓存机制,带来了性能改进,默认值是-128到127之间。
7.3 考点分析:
上面的回答覆盖了,Java里面的两个基本要素,原始数据类型,包装类。
自然的就扩展:自动装箱,自动拆箱机制,可能会考察对封装类的一些设计和实践。
7.3.1 理解自动装箱,拆箱
7.3.1.1 自动装箱实际上是一种语法糖。
javac替我们自动把装箱转换为Integer.valueOf(),把拆箱替换为Integer.intValue(),调用的是Integer.valueOf,采取缓存。
valueOf()方法中,它是Integer类的静态方法,用于将基本类型int或字符串转换为对应的Integer对象。在valueOf()方法中,针对-128到127范围内的整数值会使用缓存,返回预先创建的对象,而不是每次都创建新的对象。
7.3.1.2 语法糖解释:
可以理解为Java平台为我们自动进行的一些转换,保证不同的写法在运行时等价,发生在编译阶段,生成的字节码是一样的。
7.3.1.2.1 语法糖例子
7.3.1.2.1 .1自动装箱和拆箱:
通过自动装箱和拆箱的语法糖,我们可以直接在基本类型和对应的包装类之间进行转换,无需手动编写繁琐的转换代码。
int num = 10; Integer integer = num; // 自动装箱 int result = integer; // 自动拆箱
7.3.1.2.1.2 泛型:
Java的泛型提供了类型安全的编程方式,使代码更加灵活和易读。在使用泛型时,编译器会进行类型擦除,将泛型参数替换为实际的类型。例如,我们可以声明一个List来存储指定类型的元素,在编译时会进行类型检查和转换。
泛型解释:
定义了一个泛型类GenericClass<T>
,它有一个类型参数T
。这个类包含了一个成员变量value
和对应的getter和setter方法
public class GenericClass<T> { private T value; public void setValue(T value) { this.value = value; } public T getValue() { return value; } }
在使用该泛型类时,我们可以传入不同的类型参数
GenericClass<String> strObj = new GenericClass<>(); strObj.setValue("Hello"); String strValue = strObj.getValue(); GenericClass<Integer> intObj = new GenericClass<>(); intObj.setValue(10); Integer intValue = intObj.getValue();
我们分别实例化了两个GenericClass
对象:strObj
和intObj
。一个使用了String
类型作为类型参数,另一个使用了Integer
类型作为类型参数。
在编译时,由于类型擦除的原因,编译器会将T
替换为实际的类型或者限定类型。在上述代码中,泛型参数T
会被擦除为Object
类型。因此,编译器会将代码转换成如下形式
public class GenericClass { private Object value; public void setValue(Object value) { this.value = value; } public Object getValue() { return value; } }
7.3.1.2.1.3 Lambda表达式:
Lambda表达式是一种语法糖,它可以简化匿名函数的编写。通过Lambda表达式,我们可以以更精炼的方式表示函数式接口的实现。
匿名函数:
一种没有具体名称的函数。它可以用来表示一段可执行的代码块或逻辑,但没有像普通函数那样被命名。匿名函数通常用于函数式编程中,以便在需要时传递给其他函数或方法作为参数。
例子1:
List<Integer> numbers = Arrays.asList(5, 3, 1, 4, 2); // 使用匿名函数进行升序排序 Collections.sort(numbers, new Comparator<Integer>() { @Override public int compare(Integer num1, Integer num2) { return num1.compareTo(num2); } }); // 使用Lambda表达式进行升序排序 Collections.sort(numbers, (num1, num2) -> num1.compareTo(num2)); System.out.println(numbers); // 输出:[1, 2, 3, 4, 5]
例子2:
int[] numbers = {1, 2, 3, 4, 5}; for (int num : numbers) { System.out.println(num); } //对比Lambda List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.forEach(num -> System.out.println(num));
7.3.2 对象的组成
对象由三部分组成,对象头,对象实例,对齐填充。
7.3.2.1对象头:
一般是十六个字节,包括两部分,
第一部分有哈希码,锁状态标志,线程持有的 锁,偏向线程id,gc分代年龄等。
第二部分是类型指针,也就是对象指向它的类元数据指针, 可以理解,对象指向它的类。
7.3.2.2对象实例:
就是对象存储的真正有效信息,也是程序中定义各种类型的字段包括父类继承的和子 类定义的。
虚拟机对对象字段的存储顺序不涉及重排序的概念。重排序主要指的是编译器或处理器对指令执行顺序进行优化的行为,而不是虚拟机对对象字段在内存中的排列顺序进行优化
第三部分对齐填充只是一个类似占位符的作用,因为内存的使用都会被填充为八字节的倍数。
7.3.2.3举个继承有父类的例子:
定义了一个Person
类和一个继承自Person
的Employee
类。Person
类有一个私有字段name
和一个公共方法getName()
用于获取姓名。Employee
类除了继承了Person
类的字段和方法外,还添加了一个私有字段employeeId
和一个公共方法getEmployeeId()
用于获取员工ID。
public class Person { private String name; public Person(String name) { this.name = name; } public String getName() { return name; } } public class Employee extends Person { private int employeeId; public Employee(String name, int employeeId) { super(name); this.employeeId = employeeId; } public int getEmployeeId() { return employeeId; } }
创建一个Employee
对象时,它在内存中的布局如下
对象头(Header):16字节 ├── 哈希码、锁状态标志、线程持有的锁、偏向线程ID、GC分代年龄等信息 └── 类型指针,指向Employee类的元数据 对象实例(Instance Data): ├── name字段(String类型,引用) - 继承自Person类 └── employeeId字段(int类型) - Employee类特有的字段 对齐填充(Padding):占位符,使对象的总大小为8字节的倍数
Employee
对象的实例数据部分包含了两个字段。其中,name
字段是从Person
类继承而来的,它是一个引用类型的字段,用于存储姓名信息。而employeeId
字段是Employee
类特有的字段,它是一个整数类型的字段,用于存储员工ID。
Employee
对象的实例数据部分存储了name
字段(继承自Person
类)和employeeId
字段(Employee
类特有),这些字段组成了对象的真正有效信息
7.3.2.4举个正常的例子:
定义了一个Person
类,它有两个字段:name
和age
。
public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } }
创建Person
对象时,它在内存中的布局大致如下所示:
对象头(Header):16字节 ├── 哈希码、锁状态标志、线程持有的锁、偏向线程ID、GC分代年龄等信息 └── 类型指针,指向Person类的元数据 对象实例(Instance Data): ├── name字段(String类型,引用) └── age字段(int类型) 对齐填充(Padding):占位符,使对象的总大小为8字节的倍数
对象头占据了16字节,并包含了对象的哈希码、锁状态等信息,以及指向Person类元数据的类型指针。接下来,对象实例部分包含了name和age两个字段,分别是一个引用类型的String和一个基本类型的int。在内存中,这两个字段按照定义的顺序存储。
需要注意的是,虚拟机对对象字段的存储顺序不涉及重排序的概念。重排序主要指的是编译器或处理器对指令执行顺序进行优化的行为,而不是虚拟机对对象字段在内存中的排列顺序进行优化。因此,在这个例子中,虚拟机不会影响对象实例中字段的存储顺序。
7.3.2.5 tips:
除了存储空间的区别外,基本数据类型是有默认值的,而对象数据类型没有默认值。比如
从数据库中查询用户年龄,如果用户并没有设置年龄信息,数据库中代表年龄的列age =null,那么在使用基本数据类型接收年龄值的时候就无法区分用户是年龄为0还是未设置年龄的情
况,所以决定使用int还是Integer的时候除了考虑性能因素还要考虑业务场景。
7.3.2.6 小结:
7.3.2.6.1 int和Integer有什么区别?
1. int:是Java的8个原始数据类型之一(boolean,byte,char,short,int,long,floa
t,double)
2. Integer:是int对应的包装类,是引用类型。在Java5中,引入了自动装箱和自动拆箱
功能,Java可以根据上下文,自动进行转化,极大的简化了相关编程。自动装箱/自动拆箱发
生在编译期,自己调用valueOf和intValue方法来使用缓存机制(默认缓存是-128到127之
间)。注意:new 不使用缓存
3. int访问是直接访问数据内存地址,Integer是通过引用找到数据内存地址
4. 内存空间占用:Integer大于int
5. 数据操作效率上:int大于Integer
6. 线程安全方面:int等原始数据类型需要使用并发相关手段。Integer等包装类可以使
用类似AtomicInteger、AtomicLong等这样的线程安全类。
7. int等原始数据类型和Java泛型不能配合使用
8. Integer和String一样有final修饰,是不可变类型
9. Integer中定义了bytes常量,避免了因为环境 64位或32位不同造成的影响
实践中,建议避免无意中的装箱、拆箱行为
7.3.2.6.2 对象的内存结构是什么样的?
在HotSpot虚拟机中,对象在内存中存储的布局都可以分为3块区域。
1. 对象头(Header)
包含两部分信息:
1. Mark Word:用于存储对象自身的运行时数据,如:哈希码(HashCode),GC
分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,
2. 类型指针:即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个
对象是哪个类的实例。
另外,如果对象是Java数组,那在对象头中还必须有一块用国语记录数组长度的数
据。
2. 实例数据(Instance Data):对象真正存储的有效信息,也是在程序代码中所定义的
各种类型的字段内容。
3. 对齐填充(Padding):对齐填充并不是必然存在的,也没有特别的含义,它仅仅起
着占位符的作用。使对象的大小必须是8字节的整数倍。