龙空技术网

java技术专家带你深入java核心技术:反射+继承设计技巧+枚举类

程序员高级码农II 1781

前言:

如今小伙伴们对“java枚举反射”大约比较关心,姐妹们都想要知道一些“java枚举反射”的相关内容。那么小编在网上搜集了一些有关“java枚举反射””的相关资讯,希望大家能喜欢,姐妹们一起来学习一下吧!

反射

反射库(reflection library)提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵Java代码的程序。这项功能被大量地应用于JavaBeans中,它是Java组件的体系结构。(有关JavaBeans的详细内容在卷II中阐述。)使用反射,Java可以支持Visual Basic用户习惯使用的工具。

特别是在设计或运行中添加新类时,快速地应用开发工具能够动态地查询新添加类的能力。

能够分析类能力的程序被称为反射(reflective)。反射机制的功能极其强大。在下面的章节中可以看到,可以用反射机制:

• 在运行时分析类的能力。

• 在运行时查看对象,例如,编写一个toString方法供所有类使用。

• 实现数组的操作代码。

• 利用Method对象,这个对象很像C++这类语言中的函数指针。

反射是一种功能强大且复杂的机制。使用它的主要对象是工具构造者,而不是应用程序员。

如果仅对设计应用程序感兴趣,而对构造工具不感兴趣,就可以跳过本章的剩余部分,稍后再返回来学习。

Class类

在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息保存着每个对象所属的类足迹。虚拟机利用运行时信息选择相应的方法执行。

然而,可以通过专门的Java类访问这些信息。保存这些信息的类被称为Class,这个名字很容易让人混淆。Object类中的getClass( )方法将会返回一个Class类型的实例。

Employee e;

. . .

Class cl = e.getClass( );

如同用一个Employee对象表示一个特定的雇员属性一样,一个Class对象将表示一个特定类的属性。最常用的Class方法是getName。这个方法将返回类的名字。例如,下面这条语句:

System.out.println(e.getClass( ).getName( ) + " " + e.getName( ));

如果e是一个雇员,则会打印输出:

Employee Harry Hacker

如果e是经理,则会打印输出:

Manager Harry Hacker

还可以利用静态方法forName获得字符串对应的Class对象。

String className = "java.util.Date";

Class cl = Class.forName(className);

如果类名保存在字符串中,并可在运行中改变,那么就可以使用这个方法。当然,这个方法只有在className是类名或接口名时才能够执行。否则,forName方法将抛出一个checked exception(已检查异常)。无论何时使用这个方法,都应该提供一个异常处理器(exception handler)。如何提供一个异常处理器详见本节后面的“捕获异常”部分。

虚拟机为每个类型管理一个Class对象。因此,可以利用 = = 运算符实现两个类对象比较的操作。例如,

if (e.getClass( ) = = Employee.class) . . .

还有一个很有用的方法newInstance( ),可以用来快速地创建一个类的实例。例如,

e.getClass( ).newInstance( );

创建了一个与e具有相同类类型的实例。newInstance方法调用默认的构造器(没有参数的构造器)初始化新创建的对象。如果这个类没有默认的构造器,就会抛出一个异常。

将forName和newInstance配合起来使用,可以根据存储在字符串中的类名创建一个对象。

String s = "java.util.Date";

Object m = Class.forName(s).newInstance( );

捕获异常

我们将在第11章中全面地讲述异常处理机制,但现在时常遇到一些方法需要抛出异常。

当程序运行过程中发生错误时,就会“抛出异常”。抛出异常比终止程序要灵活得多,这是因为可以提供一个“捕获”异常的处理器(handler),来对异常情况进行处理。

如果没有提供处理器,程序就会终止,并在控制台上打印出一条信息,其中给出了异常的类型。大家可能在前面已经看到过一些异常报告,例如,偶然使用了null引用或者数组越界等。

