黑马程序员——Java高新技术---枚举、内省、类加载器、注解

时间:2023-02-17 17:19:23

------- android培训java培训、java学习型技术博客、期待与您交流! ----------

第一讲     枚举

一、概述

  • 这里说的枚举,不是集合vector的特有枚举迭代器,而是JDK1.5的一个新特性。
  • 为什么要有枚举?
  • 问题:要定义星期几或性别的变量,该怎么定义?假设用1-7分别表示星期一到星期日,但有人可能会写成int weekday = 0;或即使使用常量方式也无法阻止意外。

  • 定义:枚举就是让某个变量的取值只能为若干个固定值中的一个。否则,编译器就会报错。
  • 好处:
    • 枚举可以让编译器在编辑时就可以控制源程序中的非法值。
    • 普通变量的方式在开发阶段无法实现这一目标。      

示例:

/* 
* 用普通类如何实现枚举功能,定义一个Weekday的类来模拟枚举功能。
1、私有的构造方法
2、每个元素分别用一个公有的静态成员变量表示
3、可以有若干公有方法或抽象方法。采用抽象方法定义nextDay就将大量的if.else语句转移成了一个个独立的类。

*/

package cn.itheima;

public abstract class WeekDay {
private WeekDay(){}

public final static WeekDay SUN=new WeekDay(){
public WeekDay nextDay(){
return MON;
}
};

public final static WeekDay MON=new WeekDay(){
public WeekDay nextDay(){
return SUN;
}
};

public abstract WeekDay nextDay();

public String toString(){
return this==SUN?"SUM":"MON";
}
}

二、枚举的基本应用

  1. 通过enum关键字定义枚举类,枚举类是一个特殊的类,每个元素都是该类的一个实例对象。
  2. 用枚举类规定值,如上面的WeekDay类。以后用此类型定义的值只能是这个类中规定好的那些值,若不是这些值,编译器不会通过。
  3. 如果调用者想打印枚举类中元素的信息,需由编写此类的人定义toString方法。
  • 注:枚举类是一个class,而且是一个不可被继承的final类,其中的元素都是类静态常量。
  • 常用方法:
    • 构造器:
      • 构造器只是在构造枚举值的时候被调用。
      • 构造器只有私有private,绝不允许有public构造器。这样可以保证外部代码无法重新构造枚举类的实例。因为枚举值是public static final的常量,但是枚举类的方法和数据域是可以被外部访问的。
      • 构造器可以有多个,调用哪个即初始化相应的值。
    • 非静态方法:(所有的枚举类都继承了Enum方法)
      • String toString() ;  返回枚举量的名称
      • int ordinal() ;  返回枚举值在枚举类中的顺序,按定义的顺序排
      • Class getClass() ;  获取对应的类名
      • String name();  返回此枚举常量的名称,在其枚举声明中对其进行声明。
    • 静态方法:
      • valueOf(String e) ;转为对应的枚举对象,即将字符串转为对象
      • values() ;获取所有的枚举对象元素

示例:

package cn.itheima;  

public class EnumDemo {
public static void main(String[] args) {
WeekDay weekDay=WeekDay.MON;
System.out.println(weekDay);//输出枚举常量名
System.out.println(weekDay.name());//输出对象名
System.out.println(weekDay.getClass());//输出对应类
System.out.println(weekDay.toString());//输出枚举对象名
System.out.println(weekDay.ordinal());//输出此对象在枚举常量的次序
System.out.println(WeekDay.valueOf("WED"));//将字符串转化为枚举常量
System.out.println(WeekDay.values().length);//获取所以的枚举元素,并打印其长度
}
//定义枚举内部类
public enum WeekDay{
SUN(1),MON,TUE,WED,THI,FRI,SAT;//分号可有可无,但如果下面还有方法或其他成员时,分号不能省。
//而且当有其他方法时,必须在这些枚举变量的下方。

//无参构造器
private WeekDay(){
System.out.println("First");
}
//带参数的构造器
private WeekDay(int day){
System.out.println("Second");
}
}
}

