黑马程序员----枚举反射

本文详细介绍了Java中的枚举和反射机制。枚举用于限制类的实例只能取预设的固定值,而反射则允许在运行时动态操作类、对象、字段和方法。枚举实质上是一个类,可以通过类名.枚举元素访问。反射通过Class类、Constructor类、Field类和Method类来获取和操作类的信息。同时,文章提到了如何通过反射创建对象、访问和修改成员变量以及调用方法。枚举和反射在框架开发中起着重要作用,允许框架根据配置文件动态加载和使用类。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

 

 

 

关键字:枚举、反射、Class类、反射、Constructor类、Field类、Method类、数组的反射、框架、类加载器、配置文件

 

枚举enum

定义一个类型,让该类的变量的取值只能是若干个固定值中的一个,否则编译报错,这个过程称为枚举。枚举可以让编译器在编译时就控制源程序中的非法值,普通变量无法实现。本质是限制了对象的创建。

普通类实现枚举:本类中,私有化构造函数,并限制了对象和引用变量的创建,将引用变量变成常量(对象类型)。外部类在使用本类时,引用变量只能赋值这些常量,不能指定规定以外的值,例如一星期的七天,3色的交通灯。

普通类实现枚举的思路:

1.       私有构造方法。

2.       每个元素分别用一个共有的静态变量表示。

3.       可以有共有方法和抽象方法。例如,采用抽象方法、匿名内部类和多态定义nextDay()就将大量的if、else转成成一个个独立的子类。

通过点击左边框里的错误图标,可以直接创建新的类、接口等,非常便捷。

    枚举相当于一个类,枚举里面的元素相当于常量,指向实例对象。可以调用一些方法对枚举的对象进行操作,就像普通类通过继承Object类获得了一些基本方法。例如,name()、toString()、ordinal()(自己的顺序)、getclass()。枚举也有一些静态方法,valueOf(),将字符串变成该枚举的对象;values(),将枚举里的元素装入一个数组中。

    枚举的对象获得:类.元素,例如,TrafficLamp.RED。

    枚举的构造方法特点

1.       构造方法必须位于元素列表之后。成员变量和成员方法都要放在元素后面,否则报错。

2.       私有化构造方法。

3.       当枚举被调用,内部的元素(静态变量)会都被加载进内存,然后逐个调用空参数的构造函数创建相应的对象。

4.       调用带参数的构造函数创建对象:在元素后面加上括号,括号内传入构造函数的参数。例如,SUN(1)。由此可见,如果括号内没有参数,调用空参数构造函数。

访问修饰符:外部类:public和默认。内部类:public、private、protected和默认,和成员函数平级,访问修饰符也一样。

    匿名内部类的另类用法:{}及其里面的内容放在引用的后面,而不是对象的后面,例子如下。再者就是,子类对象可以调用父类的带参数的构造方法,例如,new data(30){};

    当枚举只有一个成员是,可以当做一个单例的实现方式,比单例设计模式方便,只要列出一个元素即可,其他方法一样。

    public enum TrafficLamp {//带有抽象的方法,但不用写出。

       RED(30){//另类的匿名内部类使用方法。

           public TrafficLamp nextLamp(){

              return GREEN;

           }  

       },

       GREEN(5){

           public TrafficLamp nextLamp(){

              return YELLOW;

           }  

       },

       YELLOW(40){

           public TrafficLamp nextLamp(){

              return RED;

           }  

       };

      

       public abstract TrafficLamp nextLamp();

       private int time;

       private TrafficLamp(int time){this.time = time;}

    }

   

反射(Reflect)与Class类

作用在于,在不知道类的具体内容(API或源代码)的情况下(只有.class文件)操作类的内容,也就是在类被编译后,再对类进行操作,操作的内存里的字节码。例如,给出参数,选择一个构造函数创造对象,这就需要调用反射来获得类的构造函数;获得对象中指定的变量的值;有对象和参数,调用某个方法。正因为此,需要抛出异常,因为这些成员可能没有。

JDK1.2出现,不是新特性。

Class类《——》java类

Constructor类《——》构造函数

Field类《——》成员变量

Method类《——》成员函数

Array《——》处理数组

 

Class类(java.lang包)

java类用于描述事物的特性。java程序中的任何java类和接口也属于同一种事物,描述这类事物使用Class类。对象——》java类和接口——》Class类。枚举是类,注释是接口。

通过Class中的各种方法,可以获得java类的信息,例如,类名、包名、父类等。但是没有构造函数,Class的实例对象就是java类在内存里的字节码具有唯一性。例如,Class ch = Data.class;,对象为Data类在内存的字节码。.class文件不是字节码,只有被类加载器加载入内存后,内存里的二进制数据才是字节码。