异常有两种类型:未检查异常和已检查异常。对于已检查异常,编译器将会检查是否提供了处理器。然而,有很多常见的异常,例如,访问null引用,都属于未检查异常。编译器不会查看是否为这些错误提供了处理器。毕竟,应该精心地编写代码以避免这些错误的发生,而不要将精力花在编写异常处理器上。

并不是所有的错误都是可以避免的。如果竭尽全力还是发生了异常,那么编译器就要求提供一个处理器。Class.forName方法就是一个抛出已检查异常的例子。在第11章中,将会看到几种异常处理的策略。现在,只介绍一下如何实现最简单的处理器。

将可能抛出已检查异常的一个或多个方法调用代码放置在try块中,然后在catch子句中提供处理器代码。

下面是一个例子:

如果类名不存在,将跳过try块中的剩余代码,程序直接进入catch子句。(这里,利用Throwable类的printStackTrace方法打印出栈的轨迹。Throwable是Exception类的超类。)如果try块中没有抛出任何异常,将跳过catch子句的处理器代码。

对于已检查异常,只需要提供一个异常处理器。可以很容易地发现会抛出已检查异常的方法。如果调用了一个抛出已检查异常的方法,而又没有提供处理器,编译器就会给出错误报告。

java.lang.Class 1.0

• static Class forName(String className)

返回类名为className的Class对象。

• Object newInstance( )

返回该类的一个新实例。

java.lang.reflect.Constructor 1.1

• Object newInstance(Object[ ] args)

构造一个该构造器所代表的类的新实例。

参数:args

这是提供给构造器的参数。有关如何提供参数的详细情况请参阅有关反射的论述

java.lang.Throwable 1.0

• void printStackTrace( )

将Throwable对象和栈的轨迹输出到标准错误流。

5.5.2 使用反射分析类的能力

下面简要地介绍一下反射机制最重要的内容—检查类的结构。

在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器。

这三个类都有一个叫做getName的方法,用来返回相应条目的名称。Field类有一个getType方法,用于返回描述域所属类型的Class类型对象。Method和Constructor类含有能够报告参数类型的方法,

Method类还可以报告返回类型。这三个类还有一个叫做getModifiers的方法,它将返回一个整型数值,用不同的位开关设置描述public和static这些修饰符的使用状况。另外,还可以利用java.lang.reflect包中的Modifier类的静态方法分析getModifiers返回的整型数值。例如,可以使用Modifier类中的isPublic、isPrivate或isFinal判断方法或构造器是否是public、private或final。我们需要做的全部工作就是调用Modifier类的相应方法,并对getModifiers返回的整型数值进行分析,另外,还可以利用Modifier.toString方法将修饰符打印出来。

Class类中的getFields、getMethods和getConstructors方法将分别返回类支持的public域、方法和构造器数组,其中包括超类的公有成员。Class类的getDeclareFields、getDeclareMethods和getDeclaredConstructors方法将分别返回类中声明的全部域、方法和构造器组成的数组。其中包括私有和受保护成员,但不包括超类的成员。

例5-5显示了如何打印一个类的全部信息。这个程序将提醒用户输入类名,然后输出类中所有的方法和构造器的签名,以及全部域名。假如用户输入:

java.lang.Double

程序将会输出:

值得注意,这个程序可以用来分析Java解释器能够加载的任何类,而不仅仅是编译这个程序时可以使用的类。在下一章中,还将利用这个程序查看Java编译器自动生成的内部类。

例5-5 ReflectionTest.java

java.lang.Class 1.0

• Field[ ] getFields( ) 1.1

• Field[ ] getDeclaredFields( ) 1.1

getFields方法将返回一个包含Field对象的数组,这些对象记录了该类或其超类的公有域。

getDeclaredField方法也将返回包含Field对象的数组,这些对象记录了该类的全部域。如果类中没有域,或者Class对象描述的是基本类型或数组类型,那么这个方法将返回一个长度为0的数组。

• Method[ ] getMethods( ) 1.1