三、枚举的高级应用

  1. 枚举就相当于一个类,其中也可以定义构造方法、成员变量、普通方法和抽象方法。
  2. 枚举元素必须位于枚举体中的最开始部分,枚举元素列表的后要有分号与其他成员分隔。把枚举中的成员方法或变量等放在枚举元素的前面,编译器报告错误。
  3. 带构造方法的枚举
    1. 构造方法必须定义成私有的
    2. 如果有多个构造方法,该如何选择哪个构造方法?
    3. 枚举元素MONMON()的效果一样,都是调用默认的构造方法。
  4. 带方法的枚举
    • 如:
      /* 
      * 抽象的枚举方法
      * 此时枚举中的常量需要子类来实现,这是可以利用内部类的方式来定义枚举常量
      * 带方法的枚举
      1)定义枚举TrafficLamp
      2)实现普通的next方法
      3)实现抽象的next方法:每个元素分别是由枚举类的子类来生成的实例对象,这些子类
      4)用类似内部类的方式进行定义。
      5)增加上表示时间的构造方法
      * */

      package cn.itheima;

      public class EnumTest {
      public enum TrafficLamp{
      RED(30){
      public TrafficLamp nextLamp(){
      return GREEN;
      }
      },
      GREEN(30){
      public TrafficLamp nextLamp(){
      return YELLOW;
      }
      },
      YELLOW(5){
      public TrafficLamp nextLamp(){
      return RED;
      }
      };
      private int time;
      //构造器
      private TrafficLamp(int time){
      this.time=time;}
      //抽象方法
      public abstract TrafficLamp nextLamp();
      }
      }

  • 注:
    1. 所有的枚举都继承自java.lang.Enum类。由于Java不支持多继承,所以枚举对象不能再继承其他类。
    2. switch语句支持int,char,enum类型,使用枚举,能让我们的代码可读性更强。

第二讲 内省

一、内省

  1. 内省对应的英文单词为IntroSpector,英文意思是检查、视察、体检之意,对于程序即对内部进行检查,了解更多的底层细节。
  2. 内省的作用:主要针对JavaBean进行操作。

二、JavaBean

1.简述

  1. JavaBean是一种特殊的Java类,主要用于传递数据信息,这种Java类中的方法主要用于访问私有的字段,且方法符合某种特殊的命名规则。
  2. 它是一种特殊的Java类,其中的方法符合特殊的规则。只要一个类中含有get或is和set打头的方法,就可以将其当做JavaBean使用。
  3. 字段和属性:
    1. 字段就是我们定义的一些成员变量,如private String name;等
    2. JavaBean的属性是根据其中的setter和getter方法来确定的,而不是依据其中的变量。
      • 如方法名为setName,则中文意思是设置Name,getName也是如此;
      • 去掉set或者get前缀,剩余部分就是属性名称。如果剩余部分的第二个字母小写,则把剩余部分改为小写。
      • 如:getAge/setAge-->age;gettime-->time;setTime-->time;getCPU-->CPU。
  • 总之,一个类被当作javaBean使用时,JavaBean的属性是根据方法名推断出来的,它根本看不到java类内部的成员变量。

2.作用

  • 如果要在两个模板之间传递多个信息,可将这些信息封装到一个JavaBean中,这种JavaBean的实例对象通常称之为值对象(Value Object,简称VO),这些信息在类中用私有字段来储存,如果读取或设置这些字段的值,则需要通过一些相应的方法来访问。
3.好处

  • 一个符合JavaBean特点的类当做普通类一样可以使用,但是把它当做JavaBean类用会带来一些额外的好处:
    • 在Java EE开发中,经常要使用到JavaBean。很多环境就要求按JavaBean方式进行操作,别人都这么用和要求这么做,那你就没什么挑选的余地!
    • JDK中提供了对JavaBean进行操作的API,这套API称为内省,若要自己通过getX的方式来访问私有x,可用内省这套API,操作JavaBean要比使用普通的方式更方便。

示例:

package cn.itheima.demo;  

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class IntroSpectorDemo {
public static void main(String[] args) throws Exception {
HashCodeTest hct=new HashCodeTest(2,3);
String propertyName="x";
//"x"-->"X"-->"getX"-->MethodGetX-->

//用内省的方式
//获取并getX方法
Object retval = getProperty1(hct, propertyName);
System.out.println(retval);

Object value=5;
//获取并调用setX方法
setProperty(hct, propertyName, value);

System.out.println(hct.getX());
}
//获取并调用setX方法
private static void setProperty(HashCodeTest hct, String propertyName,
Object value) throws IntrospectionException,
IllegalAccessException, InvocationTargetException {
PropertyDescriptor pd=new PropertyDescriptor(propertyName,hct.getClass());//创建对象关联
Method methodSetX=pd.getWriteMethod();//获取JavaBean类中的setX方法
methodSetX.invoke(hct,value);//调用setX方法
}
//获取并getX方法
private static Object getProperty1(HashCodeTest hct, String propertyName)
throws IntrospectionException, IllegalAccessException,
InvocationTargetException {
PropertyDescriptor pd=new PropertyDescriptor(propertyName,hct.getClass());//创建对象关联
Method methodGetX=pd.getReadMethod();//获取JavaBean类中的getX方法
Object retval=methodGetX.invoke(hct);//调用getX方法
return retval;
}
}

三、对JavaBean的复杂内省操作

  1. IntroSpector类中有getBeanInfo(Class cls)的方法,通过此方法获取BeanInfo实例。参数是相应对象的字节码,即Class对象。
  2. BeanInfo类中有getPropertyDescriptors()的方法,可获取所有的JavaBean类中的属性信息,返回一个PropertyDescriptor[]
  3. 在通过遍历的形式,获取与想要的那个属性信息。

如:获取并调用getX方法

//第二种较复杂的获取并调用JavaBean中的getX方法  
private static Object getProperty2(HashCodeTest hct, String propertyName)
throws IntrospectionException, IllegalAccessException,
InvocationTargetException {
BeanInfo beanInfo=Introspector.getBeanInfo(hct.getClass());//创建对象关联
PropertyDescriptor[] pds=beanInfo.getPropertyDescriptors();//获取所有的属性描述
Object retval=null;

//遍历
for (PropertyDescriptor pd : pds) {
//如果属性跟参数的属性相等,就获取它的getX方法
if (pd.getName().equals(propertyName)) {
Method methodGetX=pd.getReadMethod();//获取getX方法
retval=methodGetX.invoke(hct);
break;
}
}
return retval;
}

四、BeanUtils工具包

  1. BeanUtils等工具包都是由阿帕奇提供的,为了便于开发。
  2. BeanUtils可以将8种基本数据类型进行自动的转换,因此对于非基本数据类型,就需要注册转换器Converter,这就需要ConverUtils包。
  3. 好处:
    1. 提供的set或get方法中,传入的是字符串,返回的还是字符串,因为在浏览器中,用户输入到文本框的都是以字符串的形式发送至服务器上的,所以操作的都是字符串。也就是说这个工具包的内部有自动将整数转换为字符串的操作。
    2. 支持属性的级联操作,即支持属性链。如可以设置:人的脑袋上的眼睛的眼珠的颜色。这种级联属性的属性连如果自己用反射,那就很困难了,通过这个工具包就可以轻松调用。
  4. 可以和Map集合进行相互转换:可将属性信息通过键值对的形式作为Map集合存储(通过static java.util.Mapdescribe(java.lang.Object bean)的方法)。也可以将Map集合转换为JavaBean中的属性信息(通过static void populate(java.lang.Objectbean, java.util.Map properties)的方法)。

  • 注:要正常使用BeanUtils工具,还要将Apache公司的logging(日志)的jar包也添加进Build Path。

示例:

package cn.itheima.demo;  

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;

