反射和代理

反射和代理

    • 一、典型面试例题及思路分析
    • 二、总结
    • 三、扩展阅读

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

问题 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 等);
二是在使用的基础上,要真正地对反射思想和实现有深入了解,才能应对更多的深入问题。

三、扩展阅读

  • 链接: 动态代理.
  • 链接: Java应该如何理解反射?.
问题 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、化繁为简,一个命令编译运行源代码

你可能感兴趣的:(Java基础技术)