• Method[ ] getDeclaredMethods( ) 1.1

返回包含Method对象的数组:getMethods将返回所有的公有方法,包括从超类继承而来的公有方法;getDeclaredMethods返回该类或接口的全部方法,但不包括由超类继承了的方法。

• Constructor[ ] getConstructors( ) 1.1

• Constructor[ ] getDeclaredConstructors( ) 1.1

返回包含Constructor对象的数组,其中包含Class对象所描述的类的所有公有构造器(get

Constructors)或所有构造器(getDeclaredConstructors)

java.lang.reflect.Field 1.1

java.lang.reflect.Method 1.1

java.lang.reflect.Constructor 1.1

• Class getDeclaringClass( )

返回一个用于描述类中定义的构造器、方法或域的Class对象。

• Class[ ] getExceptionTypes( )(在Constructor和Method类中)

返回一个用于描述方法抛出的异常类型的Class对象数组。

• int getModifiers( )

返回一个用于描述构造器、方法或域的修饰符的整型数值。使用Modifier类中的这个方法可以分析这个返回值。

• String getName( )

返回一个用于描述构造器、方法或域名的字符串。

• Class[ ] getParameterTypes( )(在Constructor和Method类中)

返回一个用于描述参数类型的Class对象数组。

• Class getReturnType( )(在Method类中)

返回一个用于描述返回类型的Class对象。

java.lang.reflect.Modifier 1.1

• static String toString(int modifiers)

返回对应modifiers位设置的修饰符的字符串表示。

• static boolean isAbstract(int modifiers)

• static boolean isFinal(int modifiers)

• static boolean isInterface(int modifiers)

• static boolean isNative(int modifiers)

• static boolean isPrivate(int modifiers)

• static boolean isProtected(int modifiers)

• static boolean isPublic(int modifiers)

• static boolean isStatic(int modifiers)

• static boolean isStrict(int modifiers)

• static boolean isSynchronized(int modifiers)

• static boolean isVolatile(int modifiers)

这些方法将检测方法名中对应的修饰符在modifiers值中的位。

在运行时使用反射分析对象

从前面一节中,已经知道如何查看任意对象的数据域名称和类型:

• 得到对应的Class对象。

• 通过Class对象调用getDeclaredFields。

在本节中,将进一步查看数据域的实际内容。当然,在编写程序时,如果知道想要查看的域名和类型,查看指定的域内容是一件很容易的事情。但利用反射机制可以查看在编译时还不清楚的对象域。

查看对象域的关键方法是Field类中的get方法。如果f是一个Field类型的对象(例如,通过getDeclaredFields得到的对象),obj是某个包含f域的类的对象,那么f.get(obj) 将返回一个对象,其值

为obj域的当前值。这样说起来显得有点抽象,让我们看看下面这个例子的运行。

实际上,这段代码存在一个问题。由于name是一个私有域,所以get方法将会抛出一个IllegalAccessException。只有利用get方法才能得到可访问域的值。除非拥有访问权限,否则Java安全机制只允许查看任意对象有哪些域,而不允许读取它们的值。

反射机制的默认行为受限于Java的访问控制。然而,如果一个Java程序没有受到安全管理器的控制,就可以覆盖访问控制。为了达到这个目的,需要调用Field、Method或Constructor对象的setAccessible方法。例如,

f.setAccessible(true); //now OK to call f.get(harry);

setAccessible方法是AccessibleObject类中的一个方法,它是Field、Method和Constructor类的公共超类。

这个特性是为调试、持久存储和相似机制提供的。本书稍后将利用它编写一个通用的toString方法。

get方法还有一个需要解决的问题。name域是一个String,因此把它作为Object返回没有什么问题。但是,假定我们想要查看salary域。它属于double类型,而Java中数值类型不是对象。要想解决这个问题,可以使用Field类中的getDouble方法,也可以调用get方法,此时,反射机制将会自动地将这个域值打包到相应的对象包装器中,这里将打包成Double。