public class IntroSpectorDemo {
public static void main(String[] args) throws Exception {
HashCodeTest hct=new HashCodeTest(2,3);
String propertyName="x";
//"x"-->"X"-->"getX"-->MethodGetX-->

//用BeanUtils工具包的方法
System.out.println(BeanUtils.getProperty(hct, propertyName));//get
BeanUtils.setProperty(hct, propertyName, "9");//set

System.out.println(hct.getX());

//对于JavaBean中的属性是对象的操作
BeanUtils.setProperty(hct, "birthday.time", "10");//set
System.out.println(BeanUtils.getProperty(hct, "birthday.time"));//get
}
}

  • BeanUtils工具包中还有一个工具类PropertyUtils,用法跟BeanUtils一样。区别:
    1. BeanUtils会对JavaBean的属性的类型进行转换,如属性本身是integer,会转换为String。
    2. PropertyUtils以属性本身的类型进行操作。

//用BeanUtils工具包的工具类BeanUtils方法  
BeanUtils.setProperty(hct, propertyName, "9");//set,9是String
System.out.println(BeanUtils.getProperty(hct, propertyName));//get
System.out.println(BeanUtils.getProperty(hct, propertyName).getClass().getName());//java.lang.String

//BeanUtils工具包中的PropertyUtils的操作
PropertyUtils.setProperty(hct, propertyName, 9);//9是Integer
System.out.println(PropertyUtils.getProperty(hct, propertyName));
System.out.println(PropertyUtils.getProperty(hct, propertyName).getClass().getName());//java.lang.Integer

 

第三讲     注解——JDK.1.5新特性

一、概述

  1. 注解(Annotation)相当于一种标记,在程序中加了注解就等于为程序打上了某种标记,没加,则等于没有某种标记。以后,javac编译器、开发工具和其他程序可以用反射来了解你的类及各种元素上有无何种标记,看你有什么标记,就去干相应的事。
  2. 标记可以加在包,类,字段,方法,方法的参数以及局部变量上。
  3. java.lang包中提供了最基本的注解(annotation)
  4. 格式:@注解类名。
    • 如果只有一个value名称的属性或其他属性缺省,则可@注解名(属性值);
    • 如果有多个或不缺省或者需重新赋值,则@注解名(属性名=”属性值,…)。

二、java中三种最基本的注解

  1. @SuppressWarning(deprecation):表示压制过时警告;或者说不要警告过时提示了
    • SupressWarning是告知编译器或开发工具等不需要再提示指定的警告了;
    • deprecation”是警告的信息,即过时警告。
  2. @Deprecated:表示告知调用者,该成员函数、字段等已经过时,不再推荐使用。
    • 源代码标记@Deprecated是在JDK1.5中作为内置的annotation引入的,用于表明类(class)、方法(method)、字段(field)已经不再推荐使用,并且在以后的JDK版本中可能将其删除,编译器在默认情况下检测到有此标记的时候会提示警告信息。
    • 例如:假定之前的某个类升级了,其中的某个方法已经过时了,不能够将过时的方法删除,因为可能会影响到之前调用此这个方法的某些程序,这时就可以通过在方法上加这个注解来标记。
  3. @Override:表示下面的方法是在覆盖(父类方法),如果不存在覆盖,就会报错。
    • 加上此注解,可对类中的方法判断是否是要覆盖的父类的方法。典型的例子即在类中覆盖equals(Object obj)方法时,其中的参数类型必须是Object,才能被覆盖;若不是,则不存在覆盖。此时如果加上了此注解就会提示警告。

三、注解的应用结构图

黑马程序员——Java高新技术---枚举、内省、类加载器、注解

  • 注解就相当于一个你的源程序中要调用的一个类,要在源程序中应用某个注解,得先准备好了这个注解类。就像你要调用某个类,得先有开发好这个类。

