反射和代理

一、典型面试例题及思路分析

问题 1:"Java 反射是指什么?它的使用场景及其优缺点分别什么?"

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

使用场景
主要用于根据运行时信息来发现该类 / 对象 / 方法 / 属性的场景,典型的场景比如 Spring 等框架的配置、动态代理等。其原理主要是通过访问装载到 JVM 中的类信息,来获取类 / 对象 / 方法 / 属性等信息。

优点:
通过在运行期访问装载到 JVM 中的类信息,来动态获取类的属性方法等信息,从而根据业务参数动态执行方法、访问属性,提高了 java 语言的灵活性和扩展性。典型就是 Spring 等应用框架。而其他常用的高级语言如 C/C++ 不具备这样的能力;
可以提高代码复用率。

缺点:
性能较差,通常慢于直接执行 java 代码;
程序的可维护性相对较差,业务代码和反射的代码交织在一起。

点评:

​反射类面试题,基本围绕着四个方向进行:
什么是反射;
反射基本操作;
反射优缺点;
反射使用场景。

这四个方向的内容,网上也有很多的论述,和上面的参考答案基本类似。但知道或者熟记答案并不等同于真正掌握了。要想真正地掌握,还需要在两个方向上突破:

一是理论结合实际,答案中理论部分最好能和实际中的例子联系起来。比如答案中说到的 Spring 框架(甚至可以更细到 Spring 中的 bean 注入这种场景),或者自己实际项目中用到场景。向面试官表明自己真的是有使用并且深入了解过。自己平时在项目也要注意留意这些点(或者找到 JDK 中使用到的反射的场景);

二是在使用的基础上,要真正地对反射思想和实现有深入了解(可以参考扩展阅读中的文章),才能应对更多的深入问题。比如说:为什么反射的性能较差?有没有什么方法可以让他变快?

问题 2:"Java 反射是指什么?它的使用场景及其优缺点分别什么?"

java反射要解析字节码,将内存中的对象进行解析,包括了一些动态类型,JVM难以优化,而且在调用时还需要拼接参数,执行步骤也更多。因此,反射操作的效率要更低

​常用的改进性能方法主要有:

m.setAccessible(true);
由于JDK的安全检查耗时较多.所以通过setAccessible(true)的方式关闭安全检查就可以达到提升反射速度的目的;

用缓存将反射得到的元数据保存起来;
利用一些高性能的反射库,如ReflectASM ReflectASM 使用字节码生成的方式实现了更为高效的反射机制。执行时会生成一个存取类来 set/get 字段,访问方法或创建实例。一看到 ASM 就能领悟到 ReflectASM 会用字节码生成的方式,而不是依赖于 Java 本身的反射机制来实现的,所以它更快,并且避免了访问原始类型因自动装箱而产生的问题。

问题 3:“动态代理是指什么?它有哪几种实现方法?”

动态代理是指在程序运行时生成代理类。

有两种实现方式:
JDK 动态代理,被代理对象必须实现接口,利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用 InvokeHandler 来处理;
字节码实现(比如说 cglib/asm 等),得用 ASM 开源包,将代理对象类的 class 文件加载进来,通过修改其字节码生成子类来处理。

点评:

​动态代理是和反射机制一脉相承的,其核心也就是两个:动态 + 代理。动态是指在在运行时生成的代理类,与此对应的就是静态代理,代理类在程序编译时就实现。现在很多框架都是利用类似机制来提供灵活性的扩展性,比如用来包装 RPC 调用,面向切面编程(AOP)等。

​ 从 JDK 1.3 开始,Java 提供了原生动态代理技术,允许开发者在运行时创建接口的代理实例,主要包括 Proxy 类和 InvocationHandler 接口。通常使用 JDK 的动态代理可以分为以下两步:

(1)定义一个接口,该接口里有需要实现的方法,并且编写实际的实现类。

//用户管理接口
public interface UserManager {
 void addUser(String userName,String password);
}
//用户管理实现类
public class UserManagerImpl implements UserManager{
 @Override
 public void addUser(String userName, String password) {
     System.out.println("传入参数为 userName: "+userName+" password: "+password);
 }
}

(2)定义一个实现 InvocationHandler 接口的代理类,重写 invoke () 方法,且添加 getProxy () 方法。

public class JdkProxy implements InvocationHandler {
  private Object target ;//需要代理的目标对象

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      System.out.println("JDK动态代理,监听开始!");
      Object result = method.invoke(target, args);
     System.out.println("JDK动态代理,监听结束!");
     return result;
 }

 //定义获取代理对象方法
 private Object getProxy(Object targetObject){
     //为目标对象target赋值
     this.target = targetObject;
     //JDK动态代理只能针对实现了接口的类进行代理,newProxyInstance 函数所需参数就可看出
     return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), 
targetObject.getClass().getInterfaces(), this);
 }
 
 public static void main(String[] args) {
     JdkProxy jdkProxy = new JdkProxy();//实例化JDKProxy对象
     UserManager user = (UserManager) jdkProxy.getProxy(new UserManagerImpl());//获取代理对象
     user.addUser("admin", "123");//执行新增方法
 }    

}