当然,可以获取就可以设置。调用f.set(obj, value) 可以将obj对象的f域设置成新值。

例5-6显示了如何编写一个可供任意类使用的通用toString方法。其中使用getDeclaredFileds获得所有的数据域,然后使用setAccessible将所有的域设置为可访问的。对于每个域,得到了名字和值,并递归调用toString方法,将每个值转换成字符串。

在例5-6的全部代码中,需要解释几个复杂的问题。循环引用将有可能导致无限地递归。因此,ObjectAbalyzer需要记录已经被访问过的对象。另外,为了能够查看数组内部,需要另外一种方式。有关这种方式的具体内容将在下一节中详细地论述。

可以使用toString方法查看任意对象的内部信息。例如,下面这个调用:

将会产生下面的打印结果:

还可以使用通用的toString方法实现自己类中的toString方法,如下所示:

这是一种公认的提供toString方法的手段,在编写程序时会发现,它是非常有用的。

例5-6 ObjectAnalyzerTest.java

java.lang.reflect.AccessibleObject 1.2

• void setAccessible(boolean flag)

为反射对象设置可访问标志。flag为true表明屏蔽Java语言的访问检查,使得对象的私有属性也可以被查询和设置。

• boolean isAccessible( )

返回反射对象的可访问标志的值。

• static void setAccessible(AccessibleObject[ ] array, boolean flag)

是一种设置对象数组可访问标志的快捷方法。

java.lang.Class 1.1

• Field getField(String name)

• Field[ ] getFields( )

返回给定名称的公有域,或包含所有域的数组。

• Field getDeclaredField(String name)

• Field[ ] getDeclaredFields( )

返回类中声明的给定名称的域,或者包含声明的全部域的数组。

java.lang.reflect.Field 1.1

• Object get(Object obj)

返回obj对象中用Field对象表示的域值。

• void set(Object obj, Object newValue)

用newValue设置Obj对象中用Field对象表示的域。

5.5.4 使用反射编写通用的数组代码

java.lang.reflect包中的Array类允许动态地创建数组。例如,将这个特性应用到第3章中讲到的arrayCopy方法时,可以在保留当前数组内容的同时动态地扩展现有数组。

我们要解决的是一个十分有代表性的问题。假设有一个元素为某种类型且已经被填满的数组,希望扩展它的长度,但不想手工编写那些扩展、拷贝元素的代码,而是想编写一个用于扩展数组的通用方法:

如何编写这样一个通用的方法呢?正好能够将Employee[ ]数组转换为Object[ ]数组,这让人感觉很有希望。下面我们尝试着编写一个通用的方法,其功能是将数组扩展到10%+10个元素(这是因为对于小型数组来说,扩展10%显得有点少)。

然而,在实际使用结果数组时会遇到一个问题。这段代码返回的数组类型是对象数组(Object[ ])类型,这是由于使用下面这行代码创建的数组:

new Object[newLength]

一个对象数组(Object[ ])不能转换成雇员数组(Employee[ ])。如果这样做,在运行时,Java将会产生ClassCastException异常。前面已经看到,Java数组会记住每个元素的类型,即创建数组时new表达式中使用的元素类型。将一个Employee[ ]临时地转换成Object[ ]数组,然后再把它转换回来是可以的,但一个从开始就是Object[ ]的数组却永远不能转换成Employee[ ]数组。为了编写这类通用的数组代码,需要能够创建与原数组类型相同的新数组。为此,需要java.lang.reflect包中Array类的一些方法。其中最关键的是Array类中的静态方法newInstance,它能够构造新数组。在调用它时必须提供两个参数,一个是数组的元素类型,一个是数组的长度。

Object newArray = Array.newInstance(componentType, newLength);

为了能够执行这条语句,需要知道新数组的长度和元素类型。