四、自定义注解及其应用

  1. 定义格式:@interface名称{statement}
    • 如:最简单的注解类:public @interface MyAnnotation{}
  2. 元注解(注解的注解):即在定义注解类的时候加注解。如两个常用于元注解的注解:RetentionTarget
    1. Retetion:用于说明注解保留在哪个阶段(即注解的生命周期)。
      1. 一个注解的生命周期包含:java源程序--(javac)-->class文件--(类加载器)-->内存中的字节码分别对应Retetion这个枚举类的值:
        • RetetionPolicy.SOURSEjava源文件时期,如@Overried@SuppressWarning
        • RetetionPolicy.CLASS class文件时期(默认阶段)
        • RetetionPolicy.RUNTIME:运行时期,如@Deprecated
        • 如:在某注解类上加@Retention(RetentionPolicy.RUNTIME),表示此注解会一直存在。
        • 注:
          • 当在源程序上加了注解,javacjava源程序编译为class文件时,会对注解的生命周期进行判断。如果该注解只保留在源程序,则编译时会将该注解进行相应的处理操作,如去掉。其他类推。
          • class文件中不是字节码,只有把class文件中的内容加载进内存,用类加载器加载处理后(进行完整的检查等处理),最终得到的二进制内容才是字节码。 
    2. Target:用于说明注解类的使用范围。如在方法上还是类上,默认值是任何地方。
      • 其值可设置为枚举类ElementType类中的任何一个,包括:包、字段、方法、方法参数、构造器、类等值。取值为:
      • PACKAGE(包声明)
      • FIELD(字段声明)
      • ANNOTATION_TYPE(注释类型声明)
      • CONSIRUCTOR(构造器声明)
      • METHOD(方法声明)
      • PARAMETER(参数声明)
      • TYPE(类、接口(包含注释类型)或枚举声明)
      • LOCAL_VARIABLE(局部变量声明)
      • 注意:其中代表类的值是TYPE。因为classenuminterface@interface等都是平级的,所以统属于Type。不可用CLASS表示。

  • 3、注解的应用
    • 通过反射方式来获取自定义的注解类,步骤跟注解的应用结构一致,如:
      1. 定义注解类:@interfaceA{}
      2. 应用了“注释类”的类:@A  class B{}
      3. 对“应用注释类的类”进行反射操作的类:class c{...},操作如下:
        1.  B.class.isAnnotionPresent(A.class);//判断是否存在此注解类
        2. A a = B.class.getAnnotation(a.class);//存在的话则得到这个注释类的对象 

示例:

package cn.itheima.Demo;  

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)//元注释
//注解类
public @interface MyAnnotation {
}

@MyAnnotation
//应用类
public class AnnotationDemo {
@SuppressWarnings("deprecation")//此注解用于抑制过时信息的提示
public static void main(String[] args) {

System.runFinalizersOnExit(true); //这是一个过时了的方法 ,如果没有注解就会有警告提示
//判断此类是否有MyAnnotation注解
if (AnnotationDemo.class.isAnnotationPresent(MyAnnotation.class)) {
//如果有,则获取该注解
MyAnnotation annotation =AnnotationDemo.class.getAnnotation(MyAnnotation.class);
System.out.println(annotation);
}
}
}

五、为注解添加基本属性

  1. 属性:一个注解相当于一个胸牌,但仅通过胸牌还不足以区别带胸牌的两个人,这时就需要给胸牌增加一个属性来区分,如颜色等。
  2. 定义格式:
    • 同接口中的方法一样:String color();
    • 定义缺省格式:String value() default “heima”;
  3. 应用:直接在注解的括号中添加自身的属性,如: @MyAnnotation(color=”red”)
    • 如果注解中有一个名称为value的属性,且你只想设置value属性(即其他属性都采用默认值或者你只有一个value属性),那么可以省略value=部分,例如:@SuppressWarnings("deprecation")。
    • 可以为属性值指定缺省值(default),应用时同样可以重新设置属性值。
    • 用反射方式获得注解对应的实例对象后,可以通过该对象调用属性对应的方法来获取属性值。