而 cglib 动态代理的步骤也是类似,但是相对来说要简单一些:

(1)定义一个实现类。

public class UserCglibServiceImpl {
 public void hobby() {
     System.out.println("跳舞");
 }
}

(2)定义一个实现 MethodInterceptor 接口的代理类,重写 intercept () 方法,且添加 getProxy () 方法。

public class UserCglibServiceProxy implements MethodInterceptor {
// 维护目标对象    
private Object target; 

public UserCglibServiceProxy(Object target) {
  this.target = target;  
}
// 给目标对象创建一个代理对象    
public Object getProxyInstance(){
 //1.工具类        
 Enhancer en = new Enhancer();        
 //2.设置父类        
 en.setSuperclass(target.getClass());        
 //3.设置回调函数        
 en.setCallback(this);        
 //4.创建子类(代理对象)        
 return en.create();    
}

@Override     
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable 
{
 System.out.println("唱歌");        
 //执行目标对象的方法        
 Object returnValue = method.invoke(target, args);        
 System.out.println("RAP");        
 return returnValue;    
}

public static void main(String[] args) {
 //目标对象        
 UserCglibServiceImpl target = new UserCglibServiceImpl();        
 //代理对象        
 UserCglibServiceImpl proxy = (UserCglibServiceImpl)new UserCglibServiceProxy(target).getProxyInstance();     
 //执行代理对象的方法       
 proxy.hobby();    
}
}

简单总结一下包括静态代理在内的三种代理方式的异同:

代理方式实现优点缺点特点
JDK 静态代理代理类与委托类实现同一接口,并且在代理类中需要硬编码接口简单粗暴代理类需要硬编码接口,在实际应用中可能会导致重复编码,浪费存储空间并且效率很低
JDK 动态代理代理类与委托类实现同一接口,主要是通过代理类实现 InvocationHandler 并重写 invoke 方法来进行动态代理的,在 invoke 方法中将对方法进行增强处理不需要硬编码接口,代码复用率高只能够代理实现了接口的委托类底层使用反射机制进行方法的调用
CGLIB 动态代理代理类将委托类作为自己的父类并为其中的非 final 委托方法创建两个方法,一个是与委托方法签名相同的方法,它在方法中会通过 super 调用委托方法;另一个是代理类独有的方法。在代理方法中,它会判断是否存在实现了 MethodInterceptor 接口的对象,若存在则将调用 intercept 方法对委托方法进行代理可以在运行时对类或者是接口进行增强操作,且被代理的类无需实现接口不能对 final 类以及 final 方法进行代理底层将方法全部存入一个数组中,通过数组索引直接进行方法调用

二、总结

​ 本单节的两个问题还是比较简单,网上也有很多的论述。但是要想真正过关,还是需要把握上文提到的两点:

一是理论结合实际,给出答案时最好能和项目或者自己看过的源码相关(JDK、Spring 等);
二是在使用的基础上,要真正地对反射思想和实现有深入了解,才能应对更多的深入问题。

三、扩展阅读

问题 4:java当中的四种引用分别指什么?

强引用,软引用,弱引用,虚引用。不同的引用类型主要体现在GC上:

强引用,如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象;

软引用,在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收;

弱引用,具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象;

虚引用,顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收;

问题 5:为什么要有不同的引用类型?

Java语言有时需要我们适当的控制对象被回收的时机,因此就诞生了不同的引用类型,可以说不同的引用类型实则是对GC回收时机不可控的妥协。比如说以下应用场景:

利用软引用和弱引用解决OOM问题:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题;

问题 6:虚拟机是如何实现多态的?

动态绑定技术(dynamic binding),执行期间判断所引用对象的实际类型,根据实际类型调用对应的方法.

问题 7:静态嵌套类(Static Nested Class)和内部类(Inner Class)的不同?

Static Nested Class是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。而通常的内部类需要在外部类实例化后才能实例化

问题 8:内部类的作用

内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。在单个外围类当中,可以让多个内部类以不同的方式实现同一接口,或者继承同一个类.

创建内部类对象的时刻不依赖于外部类对象的创建.内部类并没有令人疑惑的”is-a”关系,它就像是一个独立的实体。内部类提供了更好的封装,除了该外围类,其他类都不能访问

问题 9:3*0.1==0.3返回值是什么

false,因为有些浮点数不能完全精确的表示出来

问题 10:a=a+b与a+=b有什么区别吗?

+=操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型,而a=a+b则不会自动进行类型转换。

举个例子,如: byte a = 23; byte b = 22; b = a + b;//编译出错 而 b += a; // 编译OK

问题 11:int 和Integer谁占用的内存更多?

Integer 对象会占用更多的内存。Integer是一个对象,需要存储对象的元数据。但是int是一个原始类型的数据,所以占用的空间更少;

问题 12:JVM、JRE、JDK及 JIT 之间有什么不同

java 虚拟机 (JVM),是实现java语言平台独立性的基础,可以理解伪代码字节码,提供对多个平台的良好支持,在用户和操作系统之间建立了一层枢纽。