通过forName()获得java类的字节码,例如,Class.forName(“java.lang.String”);获得String在内存的字节码。forName()获得字节码的情况有两种

1. 如果该类已经在虚拟机,无需加载。直接找到字节码返回即可。

2. 如果该类不在虚拟机,使用类加载器,将类加载入虚拟机,然后从虚拟机中获得字节码。

java类的对象调用getClass()方法,获得对应的java类。例如,stud.getClass();。

获得该字节码对应的Class类对象的3种方式

1. 类名.class,例如,System.class

2. 对象.getClass(),例如,new Data.getClass()//获得该对象的类的Class类对象

3. Class.forName(“类名’),静态方法,例如,Class.forName(“java.lang.String”);反射中使用比较多,类名也可以是变量,不用提前知道java类的名字。需要处理异常,可能类没有加载,但没有使用类加载器ClassLoader 

通过上面三种方法都可以获得字节码,一个类在内存中只有唯一的一份字节码。

8个基本类型也有对应的Class对象。void也有对应的Class对象,例如,Class ch = void.class;。所以共有9个预定义的Class对象。

isPrimitive(),是否是基本数据类型。基本类型获得字节码,例如,int.class()。注意,基本类型的字节码和包装类的字节码不一样,例如,int.class 和Integer.class对应的字节码不一样。但是,Integer.TYPE表示包装类内部基本数据类型的字节码,所以int.class 和Integer.TYPE是一致的。9个预定义类型都是如此。

基本数据类型组成的数组,也有对应的字节码,是Class的实例对象,但不是基本类型。是数组类型,可以用isArray()判断是否是数组类型。

将鼠标放在错误图标上,可以看到错误原因。

总之,只要是在源程序中出现的类型,都有各自对应的Class实例对象。例如,数组等。

 

反射

反射就是将java类中的各种成分都解析成对应的java类(解构主义),例如,函数用Method类描述、包用Package、成员变量用Field、构造函数用Constructor。java类,纵向形成对象,横向的各种成分形成对应的java类。

具体的一个java类中的成分,用反射类的对象表示,例如,System类中有许多函数,exits()、out.println(),用Method类下不同的对象obj1、obj2表示,一个Method对象对应一个System类的函数。

一个类中的所有成员都可以用相应的反射API类的一个实例对象表示,通过调用Class类的方法获得这些对象,并对其进行操作。

 

Constructor类

描述类中的构造方法。通过调用Class中的方法获得Constructor对象。与java类相关。

获得构造函数:Constructor[] getConstructors();返回的是构造方法的数组。

Constructor<T> getConstructor(Class… type),传入想要的构造函数中的参数类型的Class类对象,获得该参数对应的一个构造函数。使用了可变参数,满足不同的参数需求。使用泛型,可以指定构造函数对象构造的类型。

用getConstructor返回的构造函数对象可以创建对应类的实例对象,使用newInstance()方法,例如

//获得String类的构造函数,该StringBuffer表示选择哪个构造方法。

Constructor con = String.class.getConstructor(StringBuffer.class);

//必须传入指定的参数(StringBuffer对象),而且需要转换类型。

String str =(String)con.newInstance(new StringBuffer("kkk"));

注意newInstance()返回的是泛型,需要在Constructor处指定类型或者创建号对象后类型转换,例如,Constructor<String> con = String.class.getConstructor(StringBuffer.class);

编译时和运行时对程序的处理不同:编译只负责检查语法错误,并不执行等号右边的语句,也不明确变量的类型,所以需要类型转换或者指定泛型。树立编译和运行的两阶段思想

需要注意,获得构造函数和用构造函数创建对象,内部使用的参数需要统一,不能上面用StringBuffer,下面用String,一种构造函数只能使用一种参数

通过反射创建实例对象的步骤:

1、class字节码—》Constructor构造函数对象—》newInstance()创建对象。

2、class字节码—》(Class类的)newInstance()创建对象。

Class类的newInstance()方法,用于创建空参数的实例对象。底层仍然是调用Constructor类的newInstance()方法,使用缓冲机制保存实例对象,等到使用时提供出去,非常占用资源。

String str1 = String.class.newInstance();//已经知道类型,无需转换。

String str2 = (String)Class.forName("java.lang.String").newInstance();//需要转换类型。

Field类

代表java类中的成员变量。

右键—》Source—》使用构造函数目标创建构造函数。

ReflectPoint rf = new ReflectPoint(3,6);

