IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 18【枚举、动态代理、设计模式】 -> 正文阅读

[Java知识库]18【枚举、动态代理、设计模式】

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;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
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;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
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;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public enum Season {
    SPRING,SUMMER,AUTUMN,WINTER
}

Tips:定义枚举类的关键字是enum,而不是Enum,在Java中所有关键字都是小写的!

其中SPRINGSUMMERAUTUMNWINTER都是枚举项它们都是本类的实例,本类一共就只有四个实例对象。并且只能通过这四个关键字获取Season类的示例对象,不能使用new来创建枚举类的对象

package com.dfbz.demo02;

import org.junit.Test;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {

    @Test
    public void test1() {
        Season spring = Season.SPRING;
        Season spring2 = Season.SPRING;

        System.out.println(spring == spring2);          // true

        Season autumn = Season.AUTUMN;
        System.out.println(spring == autumn);           // false
    }
}

1.2.3 枚举与switch

使用枚举,能让我们的代码可读性更强。

package com.dfbz.demo02;

import org.junit.Test;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
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;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
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;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
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;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public enum Direction {

    // 在枚举常量后面还有其他成员时,分号是必须的。枚举常量必须在枚举类中所有成员的上方声明
    FRONT,BEHIND,LEFT,RIGHT;

    // 枚举类的构造方法都是private修饰的,可写可不写
    Direction(){
        System.out.println("Direction创建了...");
    }
}

Tips:

  • 1:当枚举项后面有其他成员(构造方法、成员变量、成员方法)时,最后一个枚举项必须加分号;
  • 2:所有的枚举类的构造方法都是私有的(private关键字可加可不加)

测试类:

package com.dfbz.demo03;

import org.junit.Test;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    @Test
    public void test1() {
        Direction direction=Direction.BEHIND;
    }
}

运行结果:

在这里插入图片描述

Tips:一旦创建了枚举实例,便会初始化里面的所有枚举项;创建枚举项就等同于调用本类的无参构造器,所以FRONT、BEHIND、LEFT、RIGHT四个枚举项等同于调用了四次无参构造器

2)枚举的有参构造方法

枚举项就是枚举类的实例,在创建定义枚举项时其实就是创建枚举类的实例,因此在定义枚举项就要传递实际的参数

  • 定义枚举项:
package com.dfbz.demo05;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
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;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
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;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public abstract class AbstractSeason {
    
    public abstract void fund();
}
  • 实例化这个抽象类的时候,我们必须抽象其所有的抽象方法:
package com.dfbz.demo06;

import org.junit.Test;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    @Test
    public void test1() {
        // 可以把abstractSeason看做是一个枚举项,在定义枚举项时必须重写枚举类的所有抽象方法
        AbstractSeason abstractSeason = new AbstractSeason() {
            @Override
            public void fund() {
                System.out.println("重写了这个抽象类的所有抽象方法");
            }
        };
    }
}

当枚举类中含有抽象方法的时候,定义枚举项时,必须重写该枚举类中所有的抽象方法,像下面这种就是一种错误的定义:

package com.dfbz.demo06;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public enum Season {

    SPRING;         // 编译报错,定义枚举项时必须重写枚举类包含的所有抽象方法

    public abstract void fun();
}
  • 正确写法:
package com.dfbz.demo06;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public enum Season {

    SPRING(){
        @Override
        public void fun() {
            System.out.println("我是春天~");
        }
    };        

    public abstract void fun();
}
  • 测试类:
package com.dfbz.demo06;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
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;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public enum  Season {
    SPRING,SUMMER,AUTUMN,WINTER;
}
  • 测试类:
package com.dfbz.demo07;

import org.junit.Test;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {


    Season spring = Season.SPRING;              // ordinal:0
    Season summer = Season.SUMMER;              // ordinal:1
    Season autumn = Season.AUTUMN;              // ordinal:2
    Season winter = Season.WINTER;              // ordinal:3

    @Test
    public void test1() {
        System.out.println(spring.compareTo(spring));           // 0-0=0
        System.out.println(spring.compareTo(summer));           // 0-1=-1
        System.out.println(winter.compareTo(summer));           // 3-1=2
        System.out.println(winter.compareTo(spring));           // 3-0=3
    }

    @Test
    public void test2() {

        Season spring2 = Season.SPRING;

        System.out.println(spring == spring2);                  // true
        System.out.println(spring.equals(spring2));             // 枚举对象的equals方法比较的是内存地址值(返回true)
    }

    @Test
    public void test3() {
        Class<Season> clazz = spring.getDeclaringClass();
        Class<? extends Season> clazz2 = spring.getClass();

        System.out.println(clazz == clazz2);            // true
    }

    @Test
    public void test4() {

        int hashCode = spring.hashCode();
        System.out.println(hashCode);           // 2027961269

        String name = spring.name();
        System.out.println(name);               // SPRING

        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);         // 0
        System.out.println(ordinal_summer);         // 1
        System.out.println(ordinal_autumn);         // 2
        System.out.println(ordinal_winter);         // 3
    }

    @Test
    public void test5() {

        Season s = Season.valueOf("SPRING");        // 通过枚举项的名称获取枚举项

        System.out.println(s == spring);            // true(返回的还是同一个枚举项)
    }
}