java 运行时环境 (JRE),是JVM 的一个超集。JVM 对于一个平台或者操作系统是明确的,而 JRE 确实一个一般的概念,他代表了完整的运行时环境。在 jre 文件夹中的jar 文件和可执行文件都会变成运行时的一部分。事实上,运行时 JRE 变成了 JVM。所以对于一般情况时候使用 JRE,对于明确的操作系统来说使用 JVM。当你下载了 JRE 的时候,也就自动下载了 JVM。

java 开发工具箱 (JDK),java 开发工具箱指的是编写一个 java 应用所需要的所有 jar 文件和可执行文件。事实上,JRE 是 JDK 的一部分。如果你下载了 JDK,你会看到一个名叫 JRE 的文件夹在里面。JDK 中要被牢记的 jar 文件就是 tools.jar,它包含了用于执行 java 文档的类还有用于类签名的 jar 包。

即时编译器 (JIT),即时编译器是种特殊的编译器,它通过有效的把字节码变成机器码来提高 JVM 的效率。JIT 这种功效很特殊,因为他把检测到的相似的字节码编译成单一运行的机器码,从而节省了 CPU 的使用。这和其他的字节码编译器不同,因为他是运行时编译(从字节码到机器码)而不是在程序运行之前。正是因为这些,动态编译这个词汇才和 JIT 有那么紧密的关系。

问题 13:Java 泛型类在什么时候确定类型?

在编译期间确定变量类型。类型擦除。

问题 14:引用类型是占用几个字节?

hotspot在64位平台上,占8个字节,在32位平台上占4个字节

问题 15:JDK主流版本的差异

Java 5(2004年发行),影响很大的一个版本;

1、泛型。
2、Metadata,元数据,描述数据的数据。
3、自动装箱和拆箱,也就是基本数据类型(如 int)和它的包装类型(如 Integer)自动帮你转换(其实背后是相关的方法帮你做了转换工作)。
4、枚举。
5、可变参数,一个函数可以传入数量不固定的参数值。
6、增强版的 for 循环。
7、改进了 Java 的内存模型,提供了 java.util.concurrent 并发包。

Java 6(2006年发行),这个版本的 Java 更多是对之前版本功能的优化,增强了用户的可用性和修复了一些漏洞,

1、提供动态语言支持。
2、提供编译 API,即 Java 程序可以调用 Java 编译器的 API。
3、Swing 库的一些改进。
4、JVM 的优化。
5、微型 HTTP 服务器 API ;

Java 7(2011年发行)

1、放宽 switch 的使用,可以在 switch 中使用字符串;
2、try-resource-with 语句,帮助自动化管理资源,如打开文件,对文件操作结束后,JVM 可以自动帮我们关闭文件资源,当然前提是你要用 try-resource-with 语句。
3、加入了类型推断功能,比如你之前版本使用泛型类型时这样写 ArrayList userList= new ArrayList();,这个版本只需要这样写 ArrayList userList= new ArrayList<>();,也即是后面一个尖括号内的类型,JVM 帮我们自动类型判断补全了。
4、简化了可变参数的使用。
5、支持二进制整数,在硬件开发中,二进制数更常用,方便人查看。
6、支持带下划线的数值,如 int a = 100000000;,0 太多不便于人阅读,这个版本支持这样写 int a = 100_000_000,这样就对数值一目了然了吧。
7、异常处理支持多个 catch 语句。
8、NIO 库的一些改进,增加多重文件的支持、文件原始数据和符号链接。

Java 8(2014年发行)

1、Lambda 表达式,简化代码
2、注解功能的增强。重复注解和注解扩展,现在几乎可以为任何东西添加注解:局部变量、泛型类、父类与接口的实现,就连方法的异常也能添加注解。
3、新的时间和日期 API,在这之前 Java 的时间和日期库被投票为最难用的 API 之一,所以这个版本就改进了。
4、JavaFX,一种用在桌面开发领域的技术(也是和其他公司竞争,这个让我们拭目以待吧)。
5、静态链接 JNI 程序库(这个做安卓开发的同学应该熟悉)。
6、接口默认方法和静态方法
7、函数式接口
8、方法引用
9、java.util.stream
10、HashMap的底层实现有变化
11、JVM内存管理方面,由元空间代替了永久代。

Java 9 (2017年发行)

1、模块化(这点也是向其他语言学习的,如 JavaScript)。
2、Java Shell(这点也是向其他语言学习的,如 Python),在这之前总有人说 Java 太麻烦,写个 Hello Word 都要新建个类,有时候测试个几行的代码都不方便,Java Shell 推出后,Java 开发者不用眼馋其他语言的 Shell 了;
3、即时编译功能的增强。
4、XML Catalogs,XML 文件自动校验。

Java 10(2018年发行)

1、局部变量的类型推断 var关键字
2、GC改进和内存管理 并行全垃圾回收器 G1
3、垃圾回收器接口
4、线程-局部变量管控
5、合并 JDK 多个代码仓库到一个单独的储存库中

Java 11(2018年发行)

1、本地变量类型推断
2、字符串加强
3、集合加强
4、Stream 加强
Optional 加强
5、InputStream 加强
6、HTTP Client API
7、化繁为简,一个命令编译运行源代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值