java面试题集合
1.Switch能否用String?
在java7之前,Switch值能支持int,byte,short,char以及他们的扩展类型,在java7之后支持了String。(ps:其实也不是支持了Switch,而是对String值去hasCode值,再相互比较)。
2.equals与==的区别?
==判断两个变量是不是指向同一个内存空间,而equals判断两个变量所指向的内存空间的值是否相等。
3.Object有哪些公用的方法?
- 方法equals判断两个对象是否相等(a.equals(b))
- 方法clone进行对象拷贝
- 方法 getClass 返回和当前对象相关的Class对象
- 方法notify,notifyall,wait都是用来给定对象进行线程同步的。
4.实际开发中强软弱虚软弱引用的使用场景:
- 利用软应用和弱引用解决OOM问题。
用HashMap保存图片的路径和相应图片对象关联的软引用之间的映射关系。在内存不足时,jvm自动回收这些缓存图片对象所占的内存空间,从而避免了OOM。例如我们要存储某员工的头像信息,且需要反复使用,那么,将图片缓存资源用弱引用包裹,存入HashMap中,要使用的时候从HashMap中取出,如果是null那么需要重新加载。 - 强引用:通常我们使用new操作符创建出来的对象时所返回的空间即为强引用,如果一个对象具有强引用,那么GC绝不会回收它,如果内存不足,则报OOM,使程序异常终止。
- 软引用:若一个对象只能通过软引用到达,那么当内存空间不足时,GC会自动回收该对象所指向的内存空间。可用于图片缓存中,当内存不足时,自动删除bitmap缓存。
- 弱引用:若一个对象只能通过弱应用到达,那么该对象会被回收(即使内存充足),同样可以用于图片缓存中,这时候只要BitMap不再使用就会被回收。
- 虚引用:是java中最弱的引用,通过它甚至无法获得被引用的对象,它存在的唯一作用就是当它指向的对象被回收时,他本身会被加入到队列中,这样我们就可以监控它指向的对象何时销毁。
5.String、StringBuffer与StringBuilder的区别
String与其他两个的区别是,String是不可变的,StringBuffer与StringBuilder实质上是基于Char[]实现的。StringBuffer是线程安全的,而StringBuilder是线程不安全的。
6.Overrider和Overload的含义及区别
Overrider:方法重写,在子类重新定义从父类继承的方法,从而覆盖父类方法,要求方法名,方法类型,参数,以及返回值类型必须一致。
Overload:方法重载,表现java的多态性,重写已有的方法,要求方法名一致,返回值一致,参数不一致 。
7.抽象类和接口的区别
一个类只能继承一个抽象类,但是一个类可以继承无数个接口。抽象类强调所属关系,抽象类里的方法不一定都是抽象的,它在一定程度上规定了子类的一些属性。而接口要求所有的方法都必须是抽象的(ps:那是在过去,现在接口已经支持default,static关键字,来定义默认方法,和静态方法。那就是说抽象方法能做的接口完全可以替代)
8.解析XML的几种方式的原理和特点
- DOM:基于文档驱动,消耗内存,读取文件时将文件先整个读取到内存中,然后在内存中建立树结构,然后用DOM api访问树结构,读取数据。使用方式简单,但是很消耗内存,手机硬件不过关的直接gg。
- SAX:基于事件驱动,占用内存少,更加简单的来说,就是对文档进行顺序扫描,当扫描到,文档的开始与结束,元素的开始与结束,等地方时通知事件处理函数,又事件处理函数完成相应动作,然后继续扫描,直至文档扫描结束。
- PULL:与SAX类似,也是基于事件驱动,我们可以调用它的next(),方法获取下一个解析事件(解析事件返回一个编号,使我们确定其正在解析的内容层级),(就是开始文档,结束文档,开始标签,结束标签),当处于某个元素是可以调用XmlPullParser的getAttributter()方法来获取属性的值,也可以调用nextText()获取本节点的值。
9.java多态的实现原理
所谓多态,指的就是父类引用指向子类对象,调用方法时会调用子类的方法而不是父类的方法。多态的实现的关键在于动态绑定。
-
java动态绑定
java虚拟机在调用一个类方法时,他会基于对象引用的类型(通常在编译时可知)来选择所调用的方法。相反,当虚拟机调用一个实例方法时,它会基于对象的实例类型(在运行可知)来选择调用的方法。这就是动态绑定。 -
java方法的存储机制
当JVM执行java字节码时,类型信息会存储在方法区中,为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针这项记录该类的方法的方法表,方法表的每一个项都是对应方法的指针。
方法区:方法区和java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的信息,常量,静态变量,即时编译器编译后的代码等数据。运行常量池:它是方法区的一部分,Class文件中除了有关类的版本,方法,字段描述信息以外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分信息在类加载时进入方法区的运行时常量池中。方法区的内存回收目标是针对常量池的回收以及对类型的卸载。
- 方法表的构造
由于java的单继承机制,一个类只能继承一个父类,而所有的类都继承了Object类,方法表中先是Object类的方法,然后是父类的方法,最后是该类本身的方法。如果子类中有重写了父类的方法,则在方法表中子类的方法的地址覆盖父类该方法的地址。
由于这样的特性,使得方法表的偏移量总是固定的,例如对于任何方类来说,其方法表的equals方法的偏移量都是一个定值,所有继承父类的方法在方法表中的偏移量也都是一个定值。
流程:调用方法时,虚拟机通过对象引用得到方法区中类型信息的方法表的指针入口,查询类的方法表,根据实例方法的符号引用解析出该方法在方法表的偏移量,子类对象声明为父类类型时,形式上调用的是父类的方法,此时虚拟机会从实际的方法表中找到方法地址,从而定位到实际类的方法。注:所有引用为父类,但方法区的类型信息中存放的是子类的信息,所以调用的是子类的方法表。
10.hashCode的作用是:
什么是hashCode:
1.HashCode的存在就是为了查找的快捷性,HashCode用来在散列存储结构中对定对象的存储地址。
2.如果两个对象equals相等,那么它们的hashCode值一定相等,反之则不一定,因为相同的hashCode只能说明其存在于散列存储结构中的同一个位置。
3.当重写equals方法时,HashCode方法也尽量重写。
HashCode有什么用?举个例子:
1.假设内存中有0 1 2 3 4 5 6 7 8这8个位置,如果我有个字段叫做ID,那么我要把这个字段存放在以上8个位置之一,如果不用HashCode而任意存放,那么当查找时就需要到8个位置中去挨个查找
使用HashCode则效率会快很多,把ID的HashCode%8,然后把ID存放在取得余数的那个位置,然后每次查找该类的时候都可以通过ID的HashCode%8求余数直接找到存放的位置了
2.如果ID的HashCode%8算出来的位置上本身已经有数据了怎么办?这就取决于算法的实现了,比如ThreadLocal中的做法就是从算出来的位置向后查找第一个为空的位置,放置数据;HashMap的做法就是通过链式结构连起来。反正,只要保证放的时候和取的时候的算法一致就行了。
3.如果ID的HashCode%8相等怎么办(这种对应的是第三点说的链式结构的场景)?这时候就需要定义equals了。先通过HashCode%8来判断类在哪一个位置,再通过equals来在这个位置上寻找需要的类。对比两个类的时候也差不多,先通过HashCode比较,假如HashCode相等再判断equals。如果两个类的HashCode都不相同,那么这两个类必定是不同的。
11.ArrayList,LinkedList,Vector的区别是什么?
- ArrayList:内部采用数组元素支持,支持随机高效访问,可动态改变大小。(当元素达到最大容量时增大为原来的150%)
- LinkedList:内部采用双向链表支持,支持快捷的增删元素操作,但不支持高效的随机访问。
- Vector :可看做线程安全版的ArrayList。(当元素达到最大容量时增大为原来的200%)
12.Map,Set,List,Queue,Stack的特点及用法。
- Map<K , V>:java中存储键值对的数据类型都实现了这个接口,表示“映射表”。支持的两个核心操作是get()和put(),分别用来获取对应的键值对和插入键值对。
- Set<E>:实现了这个接口的类型不允许存在重复的元素(LinkedList除外),代表数学意义上的“集合”。它所支持的核心操作有add(E e),remove(Object o),contains(Object o),分别用于添加元素,删除元素以及判断给定的元素是否存在于集中。
- List<E>:java集合框架中的列表类型都实现了这个接口,表示一种有序序列。支持get(int index),add(E e)等操作。
- Queue<E>:java集合框架中的队列接口。代表了先进先出的队列,支持add(E e),remove()等操作。
- Stack<E>:java集合框架中表示堆栈的数据类型,堆栈是一种“后进先出”的数据结构,支出push(E item),pop()等操作。
13.HashMap和HashTable的区别
- hashmap是非线程安全的,HashTable线程安全的
- HashMap允许null键,和null值,HashTable不允许。
- 因为线程安全问题,HashMap效率要比hashTable高。
14.Java中的另一个线程安全的与HashMap极其类似的类是什么?同样是线程安全,它与HashTable在线程同步上有什么不同?
concurrentHashMap是HashMap的线程安全的实现,而它与hashTable不同的是,HashTable使用synchronized实现同步机制,无论你在哪个位置添加语句块,封锁的都是对象整体,而concurrentHashMap是利用lock实现的同步机制,它基于concurrentLevel划分出了多个Segment来对Key-value进行存储,从而避免了每次锁定整个数组,在默认情况下,允许16个线程并发无阻塞的操作集合对象,尽可能的减少并发时的阻塞现象。
15.ConcurrentHashMap的实现原理
ConcurrentHashMap是支持并发读写的HashMap,他的特点是读取数据时无需加锁,写数据是可以保证枷锁粒度尽可能的小。由于其内部采用分段存储,只需对要进行写操作的数据所在的“段”进行加锁。
16.TreeMap,LinkedMap,HashMap的区别是什么?
- HashMap的底层实现是函数表。因此它内部存储的元素是无序的。
- TreeMap的底层实现是红黑树,所以它内部的元素是有序的。排序的依据是自然序或者是创建TreeMap时所提供的比较器(Comparator)对象。
- LinkedHashMap可以看作是,能够记住插入元素的顺序的HashMap。
17.Collection与Collections的区别是:
Collection是一个接口,它是Set,List,Queue等容器类的父接口;Collections是一个工具类,提供了一些静态方法辅助容器类的操作,包括对容器类的搜索,排序,线程安全化等等。
18.Set,List,Map的区别是什么:
Collection是最基本的集合接口,Set和List都继承了Collection,Map没有。
- Set是一种简单的集合,集合中的对象不按特定的规则排序。集合元素不能重复。
- List的特征是,元素按线性存储,称为列表,列表中元素可重复。
- Map 是一种把键对象和值对象映射的集合,他的每一个元素都包含一对键对象和值对象。
19.特殊关键字:final
- 可以修饰类、函数、变量;
- 被final修饰的类不可以被继承。为了避免被继承,被子类复写。final class Demo{}
- 被final修饰的方法不能被复写
- 被final修饰的变量只能是一个常量
- 内部定义在类中的布局位置上时,只能访问访问该布局内部被final修饰的局部变量。
20.对于“try{}catch(){}finally{}”,若try语句块中包含“return”语句,finally语句块会执行吗?
会执行,无论是try,还是catch中含有return,都会执行return后的运算,然后将返回值保存起来,然后执行finally块儿的内容,最后返回保存的值。(如果return i (此时i=1),而finally中赋值i=2,此时返回值仍然是1)
有两种情况能使finally中的语句块不会执行:
- 调用了System.exit()方法
- JVM“崩溃”了
21.泛型的优缺点
- 优点:
使用泛型可以最大限度的重用代码,保护类型的安全提高性能。
泛型最常见的用途是创建集合类。 - 缺点:
在性能上不如数组快。 - 泛型解析:
泛型在编译时被编译器,进行泛型擦除操作,泛型类型将变为其最大限制类型。一般为Object。也就是说,在jvm中不存在泛型。
22.java中的异常层级结构
java中的异常层次结构如下图所示:
java中的异常Throwable类是异常层级中的基类。Error表示内部错误,这类错误是我们无法控制的;Exception表示异常,RuntimeException及其子类属于未检查异常,这类异常包括下标越界,空指针异常等,我们应该通过条件判断等方式语句避免未检查异常的发生。IOExcepion及其子类属于已检查异常,编译器会检查我们是否为所有可能抛出的已检查异常提供了异常捕获器,若没有则报错。对于未检查异常我们无需捕获。
23.Interface与abstract类的区别。
抽象类和接口都不能够实例化,但是可以定义抽象类和接口类型的引用。一个类如果继承了某个抽象类或是接口都需要对其中的抽象方法进行事实现,除非子类也是抽象类。有抽象方法的类必须被声明为抽象类,而抽象类不一定有抽象方法。
24.java面向对象的三个特征与含义。
三大特征:封装,多态,继承
- 继承
(1)继承是一种联结类的层次模型,并允许和鼓励类的重用,它提供了一种明确的表述共性的方法。
(2)对象的一个新类可以从现有的类中派生,这个过程成为类继承,新类继承了原始类型的特性,新类称为原始类型的派生类(子类),而原始类成为新类的基类(父类)
(3)派生类可以从他的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。 - 封装
封装是把数据和过程包裹起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一些列完全自治,封装的对象,这些对象通过一个受保护的接口访问其他对象。 - 多态性
(1)多态性是指允许不同类的对象对同一消息作出响应。
(2)多态性包括参数化多态性和包含多态性
(3)多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。
25.接口与抽象类的区别
接口是一种约定,实现接口的类要遵循这个约定;抽象类本质是一个类。使用抽象类的代价要比接口大。接口与抽象类的对比如下:
- 抽象类中可以包含属性方法(抽象方法与具体实现的方法),常量;接口只能包含抽象方法声明(java8中为接口添加了接口静态方法和定义实现方法的关键字)default(java8)实现的方法,以及static(java8)类方法。
- 一个子类只能继承一个抽象类,但一个接口可以继承多个接口。
- 子类中实现父类的抽象方法时,可见性大于等于父类中的;而实现接口实现类中接口方法的可见性只能是public.
- 抽象类成员和方法可以定义可见性;而一个接口中的方法只能为public。
26.静态内部类与非静态内部类的区别
内部静态类不需要有指向外部类的引用,但非静态内部类需要持有外部类的引用。非静态内部类能够访问,外部类的非静态成员和静态成员,而静态内部类不可访问外部类的非静态成员,只能访问静态成员。一个非静态内部类不能脱离外部类实例而被创建,一个非静态内部类可以访问外部类的数据和方法。
27.简述java中创建新线程的两种用法。
- 继承Thread类,并重写run()方法,子类对象调用start()方法。
- 实现Runnable接口,然后将实现接口的类的对象作为参数实例化Thread,得到Thread对象后调用start。
实现接口与继承方式有什么区别?
- 实现方式相比继承方式的好处:避免了单继承的局限性,在定义线程时,推荐使用。
- 存放代码位置不一样
继承Thread: 线程代码存放在Thread子类的run方法中。
实现Runnable:线程代码存在接口的子类的方法中。
28简述java中进行线程同步的方法。
- volatile:轻量级的synchronized,用它修饰一个变量时,保证了由线程修改这个变量时,直接保存到主内存中,读取该变量时也是直接读取主内存的变量数据,从而保证了进程同步。
- synchronized:可以来对一个代码块儿或是对一个方法上锁,被锁住的地方成为临界区,进入临界区的线程会获取对象的monitor,这样其他尝试进入临界区的线程会因为无法获得monitor而被阻塞。等待另一个线程释放monitor而被阻塞的线程无法被中断。
- ReentranLock:尝试获取锁的线程可以被中断并可以设置超时参数。
29.简述java中具有哪几种粒度的锁
java中可以对,类,对象,方法,和代码块上锁。
30.ThreadLocal的设计理念与作用
ThreadLocal的作用是提供该线程内的局部变量,在多线程环境下访问时能保证各个线程内的ThreadLocal变量独立。也就是说,每个线程的ThreadLocal变量是自己专用的,其他线程是访问不到的。ThreadLocal最常用于以下这个场景:多线程环境下存在对线程安全对象的并发访问,而且该对象不在线程间共享,但我们不想加锁,这时候可以使用ThreadLocal
来使得每个线程都持有一个该对象的副本。
31.concurrent包的整体架构
concurrent包的整体架构32.同步器(Synchronizer)
concurrent包为我们提供了以下几个帮助我们管理相互合作的线程的类:
- CyclicBarrier:他允许线程集等待直至其中预定数目的线程达到某个状态,然后可以选择执行一个处理状态的动作。使用场景:当多个线程都完成其操作,这些线程才能继续执行,或都完成了指定任务后,执行处理状态操作,这些线程就可以继续进行。
- CountDownLatch:允许线程集等待直到计数器减为0。当一个线程需要等待某指定数量的事件发生时使用。
- Exchanger:允许两个线程在要交换的对象准备好是交换对象。适用场景:当两个线程工作者在统一数据结构的两个实例上,一个从实例中取出数据,一个向实例中添加数据。
- Semaphore:允许线程集等待直到被允许继续运行为止。适用场景:限制同一时刻对资源的并发访问的线程数,初始化Semaphore需要指定许可的数目,线程要访问受限资源时需要获取一个许可,当所有的许可都被获取,其他线程就只有等待许可被释放后才能被获取。
- SynchronousQueue:允许一个线程把对象交给另一个线程。适用场景:在没有显式同步的情况下,当两个线程准备好将一个对象从一个线程传递到另一个线程。
33.ArrayBlockingQueue,CountDownLatch类的作用。
- CountDownLatch:允许线程集等待直到计数器减为0。当一个线程需要等待某指定数量的事件发生时使用。
- ArrayBlockingQueue,一个基于数组实现的阻塞队列,它在构造时需要指定相关容量。当队列满时,试图往队列中添加元素,则会阻塞,当队列为空时,试图取出元素时也会阻塞。阻塞至操作可执行时,继续执行。
34.wait()和sleep()的区别:
- wait是Object类中定义的方法,在指定对象调用时,会使当前线程进入等待状态(前提是持有monitor),并释放monitor,这样一来其他线程便有机会获取这个对象的monitor,当其他线程获得monitor完成操作之后,可以调用notify()方法,唤醒等待的线程。
- sleep():Thread类中的静态方法,作用是让当前线程进入休眠状态,以便让其他线程有机会执行,进入休眠状态时不会释放它所持有的锁。
35.线程池的用法和优势
- 实现线程的复用,避免了大量的线程创建和销毁的开销,使用线程池统一管理线程可以减少并发线程的数目,而线程过多往往会在线程上下文切换上即线程同步上浪费过多的事件。
- 用法:我们可以调用ThreadPoolExecutor的某个构造方法来创建一个自己的线程池。但通常情况下我们可以使用Executor类提供给我们静态工厂方法来更方便的创建一个线程池对象。创建完线程池对象以后,我们就可以调用submit方法提交到任务到线程池中去执行了,线程池使用完毕后记得用shutDown方法关闭。
36.for-each与常规的for循环的效率对比
使用for-each循环与常规的for循环相比,并不存在性能损失,即使对数组进行迭代也是如此。实际上在有些场合他还能带来微小的性能提升,因为它只计算一次数组上限。
所以我们要尽可能的选择for-each。但是有些场景无法使用for-each。
- 遍历过程中对元素进行更改操作。
- 遍历元素过程中,对元素进行位置替换等等。
37.简述java IO 与java NIO的区别
- javaIO是面向流的,这意味着我们需要每次从流中读取一个或多个字节,直到读取完所有字节,NIO是面向缓冲的,也就是说会把数据读到一个缓冲区中,然后对缓冲区中的数组进行相应的处理。
- javaIO是阻塞IO , 就javaNIO是非阻塞IO。
- java NIO中存在一个称为选择器的东西,他允许你把多个通道注册在一个选择器上,然后使用一个线程来监视这些通道:若这些通道里有某个准备好可以开始读或者写的操作,则开始对相应的通道进行读写,而在等待某通道变为可读、可写期间,请求对通道进行读写的线程可以去干别的事情。
38.父类的静态方法能否被子类重写?
父类的静态方法可以被子类继承,也就是说子类可以调用父类的静态方法,但是不能重写。
39.反射的作用与原理
java反射机制是在运行状态中,对于任意一个类,都能知道这个类的所有方法和属性;对于任意一个对象,都能够调用他的任意一个方法和属性,这种动态获取信息以及动态调用对象的方法的功能称为java语言的反射机制。
主要作用有三:
- 运行时取得类的方法和字段的相关信息。
- 创建某各类的新实例(.newIstance())
- 取得字段引用直接获取和设置对象字段,无论修饰符
用处如下:
- 观察或操作应用程序的运行时行为。
- 调试或测试程序,因为可以直接访问方法,构造函数和字段成员
- 通过名字调用不知道的方法并使用该信息来创建对象和调用方法。
40.java中的泛型机制
泛型:是一个类安全机制。其好处是:
- 将运行时期出现的问题ClassCastException(类型匹配异常),转移到了编译时期,方便与程序员解决问题,让运行时期的问题减少,安全了。
- 避免了强制类型装换,且运行时不会提示程序类型没检查的不安全提示。
泛型技术是给编译器使用的技术,用于编译时期。确保了类型的安全。运行时会将泛型擦除掉,生成的class文件是不带泛型的。为了兼容运行的类加载器,我们将泛型替换为最大容许类型。泛型的补偿:在类加载器原有的基础上,编写一个补偿程序,在运行时,通过反射,获取元素的类型进行转换动作。
41.进程和线程的区别
进程就是一个应用进程在处理机上的一次执行过程,它是一个动态的概念,而线程是进程的一部分,一个进程可以包含很多线程,进程是一个具有独立功能的程序关于某个数据集合的一次活动过程。他可以申请资源是一个动态的概念,是一个活动的实体。他不只表示程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。
进程是一个“执行中的程序”。程序是一个没有生命的个体,只有处理器赋予生命时,才能成为一个有生命的活体,我们称之为进程。
通常在一个进程中包含若干个线程,他们可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和调度的基本单位。由于进程比线程更小,基本上不拥有系统资源,故而对它的调度所付出的开销就会很多,能够高效的提高系统内部多个程序之间并发执行的程度。
线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程都要有自己的执行堆栈和程序计数器为其执行上下文。多线程主要为了节约CPU空间,发挥利用,根据具体情况而定。线程的运行需要计算机的内存资源与CPU。
线程与进程的区别归纳:
- 地址空间和其他资源:进程间相互独立,同一进程的个线程之间信息可共享,其他进程不可见。
- 通信:进程间通信IPC ,线程间可以直接读写进程数据段(如全局变量)来进行通信--需要进程同步和互斥的手段辅助,以保证数据的一致性。
- 调度和切换:线程的上下文切换要比进程的上下文切换快得多。
- 在多线程OS中,进程是一个不可执行的实体。
进程是具有一定独立功能的程序关于某个数据集合上的一次活动,进程是系统进行资源分布和调度的一个独立单位。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如:程序计数器,一组寄存器和栈)。但是它可与同属一个进程的其他线程共享所拥有的全部资源。
42.java7与java8的新特性
java7:
- 对集合的支持更加完善
创建List/Set/Map时写法更简单 - 对资源的走动回收管理
- 泛型实例创建过程中类型的简化
- 在数字中使用下划线
- 对字符串支持switch
- 二进制符号
- 一个catch里捕捉多个异常类型
java8:
- Lambdas表达式与Function接口
- 接口的默认与静态方法
- 方法引用
- 重复注解
- 更好地类型推测机制
- 扩展注解的支持
43.常见的设计模式
- 创建型模式:工厂模式,建造者模式,单例模式。
- 结构性模式:包括适配器模式,桥接模式,装饰模式,外观模式,享元模式,代理模式。
- 行为型模式:包括命令模式,中介者模式,观察者模式,状态模式,策略模式。
44.注解的基本概念与使用
- 注解可以看做是“增强版的注释”,它可以向编译器,虚拟机说明一些事情。
- 注解是描述java代码的代码,体能够被代码解析器解析,注解处理工具在运行时也能够解析注解。注解本身是“被动”的消息,只有主动解析才能表达它的意义。
- 除了向编译器、虚拟机传递信息。我们也可以使用注解来生成一些“模板化”的代码。
45.java动态代理
-
代理
在某些情况下,我们不希望A类被直接访问,而是通过访问一个中介对象B类,由B去访问A达成目的,这种方式我们就称为代理。
这里的A对象所属的类我们就称为委托类。B对象所属的类称为代理类。
代理优点有:
1.隐藏委托类的实现
2.解耦,不改变委托类代码情况下做一些额外处理。
根据程序运行前,代理是否已经存在,我们将代理分为静态代理和动态代理 -
静态代理
代理类在程序运行前已经存在的代理方式称为静态代理。
通过上面解释可以知道,由开发人员编写或是编译器生成代理类的方式都属于静态代理
``
public class ClassA {
public void operateMethod1() {};
public void operateMethod2() {};
public void operateMethod3() {};
}
public class ClassB {
private ClassA a;
public ClassB(ClassA a) {
this.a = a;
}
public void operateMethod1() {
a.operateMethod1();
};
public void operateMethod2() {
a.operateMethod2();
};
// not export operateMethod3()
}
``
上面ClassA是委托类,ClassB是代理类,ClassB中的函数都是直接调用ClassA相应函数,并且隐藏了Class的operateMethod3()函数。
静态代理中代理类和委托类也常常继承同一父类或实现同一接口。
- 动态代理
代理类在程序运行前不存在、运行时由程序动态生成的代理方式称为动态代理。
Java 提供了动态代理的实现方式,可以在运行时刻动态生成代理类。这种代理方式的一大好处是可以方便对代理类的函数做统一或特殊处理,如记录所有函数执行时间、所有函数执行前添加验证判断、对某个特殊函数进行特殊操作,而不用像静态代理方式那样需要修改每个函数。
动态代理实现
(1)新建委托类
``
public interface Operate {
public void operateMethod1();
public void operateMethod2();
public void operateMethod3();
}
public class OperateImpl implements Operate {
@Override
public void operateMethod1() {
System.out.println("Invoke operateMethod1");
sleep(110);
}
@Override
public void operateMethod2() {
System.out.println("Invoke operateMethod2");
sleep(120);
}
@Override
public void operateMethod3() {
System.out.println("Invoke operateMethod3");
sleep(130);
}
private static void sleep(long millSeconds) {
try {
Thread.sleep(millSeconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
``
Operate是一个接口,定了了一些函数,我们要统计这些函数的执行时间。
OperateImpl是委托类,实现Operate接口。每个函数简单输出字符串,并等待一段时间。
动态代理要求委托类必须实现了某个接口,比如这里委托类OperateImpl实现了Operate,原因会后续在微博公布。
(2)实现InvocationHandler接口,这是负责连接代理类和委托类的中间类,必须实现InvocationHandler接口。
``
public class TimingInvocationHandler implements InvocationHandler {
private Object target;
public TimingInvocationHandler() {}
public TimingInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();
Object obj = method.invoke(target, args);
System.out.println(method.getName() + " cost time is:" + (System.currentTimeMillis() - start));
return obj;
}
}
``
target属性表示委托类对象。
InvocationHandler是负责连接代理类和委托类的中间类必须实现的接口。其中只有一个
public Object invoke(Object proxy, Method method, Object[] args)
函数需要去实现,参数:
proxy表示下面2.3 通过 Proxy.newProxyInstance() 生成的代理类对象。
method表示代理对象被调用的函数。
args表示代理对象被调用的函数的参数。
调用代理对象的每个函数实际最终都是调用了InvocationHandler的invoke函数。这里我们在invoke实现中添加了开始结束计时,其中还调用了委托类对象target的相应函数,这样便完成了统计执行时间的需求。
invoke函数中我们也可以通过对method做一些判断,从而对某些函数特殊处理。
(3)通过Proxy类新建代理类对象。
``
public class demo {
public class Main {
public static void main(String[] args) {
// create proxy instance
TimingInvocationHandler timingInvocationHandler = new TimingInvocationHandler(new OperateImpl());
Operate operate = (Operate)(Proxy.newProxyInstance(Operate.class.getClassLoader(), new Class[] {Operate.class},
timingInvocationHandler));
// call method of proxy instance
operate.operateMethod1();
System.out.println();
operate.operateMethod2();
System.out.println();
operate.operateMethod3();
}
}
}
这里我们先将委托类对象new OperateImpl()作为TimingInvocationHandler构造函数入参创建timingInvocationHandler对象;
然后通过Proxy.newProxyInstance(…)函数新建了一个代理对象,实际代理类就是在这时候动态生成的。我们调用该代理对象的函数就会调用到timingInvocationHandler的invoke函数(是不是有点类似静态代理),而invoke函数实现中调用委托类对象new OperateImpl()相应的 method(是不是有点类似静态代理)。
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
loader表示类加载器
interfaces表示委托类的接口,生成代理类时需要实现这些接口
h是InvocationHandler实现类对象,负责连接代理类和委托类的中间类
我们可以这样理解,如上的动态代理实现实际是双层的静态代理,开发者提供了委托类 B,程序动态生成了代理类 A。开发者还需要提供一个实现了InvocationHandler的子类 C,子类 C 连接代理类 A 和委托类 B,它是代理类 A 的委托类,委托类 B 的代理类。用户直接调用代理类 A 的对象,A 将调用转发给委托类 C,委托类 C 再将调用转发给它的委托类 B。