可以通过调用Array.getLength(a) 获得数组的长度,也可以通过Array类的静态getLength方法的返回值得到任意数组的长度。而要获得新数组元素类型,就需要进行以下工作:

1)首先获取a数组的类对象。

2)确认它是一个数组。

3)使用Class类(只能定义表示数组的类对象)的getComponentType方法确定数组对应的类型。

为什么getLength是Array的方法,而getComponentType是Class的方法呢?我们也不清楚。反射方法的分类有时确实显得有点古怪。

下面是这段代码:

请注意,arrayGrow方法可以用来扩展任意类型的数组,而不仅是对象数组。

int[ ] a = {1, 2, 3, 4};

a = (int[ ]) goodArrayGrow(a);

为了能够实现上述操作,应该将goodArrayGrow的参数声明为Object类型,而不要声明为对象数组(Object[ ])。整型数组类型int[ ]可以被转换成Object,但不能转换成对象数组。

例5-7显示了两个扩展数组的方法。请注意,将badArrayGrow的返回值进行类型转换将会抛出一个异常。

例5-7 ArrayGrowTest.java

java.lang.reflect.Array 1.1

• static Object get(Object array,int index)

• static xxx getXxx(Object array,int index)

(Xxx是boolean、byte、char、double、float、int、long、short之中的一种基本类型。)这些方法将返回存储在给定位置上的给定数组的内容。

• static void set(Object array, int index, Object newValue)

• static setXxx(Object array, int index, Xxx newValue)

(Xxx是boolean、byte、char、double、float、int、long、short之中的一种基本类型。)这些方法将一个新值存储到给定位置上的给定数组中。

• static int getLength(Object array)

返回数组的长度。

• static Object newInstance(Class componentType, int length)

• static Object newInstance(Class componentType, int[ ] lengths)

返回一个具有给定类型、给定维数的新数组。

方法指针

从表面上看,Java没有提供方法指针,即将一个方法的存储地址传给另外一个方法,以便第二个方法能够随后调用它。事实上,Java的设计者曾说过:方法指针是很危险的,并且常常会带来隐患。他们认为 Java提供的接口(interface)(将在下一章讨论)是一种更好的解决方案。然而,在Java的JDK 1.1中方法指针已经作为反射包的(也许是)副产品出现了。

为了能够看到方法指针的工作过程,先回忆一下利用Field类的get方法查看对象域的过程。与之类似,在Method类中有一个invoke方法,它允许调用包装在当前Method对象中的方法。invoke方法的签名是:

Object invoke(Object obj, Object. . .args)

第一个参数是隐式参数,其余的对象提供了显式参数。(在JDK 5.0以前的版本中,必须传递一个对象数组,如果没有显式参数就传递一个null。)

对于静态方法,第一个参数可以被忽略,即可以将它设置为null。

例如,假设用ml代表Employee类的getName方法,下面这条语句显示了调用这个方法的格式:

String n = (String)m1.invoke(harry);

如果参数或返回类型不是类而是基本类型,那么在调用Field类的get和set方法时会存在一些问题。需要依靠自动打包功能将其打包,或者在JDK 5.0之前将基本类型打包成对应的包装器。

相反,如果返回类型是一种基本类型,invoke方法将返回包装器类型。例如,假设m2代表Employee类的getSalary方法,那么返回的实际类型是Double,因此必须相应地进行类型转换。对于JDK 5.0来说,这项操作可以由自动拆包来完成。

double s = (Double) m2.invoke(harry);

如何得到Method对象呢?当然,可以通过调用getDeclareMethod方法,然后对返回的Method对象数组进行查找,直到发现想要的方法为止。也可以通过调用Class类中的getMethod方法得到想要的方法。它与getField方法类似。getField方法根据表示域名的字符串,返回一个Field对象。然而,有可能存在若干个相同名字的方法,因此要格外地小心,以确保能够准确地得到想要的那个方法。鉴于此原因,还必须提供想要的方法的参数类型。getMethod的签名是:

Method getMethod(String name, Class. . . parameterTypes)

例如,下面说明了如何获得Employee类的getName方法和raiseSalary方法的方法指针。

Method m1 = Employee.class.getMethod("getName");

Method m2 = Employee class.getMethod("raiseSalary", double.class);

(对于JDK 5.0以前的版本,必须将Class对象包装到一个数组中,如果没有参数,需要给出一个null。)

到此为止,我们已经学习了使用Method对象的规则。下面看一下如何将它们组织在一起。例5-8是一个打印诸如Math.sqrt、Math.sin这样的数学函数值表的程序。打印的结果如下所示:

当然,这段打印数学函数表格的代码与具体打印的数学函数无关。

在这里,f是一个Method类型的对象。由于正在调用的方法是一个静态方法,所以invoke的第1个参数是null。

为了将Math.sqrt函数表格化,需要将f设置为:

Math.class.getMethod("sqrt", double.class)

这是Math类中的一个方法,通过参数向它提供了一个函数名sqrt和一个double类型的参数。

例5-8给出了通用制表和两个测试程序的全部代码。

例5-8 MethodPointerTest.java

上述程序清楚地表明,可以使用method对象实现C(或C#中的委派)语言中函数指针的所有操作。同C一样,这种程序设计风格并不太简便,出错的可能性也比较大。如果在调用方法的时候提供了一个错误的参数,invoke方法将会抛出一个异常。

另外,invoke的参数和返回值必须是Object类型的。这就意味着必须进行多次的类型转换。这样做将会使编译器错过检查代码的机会。因此,等到测试阶段才会发现这些错误,找到并改正它们将会更加困难。不仅如此,使用反射获取方法指针的代码要比仅仅直接调用方法明显慢一些。

鉴于此原因,建议仅在必要的时候,才使用Method对象,而最好使用接口和内部类(下一章中介绍)。特别要重申:建议Java开发者不要使用Method对象的回调功能。使用接口进行回调会使得代码的执行速度更快,更易于维护。

java.lang.reflect.Method 1.1

• public Object invoke(Object implicitParameter, Object[ ] explicitParameters)

调用该对象所描述的方法,传递给定参数,并返回方法的返回值。对于静态方法,把null作为隐式参数传递。在使用包装器传递基本类型的值时,基本类型的返回值必须是未包装的。

枚举类

我们在第3章已经看到,如何在JDK 5.0以后的版本中定义枚举类型。下面是一个典型的例子:

public enum Size{SMALL, MEDIUM, LARGE, EXTRA_LARGE};

实际上,这个声明定义的类型是一个类,它刚好有4个实例,在此尽量不要构造新对象。

因此,在比较两个枚举类型的值时,永远不需要调用equals,而直接使用“= =”就可以了。

如果需要的话,可以在枚举类型中添加一些构造器、方法和域。当然,构造器只是在构造枚举常量的时候被调用。下面是一个例子:

所有的枚举类型都是Enum类的子类。它们继承了这个类的许多方法。其中最有用的一个是

toString,这个方法能够返回枚举常量名。例如,Size.SMALL.toString( )将返回字符串"SMALL"。

toString的逆方法是静态方法valueOf。例如,语句:

Size s = (Size) Enum.valueOf(Size.class, "SMALL");

将s设置成Size.SMALL。

每个枚举类型都有一个静态的value方法,它将返回一个包含全部枚举值的数组:

Size[ ] values = Size.values( );

例5-9这个小程序演示了枚举类型的工作方式。

例5-9 EnumTest.java

java.lang.Enum 5.0

• static Enum valueOf(Class enumClass,String name)

返回给定名字、给定类的枚举常量。

• String toString( )

返回枚举常量名。

继承设计技巧

下面给出一些对设计继承关系很有帮助的建议,以此结束本章的内容。

1)将公共操作和域放置在超类。