六、为注解增加高级属性

  1. 可以为注解增加的高级属性的返回值类型有:
    1. 八种基本数据类型   
    2. String类型 
    3. Class类型
    4. 枚举类型  
    5. 注解类型  
    6. 前五种类型的数组
  2. 数组类型的属性:
    • 如:int[]arrayArr() default {1,2,3};//可不定义默认值
    • 应用:@MyAnnotation(arrayArr={2,3,4}) //可重新赋值
    • 注:若数组属性中只有一个元素(或重新赋值为一个元素),这时属性值部分可省略大括号。
  3. 枚举类型的属性:
    • 假设定义了一个枚举类TrafficLamp,它是EnumTest的内部类,其值是交通灯的三色。
    • 定义:EnumTest.TrafficLamplamp();
    • 应用:@MyAnnotation(lamp=EnumTestTrafficLamp.GREEN)
  4. 注解类型的属性:
    • 假定有个注解类:MetaAnnotation,其中定义了一个属性:String value()
    • 定义:MetaAnnotation annotation() default @MetaAnnotation(”xxx”);
    • 应用:@MyAnnotation(annotation=@MetaAnnotation(”yyy”))//重新赋值
    • 可以认为上面这个@MyAnnotation是MyAnnotaion类的一个实例对象,同样的道理,可以认为上面这个@MetaAnnotation是MetaAnnotation类的一个实例对象,调用代码如下:
    • MetaAnnotationma =MyAnnotation.annotation();
    • System.out.println(ma.value());
  5. Class类型的属性:
    • 定义:Class cls();
    • 应用:@MyAnnotation(cls=AnnotationDemo.class)
    • 注:这里的.class必须是已定义的类,或是已有的字节码对象
  6. 注解的详细语法可通过查看java语言规范了解即javaLanguage Specification

示例:

package cn.itheima.demo;  

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)//元注释
@Target({ElementType.METHOD,ElementType.TYPE})//元注解,指定使用范围
//注解类
public @interface MyAnnotation {
String color() default "red" ;
String value();
//数组
int[] arr() default {1,2,3};
//枚举
EnumTest.TrafficLamp lamp() default EnumTest.TrafficLamp.GREEN;
//注解类
MetaAnnotation annotation() default @MetaAnnotation("heima");
//Class类
Class clazz() default System.class;
}

import java.lang.reflect.Method;

//注解类的应用,给属性赋值或者重新赋值
@MyAnnotation(lamp=EnumTest.TrafficLamp.YELLOW,value="heima",
clazz=AnnotationDemo.class,annotation=@MetaAnnotation("itheima"))
//应用类
public class AnnotationDemo {
@SuppressWarnings("deprecation")//此注解用于抑制过时信息的提示
@MyAnnotation("Method")//自定义注解应用在方法上
public static void main(String[] args) throws NoSuchMethodException, SecurityException {

System.runFinalizersOnExit(true); //这是一个过时了的方法 ,如果没有注解就会有警告提示
//判断此类是否有MyAnnotation注解
if (AnnotationDemo.class.isAnnotationPresent(MyAnnotation.class)) {
//如果有,则获取该注解
MyAnnotation annotation =AnnotationDemo.class.getAnnotation(MyAnnotation.class);
System.out.println(annotation);//@cn.itheima.Demo.MyAnnotation()
System.out.println(annotation.color());//red
System.out.println(annotation.value());//heima
System.out.println(annotation.arr().length);//3
System.out.println(annotation.lamp());//YEllOW
System.out.println(annotation.annotation().value());//itheima
System.out.println(annotation.clazz());//class cn.itheima.demo.AnnotationDemo
}

//获取方法上的注解
Method mainMethod=AnnotationDemo.class.getMethod("main",String[].class);
MyAnnotation annotationMethod=(MyAnnotation) mainMethod.getAnnotation(MetaAnnotation.class);
SuppressWarnings sw=mainMethod.getAnnotation(SuppressWarnings.class);
System.out.println(sw);//null
System.out.println(annotationMethod);//null
}
}

 

第四讲      类加载器

 

一、概述

  1. 定义:简单说,类加载器就是加载类的工具。
    •  在java程序中用到一个类,出现了这个类的名字。java虚拟机首先将这个类的字节码加载到内存中,通常这个类的字节码的原始信息放在硬盘上的classpath指定的目录下,把.class文件的内容加载到内存里面来,再对它进行处理,处理之后的结果就是字节码。这些工作就是类加载器在操作。
  2. 类加载器作用:将.class文件中的内容变为字节码加载进内存。
  3. 默认类加载器:
    1. Java虚拟机中可安装多个类加载器,系统默认的有三个主要的,每个类负责加载特定位置的类:BootStrap、ExtClassLoader、AppClassLoader
    2. 类加载器本身也是Java类,因为它是Java类的加载器,本身也需要被类加载器加载,显然必须有第一个类加载器而不是java类的,这正是BootStrap。它是嵌套在Java虚拟机内核中的,已启动即出现在虚拟机中,是用c++写的一段二进制代码。所以不能通过java程序获取其名字,获得的只能是null。
  4. Java虚拟机中的所有类加载器采用父子关系的树形结构进行组织,在实例化每个类装载器对象时,需要为其指定一个父级类装载器对象或者默认采用系统类装载器为其父级类加载。
  5. 类加载器之间的父子关系和管辖范围图:

黑马程序员——Java高新技术---枚举、内省、类加载器、注解

示例:

package cn.itheima.demo;  
public class ClassLoaderDemo {
public static void main(String[] args) {
System.out.println(
ClassLoaderDemo.class.getClassLoader().getClass().getName()
);//sun.misc.Launcher$AppClassLoader,表示由AppClassLoader加载
System.out.println(System.class.getClassLoader());//null,表示System这个类时由RootStrap加载的
}
}

二、类加载器的委托机制

  1. 每个ClassLoader本身只能分别加载特定位置和目录中的类,但它们可以委托其他的类加载器去加载类,这就是类加载器的委托模式。
  2. 加载类的方式,Java虚拟机要加载一个类时,到底要用哪个类加载器加载呢?
    1. 首先,当前线程的类加载器去加载线程中的第一个类。
    2. A引用类B(继承或者使用了B),Java虚拟机将使用加载类的类加载器来加载类B
    3. 还可直接调用ClassLoaderLoaderClass()方法,来指定某个类加载器去加载某个类。
  3. 每个类加载器加载类时,又先委托给上级类加载器。
    • 类装载器一级级委托到BootStrap类加载器,当BootStrap无法加载当前所要加载的类时,然后才一级级回退到子孙类加载器去进行加载。当回退到最初的发起者类装载器时,如果它自己也不能完成类的装载,那就会抛出ClassNotFoundException异常。这时就不会再委托发起者加载器的子类去加载了,如果它还有子类的话。
    • 简单说,就是先由发起者将类一级级委托为BootStrap,从父级开始找,找到了直接返回,没找到再助剂让其子级找,直到发起者,再没找到就报异常。
  4. 委托机制的优点:可以集中管理,不会产生多字节码重复的现象。

  • 问题:可不可以自己写个类为:java.lang.System呢?
  • 回答:
    • 第一、通常是不可以的,由于类加载器的委托机制,会先将System这个类一级级委托给最*的BootStrap,由于BootStrap在其指定的目录中加载的是rt.jar中的类,且其中有System这个类,那么就会直接加载自己目录中的,也就是Java已经定义好的System这个类,而不会加载自定义的这个System
    • 第二、但是还是有办法加载这个自定义的System类的,此时就不能交给上级加载了,需要用自定义的类加载器加载,这就需要有特殊的写法才能去加载这个自定义的System类的。

体现委托机制的示例:

package cn.itheima.demo;  

public class ClassLoaderDemo {

public static void main(String[] args) {
/*
* 用eclipse的打包工具将ClassLoaderTest输出成jre/lib/ext目录下的itheima.jar包
* 此时再在eclipse中运行这个类时,下面代码的while循环内的运行结果显示为ExtClassLoadr。
* 这就表示,AppClassLoader在加载这个类时,会先委托给其上一级ExtClassLoader加载器去加载,而上级又委托上级
* 但是ExtClassloader的上级没有找到要加载的类,就回到ExtClassLoader,此时它在jre/lib/ext中找到了,所以就结果就显示它了。
* */
ClassLoader loader=ClassLoaderDemo.class.getClassLoader();
while (loader!=null) {
System.out.println(loader.getClass().getName());
loader=loader.getParent();//将此loader的上级赋给loader
}
System.out.println(loader);
}
}

