八种基本数据类型的大小,以及他们的封装类
Switch能否用string做参数
equals与==的区别
自动装箱,常量池
Object有哪些公用方法
Java的四种引用,强弱软虚,用到的场景
HashMap和ConcurrentHashMap的区别
ConcurrentHashMap能完全替代HashTable吗
为什么HashMap是线程不安全的
如何线程安全的使用HashMap
多并发情况下HashMap是否还会产生死循环
Collection包结构,与Collections的区别
try catch finally,try里有return,finally还执行么
Excption与Error包结构,OOM你遇到过哪些情况,SOF你遇到过哪些情况
Java(OOP)面向对象的三个特征与含义
Override和Overload的含义去区别
Interface与abstract类的区别
Static class与non static class的区别
java多态的实现原理
foreach与正常for循环效率对比
Java IO与NIO
java反射的作用于原理
泛型常用特点
解析XML的几种方式的原理与特点:DOM、SAX
Java1.7与1.8,1.9,10 新特性
设计模式:单例、工厂、适配器、责任链、观察者等等
JNI的使用
AOP与OOP的区别
八种基本数据类型的大小,以及他们的封装类
int,short,long,float,boolean,byte,double,char
32,16,64,32,vm depend,8,64,16
Switch能否用string做参数
jdk7之前 switch 只能支持 byte、short、char、int 这几个基本数据类型和其对应的封装类型。
switch后面的括号里面只能放int类型的值,但由于byte,short,char类型,它们会?自动?转换为int类型(精精度小的向大的转化),所以它们也支持。
jdk1.7后 整形,枚举类型,boolean,字符串都可以。
jdk1.7并没有新的指令来处理switch string,而是通过调用switch中string.hashCode,将string转换为int从而进行判断。
equals 和 == 区别
使用==比较原生类型如:boolean、int、char等等,使用equals()比较对象。
1、==是判断两个变量或实例是不是指向同一个内存空间。 equals是判断两个变量或实例所指向的内存空间的值是不是相同。
2、==是指对内存地址进行比较。 equals()是对字符串的内容进行比较。
3、==指引用是否相同。 equals()指的是值是否相同。
自动装箱
int 到Integer(自动装箱), Integer 到 int(自动拆箱); 还有其他基本类型 到 封装类型 也是。
上面的答案是,对错对对对。
因为Integer.valueOf() 是会给我们一个系统分好的箱子,所以会指向同一个。
这个自动拆箱,装箱的范围是在-128,128的。
所以 Integer,valueOf(129) != Integer,valueOf(129)
Object 公用方法
clone 保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
equals 在Object中与==是一样的,子类一般需要重写该方法。
hashCode 该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。
getClass final方法,获得运行时类型
wait 使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。 wait() 方法一直等待,直到获得锁或者被中断。 wait(long timeout) 设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生
1、其他线程调用了该对象的notify方法。 2、其他线程调用了该对象的notifyAll方法。 3、其他线程调用了interrupt中断该线程。 4、时间间隔到了。 5、此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
notify 唤醒在该对象上等待的某个线程。
notifyAll 唤醒在该对象上等待的所有线程。
toString 转换成字符串,一般子类都有重写,否则打印句柄。
JAVA 的四种引用
1、强引用
最普遍的一种引用方式,如String s = "abc",变量s就是字符串“abc”的强引用,只要强引用存在,则垃圾回收器就不会回收这个对象。 这个到处都在用,就不细说了。
2、软引用(SoftReference)
用于描述还有用但非必须的对象,如果内存足够,不回收,如果内存不足,则回收。一般用于实现内存敏感的高速缓存,软引用可以和引用队列ReferenceQueue联合使用,如果软引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中。
使用场景:
public class LeakyChecksum {
private byte[] byteArray;
public synchronized int getFileChecksum(String fileName) {
int len = getFileSize(fileName);
if (byteArray == null || byteArray.length < len)
byteArray = new byte[len];
readFileContents(fileName, byteArray);
// calculate checksum and return it
}
}
上述代码的主要功能就是根据文件的内容去计算它的checksum,如果上述代码的if 条件不成立,它会不断地重用字节数组,而不是重新分配它。除非LeakyChecksum对象被gc,否则这个字节数组始终不会被gc,由于程序到它一直是可达的。而且更糟糕的是,随着程序的不断运行,这个字节数组只会不断增大,不会减小,它的大小始终都和它处理过的最大的文件的大小一致,这样很可能会导致JVM更频繁地GC,降低应用程序地性能。大多数情况下,这个字节数组所占的空间要比它实际要用的空间要大,而多余的空间又不能被回收利用,这导致了内存泄露。
Soft references 解决上面的内存泄露问题
对于只被Soft references所引用的对象,我们称它为softly reachable objects. 只要可得到的内存很充足,softly reachable objects 通常不会被gc. JVM要比我们的程序更加了解内存的使用情况,如果可得到的内存紧张,那么JVM就会频繁地进行垃圾回收,从而释放更多的内存空间,供我们使用。因此,上述程序的字节数组缓存由于一直是可达的,即使在内存很紧张的情况下,它也不会被回收掉,这无疑给垃圾收集器更大的压力,使其更频繁地GC.
那么有没有一种解决方案可以做到这样呢,如果我们的内存很充足,我们就保持这样的缓存在内存中,不被gc; 但是,当我们的内存吃紧时,就把它释放掉。那么大家想一想,谁可以做到这一点呢?答案是JVM,因为它最了解内存的使用情况,我们可以借助它的力量来达到我们的目标,而Soft references 可以帮我们Java 程序员借助JVM的力量。下面,让我们来看看如果用SoftReference 改写上面的代码。
public class CachingChecksum {
private SoftReference bufferRef;
public synchronized int getFileChecksum(String fileName) {
int len = getFileSize(fileName);
byte[] byteArray = bufferRef.get();
if (byteArray == null || byteArray.length < len) {
byteArray = new byte[len];
bufferRef.set(byteArray);
}
readFileContents(fileName, byteArray);
// calculate checksum and return it
}
}
从上面的代码我们可以看出,一旦走出if 语句,字节数组对象就只被Soft references 所引用,成为了softly reachable objects. 对于垃圾收集器来说,它只会在真正需要内存的时候才会去回收softly reachable objects. 现在,如果我们的内存不算吃紧,这个字节数组buffer会一直保存在内存中。在抛出OutOfMemoryError 之前,垃圾收集器一定会clear掉所有的soft references.
3、弱引用(WeakReference)
弱引用和软引用大致相同,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
使用场景:
public class SocketManager {
private Map m = new HashMap();
public void setUser(Socket s, User u) {
m.put(s, u);
}
public User getUser(Socket s) {
return m.get(s);
}
public void removeUser(Socket s) {
m.remove(s);
}
}
通常情况下,Socket 对象的生命周期要比整个应用的生命周期要短,同时,它也会比用到它的方法调用要长。上述代码把User 对象的生命周期与Socket 对象绑在一起,因为我们不能准确地知道Socket连接在什么时候被关闭,所以我们不能手动地去把它从Map中移除。而只要SocketManager 对象不死,HashMap 对象就始终是可达的。这样就会出现一个问题,就是即使服务完来自客户端的请求,Socket已经关闭,但是Socket 和 User 对象一直都不会被gc,它们会一直被保留在内存中。如果这样一直下去,就会导致程序出现内存溢出的错误。
public class SocketManager {
private Map m = new HashMap();
public void setUser(Socket s, User u) {
m.put(s, u);
}
public User getUser(Socket s) {
return m.get(s);
}
public void removeUser(Socket s) {
m.remove(s);
}
}
既然上面的代码有内存泄露的问题,我们应该如何解决呢?如果有一种手段可以做到,比如: 当Map中Entry的Key不再被使用了,就会把这个Entry自动移除,这样我们就可以解决上面的问题了。幸运的是,Java团队给我们提供了一个这样的类可以做到这点,它就是WeakHashMap ,我们只要把我们的代码做如下修改就行:
public class SocketManager {
private Map m = new WeakHashMap();
public void setUser(Socket s, User u) {
m.put(s, u);
}
public User getUser(Socket s) {
return m.get(s);
}
}
4、虚引用(PhantomReference)
就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。
使用场景:
这个真的比较冷门,我也是后来发现的。特地回来记录一下。
在JAVA NIO里有一个叫DIRECT BUFFER的类。他是可以在JVM堆外开空间的,那么传统的GC就没法COVER。但是这块空间开出来,GC还是要回收的。怎么办呢?
我们就用一个虚引用,用虚引用套上你的指向堆外内存的指针。一旦这个指针用完了,被回收了。那么虚引用就把它加入到一个REFERENCE QUEUE的队列里。 表示这个东西要被垃圾回收了。
这个时候,你可以去这个队列里把那些对象取出来,然后依次释放堆外内存。
Cleaner是PhantomReference的子类,并通过自身的next和prev字段维护的一个双向链表。PhantomReference的作用在于跟踪垃圾回收过程,并不会对对象的垃圾回收过程造成任何的影响。
所以cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); 用于对当前构造的DirectByteBuffer对象的垃圾回收过程进行跟踪。
当DirectByteBuffer对象从pending状态 ——> enqueue状态时,会触发Cleaner的clean(),而Cleaner的clean()的方法会实现通过unsafe对堆外内存的释放。
HashMap和ConcurrentHashMap的区别
为了线程安全从ConcurrentHashMap代码中可以看出,它引入了一个“分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的HashTable,根据key.hashCode()来决定把key放到哪个HashTable中。
Hashmap本质是数组加链表。根据key取得hash值,然后计算出数组下标,如果多个key对应到同一个下标,就用链表串起来,新插入的在前面。
ConcurrentHashMap:在hashMap的基础上,ConcurrentHashMap将数据分为多个segment,默认16个(concurrency level),然后每次操作对一个segment加锁,避免多线程锁的几率,提高并发效率。
总结
JDK6,7中的ConcurrentHashmap主要使用Segment来实现减小锁粒度,把HashMap分割成若干个Segment,在put的时候需要锁住Segment,get时候不加锁,使用volatile来保证可见性,当要统计全局时(比如size),首先会尝试多次计算modcount来确定,这几次尝试中,是否有其他线程进行了修改操作,如果没有,则直接返回size。如果有,则需要依次锁住所有的Segment来计算。
jdk7中ConcurrentHashmap中,当长度过长碰撞会很频繁,链表的增改删查操作都会消耗很长的时间,影响性能。
jdk8 中完全重写了concurrentHashmap,代码量从原来的1000多行变成了 6000多 行,实现上也和原来的分段式存储有很大的区别。
JDK8中采用的是位桶+链表/红黑树(有关红黑树请查看红黑树)的方式,也是非线程安全的。当某个位桶的链表的长度达到某个阀值的时候,这个链表就将转换成红黑树。
JDK8中,当同一个hash值的节点数不小于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树(上图中null节点没画)。这就是JDK7与JDK8中HashMap实现的最大区别。
主要设计上的变化有以下几点
1.jdk8不采用segment而采用node,锁住node来实现减小锁粒度。 2.设计了MOVED状态 当resize的中过程中 线程2还在put数据,线程2会帮助resize。 3.使用3个CAS操作来确保node的一些操作的原子性,这种方式代替了锁。 4.sizeCtl的不同值来代表不同含义,起到了控制的作用。
至于为什么JDK8中使用synchronized而不是ReentrantLock,我猜是因为JDK8中对synchronized有了足够的优化吧。
ConcurrentHashMap能完全替代HashTable吗
hashTable虽然性能上不如ConcurrentHashMap,但并不能完全被取代,两者的迭代器的一致性不同的,hash table的迭代器是强一致性的,而concurrenthashmap是弱一致的。
ConcurrentHashMap的get,clear,iterator 都是弱一致性的。 Doug Lea 也将这个判断留给用户自己决定是否使用ConcurrentHashMap。
ConcurrentHashMap与HashTable都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map
多并发情况下HashMap是否还会产生死循环
会的,因为JDK7以前,HASHMAP 默认会插链表的时候插在头部。
详细解释
try catch finally,try里有return,finally还执行么
肯定会执行。finally{}块的代码。 只有在try{}块中包含遇到System.exit(0)。 之类的导致Java虚拟机直接退出的语句才会不执行。
当程序执行try{}遇到return时,程序会先执行return语句,但并不会立即返回——也就是把return语句要做的一切事情都准备好,也就是在将要返回、但并未返回的时候,程序把执行流程转去执行finally块,当finally块执行完成后就直接返回刚才return语句已经准备好的结果。
Excption与Error包结构。OOM你遇到过哪些情况,SO F你遇到过哪些情况
Throwable是 Java 语言中所有错误或异常的超类。 Throwable包含两个子类: Error 和 Exception 。它们通常用于指示发生了异常情况。 Throwable包含了其线程创建时线程执行堆栈的快照,它提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。
Java将可抛出(Throwable)的结构分为三种类型:
被检查的异常(Checked Exception)。 运行时异常(RuntimeException)。 错误(Error)。
RuntimeException及其子类都被称为运行时异常。 特点 : Java编译器不会检查它 也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。
例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fail机制产生的ConcurrentModificationException异常等,都属于运行时异常。
除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。
Java Heap 溢出。 一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess。 java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。
StackOverflowError 的定义: 当应用程序递归太深而发生堆栈溢出时,抛出该错误。 因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。
OOP面向对象的三个特征与含义
封装(高内聚低耦合 -->解耦)
封装是指将某事物的属性和行为包装到对象中,这个对象只对外公布需要公开的属性和行为,而这个公布也是可以有选择性的公布给其它对象。在java中能使用private、protected、public三种修饰符或不用(即默认defalut)对外部对象访问该对象的属性和行为进行限制。
java的继承(重用父类的代码)
继承是子对象可以继承父对象的属性和行为,亦即父对象拥有的属性和行为,其子对象也就拥有了这些属性和行为。
java中的多态(父类引用指向子类对象)
多态是指父对象中的同一个行为能在其多个子对象中有不同的表现。
有两种多态的机制:编译时多态(方法重载,方法覆盖并且对象引用为本类实例)、运行时多态(父类对象p引用子类实例)。
Override和Overload的含义与区别
重载 Overload方法名相同,参数列表不同(个数、顺序、类型不同)与返回类型无关。 重写 Override 覆盖。 将父类的方法覆盖。 重写方法重写:方法名相同,访问修饰符只能大于被重写的方法访问修饰符,方法签名个数,顺序个数类型相同。
重写方法的规则
1、参数列表必须完全与被重写的方法相同,否则不能称其为重写而是重载。 2、返回的类型必须一直与被重写的方法的返回类型相同,否则不能称其为重写而是重载。 3、访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)。 4、重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常。
例如: 父类的一个方法申明了一个检查异常IOException,在重写这个方法是就不能抛出Exception,只能抛出IOException的子类异常,可以抛出非检查异常。
Interface与abstract类的区别
Interface 只能有成员常量,只能是方法的声明。 Abstract class可以有成员变量,可以声明普通方法和抽象方法。
interface是接口,所有的方法都是抽象方法,成员变量是默认的public static final 类型。接口不能实例化自己。(JDK 8 ,可以有DEFAULT方法)
abstract class是抽象类,至少包含一个抽象方法的累叫抽象类,抽象类不能被自身实例化,并用abstract关键字来修饰。
static class 和 non-static class
static class(内部静态类)
1、用static修饰的是内部类,此时这个内部类变为静态内部类;对测试有用。 2、内部静态类不需要有指向外部类的引用。 3、静态类只能访问外部类的静态成员,不能访问外部类的非静态成员。
non static class(非静态内部类)
1、非静态内部类需要持有对外部类的引用。 2、非静态内部类能够访问外部类的静态和非静态成员。 3、一个非静态内部类不能脱离外部类实体被创建。 4、一个非静态内部类可以访问外部类的数据和方法
java 多态实现原理
动态绑定具体的调用过程为:
1.首先会找到被调用方法所属类的全限定名
2.在此类的方法表中寻找被调用方法,如果找到,会将方法表中此方法的索引项记录到常量池中(这个过程叫常量池解析),如果没有,编译失败。
3.根据具体实例化的对象找到方法区中此对象的方法表,再找到方法表中的被调用方法, 如果找到都符合的方法,进行访问权限效验,如果通过则通过直接地址找到字节码所在的内存空间。 如果不通过,抛出异常。
- 按照继承关系,从下往上依次对实际类型的各父类进行搜索和验证。
5.如果始终没有找到,抛出ABSTRACT METHOD ERROR
foreach与正常for循环效率对比
用for循环arrayList 10万次花费时间:5毫秒。 用foreach循环arrayList 10万次花费时间:7毫秒。
用for循环linkList 10万次花费时间:4481毫秒。 用foreach循环linkList 10万次花费时间:5毫秒。
结论:
需要循环数组结构的数据时,建议使用普通for循环,因为for循环采用下标访问,对于数组结构的数据来说,采用下标访问比较好。
需要循环链表结构的数据时,一定不要使用普通for循环,这种做法很糟糕,数据量大的时候有可能会导致系统崩溃。
Java?IO与NIO
NIO的一些新特性有:非阻塞I/O,选择器,缓冲以及管道。管道(Channel),缓冲(Buffer) ,选择器( Selector)是其主要特征。
Channel——管道实际上就像传统IO中的流,到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。一个 Buffer 实质上是一个容器对象。Selector——选择器用于监听多个管道的事件,使用传统的阻塞IO时我们可以方便的知道什么时候可以进行读写,而使用非阻塞通道,我们需要一些方法来知道什么时候通道准备好了,选择器正是为这个需要而诞生的。
IO是阻塞的,NIO是非阻塞的
对于传统的IO,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
而对于NIO,使用一个线程发送读取数据请求,没有得到响应之前,线程是空闲的,此时线程可以去执行别的任务,而不是像IO中那样只能等待响应完成。
那么NIO和IO各适用的场景是什么呢?
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,这时候用NIO处理数据可能是个很好的选择。
而如果只有少量的连接,而这些连接每次要发送大量的数据,这时候传统的IO更合适。使用哪种处理数据,需要在数据的响应等待时间和检查缓冲区数据的时间上作比较来权衡选择。
JAVA反射与原理
Java 反射是可以让我们在运行时,通过一个类的Class对象来获取它获取类的方法、属性、父类、接口等类的内部信息的机制。
这种动态获取信息以及动态调用对象的方法的功能称为JAVA的反射。
反射机制主要提供了以下功能:
在运行时判断任意一个对象所属的类。
在运行时构造任意一个类的对象。
在运行时判断任意一个类所具有的成员变量和方法。
在运行时调用任意一个对象的方法。
生成动态代理。
反射的原理
JAVA语言编译之后会生成一个.class文件,反射就是通过字节码文件找到某一个类、类中的方法以及属性等。
解析XML的几种方式的原理与特点:DOM、SAX
SAX解析器:
SAX(Simple API for XML)解析器是一种基于事件的解析器,它的核心是事件处理模式,主要是围绕着事件源以及事件处理器来工作的。当事件源产生事件后,调用事件处理器相应的处理方法,一个事件就可以得到处理。在事件源调用事件处理器中特定方法的时候,还要传递给事件处理器相应事件的状态信息,这样事件处理器才能够根据提供的事件信息来决定自己的行为。
SAX解析器的优点是解析速度快,占用内存少。非常适合在Android移动设备中使用。
DOM解析器:
DOM是基于树形结构的的节点或信息片段的集合,允许开发人员使用DOM API遍历XML树、检索所需数据。分析该结构通常需要加载整个文档和构造树形结构,然后才可以检索和更新节点信息。
由于DOM在内存中以树形结构存放,因此检索和更新效率会更高。但是对于特别大的文档,解析和加载整个文档将会很耗资源。
设计模式六大原则
1、开闭原则(Open Close Principle)
开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。
2、里氏代换原则(Liskov Substitution Principle)
里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
3、依赖倒转原则(Dependence Inversion Principle)
这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。
4、接口隔离原则(Interface Segregation Principle)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。
5、迪米特法则,又称最少知道原则(Demeter Principle)
最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
6、合成复用原则(Composite Reuse Principle)
合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。
AOP是什么
AOP(Aspect Oriented Programming) 面向切面编程,是目前软件开发中的一个热点,是Spring框架内容,利用AOP可以对业务逻辑的各个部分隔离,从而使的业务逻辑各部分的耦合性降低,提高程序的可重用性,踢开开发效率,主要功能:日志记录,性能统计,安全控制,事务处理,异常处理等。
AOP实现原理是java动态代理,但是jdk的动态代理必须实现接口,所以spring的aop是用cglib这个库实现的,cglis使用里asm这个直接操纵字节码的框架,所以可以做到不使用接口的情况下实现动态代理。