这就是为什么将姓名(name)域放置在person类中,而没有将它放置在Employee和Student类中的原因。

2)不要使用受保护的域。

有些程序员认为,将大多数的实例域定义为protected是一个不错的主意,只有这样,子类才能够在需要的时候直接访问它们。然而,protected机制并不能够带来更好的保护,其原因主要有两点。第一,子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected的实例域,从而破坏了封装性。第二,在Java程序设计语言中,在同一个包中的所有类都可以访问proteced域,而不管它是否为这个类的子类。

3)使用继承实现“is-a”关系。

使用继承很容易达到节省代码的目的,但有时候也被人们滥用了。例如,假设需要定义一个钟点工(Contractor)类。钟点工的信息包含姓名和雇佣日期,但是没有薪水。他们按小时计薪,并且不会因为拖延时间而获得加薪。这似乎在诱导人们由Employee派生出子类Contractor,然后再增加一个hourlyWage域。

class Contractor extends Employee{ . . .private double hourlyWage;}

这并不是一个好注意。因为这样一来,每个钟点工对象中都包含了薪水和计时工资这两个域。在实现打印支票或税单方法的时候,会带来无尽的麻烦,并且会多写很多代码。

钟点工与雇员之间不属于“is-a”关系。钟点工不是特殊的雇员。

4)除非所有继承的方法都有意义,否则不要使用继承。

假设想编写一个Holiday类。毫无疑问,每个假日也是一日,并且一日可以用GregorianCalendar类的实例表示,因此可以使用继承。

class Holiday extends GregorianCalendar{. . .}

很遗憾,在继承的操作中,假日集不是封闭的。在GregorianCalendar中有一个公有方法add,可以将假日转换成非假日:

Holiday christmas;

christmas.add(Calendar.DAY_OF_MONTH, 12);

因此,继承对于这个例子来说并不太适宜。

5)在覆盖方法的时候,不要改变预期的行为。

置换原则不仅应用于语法,而且也可以应用于行为,这似乎更加重要。在覆盖一个方法的时候,不应该毫无原由地改变行为的内涵。就这一点而言,编译器不会提供任何帮助,即编译器不会检查重新定义的方法是否有意义。例如,可以重定义Holiday类中add方法“修正”原方法的问题,或什么也不做,或抛出一个异常,或继续到下一个假日。然而这些都违反了置换原则。

语句序列

int d1 = x.get(Calendar.DAY_OF_MONTH);x.add(Calendar.DAY_OF_MONTH, 1);int d2 = x.get(Calendar.DAY_OF_MONTH);System.out.println(d2-d1);

不管x属于GregorianCalendar类,还是属于Holiday类,执行上述语句后都应该得到预期的行为。

当然,这样可能会引起某些争议。人们可能就预期行为的含义争论不休。例如,有些人争论说,置换原则要求Manager.equals不处理bonus域,因为Employee.equals没有它。实际上,凭空讨论这些问题毫无意义。关键在于,在覆盖子类中的方法时,不要偏离最初的设计想法。

6)使用多态,而非类型信息。

无论什么时候,对于下面这种形式的代码:

if (x is of type 1)action1(x)else if (x is of type 2)action2(x);

都应该考虑使用多态性。

action1与action2表示的是相同的概念吗?如果是相同的概念,就应该为这个概念定义一个方法,并将其放置在两个类的超类或接口中,然后,就可以调用x.action( );

以便使用多态性提供的动态分派机制执行相应的动作。

使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展。

7)不要过多地使用反射。

反射机制使得人们可以通过在运行时查看域和方法,让人们编写出更具有通用性的程序。

这种功能对于编写系统程序来说极其实用,但是通常不适于编写应用程序。反射是很脆弱的,即编译器很难帮助人们发现程序中的错误。任何错误只能在运行时才被发现,并导致异常。

觉得文章还不错的话,可以转发关注小编,之后持续更新干货文章~~~

标签: #java枚举反射