18【枚举、类加载器、动态代理】
一、枚举(Enmu)
1.1 枚举概述
枚举(enum),全称enumeration ,是JDK 1.5 中引入的新特性。Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一个年的 12 个月份,一个星期的 7 天,方向有东南西北等。
在JDK1.5 之前,我们定义常量都是: public static fianl 。有了枚举之后,可以把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。
1.2 定义枚举类型
1.2.1 静态常量案例
我们使用静态常量来设置一个季节类:
package com.dfbz.demo;
public class Season {
public static final Integer SPRING = 1;
public static final Integer SUMMER = 2;
public static final Integer AUTUMN = 3;
public static final Integer WINTER = 4;
}
1/2/3/4分别代表不同的含义
测试类:
package com.dfbz.demo01;
import org.junit.Test;
public class Demo01 {
@Test
public void test1() {
method(Season.SPRING);
method(Season.WINTER);
}
public void method(Integer season) {
switch (season) {
case 1:
System.out.println("Spring!");
break;
case 2:
System.out.println("Summer!");
break;
case 3:
System.out.println("Autumn!");
break;
case 4:
System.out.println("Winter!");
break;
}
}
}
1.2.2 枚举案例
Java 枚举类使用enum 关键字来定义,各个常量使用逗号来分割。
package com.dfbz.demo02;
public enum Season {
SPRING,SUMMER,AUTUMN,WINTER
}
Tips:定义枚举类的关键字是enum,而不是Enum,在Java中所有关键字都是小写的!
其中SPRING 、SUMMER 、AUTUMN 、WINTER 都是枚举项,它们都是本类的实例,本类一共就只有四个实例对象。并且只能通过这四个关键字获取Season类的示例对象,不能使用new来创建枚举类的对象
package com.dfbz.demo02;
import org.junit.Test;
public class Demo01 {
@Test
public void test1() {
Season spring = Season.SPRING;
Season spring2 = Season.SPRING;
System.out.println(spring == spring2);
Season autumn = Season.AUTUMN;
System.out.println(spring == autumn);
}
}
1.2.3 枚举与switch
使用枚举,能让我们的代码可读性更强。
package com.dfbz.demo02;
import org.junit.Test;
public class Demo01 {
@Test
public void test1() {
Season season = Season.AUTUMN;
switch (season){
case SPRING:
System.out.println("春天~");
break;
case SUMMER:
System.out.println("夏天!");
break;
case AUTUMN:
System.out.println("秋天@");
break;
case WINTER:
System.out.println("冬天&");
break;
default:
System.out.println("错误的季节");
}
}
}
1.3 枚举的用法
1.3.1 枚举类的成员
枚举类和正常的类一样,可以有实例变量,实例方法,静态方法等等
package com.dfbz.demo03;
public enum Color {
RED, GREEN, BLUE;
public String aaa = "AAA";
public static String bbb = "BBB";
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void method() {
System.out.println("enum hello~");
}
}
Tips:当枚举项后面有其他成员(构造方法、成员变量、成员方法)时,最后一个枚举项必须加分号;
package com.dfbz.demo03;
import com.dfbz.demo04.Direction;
import org.junit.Test;
public class Demo01 {
@Test
public void test1(){
System.out.println(Color.bbb);
Color.method();
Color red = Color.RED;
red.setName("红色");
System.out.println(red.getName());
}
}
1.3.2 枚举类的构造方法
1)枚举的无参构造方法
枚举类也可以有构造方法,构造方法默认都是private修饰,而且只能是private。因为枚举类的实例不能让外界来创建!
默认情况下,所有的枚举项的创建都是调用枚举类的无参构造方法,
package com.dfbz.demo03;
public enum Direction {
FRONT,BEHIND,LEFT,RIGHT;
Direction(){
System.out.println("Direction创建了...");
}
}
Tips:
- 1:当枚举项后面有其他成员(构造方法、成员变量、成员方法)时,最后一个枚举项必须加分号;
- 2:所有的枚举类的构造方法都是私有的(private关键字可加可不加)
测试类:
package com.dfbz.demo03;
import org.junit.Test;
public class Demo01 {
@Test
public void test1() {
Direction direction=Direction.BEHIND;
}
}
运行结果:
Tips:一旦创建了枚举实例,便会初始化里面的所有枚举项;创建枚举项就等同于调用本类的无参构造器,所以FRONT、BEHIND、LEFT、RIGHT四个枚举项等同于调用了四次无参构造器
2)枚举的有参构造方法
枚举项就是枚举类的实例,在创建定义枚举项时其实就是创建枚举类的实例,因此在定义枚举项就要传递实际的参数
package com.dfbz.demo05;
public enum Week {
MONDAY("星期一",1),
TUESDAY("星期二"),
WEDNESDAY,
THURSDAY("星期四",3),
FRIDAY("星期五",4),
SATURDAY("星期六",5),
SUNDAY("星期六",4);
private String name;
private Integer loveLevel;
Week() {
}
Week(String name) {
this.name = name;
}
Week(String name, Integer loveLevel) {
this.name = name;
this.loveLevel=loveLevel;
}
public void show() {
System.out.println("我是【" + name + "】,我的喜好程度是【" + loveLevel + "】颗星");
}
@Override
public String toString() {
return "Week{" +
"name='" + name + '\'' +
", loveLevel=" + loveLevel +
'}';
}
}
package com.dfbz.demo05;
import org.junit.Test;
public class Demo01 {
@Test
public void test1(){
Week friday = Week.FRIDAY;
friday.show();
System.out.println(friday);
System.out.println("---------------");
Week saturday = Week.SATURDAY;
saturday.show();
System.out.println(saturday);
System.out.println("---------------");
Week tuesday = Week.TUESDAY;
tuesday.show();
System.out.println(tuesday);
System.out.println("---------------");
Week wednesday = Week.WEDNESDAY;
wednesday.show();
System.out.println(wednesday);
System.out.println("---------------");
}
}
1.3.3 枚举中的抽象方法
枚举类中可以包含抽象方法,但是在定义枚举项时必须重写该枚举类中的所有抽象方法;
我们前面说过,每一个枚举项其实都是枚举类的实例对象,因此如果当前枚举类包含抽象方法时,在定义枚举项时就需要重写此枚举类的所有抽象方法,这跟我们以前使用的匿名内部类很相似;
package com.dfbz.demo06;
public abstract class AbstractSeason {
public abstract void fund();
}
- 实例化这个抽象类的时候,我们必须抽象其所有的抽象方法:
package com.dfbz.demo06;
import org.junit.Test;
public class Demo01 {
@Test
public void test1() {
AbstractSeason abstractSeason = new AbstractSeason() {
@Override
public void fund() {
System.out.println("重写了这个抽象类的所有抽象方法");
}
};
}
}
当枚举类中含有抽象方法的时候,定义枚举项时,必须重写该枚举类中所有的抽象方法,像下面这种就是一种错误的定义:
package com.dfbz.demo06;
public enum Season {
SPRING;
public abstract void fun();
}
package com.dfbz.demo06;
public enum Season {
SPRING(){
@Override
public void fun() {
System.out.println("我是春天~");
}
};
public abstract void fun();
}
package com.dfbz.demo06;
public enum Season {
SPRING(){
@Override
public void fun() {
System.out.println("我是春天...");
}
},
SUMMER(){
@Override
public void fun() {
System.out.println("我是夏天~");
}
};
public abstract void fun();
}
1.4 Enum 类
1.4.1 Enum类中的方法
Java中,所有的枚举类都默认继承与java.lang.Enum 类,这说明Enum中的方法所有枚举类都拥有。另外Enum也继承与Object,因此所有的枚举类都拥有与Object类一样的方法;
Enum类中的方法如下:
Tips:枚举类除了不能拥有Object中的clone、finalize方法外,其他方法都能拥有;
Enum类新增(或重写Object)的方法:
int compareTo(E e) :比较两个枚举常量谁大谁小,其实比较的就是枚举常量在枚举类中声明的顺序(ordinal值)boolean equals(Object o) :比较两个枚举常量是否相等;Class<E> getDeclaringClass() :返回此枚举类的Class对象,这与Object中的getClass()类似;int hashCode() :返回枚举常量的hashCode ;String name() :返回枚举常量的名字;int ordinal() :返回枚举常量在枚举类中声明的序号,第一个枚举常量序号为0;String toString() :把枚举常量转换成字符串;static T valueOf(Class enumType, String name) :把字符串转换成枚举常量。
1.4.2 测试方法功能
package com.dfbz.demo07;
public enum Season {
SPRING,SUMMER,AUTUMN,WINTER;
}
package com.dfbz.demo07;
import org.junit.Test;
public class Demo01 {
Season spring = Season.SPRING;
Season summer = Season.SUMMER;
Season autumn = Season.AUTUMN;
Season winter = Season.WINTER;
@Test
public void test1() {
System.out.println(spring.compareTo(spring));
System.out.println(spring.compareTo(summer));
System.out.println(winter.compareTo(summer));
System.out.println(winter.compareTo(spring));
}
@Test
public void test2() {
Season spring2 = Season.SPRING;
System.out.println(spring == spring2);
System.out.println(spring.equals(spring2));
}
@Test
public void test3() {
Class<Season> clazz = spring.getDeclaringClass();
Class<? extends Season> clazz2 = spring.getClass();
System.out.println(clazz == clazz2);
}
@Test
public void test4() {
int hashCode = spring.hashCode();
System.out.println(hashCode);
String name = spring.name();
System.out.println(name);
int ordinal_spring = spring.ordinal();
int ordinal_summer = summer.ordinal();
int ordinal_autumn = autumn.ordinal();
int ordinal_winter = winter.ordinal();
System.out.println(ordinal_spring);
System.out.println(ordinal_summer);
System.out.println(ordinal_autumn);
System.out.println(ordinal_winter);
}
@Test
public void test5() {
Season s = Season.valueOf("SPRING");
System.out.println(s == spring);
}
}
1.4.3 枚举的两个抽象方法
每个枚举类都有两个静态方法,而且这两个方法不是父类中的方法。这又是枚举类特殊的地方;
@Test
public void test6() {
Season[] values = Season.values();
for (Season value : values) {
System.out.println(value);
}
Season s = Season.valueOf("SPRING");
System.out.println(s == spring);
}
二、类加载器
2.1 类加载时机
我们知道,所有的代码都是运行在内存中的,我们必须把类加载到内存中才能运行;在Java中,所有的Java类都是通过类加载器加载到内存进行执行的;
- 一个类何时被加载?
- 1)创建该类对象时,首先会将内加载到内存(如果该类存在父类,那么首先加载父类到内存,创建父类的对象(super))
- 2)访问该类的静态成员时,会将类加载到内容
- 3)class.forName(“类的全包名”)
package com.dfbz.demo01;
import org.junit.Test;
public class Demo01 {
@Test
public void test1() throws ClassNotFoundException {
Class<?> clazz = Class.forName("com.dfbz.demo01.B");
}
}
class A {
public static Integer num = 10;
static {
System.out.println("A loader...");
}
}
class B extends A {
public static Integer num = 20;
static {
System.out.println("B loader...");
}
}
Tips:不管是用什么方法加载,类从始至终只会加载一次;
2.3 类加载器
2.3.1 类加载器的种类
- 启动类加载器Bootstrap ClassLoader: 是嵌在JVM内核中的加载器,该加载器是用C++语言写的,主要负则加载JAVA_HOME/lib下的类库,启动类加载器无法被应用程序直接使用。
- 扩展类加载器Extension ClassLoader: 该加载器器是用JAVA编写,且它的父类加载器是Bootstrap,是由sun.misc.Launcher$ExtClassLoader实现的,主要加载JAVA_HOME/lib/ext目录中的类库。开发者可以这几使用扩展类加载器。
- 系统类加载器App ClassLoader: 系统类加载器,也称为应用程序类加载器,负责加载应用程序classpath目录下的所有jar和class文件(第三方jar)。它的父加载器为Ext ClassLoader。
测试类:
package com.dfbz.demo01;
import com.sun.java.accessibility.AccessBridge;
import org.junit.Test;
public class Demo02 {
@Test
public void test1(){
System.out.println("Bootstrap ClassLoader: "+ String.class.getClassLoader());
System.out.println("ExtClassLoader ClassLoader: "+ AccessBridge.class.getClassLoader());
System.out.println("AppClassLoader ClassLoader: "+ Demo02.class.getClassLoader());
}
}
2.3.2 双亲委派机制
从JDK1.2开始,类的加载过程采用双亲委派机制,它是一种任务委派模式。即把加载类的请求交由父加载器处理,一直到顶层的父加载器(BootstrapClassLoader);如果父加载器能加载则用父加载器加载,否则才用子加载器加载该类;
ClassLoader类加载源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
findClass方法源码:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
可以看到,默认情况下ClassLoader的findClass方法只是抛出了一个异常而已(这个方法是留给我们写的)
从上图我们可以分析,当一个Demo.class这样的文件要被加载时。首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法进行加载。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
2.3.3 双亲委派的好处
我们已经了解了Java中类加载的双亲委派机制,即加载类时交给父加载器加载,如果不能加载,再交给子加载器加载;这样做有何好处呢?
在指定的系统包下建立指定的类(由BootstrapClassLoader、ExtClassLoader加载的系统类):
package java.lang;
public class Object {
static {
System.out.println("自定义的Object类被加载了....");
}
}
package com.sun.java.accessibility;
public class AccessBridge {
static {
System.out.println("自定义的AccessBridge类被加载了.....");
}
}
package com.dfbz.demo01;
import org.junit.Test;
public class Demo03 {
@Test
public void test1(){
Class<Object> clazz = Object.class;
}
}
Tips:根据双亲委派机制,我们自定义的Object、AccessBridge类不可能被加载;
另外,JVM的类加载器对包名的定义也有限制;不允许我们自定义系统包名
在系统包名下创建任意一个类:
@Test
public void test2(){
Class<AA> clazz = AA.class;
}
运行结果:
2.3.4 URLClassLoader类加载器
在 java.net 包中,JDK提供了一个更加易用的类加载器URLClassLoader,它扩展了 ClassLoader,能够从本地或者网络上指定的位置加载类,我们可以使用该类作为自定义的类加载器使用。
URLClassLoader的构造方法:
-
public URLClassLoader(URL[] urls) :指定要加载的类所在的URL地址,父类加载器默认为系统类加载器 -
public URLClassLoader(URL[] urls, ClassLoader parent) :指定要加载的类所在的URL地址,并指定父类加载器。
1)加载本地磁盘上的类:
在指定目录下准备一个Java文件并把它编译成class文件:
package com.dfbz.demo01;
public class Show {
public Show(){
System.out.println("new Show....");
}
}
D:\000\com\dfbz\demo01>javac Show.java
D:\000\com\dfbz\demo01>
package com.dfbz.demo01;
import org.junit.Test;
import java.io.File;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
public class Demo04 {
@Test
public void test() throws Exception{
File file = new File("D:\\000");
URI uri = file.toURI();
URL url = uri.toURL();
URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
System.out.println("父类加载器:" + classLoader.getParent());
Class clazz = classLoader.loadClass("com.dfbz.demo01.Show");
clazz.newInstance();
}
}
运行结果:
2)加载网络上的类:
@Test
public void test2() throws Exception{
URL url = new URL("http://www.baidu.com/class/");
URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
System.out.println("父类加载器:" + classLoader.getParent());
Class clazz = classLoader.loadClass("com.baidu.demo.Show");
clazz.newInstance();
}
Tips:关于加载网络上的类,等我们以后学习了服务器编程再来体验!
2.3.5 自定义类加载器
我们如果需要自定义类加载器,只需要继承ClassLoader,并覆盖掉findClass方法即可。
package com.dfbz.demo02;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader {
private String dir;
public MyClassLoader(String dir) {
this.dir = dir;
}
public MyClassLoader(ClassLoader parent, String dir) {
super(parent);
this.dir = dir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
String file = dir + "/" + name.replace(".", "/") + ".class";
InputStream in = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len ;
while ((len = in.read(buf)) != -1) {
baos.write(buf, 0, len);
}
byte[] data = baos.toByteArray();
in.close();
baos.close();
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
package com.dfbz.demo02;
public class Demo01 {
public static void main(String[] args) throws Exception{
MyClassLoader classLoader = new MyClassLoader("d:/000");
Class<?> clazz = classLoader.loadClass("com.dfbz.demo01.Show");
clazz.newInstance();
}
}
2.3.5 打破双亲委派
我们前面自定义了类加载器,观察下面代码:
package com.dfbz.demo02;
public class Demo01 {
public static void main(String[] args) throws Exception{
MyClassLoader classLoader = new MyClassLoader("d:/000");
MyClassLoader classLoader2 = new MyClassLoader("d:/000");
Class<?> clazz = classLoader.loadClass("com.dfbz.demo01.Show");
Class<?> clazz2 = classLoader2.loadClass("com.dfbz.demo01.Show");
System.out.println(clazz == clazz2);
System.out.println(clazz.getClassLoader());
System.out.println(clazz2.getClassLoader());
}
}
运行结果:
根据我们之前学习双亲委派机制,上面两个类加载器在加载Show类时,都会判断有没有加载这个类,没有加载则使用父加载器加载,MyClassLoader的父加载器是AppClassLoader,而AppClassLoader正好可以加载这个类;所以其实这两次的加载都是由AppClassLoader来加载的,而AppClassLoader在加载时会判断是否已经加载过,加载过了则不加载;因此Show类只会加载一次;
但是需要注意的是,双亲委派机制的逻辑是写在ClassLoader类的loadClass方法中的,通过一系列逻辑判断最终执行findClass方法来加载类;如果我们加载类直接使用findClass方法呢?那就相当于避开了双亲委派;(当然也可以重写loadClass方法,重新自定义loadClass规则)
package com.dfbz.demo02;
public class Demo02 {
public static void main(String[] args) throws Exception{
MyClassLoader classLoader = new MyClassLoader("d:/000");
MyClassLoader classLoader2 = new MyClassLoader("d:/000");
Class<?> clazz = classLoader.findClass("com.dfbz.demo01.Show");
Class<?> clazz2 = classLoader2.findClass("com.dfbz.demo01.Show");
System.out.println(clazz == clazz2);
System.out.println(clazz.getClassLoader());
System.out.println(clazz2.getClassLoader());
}
}
运行结果:
2.2 类的加载过程
2.2.1 类的生命周期
一个Java类从开始到结束整个生命周期会经历7个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。
其中验证、准备、解析三个部分又统称为连接(Linking)。
1)加载
加载过程就是把class字节码文件载入到虚拟机中,至于从哪儿加载,虚拟机设计者并没有限定,你可以从文件、压缩包、网络、数据库等等地方加载class字节码。
- 通过类的全限定名来获取定义此类的二进制字节流
- 将此二进制字节流所代表的静态存储结构转化成方法区的运行时数据结构
- 在内存中生成代表此类的java.lang.Class对象,作为该类访问入口;
2)连接
连接阶段的开始,并不一定等到加载阶段结束。加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹杂在加载阶段之中的动作任然属于连接阶段,加载和连接这两个阶段的开始顺序是固定的。
-
验证:连接阶段的开始,并不一定等到加载阶段结束。加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹杂在加载阶段之中的动作任然属于连接阶段,加载和连接这两个阶段的开始顺序是固定的。 -
准备:准备阶段会为类变量(被static修饰的变量)分配内存并设置类变量的初始值,这些变量所使用的内存都将在方法区中分配。假如有一个变量private static int value = 123; 那么value在准备阶段过后值是0,而不是123;因为这个时候尚未执行任何java方法,而把value赋值为123的动作在初始化阶段才会执行。 但是如果上面的变量被final修饰,变为:private static final int value = 123; 编译时javac会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。 -
解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
Tips:
- 符号引用:符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。个人理解为:在编译的时候每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
- 直接引用:直接引用可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的布局内存有关,同一个符号引用在不同虚拟机示例上翻译出来的直接引用一般不同。如果有了直接引用,那引用的目标必定已经在内存中存在。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型方法句柄和调用点限定符7类符号引用进行。
3)初始化
类初始化是类加载过程的最后一步,这一步会真正开始执行类中定义的Java程序代码(或者说字节码)。 在准备阶段,变量已经被赋过一次系统要求的初始值,在初始化阶段,变量会再次赋值为程序员设置的值。比如变量:private static int value = 123; 那么value在准备阶段过后值是0,初始化阶段后值是123。
三、动态代理模式
代理模式有两种:动态代理,静态代理。
现实生活中的代理
调用者 | 代理角色 | 真实角色 |
---|
买电脑的人 | 电脑代理商 | 生产电脑的厂商 | 买火车票 | 黄牛 | 火车站,12306 | 租房子 | 房子中介 | 房东 |
我们发现代理角色 和真正角色的目标 都具有相同的功能(卖电脑/卖票 ),代理商可以在中间赚取差价(修改原有的功能)。
3.1 代理模式的作用
用于对真实角色的功能进行修改,代理真实角色实现它的功能。
代理的目的就是为了增强原有的功能;
3.2 静态代理
3.2.1 定义约定接口:
package com.dfbz.service;
public interface UserService {
void save();
void delete();
void update();
}
3.2.2 定义目标对象(房东):
package com.dfbz.service;
public class UserServiceImpl implements UserService{
@Override
public void save() {
System.out.println("保存对象");
}
@Override
public void delete() {
System.out.println("删除对象");
}
@Override
public void update(){
System.out.println("修改对象");
}
}
3.2.3 定义代理对象(中介):
package com.dfbz.service;
public class UserServiceProxy implements UserService{
private UserService userService;
public UserServiceProxy(UserService userService){
this.userService=userService;
}
public void save(){
System.out.println("记录日志");
userService.save();
System.out.println("记录日志");
}
@Override
public void delete() {
System.out.println("记录日志");
userService.delete();
System.out.println("记录日志");
}
@Override
public void update() {
System.out.println("记录日志");
userService.update();
System.out.println("记录日志");
}
}
3.2.3 测试类:
package com.dfbz;
import com.dfbz.demo01.service.UserServiceImpl;
import com.dfbz.demo01.service.UserServiceProxy;
public class Demo01 {
public static void main(String[] args) {
UserServiceImpl userService=new UserServiceImpl();
UserServiceProxy proxy=new UserServiceProxy(userService);
proxy.save();
proxy.delete();
}
}
我们发现使用静态代理的方式可以对一个类进行增强,但是代码耦合度太高了!如果目标对象有其他方法需要增强(代理)呢?那么我们必须要修改代理类,也就是说代理类对目标对象进行代理不是动态的;
3.3 动态代理
3.3.1 什么是动态代理
动态代理的主要功能是在不修改源码的情况下对原有对象进行动态的代理,对原有对象的方法进行增强最终返回一个代理对象,此代理对象包含对象原有的功能和自己另加的功能
3.3.2 动态代理类相应的API:
static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 作用:生成一个代理对象 | |
---|
loader | 和目标对象一类的类加载器 | interfaces | 目标对象所有实现的接口 | h | 是一个接口,传一个匿名内部类做为实现类,并且重写其中的方法来实现代理的功能 | 返回值 | 返回代理对象 |
Object invoke(Object proxy, Method method, Object[] args) 作用:这个接口中的方法会调用多次,每个方法都会调用一次,用来实现代理方法的功能 | |
---|
proxy | 代表生成的代理对象,不建议在方法中直接调用,不然会出现递归调用。 | method | 代理的方法对象 | args | 调用方法时传递的参数数组 | 返回 | 返回当前这个方法调用的返回值 |
3.3 动态代理模式的案例
3.3.1 动态代理模式的开发步骤
-
首先需要存在抽象角色,定义所有的功能 -
真实对象实现抽象角色所有的功能 -
通过Proxy类,创建代理对象,调用代理方法 -
在InvocationHandler的invoke对代理的方法有选择的修改或不修改。
3.3.2 测试类:
package com.dfbz;
import com.dfbz.service.UserService;
import com.dfbz.service.UserServiceImpl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Demo02 {
public static void main(String[] args) {
UserService service = (UserService) Proxy.newProxyInstance(
UserServiceImpl.class.getClassLoader(),
UserServiceImpl.class.getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("增强的代码");
Object invoke = method.invoke(new UserServiceImpl());
System.out.println("增强的代码");
return invoke;
}
}
);
service.save();
}
}
小结:JDK提供的动态代理是基于接口的,把目标对象所实现的所有接口的字节码对象传入方法中,JDK能在内存中动态的帮我们创建一个对象,该对象实现的目标对象实现的所有方法。即保证目标对象的所有方法代理对象都能代理到
上一篇:17【测试单元、反射、注解】
恭喜你已经把JavaSE全部掌握啦!奖励一下自己吧~!
目录:【JavaSE零基础系列教程目录】
|