A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

© Summer丶time 初级黑马   /  2019-3-21 15:07  /  573 人查看  /  0 人回复  /   0 人收藏 转载请遵从CC协议 禁止商业使用本文

### 1.类的加载和初始化

#### 1.1 JAVA虚拟机和类

1. Java程序的执行系统会首先启动一个JAVA虚拟机进程,再在这个进程中运行Java程序,无论这个程序在运行过程中启动多少个线程,都只会执行在这个虚拟机进程中,而当程序执行终止时储存在内除中的变量也随之清除,运行两次程序即使时同一个类的变量也不会相互影响。

2. 类的加载过程由JAVA虚拟机负责主要分为:加载、链接、初始化三个过程

#### 1.2类的加载机制

1. 加载阶段就是JAVA虚拟机启动一个类的加载器``` ClassLoader```,将.class文件加载进内存并生成一个 ```  java.lang.class``` 实例化对象

2. 链接阶段又分为:验证、准备和解析三个阶段:
   - 验证阶段就是```JVM``` 为了确保class文件的字节流的信息流不会产生危害虚拟机自身安全的一种验证机制;
   - 准备阶段就是为类变量分配内存空间和初始化的过程;
   - 解析阶段就是将主要将常量池中的符号引用替换为直接引用的过程。

#### 1.3 类的初始化时机

1. 当Java程序首次通过下面6种方式来使用某个类或接口时,系统就会初始化该类或接口:
   - 创建类的实例:new关键字创建对象、反射、反序列化;
   - 调用类的静态方法(类方法):static修饰;
   - 访问类或接口的类变量并为其赋值;
   - 通过反射方式创建类的```java.lang.class``` 对象比如```Class.forName()```;
   - 初始化某个类会导致他的父类被初始化;
   - 直接使用命令```java.exe```来运行某个主类。
2. 以下情况不会导致类的初始化
   - 当类变量被```final```关键字修饰时,这个类常类被调用时不会导致类被初始化,因为在JAVA中这样的变量被视为"宏变量",在程序的编译时期就已经在这个常类出现的地方替换为这个变量的值;
   - 使用```ClassLoader```类的```loaderclass()```方法获取字节码对象;
3. ```ClassLoader```类的```loaderClass()```和```java.lang.class```的```forName()```方法
   - ```java.lang.class```的```forName()```方法实际调用的是```class.forName(className,true,classLoader)```方法而第二个参数true会导致类的初始化从而倒是```static```代码块和```static```参数被初始化;
   - ```classLoader.loaderClass()```方法实际调用的是```classLoader.loaderClass(className,false)```方法第二个参数代表是否链接,false表示不执行链接而不执行该阶段类就不会导致类变量的初始化。

#### 1.4类的加载器

1. 类的加载器主要分为四类(也有不将自定义加载器算入的三类分法,这里采用四类)

   - 启动类(Bootstrap)加载器:这个加载器位于所有加载器的最顶端,主要加载的是虚拟机运行所需要的的类,这个加载器由C++语言写成,其他加载器不能识别到它如果其他加载器调用```.getParent()```方法获取到的是这个加载器会返回null;
   - 拓展(Extension)类加载器;
   - 系统(System)类加载器;
   - 自定义加载器:我们可以通过继承```java.lang.classLoader```来写一个自己的加载器。

2. 类的加载器运行模式

   - JAVA虚拟机的运行中为了取识别```java.lang.class```对象会加入类的全限定名和加载它的类加载器标识来取别不同的class对象,所有在虚拟机中同一个class文件被不同的类加载器加载内存中存在两个不同的```java.lang.class```对象;
   - 类加载器的运行主要有三个特点:
     - 全盘负责:当一个类被一个加载器加载时它的相关依赖都会被这个加载器加载;
     - 缓存:当一个类需要加载时先会在缓存查找这个类被加载的的二进制缓存,没有找到才会去加载这个类,在进行加载后也会将它缓存到内存中;
     - 双亲委派
   - JAVA虚拟器允许类加载预先加载某些类

3.  双亲委派

   - 双亲委派时为了保证```.class```加载后的```java.lang.class```对象的唯一性和部分安全性的考虑;

   - 当一个类加载器被调用需要加载某个```.class```文件到内存的时候,它首先不会自己取加载这个文件,它会先调用自己的父类加载器加载这个字节码文件,当父类返回为null时(父加载器为启动类加载器或自己就是启动类加载器)时它会调用启动类加载器去加载这个文件,而当父加载器接到这个请求时它也会重复这个过程直到将这个文件传给启动类加载器,而当启动类加载器最终接到这个加载任务时,它会确认自己是否能够加载,若能它就会自己加载,若不能它就会将这个类返回给子类加载器加载,若返回到最终子类加载器都不能加载就会抛出```ClassNotFoundException```异常;
   - 双亲委派的机制实际上使用了递归的思想确认唯一性;
   - 安全性是因为某些Java的重要类如果通过重写该类并注入恶意代码的形式那么因为这些重要的系统类已经被启动类加载器加载了,递归到这个加载时为了保证唯一性加载器并不会加载,保证了运行的安全。

4. 双亲委派破坏者

   - 双亲委派破坏者:上下文类加载器:Java程序在运行时因为某些提供服务的接口的基类由启动类加载器加载的,而它的实现类是由第三方提供的通常又由系统类加载器加载,这样使用这些实现类的时候就需要父类去委托子类去加载这些接口类,这样就破坏了双亲委派机制。

#### 1.5自定义加载类

1. 自定义加载类需要继承继承```java.lang.classLoader```类并重写其中的```findClass()```方法读取```.class```文件到内存,以及内部方法```getData()```去将文件读取为```byte[]```数组;