Field fy = ReflectPoint.class.getField("y");//fy是ReflectPoint类的共有的成员变量y

System.out.println(fy.getInt(rf));//想要获得fy对应值,需要指定具体的ReflectPoint对象。

获得变量:Class类中的getField()只能获得共有的变量,getFields()返回变量的数组,getDeclaredField(),返回所有的变量,包括私有

Field类中的get()方法获得变量对应的值,返回Object类型,需要转换。对于私有变量,还需要设置“暴力反射”才能获得对应值,例如,fx.setAccessible(true);。也有一些可以获得基本类型的值的方法,但是必须预先知道变量的类型才能使用,例如,getInt()。总之,值是和对象有关的

字节码之间的比较应该使用==,能够确定是否是同一份字节码,equals会比较内容,不够严谨。

set(Object obj, Object value),将某个对象的某个成员变量的值换成新值。

getType(),获得某个变量的类型,注意返回Class对象。

getName(),获得某个变量的变量名。

变量的组成:类型、变量名和值,对应的获得方法:getType()、getName()和get()。这些方法都需要抛出异常。

 

Method类

代表类中成员方法。与java类相关,与对象无关。先获得方法,再用对象调用。

需要导包:import java.lang.reflect.*;。

获得java类中方法:使用Class类中的方法,

1、getMethod(String name, Class... parameterTypes),指定方法名和参数列表,获得方法。参数类型使用Class类。例如,int.class

2、getMethods(),获得方法组成的数组,Method[]。

对象调用方法invoke(Object obj, Object... args),某个对象调用该方法,并传入参数,符合面向对象的思想。参数为可变参数,因为不同的方法对应不同的参数,可以使用基本类型。返回Object类型,需要转换。例如,Character ch = (Character)methodCharAt.invoke(s1, 2);相当于,s1.charAt(2)。如果s1是null,意味着这个Method对象对应的是静态方法。

JDK1.4时没有可变参数,传入数组,例如,Character ch = (Character)meCharAt.invoke(s1, new Object[]{2});。将数组打开,获得里面的元素作为参数,这个数组可以放入各种对象,例如,Integer、String等,使用多态。JDK1.5兼容了1.4的方法,会自动拆包数组。

专家模式:谁拥有数据,谁就可以调用方法。

 

用反射执行main主函数

可以直接用类调用主函数,例如,TestArguments.main(new String[]{});。写程序时,并不知道需要调用哪个类的main函数,等程序运行后在告诉调用哪个类的main,相当于主函数传入参数。先调用类,即使这个类没有被写好,只要运行时写好就可以。

需要给本类传入参数,右键—》Run As—》Arguments—》Program arguments,将外面类放入,即主函数传入参数。

String classname = args[0];

Method mainMethod = Class.forName(classname).getMethod("main", String[].class);

mainMethod.invoke(null,(Object)new String[]{"kk","qq"});//

JDK1.5为了兼容JDK1.4,如果传入一个数组,会默认为Object数组,自动拆包,将数组中的元素作为参数。将上例的数组拆包成2个字符串对象。为了防止这种情况,处理方法有二种:

1、将数组变成一个Object数组对象,例如,new Object[]{(new String[]{"kk","qq"})}。因为数组也属于Object类的子类。

2、转换数组的类型,例如,(Object)new String[]{"kk","qq"}。

 

数组的反射

相同的维度和数据类型的数组,其Class对象一样

