Java基础(持续更新..)
HashMap
-
HashMap
是基于Map接口实现的一种键-值对<key,value>
的存储结构,允许null
值,同时非有序,非同步(即线程不安全)。HashMap
的底层实现是数组 + 链表 + 红黑树(JDK1.8增加了红黑树部分)。 -
HashMap
定位元素位置是通过键key
经过扰动函数扰动后得到hash
值,然后再通过hash & (length - 1)
代替取模的方式进行元素定位的。 -
HashMap
是使用链地址法解决hash
冲突的,当有冲突元素放进来时,会将此元素插入至此位置链表的最后一位,形成单链表。当存在位置的链表长度 大于等于 8 时,HashMap
会将链表 转变为 红黑树,以此提高查找效率。 -
HashMap
的容量是2的n次方,有利于提高计算元素存放位置时的效率,也降低了hash
冲突的几率。因此,我们使用HashMap
存储大量数据的时候,最好先预先指定容器的大小为2的n次方,即使我们不指定为2的n次方,HashMap
也会把容器的大小设置成最接近设置数的2的n次方,如,设置HashMap
的大小为 7 ,则HashMap
会将容器大小设置成最接近7的一个2的n次方数,此值为 8 。 -
HashMap
的负载因子表示哈希表空间的使用程度(或者说是哈希表空间的利用率)。当负载因子越大,则HashMap
的装载程度就越高。也就是能容纳更多的元素,元素多了,发生hash
碰撞的几率就会加大,从而链表就会拉长,此时的查询效率就会降低。当负载因子越小,则链表中的数据量就越稀疏,此时会对空间造成浪费,但是此时查询效率高。 -
HashMap
不是线程安全的,Hashtable
则是线程安全的。但Hashtable
是一个遗留容器,如果我们不需要线程同步,则建议使用HashMap
,如果需要线程同步,则建议使用ConcurrentHashMap
。 -
在多线程下操作
HashMap
,由于存在扩容机制(创建一个新的Entry空数组,长度是原数组的2倍 --> 遍历原Entry数组,把所有的Entry重新Hash到新数组),当HashMap
调用resize()
进行自动扩容时,可能会导致死循环的发生(指针形成环)。 -
我们在使用
HashMap
时,最好选择不可变对象作为key
。例如String
,Integer
等不可变类型作为key
是非常明智的。
Java内存模型
概念
在特定操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。
作用
Java内存模型(Java Memory Model,简称JMM
)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
目的
定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量的底层细节。
组成
①主内存: Java内存模型规定了所有变量都存储在主内存(Main Memory)中。
②工作内存: 每条线程都有自己的工作内存(Working Memory,本地内存),线程的工作内存中保存了该线程使用到的变量在主内存中的共享变量的副本。工作内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
Java内存需要解决的问题
①工作内存数据一致性
②指令重排序优化 : 编译期重排序 / 运行期重排序
内存交互的基本操作
①lock (锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
②unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
③read (读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load
操作使用。
④load (载入):作用于工作内存的变量,它把read
操作从主内存中得到的变量值放入工作内存的变量副本中。
⑤use (使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时就会执行这个操作。
⑥assign (赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
⑦store (存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后write
操作使用。
⑧write (写入):作用于主内存的变量,它把store
操作从工作内存中得到的变量的值放入主内存的变量中。
内存交互操作的特性
①原子性(Atomicity):一个操作或者多个操作 要么全部执行并且执行过程不会打断,要么就都不执行。即使在多个线程一起执行的时候,一个操作一旦开始就不会被其他线程所干扰。
②可见性(Visibility):指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。通过依赖主内存作为传递媒介的方式来实现可见性。
③有序性(Ordering):有序性规则表现在以下两种场景:线程内: 指令会按照一种叫“串行”(as-if-serial)的方式执行线程间: 由于指令重排序优化,任何代码都有可能交叉执行。唯一起作用的约束是:对于同步方法,同步块(synchronized
关键字修饰)以及volatile
字段的操作仍维持相对有序。
内存屏障
①Java中通过内存屏障保证底层操作的有序性和可见性。
②内存屏障:插入两个CPU指令之间的一种指令,用来禁止处理器指令发生重排序,从而保障有序性。为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障可见性。
③通过volatile
和synchronized
关键字修饰的代码块,Unsafe
类来实现内存屏障。
volatile型变量
①保证可见性:保证了不同线程对该变量操作的内存可见性。
线程写volatile
变量的过程:
⑴改变线程工作内存中volatile
变量副本的值
⑵将改变后的副本的值从工作内存刷新到主内存
线程读volatile变量的过程:
⑴从主内存中读取volatile
变量的最新值到线程的工作内存中
⑵从工作内存中读取volatile
变量的副本
②禁止进行指令重排序
当程序执行到 volatile
变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
在进行指令优化时,不能将在对volatile
变量访问的语句放在其后面执行,也不能把volatile
变量后面的语句放到其前面执行。
final型变量的特殊规则
final修饰的字段在声明时或者初始化完成,final变量的值立刻写到主内存,那么在其他线程无须同步就能正确看见final字段的值。
synchronized的特殊规则
①读数据:当线程进入到该区域读取变量信息时,对数据的读取也不能从工作内存读取,只能从主内存中读取,保证读到的是最新的值。
②写数据:在同步区内对变量的写入操作,在离开同步区时就将当前线程内的数据刷新到主内存中,保证更新的数据对其他线程的可见性。
JVM
运行时数据区域
①程序计数器:内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。
如果线程正在执行的是Java
方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native
方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 JVM规范中没有规定任何OutOfMemoryError
情况的区域。
②Java虚拟机栈:线程私有,生命周期和线程一致。在执行Java
方法时都会创建一个栈帧Stack Frame
用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
③本地方法栈:区别于Java
虚拟机栈的是,Java
虚拟机栈为虚拟机执行Java
方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native
方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
④Java 堆:这块区域是JVM
所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB
)。可以位于物理上不连续的空间,但是逻辑上要连续。
⑤方法区:属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即为编译器编译后的代码等数据。
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
HotSpot 虚拟机对象探秘(介绍数据是如何创建、如何布局以及如何访问的)
①对象的创建:遇到 new 指令时,首先检查这个类的符号引用是否能在常量池中定位到,并检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有,则执行相应的类加载。
类加载检查通过后,在堆的空闲内存中划分一块区域为新对象分配内存。内存空间分配完成后会初始化为 0,然后把对象对应类的实例、类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息填充存入对象头。
执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成。每个线程在堆中都会有私有的分配缓冲区TLAB
,在并发情况下频繁创建对象可以避免线程不安全。
②对象的内存布局:分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
⑴对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是Java
数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过Java
对象元数据确定大小,而数组对象不可以。
⑵实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。
⑶对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍
③对象的访问定位:使用对象时,通过栈上的reference
数据来操作堆上的具体对象。
⑴通过句柄访问:Java堆中会分配一块内存作为句柄池。reference
存储的是句柄地址
⑵通过直接指针访问:reference
中直接存储对象地址。
比较:使用句柄的最大好处是reference
中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference
自身不需要修改**。** 直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。如果是对象频繁 GC 那么句柄方法好,如果是对象频繁访问则直接指针访问好。
垃圾回收器与内存分配策略
①概述:程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。只有在程序处于运行期才知道哪些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收器所关注的就是这部分内存。
②判断对象的存活状态:
⑴引用计数法(给对象添加一个引用计数器。但是难以解决循环引用问题)。
⑵可达性分析法(通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用)。
⑶可作为 GC Roots 的对象:虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象。
即使在可达性分析算法中不可达的对象,也并非是“facebook”的,这时候它们暂时出于“缓刑”阶段,一个对象的真正死亡至少要经历两次标记过程:如果对象在进行中可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行 finalize()
方法。当对象没有覆盖 finalize()
方法,或者 finalize()
方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行 finalize()
方法,那么这个对象竟会放置在一个叫做 F-Queue
的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer
线程去执行它。这里所谓的“执行”是指虚拟机会出发这个方法,并不承诺或等待他运行结束。finalize()
方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue
中的对象进行第二次小规模的标记,如果对象要在 finalize()
中成功拯救自己 —— 只要重新与引用链上的任何一个对象简历关联即可。
finalize()
方法只会被系统自动调用一次。
③引用:下面四种引用强度一次逐渐减弱
⑴强引用:类似于 Object obj = new Object()
; 创建的,只要强引用在就不回收。
⑵软引用:SoftReference
类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。
⑶弱引用:WeakReference
类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
⑷虚引用:PhantomReference
类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
④回收方法区:在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。永久代垃圾回收主要两部分内容:废弃的常量和无用的类。
⑴判断废弃常量:一般是判断没有该常量的引用。
⑵判断无用的类:满足以下三个条件:类的所有实例都已经回收( Java 堆
中不存在该类的任何实例)、加载该类的 ClassLoader
已经被回收、该类对应的 java.lang.Class
对象没有被引用,无法通过反射访问该类的方法。
⑤垃圾回收算法:
⑴标记-清除算法:直接标记清除,但效率不高、空间会产生大量碎片
⑵复制算法:把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。
⑶标记-整理算法:不同于针对新生代的复制算法,针对老年代的特点,创建该算法。主要是把存活对象移到内存的一端。
⑷分代回收:根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。
⑸新生代:每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。
⑥垃圾回收器:收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。
⑴Parallel Scavenge
收集器:这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。
⑵Serial Old
收集器:收集器的老年代版本,单线程,使用标记-整理算法。
⑶Parallel Old
收集器:Parallel Old
是 Parallel Scavenge
收集器的老年代版本。多线程,使用标记-整理算法。
⑷CMS
收集器:CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现。(缺点:对 CPU 资源敏感、无法收集浮动垃圾、标记-清除算法带来的空间碎片)
CMS
收集器运作步骤:初始标记(CMS initial mark):标记 GC Roots
能直接关联到的对象/并发标记(CMS concurrent mark):进行 GC Roots Tracing/重新标记(CMS remark):修正并发标记期间的变动部分/并发清除(CMS concurrent sweep)
⑸G1
收集器:面向服务端的垃圾回收器。
优点:并行与并发、分代收集、空间整合、可预测停顿。
G1收集器运作步骤::初始标记、并发标记、最终标记、筛选回收
⑦新生代 GC(Minor GC):发生在新生代的垃圾回收动作,频繁,速度快。
⑧老年代 GC(Major GC / Full GC):发生在老年代的垃圾回收动作,出现了 Major GC
经常会伴随至少一次 Minor GC
(非绝对)。Major GC
的速度一般会比 Minor GC
慢十倍以上。
什么是DVM,和JVM有什么不同?
DVM
就是 Dalvik Virtual Machine
,是安卓中使用的虚拟机,所有安卓程序都运行在安卓系统进程里,每个进程对应着一个Dalvik
虚拟机实例。DVM
和JVM
都提供了对象生命周期管理、堆栈管理、线程管理、安全和异常管理以及垃圾回收等重要功能,各自拥有一套完整的指令系统。
两种虚拟机的不同:
①JAVA
虚拟机运行的是JAVA字节码,Dalvik
虚拟机运行的是Dalvik字节码
JAVA程序经过编译,生成JAVA字节码保存在class
文件中,JVM
通过解码class
文件中的内容来运行程序。
DVM程序运行的是Dalvik字节码,所有的Dalvik字节码由JAVA字节码转换而来,并被打包到一个DEXDalvik Executable
可执行文件中,DVM
通过解释DEX文件来执行这些字节码。
②Dalvik可执行文件体积更小
SDK中有个dx工具负责将JAVA字节码转换为Dalvik字节码,将所有java
文件中的常量池合并为一个常量池,使得相同的字符串和常量只在DEX
文件中出现一次。
③JVM基于栈,DVM基于寄存器
JAVA虚拟机基于栈结构,程序在运行时虚拟机需要频繁的从栈上读取写入数据,这个过程需要更多的指令分派与内存访问次数,会耗费很多CPU
时间。
Dalvik虚拟机基于寄存器架构,数据的访问通过寄存器间直接传递,这样的访问方式比基于栈方式要快很多。
什么是ART虚拟机,和JVM/DVM有什么不同?
①JIT
会在运行时分析应用程序的代码,识别哪些方法可以归类为热方法,这些方法会被JIT
编译器编译成对应的汇编代码,然后存储到代码缓存中。以后调用这些方法时就不用解释执行了,可以直接使用代码缓存中已编译好的汇编代码。这能显著提升应用程序的执行效率。
②Dalvik虚拟机执行的是dex字节码,ART虚拟机执行的是本地机器码:
③Dalvik执行的是dex字节码,依靠JIT
编译器去解释执行。运行时动态地将执行频率高的dex字节码翻译成本地机器码,然后再执行。但是将dex字节码翻译成本地机器码是发生在应用程序的运行过程中,但应用程序每次重新运行时,都要重新做这个翻译工作。所以即使采用了JIT
,Dalvik虚拟机的总体性能还是不如直接执行本地机器码的ART虚拟机。
④安卓运行时从Dalvik虚拟机替换成ART虚拟机, 应用程序仍然是一个包含dex字节码的apk文件。ART应用安装的时候把dex中的字节码将被编译成本地机器码,之后每次打开应用,执行的都是本地机器码。去除了运行时的解释执行,效率更高,启动更快。
ART优点:
①系统性能显著提升
②应用启动更快、运行更快、体验更流畅、触感反馈更及时
③续航能力提升
④支持更低的硬件
ART缺点:
①更大的存储空间占用,可能增加10%-20%
②更长的应用安装时间
总的来说ART就是“空间换时间”
反射机制&动态代理
反射的定义
①反射是动态语言的关键,反射允许程序在执行期间借助Reflection API
取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。。
②在运行状态中通过反射机制做到:
⑴对于任意一个类,都能够知道这个类的所有属性和方法;
⑵对于任意一个对象,都能够调用它的任意一个方法和属性;
这种动态获取的信息以及动态调用对象的方法的功能称为java
语言的反射机制。
反射的作用
动态(运行时)获取类的完整结构信息 & 调用对象的方法
反射的优点
灵活性高。因为反射属于动态编译,即只有到运行时才动态创建 &获取对象实例。
编译方式说明
①静态编译:在编译时确定类型 & 绑定对象。如常见的使用new
关键字创建对象
②动态编译:在运行时确定类型 & 绑定对象。动态编译体现了Java
的灵活性、多态特性 & 降低类之间的藕合性
反射的缺点
执行效率低,因为反射的操作 主要通过JVM执行,所以时间成本会 高于 直接执行相同操作
①因为接口的通用性,Java的invoke方法是传object和object[]数组的。基本类型参数需要装箱和拆箱,产生大量额外的对象和内存开销,频繁促发GC。
②编译器难以对动态调用的代码提前做优化,比如方法内联。
③反射需要按名检索类和方法,有一定的时间开销。
反射的使用
Java
反射机制的实现主要通过操作java.lang.Class
类,java.lang.Class
类是反射机制的基础。还需要依靠:Constructor
类、Field
类、Method
类,分别作用于类的各个组成部分。
反射使用的步骤
①获取目标类型的Class对象
方式1:Object.getClass() -> Class<?> classType = Boolean.getClass();
方式2:T.class 语法 -> Class<?> classType = Boolean.class
方式3:Class.forName -> Class<?> classType = Class.forName("java.lang.Boolean"
方式4:TYPE语法 -> Class<?> classType = Boolean.TYPE;
②通过Class 对象分别获取Constructor类对象、Method类对象 & Field 类对象
<-- 1. 获取类的构造函数(传入构造函数的参数类型)->>
方式1:获取指定的构造函数 (公共 / 继承)
Constructor getConstructor(Class<?>... parameterTypes)
方式2:获取所有的构造函数(公共 / 继承)
Constructor<?>[] getConstructors();
方式3:获取指定的构造函数 ( 不包括继承)
Constructor getDeclaredConstructor(Class<?>... parameterTypes)
方式4:获取所有的构造函数( 不包括继承)
Constructor<?>[] getDeclaredConstructors();
<-- 2. 获取类的属性(传入属性名) -->
方式1:获取指定的属性(公共 / 继承)
Field getField(String name) ;
方式2:获取所有的属性(公共 / 继承)
Field[] getFields() ;
方式3: 获取指定的所有属性 (不包括继承)
Field getDeclaredField(String name) ;
方式4: 获取所有的所有属性 (不包括继承)
Field[] getDeclaredFields() ;
<-- 3. 获取类的方法(传入方法名 & 参数类型)-->
方式1:获取指定的方法(公共 / 继承)
Method getMethod(String name, Class<?>... parameterTypes) ;
方式2:获取所有的方法(公共 / 继承)
Method[] getMethods() ;
方式3:获取指定的方法 (不包括继承)
Method getDeclaredMethod(String name, Class<?>... parameterTypes) ;
方式4:获取所有的方法(不包括继承)
Method[] getDeclaredMethods() ;
特别注意:
⑴不带"Declared"的方法支持取出包括继承、公有(Public
)& 不包括私有(Private
)的构造函数
⑵带"Declared"的方法是支持取出包括公共(Public
)、保护(Protected)
、默认(包)访问和私有(Private
)的构造方法,但不包括继承的构造函数
③通过Constructor类对象、Method类对象 & Field类对象分别获取类的构造函数、方法&属性的具体信息,并进行后续操作
Java反射提供的功能
①在运行时判断任意一个对象所属的类
②在运行时构造任意一个类的对象
③在运行时判断任意一个类具有的成员变量和方法
④在运行时调用任意一个对象的成员变量和方法
访问权限限制
反射机制的默认行为受限于Java
的访问控制,如无法访问私有(private
)的方法、字段。若强制读取,将抛出异常。
解决权限限制方案
①脱离Java
程序中安全管理器的控制、屏蔽Java
语言的访问检查,从而脱离访问控制。
②实现手段:使用Field
类、Method
类 & Constructor
类对象的setAccessible()
void setAccessible(boolean flag) 的作用:为反射对象设置可访问标志
规则:flag = true时 ,表示已屏蔽Java
语言的访问检查,使得可以访问 & 修改对象的私有属性
反射的运用---代理模式
①定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
②代理模式中的主要角色:
⑴抽象角色(Subject):通过接口或抽象类声明真实主题和代理对象实现的业务方法。
⑵真实角色(Real Subject):实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
⑶代理(Proxy):提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
⑷客户(Client) : 使用代理角色来进行一些操作。
③代理模式的优点:
⑴代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用。
⑵代理对象可以扩展目标对象的功能。
⑶代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性。
④静态代理(静态代理其实就是最基础、最标准的代理模式实现方案)的缺点:
⑴冗余,由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。
⑵系统设计中类的数量增加,变得难以维护。
⑤动态代理:具备代理模式的优点的同时,巧妙的解决了静态代理代码冗余,难维护的缺点。
⑥动态代理应用场景:
⑴基于静态代理应用场景下,需要代理对象数量较多的情况下使用动态代理
⑵AOP 领域
定义:即 Aspect Oriented Programming = 面向切面编程,是OOP的延续、函数式编程的一种衍生范型
作用:通过预编译方式和运行期动态代理实现程序功能的统一维护。
优点:降低业务逻辑各部分之间的耦合度 、 提高程序的可重用性 & 提高了开发的效率
具体应用场景:日志记录、性能统计、安全控制、异常处理等
⑦动态代理使用步骤:
⑴声明调用处理器类
⑵声明目标对象类的抽象接口
⑶声明目标对象类
⑷通过动态代理对象,调用目标对象的方法
⑧总结:
转载自:https://juejin.cn/post/6993218160862167070