2. 代码

   ```java
   import java.io.*;
   
   /**
    * @author zhang
    * @title: PathClassLoader
    * @projectName untitled
    * @date 2019/3/2111:21
    */
   public class PathClassLoader extends ClassLoader {
   
   
       private String classpath;
       private String packageName;
   
   
       /**
        * 构造器
        *
        * @param classpath
        * @param packageName
        */
       public PathClassLoader(String classpath, String packageName) {
           this.classpath = classpath;
           this.packageName = packageName;
       }
   
       /**
        * 重写classLoader类下的findClass加载自定义的class文件为java.lang.class实例对象
        *
        * @param name 类的全限定名
        * @return
        * @throws ClassNotFoundException
        */
       @Override
       protected Class<?> findClass(String name) throws ClassNotFoundException {
           //判断全限定名是否可用
           if (packageName.startsWith(name)) {
               //将name的文件转化为byte[]数组
               byte[] classData = new byte[0];
               try {
                   classData = getData(name);
               } catch (Exception e) {
   
   
               }
               //判断传入是否为空
               if (classData != null) {
                   return defineClass(name, classData, 0, classData.length);
               } else {
                   throw new ClassNotFoundException();
               }
           } else {
               return super.findClass(name);
           }
       }
   
       /**
        * 通过类的全限定名来读取文件数据
        *
        * @param name
        * @return
        */
       private byte[] getData(String name) throws ClassNotFoundException, IOException {
           //将全限定名转换为url d:\..\a.class 的形式
           String fileName = classpath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
           //将文件写入字节输入流
           InputStream is = new FileInputStream(new File((fileName)));
           ByteArrayOutputStream os = new ByteArrayOutputStream();
           //创建byte[]容器
           byte[] buffer = new byte[1024];
           int n;
           while ((n = (is.read(buffer))) != -1) {
               os.write(buffer, 0, n);
           }
           return os.toByteArray();
       }
   }
   ```

#### 1.6 URL加载类

1. 说明:```URLClassLoader```是```ClassLoader```的子类,它用于从指向 JAR 文件和目录的 URL 的搜索路径加载类和资源。也就是说,通过```URLClassLoader```就可以加载指定jar中的class到内存中。

### 2.反射

#### 2.1反射的定义

- 反射:程序在运行状态中,可以动态加载一个只有名称的类,加载完类之后,在堆内存中,就产生了一个 Class 类型的对象,这个对象就包含了完整的结构信息,通过这个对象我们可以看到类的结构。这个对象就像一面镜子,所以我们形象的称之为——反射。

  #### 2.2通过反射查看类信息

1. 因为Java程序在加载类文件的过程中会产生```java.lang.class```对象,而我们可以通过这个对象获取类的很多信息,而```java.lang.class```对象的获取可以依靠反射:例如

   ```java
   public class demo {
       public void say() {
           System.out.println("你好");
       }
   }
   ```

   

   对于这个demo类,我们可以通过三个方法获取它的```java.lang.class```实例化对象:

   ```java
   public class reflect {
       public static void main(String[] args) throws ClassNotFoundException {
           //1.通过Class类中的forName静态方法
           Class Class1 = Class.forName("Demo");
           //2.通过创建该类的对象
           //再通过对象的.getClass方法获得(该方法是Object类里面的方法)
           Demo demo=new Demo();
           Class class2 = demo.getClass();
           //3.类名.class获取
           Class class3 = Demo.class;
       }
   }
   ```

   - 其中第一种方法以为需要类的全路径,一旦全路径写错了运行时就会报错;
   - 使用第二中方法就必须引入这个类(要么导包,同一个包)
   - 只有第三种方法支持八种+void类型。原因:
     - 这九种类型没有对象无法使用第二种;
     - 这九种类型也不是类没有路径。

2. 获取到这个对象后我们可以通过对象的方法获取该对象所代表的类的许多信息(包括但不限于构造器、方法、属性、参数、注解、对象、类加载器、类名和包名),详情见文档。

#### 2.2通过反射创建对象

1. 通过反射创建对象的方法:

   - 通过Class对象的```newInstance()```方法来创建Class对象对应类的实例。这个方法是使用Class对象对应类的默认构造器创建对象,这就要求Class对象对应类必须要有默认构造器。

   - 使用Class对象的```getConstructor()```获取指定的Constructor对象传入参数的```.class```对象,调用Constructor对象的```newInstance()```方法来创建Class对象对应类的实例。这个方法可以使用Class对象对应类的任意指定的构造器来创建实例。代码如下:

     ```java
     public class Demo {
     
         public Demo(){
             System.out.println("这是一个无参构造器");
         }
         public Demo(String name){
             System.out.println("这是一个含参构造器:name="+name);
         }
     }
     ```

     ```java
     public class Reflect {
         public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
             Class clazz = Class.forName("Demo");
             //通过class对象的newInstance()方法生成的对象是使用无参构造器产生的
             Object O1 = (Object) clazz.newInstance();
             //通过getConstructor最终生成的对象是根据传入的参数.class对象来使用构造器
             Constructor constructor1 = clazz.getConstructor();
             Object o2 = constructor1.newInstance();
             Constructor constructor2 = clazz.getConstructor(String.class);
             Object o3 = constructor2.newInstance("张三");
         }
     }
     
     ```

     控制面板输出

     > 这是一个无参构造器
     > 这是一个无参构造器
     > 这是一个含参构造器:name=张三

0 个回复

您需要登录后才可以回帖 登录 | 加入黑马