String getName(),返回类名,即该Class对象对应的类、接口、数组类、基本类型的名称。如果是数组,会按照数组给出简写字母。例如,int[]数组的Class对象的名字是[I,[表示数组,I表示int类型。

int[] a1 = new int[]{2,5,7};

    int[] a2 = new int[4];

    int[][] a3 = new int[3][5];

String[] a4 = new String[]{"d","j","f"};

Class getSuperclass(),获得该Class对象的父类的Class对象。

所有一维数组的父类是Object,二维数组可以看做Object[]。也就是说,整个一维数组、二维数组中的数组都可以看做Object类型。例如,a1、a3的元素、a4的元素都是Object。另外,引用数据的数组既可以当做Object,也可以当做Object[]

Arrays的asList()方法在处理基本类型数组(int[])和引用类型数组(String[])时的差异:

可以将引用数据的数组变成集合,数组中的元素变成集合中的元素;会将整个基本数据的数组变成一个Object对象。因为JDK1.4中,asList()接受的是数组Object[];JDK1.5中,asList()接受的是可变参数T…a,T可以是基本型或引用型。例如,a4按照1.4处理,直接打印;a1按照1.5处理,打印出哈希值。想要打印基本数据的数组,可以逐个元素录入,或者

使用反射中的Array工具类对数组进行反射操作。Class中拥有Arrays没有的方法,例如判断对象所属的类是否是数组等,再用Array中的方法操作数组。这些方法基本是静态方法,需要将数组对象传入。例如,打印对象,类似拆包

public static void printObject(Object obj){

       Class oss = obj.getClass();//变成Class对象进行判断。

       if(oss.isArray()){

           int len = Array.getLength(obj);

           for(int i=0; i<len; i++){

              System.out.println(Array.get(obj, i));

       }

       }else{

           System.out.println(obj);

       }

}

目前还无法通过数组中的元素获得数组的类型,因为基本数据无法使用getClass()方法,引用数据可以使用该方法。因为getClass()方法属于Object类。通过getClass().getName()获得该元素的类型。考虑到多态,无法确定数组的类型。

 

    几种集合的比较

    ArrayList,内部存入的是对象的引用,不是对象本身,所以同一个对象可以放入多次。有索引,按照顺序排列。

HashSet,存入的也是对象的引用。先判断该对象是否存在,如果已经存在就不放入;不存在的话,再通过哈希值排序后存入。要替换已有的相同对象,必须先删除原有的对象,再存入新对象。

按照hashCode()和equals()方法进行比较。将HashSet中的元素按照哈希值分成若干区域,先将对象按照哈希值放入对应的区域,再在区域内使用哈希值和equals()排序。这种算法只在HashSet集合中才有效。只有哈希值和equals()都相等,才能是同一个元素。在其他集合中,不需要按照哈希值排序。

当一个对象被存入HashSet集合后,就不能修改对象中参与计算哈希值的变量了。对于元素的操作,都是在区域中完成,如果元素的哈希值变化,会进入其他区域,也就无法操作,例如remove()、contains()。

内存泄露:对于某些不用的数据,虽然代码上已经被释放,但实际内存里并没有释放,一直在运行。或者调用了系统底层的资源,但是没有关闭资源。日积月累,内存会被消耗殆尽。例如,在HashSet集合中,由于修改了元素的哈希值,导致无法对元素进行操作,尽管代码上的确进行了操作。

 

反射的作用——实现框架功能

先建框架,然后再写类。运行时,框架调用这些类。

使用别人的类的方式有两种:

1、直接调用别人的类。

2、通过反射,让别人的类调用我的类。

自己的类是工具,别人的类是框架。通过工具类,使得框架合乎自己的需求。例如,框架是房子,我的类是门、窗户等配件。

框架解决的核心问题:写框架时,还不知道需要调用什么类,需要使用反射Class.forName(“类名”),等到类写好后,框架获得类名即可使用类、操作类的对象。

类的名字一般放在配置文件中。创建一个File文件config.properties作为配置文件,设置className=类名,需要时可以通过更改配置文件,将框架改造成自己需要的样子。配置文件放在工程目录下。举例,写个小框架

InputStream ips = new FileInputStream("config.properties");

Properties props = new Properties();

props.load(ips);

ips.close();

String classname = props.getProperty("className"); 

Collection collections = (Collection)Class.forName(classname).newInstance();

通过配置文件创建想要的对象,更改classname对应的类,即可获得其他想要的对象,例如,HashSet对象。

 

类加载器

得到配置文件的方式:

1、一定要使用完整的路径,但完整的路径不是固定的,是利用getRealPath()方法运算获得。项目的路径不固定,但配置文件在项目内的路径是固定的,例如,金山词霸\\配置文件,金山词霸安装位置不固定,但配置文件的目录是固定的。

这种方式还可以保存对配置文件的更改,使用更广泛。

2、类加载器(ClassLoader)会将.class文件和配置文件都加载进内存,所以可以使用类加载器加载指定的配置文件。编写代码时,将配置文件放在classpath目录下,Eclipse放在src目录或子目录下;编译后,Eclipse会自动将所有源代码编译后的.class文件存放到classpath目录或bin目录下,并将非.class文件原样复制到这些目录下。例如,

InputStream ips =

              ReflectTest2.class.getClassLoader().getResourceAsStream("cn/itcast/day1/config.properties");//从根目录开始向下找配置文件。

    但这种方式只读,不能改,有局限性。SSH使用的都是类加载器。

    Class内部提供了直接获得配置文件的方法,跳过了类加载器。但底层仍然是在调用类加载器。默认在该.class文件的包中,不是的话,使用绝对路径:自己指定包,或者使用根目录。例如,

InputStream ips = ReflectTest2.class.getResourceAsStream("config.properties");

 

 

 

 

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值