三、自定义类加载器

  1. 自定义的类加载器必须继承抽象类ClassLoader,要覆写其中的findClass(String name)方法,而不用覆写loadClass()方法。
  2. 覆写findClass(Stringname)方法的原因:
    1. loadClass()内部是会先委托给父级,当父级找不到后返回,再调用findClass(String name)方法,也就是你自定义的类加载器去找。所以只需要覆写findClass方法,就能实现用自定义的类加载器加载类的目的。
      • 因为,一般自定义类加载器,会把需要加载的类放在自己指定的目录中,而java中已有的类加载器是不知道你这个目录的,所以会找不到。这样才会调用你复写的findClass()方法,用你自定义的类加载器去指定的目录加载类。
    2. 这是一种模板方法设计模式。这样就保留了loadClass()方法中的流程(这个流程就是先找父级,找不到再调用自定义的类加载器),而我们只需复写findClass方法,实现局部细节就行了。
      • ClassLoader提供了一个protected  Class<?>defineClass(String name, byte[] b, int off, int len)方法,只需要将类对应的class文件传入,就可以将其变为字节码。
  3. 编程步骤:
    1. 编写一个对文件内容进行简单加密的程序
    2. 编写好了一个自己的类加载器,可实现对加密过来的类进行加载和解密。
    3. 编写一个程序,调用类加载器加载类,在源程序中不能用该类名定义引用变量,因为编译器无法识别这个类,程序中除了可使用ClassLoaderloadClass方法外,还可以使用设置线程的上下文类加载器或系统类加载器,然后再使用Class.forName
  4. 编码步骤:
    1. 对不带包名的class文件进行加密,加密结果存放到另外一个目录,例如: java MyClassLoader MyTest.class F:\itcast
    2. 运行加载类的程序,结果能够被正常加载,但打印出来的类装载器名称为AppClassLoaderjava MyClassLoader MyTest F:\itcast
    3. 用加密后的类文件替换CLASSPATH环境下的类文件,再执行上一步操作就出问题了,错误说明是AppClassLoader类装载器装载失败。
    4. 删除CLASSPATH环境下的类文件,再执行上一步操作就没问题了。

示例:

package cn.itheima.demo;  

import java.util.Date;
//定义一个测试类,继承Date,便于使用时加载
public class ClassLoaderAttachment extends Date{
//复写toString方法
public String toString(){
return "Hello World!";
}
}

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class MyClassLoader extends ClassLoader{
public static void main(String[] args) throws Exception {
String srcPath=args[0];//文件源
String destDir=args[1];//文件目的
InputStream ips=new FileInputStream(srcPath);
String destFileName=srcPath.substring(srcPath.lastIndexOf("\\")+1);
String destFilePath=destDir+"\\"+destFileName;
OutputStream ops=new FileOutputStream(destFilePath);
cypher(ips,ops);//加密class字节码
ips.close();
ops.close();
}
//加密方法
private static void cypher(InputStream ips,OutputStream ops) throws Exception{
int b=-1;
while((b=ips.read())!=-1){
ops.write(b^0xff);
}
}
@Override
//覆盖ClassLoader的findClass方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
name=name.substring(name.lastIndexOf(".")+1);
String classFileName=classDir+"\\"+name+".class";//获取class文件名
InputStream ips=null;
try {
ips=new FileInputStream(classFileName);
ByteArrayOutputStream bos=new ByteArrayOutputStream();//定义字节数组流
cypher(ips,bos);//解密
ips.close();
byte[] buf=bos.toByteArray();//取出字节数组流中的数据
return defineClass(null, buf,0,buf.length);//加载进内存

} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
return null;
//return super.findClass(name);
}
private String classDir;
public MyClassLoader(){}
//带参数的构造函数
public MyClassLoader(String classDir){
this.classDir=classDir;
}
}

import java.util.Date;
public class ClassLoaderDemo {
public static void main(String[] args) throws Exception {
//将用自定义的类加载器加载.class文件
Class clazz=new MyClassLoader("itheimalib").loadClass("cn.itheima.demo.ClassLoaderAttachment");
Date d1 = (Date)clazz.newInstance();//获取Class类的实例对象
System.out.println(d1);
}
}


------- android培训java培训、java学习型技术博客、期待与您交流! ----------