1.4.3 枚举的两个抽象方法

每个枚举类都有两个静态方法,而且这两个方法不是父类中的方法。这又是枚举类特殊的地方;

  • static T[] values():返回本类所有枚举项;

  • static T valueOf(String name):通过枚举项的名字返回枚举项;


  • 测试类:
@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);            // true
}

二、类加载器

2.1 类加载时机

我们知道,所有的代码都是运行在内存中的,我们必须把类加载到内存中才能运行;在Java中,所有的Java类都是通过类加载器加载到内存进行执行的;

  • 一个类何时被加载?
    • 1)创建该类对象时,首先会将内加载到内存(如果该类存在父类,那么首先加载父类到内存,创建父类的对象(super))
    • 2)访问该类的静态成员时,会将类加载到内容
    • 3)class.forName(“类的全包名”)
package com.dfbz.demo01;

import org.junit.Test;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {

    @Test
    public void test1() throws ClassNotFoundException {
//        new B();          // 先加载A再加载B

//        Integer num = B.num;        // 先加载A再加载B

        Class<?> clazz = Class.forName("com.dfbz.demo01.B");        // 先加载A再加载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;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02 {
    @Test
    public void test1(){
        // Bootstrap类加载器是获取不到的,为null
        System.out.println("Bootstrap ClassLoader: "+ String.class.getClassLoader());

        // jre\lib\ext\access-bridge-64.jar
        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 {
                    
                    // 如果没有父加载器则使用BootstrapClassLoader加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // 如果依旧没有加载,则调用findClass方法进行加载
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                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中类加载的双亲委派机制,即加载类时交给父加载器加载,如果不能加载,再交给子加载器加载;这样做有何好处呢?

  • 1)避免类的重复加载:当父类加载器已经加载了该类时,就没有必要子 ClassLoader 再加载一次。

  • 2)安全问题:有了双亲委派机制,当有人想要替换系统级别的类时,在双亲委派机制下,系统类都已经被BootstrapClassLoader加载过了,而当一个类需要被加载时必定会轮到BootstrapClassLoader来加载(只是是否能加载的问题);


在指定的系统包下建立指定的类(由BootstrapClassLoader、ExtClassLoader加载的系统类):

在这里插入图片描述

  • Object:
package java.lang;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Object {
    static {
        System.out.println("自定义的Object类被加载了....");
    }
}
  • AccessBridge:
package com.sun.java.accessibility;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class AccessBridge {
    static {
        System.out.println("自定义的AccessBridge类被加载了.....");
    }
}

  • 测试类:
package com.dfbz.demo01;

import org.junit.Test;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
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文件:

  • Show.java:
package com.dfbz.demo01;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
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;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo04 {
    @Test
    public void test() throws Exception{
        File file = new File("D:\\000");

        // file   --->  URI
        URI uri = file.toURI();

        // URI    --->  URL
        URL url = uri.toURL();

        // 根据URL构建一个类加载器
        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;

// 1. 继承 ClassLoader
// 2. 覆盖 findClass方法
public class MyClassLoader extends ClassLoader {

    // 被加载类所在的目录
    private String dir;

    public MyClassLoader(String dir) {          // 默认父类加载器就是系统类加载器 AppClassLoader
        this.dir = dir;
    }

    public MyClassLoader(ClassLoader parent, String dir) {
        super(parent);
        this.dir = dir;
    }

    /**
     *
     * @param name
     * @return 重写findClass方法
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 把类名转换为目录  --->  D:/000/com/dfbz/demo01/Show.class
            String file = dir + "/" + name.replace(".", "/") + ".class";

            // 从文件中读取这个Class文件
            InputStream in = new FileInputStream(file);

            // 构建一个内存输出流(将读取到的Class文件写在内存中)
            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();

            /*
            defineClass: 根据类的全包名和内存中的数据流来加载一个类
             - 参数1: 需要加载类的全包名
             - 参数2: 已经加载到内存中的数据流
             - 参数3: 从指定的数据下表开始读取
             - 参数4: 读取到什么位置
             */
            return defineClass(name, data, 0, data.length);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
  • 测试类:
package com.dfbz.demo02;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
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;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
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);                // true

        System.out.println(clazz.getClassLoader());         // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(clazz2.getClassLoader());        // sun.misc.Launcher$AppClassLoader@18b4aac2
    }
}

运行结果:

在这里插入图片描述

根据我们之前学习双亲委派机制,上面两个类加载器在加载Show类时,都会判断有没有加载这个类,没有加载则使用父加载器加载,MyClassLoader的父加载器是AppClassLoader,而AppClassLoader正好可以加载这个类;所以其实这两次的加载都是由AppClassLoader来加载的,而AppClassLoader在加载时会判断是否已经加载过,加载过了则不加载;因此Show类只会加载一次;


但是需要注意的是,双亲委派机制的逻辑是写在ClassLoader类的loadClass方法中的,通过一系列逻辑判断最终执行findClass方法来加载类;如果我们加载类直接使用findClass方法呢?那就相当于避开了双亲委派;(当然也可以重写loadClass方法,重新自定义loadClass规则)

  • 测试类:
package com.dfbz.demo02;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02 {
    public static void main(String[] args) throws Exception{
        MyClassLoader classLoader = new MyClassLoader("d:/000");
        MyClassLoader classLoader2 = new MyClassLoader("d:/000");

        // 直接使用findClass方法去加载类,每一次调用findClass都相当于是加载一次新的类
        Class<?> clazz = classLoader.findClass("com.dfbz.demo01.Show");
        Class<?> clazz2 = classLoader2.findClass("com.dfbz.demo01.Show");

        System.out.println(clazz == clazz2);                // false

        System.out.println(clazz.getClassLoader());         // com.dfbz.demo02.MyClassLoader@135fbaa4
        System.out.println(clazz2.getClassLoader());        // com.dfbz.demo02.MyClassLoader@330bedb4
    }
}

运行结果:

在这里插入图片描述

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:

  • Proxy类
static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 作用:生成一个代理对象
loader和目标对象一类的类加载器
interfaces目标对象所有实现的接口
h是一个接口,传一个匿名内部类做为实现类,并且重写其中的方法来实现代理的功能
返回值返回代理对象
  • InvocationHandler接口
Object invoke(Object proxy, Method method, Object[] args) 作用:这个接口中的方法会调用多次,每个方法都会调用一次,用来实现代理方法的功能
proxy代表生成的代理对象,不建议在方法中直接调用,不然会出现递归调用。
method代理的方法对象
args调用方法时传递的参数数组
返回返回当前这个方法调用的返回值

3.3 动态代理模式的案例

3.3.1 动态代理模式的开发步骤

  1. 首先需要存在抽象角色,定义所有的功能

  2. 真实对象实现抽象角色所有的功能

  3. 通过Proxy类,创建代理对象,调用代理方法

  4. 在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) {
 /*
        最终返回一个代理对象,此代理对象对目标对象的所有方法都进行了代理
        最终执行代理对象的任何方法都会执行InvocationHandler中的invoke方法
         */
        UserService service = (UserService) Proxy.newProxyInstance(
                /*
               传入目标对象的类加载器
                 */
                UserServiceImpl.class.getClassLoader(),
                /*
             传入目标对象的所有实现接口的字节码对象
             代理对象就是根据此接口字节码对象来指定代理方法,实现动态方法代理
               要不然目标对象增加一个方法,动态代理需要手动添加一个代理方法
                 */
//                new Class[]{UserService.class},
                UserServiceImpl.class.getInterfaces(),
                new InvocationHandler() {
                    /**
                     *
                     * @param proxy: 代理对象
                     * @param method: 目标对象的方法
                     * @param args: 目标对象方法的参数
                     * @return
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("增强的代码");
                        /*
                        因为method.invoke执行的是目标对象的具体方法,因此必须传入目标对象
                         */
                        Object invoke = method.invoke(new UserServiceImpl());
                        System.out.println("增强的代码");
                        return invoke;
                    }
                }
        );
        // 代理对象执行save方法,最终会执行InvocationHandler中的invoke方法
        service.save();
    }
}

小结:JDK提供的动态代理是基于接口的,把目标对象所实现的所有接口的字节码对象传入方法中,JDK能在内存中动态的帮我们创建一个对象,该对象实现的目标对象实现的所有方法。即保证目标对象的所有方法代理对象都能代理到


上一篇17【测试单元、反射、注解】


恭喜你已经把JavaSE全部掌握啦!奖励一下自己吧~!

目录【JavaSE零基础系列教程目录】


  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-03-12 17:18:07  更:2022-03-12 17:20:09 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 10:48:32-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码