第2部分 Java程序设计
本部分主要以Java设计语言为基础,通过大量实际的例子分析各大公司Java面试题目,从技术上分析面试题的内涵。一般公司的面试题都是两套——C++或Java,面试者可以选择。
许多面试题看似简单,却需要深厚的基本功才能给出完美的解答。企业要求面试者写一个最简单的final方法就可看出面试者在技术上究竟达到了怎样的程度,我们能真正写好一个final方法吗?我们都觉得自己能,可是我们写出的final很可能只能拿到10分中的2分。读者可从本部分中关于Java的几个常见考点,如reflection问题、I/O问题、面向对象及接口等方面看看自己属于什么样的层次。此外,还有一些面试题考查面试者敏捷的思维能力。
分析这些面试题,本身包含很强的趣味性。而作为一名研发人员,通过对这些面试题的深入剖析则可进一步增强自身的内功。
第5章 Java程序设计基本概念
对于一个求职者或者应届毕业生来说,公司除了对项目经验有所问询之外,最好的考试办法就是检查基本功,包括编程风格,以及对赋值语句、递增语句、类型转换、数据交换等程序设计基本概念的理解。当然,在考试之前最好对自己所掌握的程序概念知识有所了解,尤其是对各种细致的考点要加以重视。本章考题来自真实的笔试资料,希望读者先不要看答案,自我解答后再与答案加以对比,找出自己的不足。
5.1 JVM
面试例题1:下面给出的Java中ClassLoader中的描述,哪些描述是正确的?()
A.ClassLoader没有层次关系
B.所有类中的ClassLoader都是AppClassLoader
C.通过Class.forName(String className),能够动态加载一个类
D.不同的ClassLoader加载同一个Class文件,所得的类是相同的
解析:
A选项错误,ClassLoader具备层次关系。
B选项错误,ClassLoader不止一种。
D选项错误,不同的类装载器分别创建的同一个类的字节码数据属于完全不同的对象,没有任何关联。
答案:C
扩展知识:ClassLoader知识。
(1)ClassLoader基本概念
与C或C++编写的程序不同,Java程序并不是一个可执行文件,而是由许多独立的类文件组成的,每一个文件对应一个Java类。此外,这些类文件并非全部装入内存,而是根据程序需要逐渐载入。ClassLoader是JVM实现的一部分,ClassLoader包括bootstrap classloader(启动类加载器),ClassLoader在JVM运行的时候加载Java核心的API,以满足Java程序最基本的需求,其中就包括用户定义的ClassLoader,这里所谓的用户定义,是指通过Java程序实现的两个ClassLoader:一个是ExtClassLoader,它的作用是用来加载Java的扩展API,也就是/lib/ext中的类;第二个是AppClassLoader,它是用来加载用户机器上CLASSPATH设置目录中的Class的,通常在没有指定ClassLoader的情况下,程序员自定义的类就由该ClassLoader进行加载。
(2)ClassLoader加载流程
当运行一个程序的时候,JVM启动,运行bootstrap classloader,该ClassLoader加载Java核心API(ExtClassLoader和AppClassLoader也在此时被加载),然后调用ExtClassLoader加载扩展API,最后AppClassLoader加载CLASSPATH目录下定义的Class,这就是一个程序最基本的加载流程。
下面来看一下ClassLoader中的一段代码:
protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先检查该name指定的class是否有被加载 Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { //如果parent不为null,则调用parent的loadClass进行加载 c = parent.loadClass(name, false); } else { //parent为null,则调用BootstrapClassLoader进行加载 c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { //如果仍然无法加载成功,则调用自身的findClass进行加载 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
从上面一段代码中可以看出,一个类加载的过程使用了一种父类委托模式。为什么要使用这种父类委托模式呢?
第1个原因就是这样可以避免重复加载,当父类已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
第2个原因就是考虑到安全因素,如果不使用这种委托模式,那么可以随时使用自定义的String来动态替代Java核心API中定义的类型,这样会存在非常大的安全隐患,而父类委托的方式就可以避免这种情况,因为String已经在启动时被加载,所以,用户自定义类是无法加载一个自定义的ClassLoader。
(3)一些重要的方法
1)loadClass方法。
ClassLoader.loadClass() 是ClassLoader的入口点。该方法的定义如下:
Class loadClass( String name, boolean resolve );
name是指JVM需要的类的名称,如Foo或java.lang.Object。resolve参数告诉方法是否需要解析类。在准备执行类之前,应考虑类解析。注意:并不总是需要解析,如果JVM只需要知道该类是否存在或找出该类的超类,那么就不需要解析。
2)defineClass方法。
defineClass方法接受由原始字节组成的数组,并把它转换成Class对象。原始数组包含如从文件系统或网络装入的数据。defineClass管理JVM的许多复杂的实现层面——它把字节码分析成运行时数据结构、校验有效性等。因为defineClass方法被标记成final的,所以也不能覆盖它。
3)findSystemClass方法。
findSystemClass方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass将原始字节转换成Class对象,以将该文件转换成类。当运行Java应用程序时,这是JVM正常装入类的默认机制。对于定制的ClassLoader,只有在尝试其他方法装入类之后,再使用findSystemClass。这是因为ClassLoader是负责执行装入类的相关步骤,不负责所有类的所有信息。例如,即使ClassLoader从远程的Web站点装入了某些类,仍然需要在本地机器上装入大量的基本Java库。而这些类库不是我们所关心的,所以要JVM以默认方式从本地文件系统装入它们,这就是findSystemClass的用途。
4)resolveClass方法。
正如前面所提到的,可以不完全地(不带解析)装入类,也可以完全地(带解析)装入类。当编写我们自己的loadClass时,可以调用resolveClass,这取决于loadClass的resolve参数的值。
5)findLoadedClass方法。
findLoadedClass充当一个缓存:当请求loadClass装入类时,它调用该方法来查看ClassLoader是否已装入这个类,这样可以避免重新装入已存在类所造成的麻烦。
6)findClass方法。
loadClass默认实现调用这个新方法。findClass的用途包含ClassLoader的所有特殊代码,而无须复制其他代码(例如,当专门的方法失败时,调用系统ClassLoader)。
目的是从本地文件系统使用实现的类装载器装载一个类。为了创建自己的类装载器,应该扩展ClassLoader类,这是一个抽象类。可以创建一个FileClassLoaderextends ClassLoader,然后覆盖ClassLoader中的findClass(String name)方法,这个方法通过类的名字得到一个Class对象。
public Class findClass(String name) { byte [] data = loadClassData(name); return defineClass(name, data, 0 , data.length); }
7)getSystemClassLoader方法。
如果覆盖findClass或loadClass,getSystemClassLoader能以实际的ClassLoader对象来访问系统ClassLoader(而不是固定地从findSystemClass调用它)。为了将类请求委托给父类ClassLoader,这个新方法允许ClassLoader获取它的父类Class Loader。当使用特殊方法,定制的ClassLoader不能找到类时,可以使用这种方法。
父类ClassLoader被定义成创建该ClassLoader所包含代码的对象的ClassLoader。
8)forName方法。
Class类中有一个静态方法forName,这个方法和ClassLoader中的loadClass方法的目的一样,都是用来加载class的,但是两者在作用上却有所区别。
Class clazz = Class.forName("something");
或者
ClassLoadercl = Thread.currentThread().getContextClassLoader(); Class clazz = cl.loadClass("something");
Class.forName()调用Class.forName(name, initialize, loader);也就是Class. forName("something");等同于Class.forName ("something", true, CALLCLASS. class.getClassLoader());
第二个参数“true”是用于设置加载类的时候是否连接该类,true就连接,否则就不连接。关于连接,在此解释一下,在JVM加载类的时候,需要经过三个步骤:装载、连接、初始化。装载就是找到相应的class文件,读入JVM;初始化就是class文件初始化。这里详述一下连接,连接分三步。
第一步是验证class是否符合规格。
第二步是准备,就是为类变量分配内存的同时设置默认初始值。
第三步就是解释,而这步是可选的,根据上面loadClass方法的第二个参数来判定是否需要解释,这里的解释是指根据类中的符号引用查找相应的实体,再把符号引用替换成一个直接引用的过程。
在Java API文档中,loadClass方法的定义是protected,也就是说,该方法是被保护的,而用户使用的方法是一个参数,一个参数的loadClass方法实际上就是调用了两个参数,第二个参数默认为false。因此,在这里可以看出通过loadClass加载类实际上就是加载的时候并不对该类进行解释,因此不会初始化该类。而Class类的forName方法则相反,使用forName加载的时候就会将Class进行解释和初始化。
面试例题2:Which characters does JVM use(JVM使用哪种字符表示)?()
A.ASCII characters
B.Unicode characters
C.Cp1252
D.UTF-8
解析:JVM的设计者当初决定JVM中所有字符的表示形式时,是不允许使用各种编码方式的字符并存的。这是因为如果在内存中的Java字符可以以GB2312、UTF-16、BIG5等各种编码形式存在,那么对开发者来说,连进行最基本的字符串打印、连接等操作都会寸步难行。例如,一个GB2312的字符串后面连接一个UTF-8的字符串,那么连接后的最终结果应该是什么编码呢?选哪一个都没有道理。
Java开发者必须牢记:在Java中字符只以一种形式存在,那就是Unicode(不选择任何特定的编码,直接使用它们在字符集中的编号,这是统一的唯一方法)。
但“在Java中”到底是指在哪里呢?是指在JVM中、在内存中、在你的代码里声明的每一个char、String类型的变量中。例如,你在程序中这样写:
char han='永';
在内存的相应区域,这个字符就表示为0x6c38,可以用下面的代码证明:
char han='永'; System.out.format("%x",(short)han);
输出是:6c38。反过来用Unicode编号来指定一个字符也可以,像这样:
char han=0x6c38; System.out.println(han);
输出是:永。
这其实也是说,只要你正确地读入了“永”字,那么它在内存中的表示形式一定是0x6c38,没有任何其他的值能代表这个字。
JVM的这种约定使得一个字符分为两部分:JVM内部和OS的文件系统。在JVM内部,统一使用Unicode表示,当这个字符被从JVM内部移到外部(即保存为文件系统中的一个文件的内容时),就进行了编码转换,使用了具体的编码方案。因此可以说,所有的编码转换只发生在边界的地方,JVM和OS的交界处,也就是各种输入/输出流(或者Reader,Writer类)起作用的地方。
所有的I/O基本上可以分为两大阵营:面向字符的输入/输出流;面向字节的输入/输出流。面向字符或者说面向字节中的所谓“面向”,是指这些类在处理输入/输出的时候,在哪个意义上保持一致。
如果面向字节,那么这类工作要保证系统中的文件二进制内容和读入JVM内部的二进制内容一致,不能变换任何0和1的顺序。这种输入/输出方式很适合读入视频文件或者音频文件,或者任何不需要做变换的文件内容。
而面向字符的I/O是指希望系统中的文件的字符和读入内存的“字符”(注意和字节的区别)要一致。例如,我们的中文版WindowsXP系统上有一个GBK的文本文件,其中有一个“永”字,这个字的GBK编码什么不用管,当我们使用面向字符的I/O把它读入内存并保存在一个char型变量中时,我希望I/O系统不要直接把“永”字的GBK编码放到这个字符(char)型变量中,我不关心这个char型变量具体的二进制内容到底是多少,我只希望这个字符读进来之后仍然是“永”字。
从这个意义上也可以看出,面向字符的I/O类,也就是Reader和Writer类,实际上隐式地做了编码转换,在输出时,将内存中的Unicode字符使用系统默认的编码方式进行了编码,而在输入时,将文件系统中已经编码过的字符使用默认编码方案进行了还原。这里要注意:Reader和Writer只会使用这个默认的编码来做转换,而不能为一个Reader或者Writer指定转换时使用的编码。这也意味着,如果使用中文版Windows XP系统,其中存放了一个UTF-8编码的文件,当采用Reader类来读入的时候,它还会使用GBK来做转换,转换后的内容当然不对。这其实是一种傻瓜式的功能提供方式,对大多数初级用户(以及不需要跨平台的高级用户)来说反而是一件好事。
如果用到GBK编码以外的文件,就必须采用编码转换:一个字符与字节之间的转换。因此,Java的I/O系统中能够指定转换编码的地方,也就在字符与字节转换的地方,那就是InputStreamReader和OutputStreamWriter。这两个类是字节流和字符流之间的适配器类,它们承担编码转换的任务。
答案:B
5.2 i++
面试例题1:下列程序的输出结果是多少?
public class Test { static { int x = 5; } static int x, y; public static void main(String[] args) { x--; myMethod(); System.out.println(x + y++ + x); } public static void myMethod() { y = x++ + ++x; } }
解析:不论是C++还是Java,对i++类的面试问题总是很多。这里要注意运算符的优先级问题。
对上面代码的解释如下:
public class Test { static{int x =5;} //在第一次被载入JVM时运行,但由于是局部变量,x=5不影响后面的值 static int x,y; //初始化时x=0;y=0; public static void main(String[] args) { x--; System.out.println(x); //步骤1:在运行myMethod();之前,x是-1,开始调用myMethod()函数 myMethod(); //步骤4:在运行myMethod();之后x是1,y是0 System.out.println(x+ y++ +x); //步骤5:运行x+(y++)+x=1+0+1=2。 } public static void myMethod() { y = x++ + ++x; System.out.println(y); //步骤2:进入myMethod()运行y = (x++) + (++x)后y=0 System.out.println(x); //步骤3:此时x=1 } }
答案:2
面试例题2:Given the following class:(给定下面的类:)
public class ZeroPrint{ public static void main(String argv[]){ int i =0; //Here } }
Which of the following lines if placed after the comment //Here will print out is not 0?(哪个选项替换掉类中的//Here不会输出结果0?)[Sun公司2006年10月面试题]
A.System.out.println(i++);
C.System.out.println(i);
B.System.out.println(i+'0');
D.System.out.println(i--);
解析:这道题主要考查后置操作符,选项A是自加运算,选项D是自减运算(但选项A和选项D都是先打印0,再自加或自减)。选项B中'0'是一个char类型,而不是一个数字类型。System.out.println(i+'0');的结果会得到48。
答案:B
面试例题3:下列程序的输出结果是()。
import java.util.*; public class Test { public static void main(String[] args) { int j = 0; for (int i = 0; i < 100; i++) { j = j++; } System.out.println(j); } }
A.0
B.99
C.100
D.101
解析:因为Java用了中间缓存变量的机制,所以,j=j++可换成如下写法:
temp=j; j=j+1; j=temp;
所以结果为0。
答案:A
面试例题4:If there are " int a=5,b=3;",the values of a and b are __ and __ after execute" if(!(a==b)&&(a==1+b++)){};".(假 如 "int a=5,b=3;",则 执 行 "if(!(a==b)&&(a==1+b++)){}; "后a和b的值分别为__和__。)[金山公司2005年面试题]
A.5,3
B.0,1
C.0,3
D.5,4
解析:表达式运算面试例题。因为“(!(a==b))”运算结束后,仍然继续执行(a==1+b++)),所以答案是5,4。
答案:D
面试例题5:以下代码的执行结果是多少?[金山公司2005年面试题]
import java.util.*; public class Test3 { public static void main(String[] args) { int i=0; i=i++ + ++i; int j=0; j=++j + j++ + j++ + j++; int k=0; k=k++ + k++ + k++ + ++k; int h=0; h=++h + ++h; int p1=0,p2=0; int q1=0,q2=0; q1=++p1; q2=p2++; System.out.println("i "+i); System.out.println("j "+j); System.out.println("k "+k); System.out.println("h "+h); System.out.println("p1 "+p1); System.out.println("p2 "+p2); System.out.println("q1 "+q1); System.out.println("q2 "+q2); } }
解析:i++和++i使用的不同点在于一个是程序完毕后自增,一个是程序开始前自增。
“i = i++ + ++i;”执行的过程是先执行i++,但是i自增1操作是稍后才执行,所以此时的i还是0,然后执行++i,++i后i的值是1,执行完++i后要补增i++,所以此时i的值实际上是2,0+2=2,然后赋值给i,最终i的值是2。
“j = ++j + j++ + j++ + j++;”执行的过程是先++j,所以j的值是1,然后执行j++,j++后j的值仍然是1,然后再执行j++,执行后的结果仍然是1,但要补增刚才的j++,所以此时j的值实际上是2,然后执行最后一个j++,执行后的结果仍然是2,但要补增刚才的j++,所以此时j的值实际上是3,所以1+1+2+3=7,然后赋值给j,最终j的值是7。
“k = k++ + k++ + k++ + ++k;”执行的过程是先k++,所以k的值是0,然后执行k++,k++后k的值仍然是0,但要补增刚才的k++,所以此时k的值实际上是1,然后再执行最后一个k++,执行后的结果仍然是1,但要补增刚才的k++,所以此时k的值实际上是2,最后执行++k,执行结果为3,再补增刚才的k++,k的实际结果是4。所以0+1+2+4=7,然后赋值给k,最终k的值是7。
“h = ++h + ++h;”是先自增h,h值为1,再自增h,h值为2。所以1+2=3,然后赋值给h,最终h的值是3。
“q1=++p1;”先自增p1,p1的值是1,再赋值给q1,所以q1的值是1。
“q2=p2++;”先赋值p2给q2,q2的值是0,然后自增p2,所以p2的值是1。
答案:
i=2,j=7,k=7,h=3,p1=1,p2=1,q1=1,q2=0。
5.3 类型转换
面试例题1:Which of the following will compile correctly? [中国杭州著名B2B软件公司2009年10月笔试题]
A.Short myshort = 99S;
C.float z = 1.0;
B.int t = "abc".length();
D.char c =17c;
解析:Short myshort = 99S;这句要执行自动装箱,调用shortValue方法,显然99S无法得到值。
将float z =1.0;改为float z =1.0f; 就行了,系统默认的浮点数是double型。
在Java中,length是属性,一般用来说明数组的长度;length()是方法,用来求数组中某个元素的字符串长度。例如:
String[] s = {"adfasf","sdfgs"}; s.length; s[1].length();
length()是字符串的方法,返回字符串的长度。
s[1].length()就是"sdfgs".length()值为5;
length是数组的属性,s. length的值为2。
答案:D
扩展知识:Java的数据类型转换。
Java的数据类型分为三大类,即布尔型、字符型和数值型,其中,数值型又分为整型和浮点型。相对于数据类型,Java的变量类型为布尔型boolean;字符型char;整型byte、short、int、long;浮点型float、double。其中四种整型变量和两种浮点型变量分别对应于不同的精度和范围。此外,编程时还经常用到两种类变量,即String和Date。
(1)数据类型转换的种类
Java数据类型的转换一般分三种,分别是:简单数据类型之间的转换、字符串与其他数据类型的转换、其他实用数据类型的转换。
(2)简单数据类型之间的转换
在Java中,整型、实型、字符型被视为简单数据类型,这些类型由低级到高级分别为(byte,short,char)—int—long—float—double。
简单数据类型之间的转换又可以分为:低级到高级的自动类型转换、高级到低级的强制类型转换、包装类过渡类型能够转换。
1)自动类型转换。
低级变量可以直接转换为高级变量,这叫自动类型转换。例如,下面的语句可以在Java中直接通过:
byte b;int i=b;long l=b;float f=b;double d=b;
如果低级类型为char型,向高级类型(整型)转换时,会转换为对应的ASCII码值,例如:
char c='c'; int i=c; System.out.println("output:"+i);
输出:output:99;
对于byte、short、char三种类型而言,它们是相同级别的,因此,不能相互自动转换,可以使用下述的强制类型转换。
short i=99;char c=(char)i;System.out.println("output:"+c);
输出:output:c;
2)强制类型转换。
将高级变量转换为低级变量时,情况会复杂一些,你可以使用强制类型转换。如:
int i=99;byte b=(byte)i;char c=(char)i;
这种转换可能会导致溢出或精度的下降。
3)包装类过渡类型转换。
Java的包装类就是可以直接将简单类型的变量表示为一个类。Java共有六个包装类,分别是Boolean、Character、Integer、Long、Float和Double,从字面上可以看出它们分别对应于boolean、char、int、long、float和double。而String和Date本身就是类,不存在包装类的概念。
在进行简单数据类型之间的转换(自动转换或强制转换)时,可以利用包装类进行中间过渡。一般情况下,首先声明一个变量,然后生成一个对应的包装类,就可以利用包装类的各种方法进行类型转换了。
例1,当希望把float型转换为double型时:
float f1=100.00f; Float F1=new float(f1); Double d1=F1.doubleValue();
例2,当希望把double型转换为int型时:
double d1=100.00; Double D1=new Double(d1); int i1=D1.intValue();
例3,当希望把int型转换为double型时,自动转换:
int i1=200; double d1=i1;;
例4,简单类型的变量转换为相应的包装类,可以利用包装类的构造函数。
Boolean(boolean value) Character(char value) Integer(int value) Long(long value) Float(float value) Double(double value)
利用这种方法也可以实现不同数值型变量间的转换,例如,对于一个双精度实型类,int Value()可以得到其对应的整型变量,而double Value()可以得到其对应的双精度实型变量。
(3)字符串型与其他数据类型的转换
通过查阅类库中各个类提供的成员方法可以看到,几乎从java.lang.Object类派生的所有类都提供了toString()方法,即将该类转换为字符串。例如,Characrer、Integer、Float、Double、Boolean、Short等类的toString()方法用于将字符、整数、浮点数、双精度数、逻辑数、短整型等类转换为字符串,如下所示:
class Test { public static void main(String args[]) { int i1 = 10; float f1 = 3.14f; double d1 = 3.1415926; Integer I1 = new Integer(i1); Float F1 = new Float(f1); Double D1 = new Double(d1); String si1=I1.toString(); String sf1=F1.toString(); String sd1=D1.toString(); System.out.println("si1" + si1); System.out.println("sf1" + sf1); System.out.println("sd1" + sd1); } }
(4)将字符型直接作为数值转换为其他数据类型
将字符型变量转换为数值型变量实际上有两种对应关系:一种是将其转换成对应的ASCII码;另一种是转换关系,例如,'1'就是指数值1,而不是其ASCII码,对于这种转换,可以使用Character的getNumericValue(char ch)方法。
面试例题2:下面代码的输出结果是()。[中国杭州著名B2B软件公司T2009年10月笔试题]
int i = 012; int j = 034; int k = (int)056L; int l = 078; System.out.println(i); System.out.println(j); System.out.println(k);
A.输出12,34,56
C.int k = (int)056L;行编译错误
B.输出10,28,46
D.int l = 078;行编译错误
解析:int l = 078;行编译错误,因为078是八进制,只能选0~7的数字,不应该有8。
答案:D
面试例题3:以下程序错误的是______。
A.short s=1;s=s+1;
B.short s=1;s+=1;
解析:s+1为int,不能直接赋值给short。
答案:A
5.4 程序结构
面试例题1:什么时候用assert?(API级的技术人员有可能会被问这个问题。)[中国台湾著名CPU生产厂商V2007年11月笔试题]
解析:assert是断言。断言是一个包含布尔表达式的语句,在执行这个语句时,假定该表达式为true,如果表达式计算为false,那么系统会报告一个Assertionerror。它用于调试目的。
assert(a > 0); // throws an Assertionerror if a <= 0
断言可以有以下两种形式:
assert Expression1; assert Expression1 : Expression2;
Expression1应该总是产生一个布尔值。Expression2可以是一个值的任意表达式,这个值用于生成显示更多调试信息的String消息。
断言在默认情况下是禁用的。要在编译时启用断言,需要使用source 1.4标记:javac-source 1.4Test.java。
要在运行时启用断言,可使用-enableassertions或者-ea标记。
要在运行时选择禁用断言,可使用-da或者-disableassertions标记。
要在系统类中启用断言,可使用-esa或者-dsa标记,还可以在包的基础上启用或者禁用断言。
答案:可以在预计正常情况下不会到达的任何位置上放置断言。断言可以用于验证传递给私有方法的参数。不过,断言不应该用于验证传递给公有方法的参数,因为不管是否启用了断言,公有方法都必须检查其参数。不过,既可以在公有方法中,也可以在非公有方法中利用断言测试后置条件。另外,断言不应该以任何方式改变程序的状态。
面试例题2:Which declaration for the main() method in a stand-alone program are NOT valid?(哪一个main函数的声明是不合法的?)[法国著名通信公司2005年和2009年面试题]
A.public static void main()
B.public static void main(String[] string)
C.public static void main(String[] exp) throws FileNot FoundException
D.static void main(String[] args)
解析:A、B选项显然是合法的。C选项抛出一个文件异常,但也是合法的,并可以通过,代码如下。
import java.io.FileNotFoundException; public class zero { public static void main(String[] exp)throws FileNotFoundException {} }
至于选项D,因为main方法必须是public的,默认的代表是protect,所以是不合法的。
答案:D
5.5 运算符
面试例题1:以下代码的输出结果是()。[中国台湾著名杀毒软件公司Q2009年11月笔试题]
public class Test { public static void main(String[] args) { int i = 42; String s = (i<40) ? "life":(i>50)?"universe":"everything"; System.out.println(s); } }
A.life
B.universe
C.everything
D.以上答案都不对
解析:语言中运算符分为3类:单目运算符、二目运算符、三目运算符。
单目运算就是只对一个参数进行运算(如++、--等);双目运算就是对两个参数进行运算(如+、-、>、<等);三目运算就是对三个参数进行运算(如?、:)。
s=a?b:c;相当于(if(a) s =b;else s =c;)。
本题中的三目运算符有点特殊,是一个嵌套三目运算符。
String s = (i<40) ? "life":(i>50)?"universe":"everything";
相当于
String s2; if(i<40) { s2 = "life"; } else if(i>50) { s2 = "universe"; }else { s2 = "everything"; }
所以答案是everything。
答案:C
面试例题2:下列程序的输出结果是()。[中国东北著名软件公司D2009年3月笔试题]
import java.util.*; public class Test { public static void main(String[] args) { boolean b = true?false:true == true?false:true; System.out.println(b); } }
A.true
B.false
C.null
D.以上答案都不对
解析:三目运算符是右结合性的,所以应该理解为:
boolean b =true?false:((true == true)?false:true);
本题考查的是对运算符的优先级别,基本的顺序是(1级优先级最高,16级最小):
1级 —— . () 2级 —— ++ -- 3级 —— new 4级 —— * / % 5级 —— + - 6级 —— >> << >> > 7级 —— > < > = <= 8级 —— == != 9级 —— & 10级 —— ^ 11级 —— ! 12级 —— && 13级 —— || 14级 —— ?: 15级 —— = += -= *= /= %= ^= 16级 —— &= <<= >>=
答案:B
面试例题3:以下代码输出结果是()。[中国东北著名软件公司D2009年3月笔试题]
public class Test { public static void main(String[] args) { int a = 5; System.out.println("value is " + ((a < 5) ? 10.9 : 9)); } }
A.编译错误
B.10.9
C.9
D.以上答案都不对
解析:如果你不假思索地直接选C,就恰恰中了题目设置的陷阱。注意到((a < 5) ?10.9 : 9)里面有一个10.9,而后面直接跟了一个9。这时Java就会根据运算符的精度类型进行自动类型转换。由于前面有一个10.9,因此,后面的9也会自动变成9.0。因此,答案选D。
答案:D
面试例题4:以下代码的输出结果是()。[中国著名网络软件公司X2009年10月笔试题]
import java.util.*; public class Test { public static void main(String[] args) { char x = 'x'; int i = 10; System.out.println(false ? i : x); System.out.println(false ? 10 : x); } }
A.120 x
B.120120
C.x 120
D.以上答案都不对
解析:int i = 10;中的i是一个变量,因此,第一个输出x被提升为int型了,因为i是int类型,x的int值为120,所以第一个输出为120。
至于第2个输出,Java编程规范中提到:当后两个表达式有一个是常量表达式(本题中是10)时,另外一个类型是T(本题中是char)时,而常量表达式可以被T表示时(representable in type T),输出结果是T类型。所以,因为10是常量,可以被char表示。输出结果是char型的。
答案:A
面试例题5:What does the following program print ?(下面程序的运行结果是多少?)[中国著名ERP软件公司Y2010年1月笔试题]
import java.util.*; public class Test { public static void main(String[] args) { int m = 5, n = 5; if((m != 5) && (n++ == 5)){} System.out.println("a." + n); m = n = 5; if((m != 5) & (n++ == 6)){} System.out.println("b." + n); m = n = 5; if((m == 5) || (n++ == 5)){} System.out.println("c." + n); m = n = 5; if((m == 5) | (n++ == 6)){} System.out.println("d." + n); int a = 1, b = 2; int c = a & b; System.out.println("a&b" + c); } }
解析:
“&”、“|”、“^”这三个是什么运算符?相信多数面试者基本上都会回答“位运算符”,但这样的回答并不完整。其实它们还可以充当布尔逻辑运算符(前提是两边的数据类型为布尔类型)。
在位运算中,int c = a & b; 的意思是首先使a和b按位与,a是1,b是2,a的二进制数位是0001,c的二进制数位是0010。“与”的结果如下表:
在布尔逻辑运算符中,这三个运算符充当着“布尔逻辑与”、“布尔逻辑或”和“布尔逻辑异或”的角色。布尔逻辑与(&)和布尔逻辑或(|)运算符的工作方式同逻辑与(&&)和逻辑或(||)的工作方式相同,布尔逻辑运算符的优先级别要高于逻辑运算符。
&、|逻辑运算与&&、||逻辑运算的重要区别是:前者是非短路运算,后者是短路运算。
编译器对于&&和||已经优化过,凡&&前面的是false,那么&&后面的表达式就不用再做了。||前面的是true,||后面的也就不做了,这就是所谓的“短路”,而布尔逻辑运算符就没有这个特点,无论运算符&、|前面的是true或false,运算符后面的表达式都得继续进行运算。这就是所谓的“非短路”。
下面分析题目中的4段代码:
代码1: int m = 5, n = 5; if((m != 5) && (n++ == 5)){} System.out.println("a." + n);
上面这段代码中,(m != 5)为false,由于中间的逻辑符是&&(短路),(n++ == 5)就不用做了,所以n还是5。
代码2: m = n = 5; if((m != 5) & (n++ == 6)){} System.out.println("b." + n);
上面这段代码中,(m != 5)为false,由于中间的逻辑符是&(非短路),(n++ == 5)仍然需要计算,所以n是6。
代码3: m = n = 5; if((m == 5) || (n++ == 5)){} System.out.println("c." + n);
上面这段代码中,(m == 5)为true,由于中间的逻辑符是||(短路),(n++ == 5)就不用做了,所以n还是5。
代码4: m = n = 5; if((m == 5) | (n++ == 6)){} System.out.println("d." + n);
上面这段代码中,(m == 5)为true,由于中间的逻辑符是|(非短路),(n++ == 5)仍然需要计算,所以n是6。
答案:5,6,5,6,0
面试例题6:下列程序的输出结果是()。[中国著名ERP软件公司Y2010年1月笔试题]
import java.util.*; public class Test { public static void main(String[] args) { int num = 32; System.out.println(num >> 32); } }
A.32
B.16
C.1
D.0
解析:移位操作符右边的参数要先进行模的32运算,并且移位是对二进制的操作,而二进制中8位是一个循环。所以,num>>32等于num>>0,而num>>33等于num>>1。
答案:A
5.6 异常
面试例题1:Which of the following is NOT true regarding to RuntimeException?(关于运行时异常说法不正确的是)[中国台湾著名CPU生产厂商V2009年9月笔试题]
A.RuntimeException is the superclass of those exceptions that must be thrown during the normal operation of the Java Virtul Machine.(运行时异常是一个超类,当Java虚拟机正常时一定抛出。)
B.A method is not required to declare in its throws clause any subclasses of RuntimeException that might be thrown during the execution of the method but not caught.(运行时异常可以不去捕捉。)
C.An RuntimeException is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.(运行时异常是一个子类,当出现严重运行问题时也不会抛出。)
D.NullPointerException is one kind of RuntimeException.(空异常是一种运行时异常。)
解析:一个合理的应用程序不应该(没必要)捕捉运行时异常。
答案:C
面试例题2:定义了如下类和测试方法,请问测试时期待要捕获下面哪个选项的异常?[英国某通信软件公司S2009年11月笔试题]
class MyException extends Exception{ MyException(){} } class A{ public int format(String str)throws MyException{ int i = Integer.valueof(str); return i; } } public void testTester(){ new A().format("1"); }
A.Exception
B.MyException
C.MyException和NumberFormatException
D.RuntimeException
解析:本题问的是期待捕捉哪个异常,因为函数format显式地声明抛出MyException异常,故期待捕捉MyException,但实际上捕捉的异常是runtime异常NumberFormatException。
答案:B
扩展知识:关于Java异常
1.什么是异常
在Java程序运行时,常常会出现一些非正常的现象,这种情况称为运行错误。根据其性质可以分为错误和异常。Java程序中(无论是谁写的代码),所有抛出(throw)的异常都必须从Throwable派生而来。类Throwable有两个直接子类:Error和Exception。
一般来说,最常见的错误有程序进入死循环、内存泄漏等。这种情况下,程序运行时本身无法解决,只能通过其他程序干预。Java对应的类为Error类。Error类对象由Java虚拟机生成并抛弃(通常Java程序不对这类异常进行处理)。
异常是程序执行时遇到的非正常情况或意外行为。以下这些情况一般都可以引发异常:代码或调用的代码(如共享库)中有错误,操作系统资源不可用,公共语言运行库遇到意外情况(如无法验证代码)等。常见的有数组下标越界、算法溢出(超出数值表达范围)、除数为零、无效参数、内存溢出等。这种情况不像错误类那样,程序运行时本身可以解决,由异常代码调整程序运行方向,使程序仍可继续运行,直至正常结束。
Java异常对应的类为Exception类。Exception类对象是Java程序处理或抛弃的对象,它有各种不同的子类分别对应于不同类型的异常。Java编译器要求程序必须捕获或声明所有的非运行时异常,但对运行时异常可以不做处理。其中类RuntimeException代表运行时由Java虚拟机生成的异常,原因是编程错误。其他则为非运行时异常,原因是程序碰到了意外情况,如输入/输出异常IOException等。
2.异常关键字
Java异常处理的关键语句有五个:try、catch、throw、throws、finally。其中,try、catch、finally三个语句块应注意的问题如下。
1)try、catch、finally三个语句块均不能单独使用,三者可以组成try...catch...finally、try...catch、try...finally三种结构,catch语句可以有一个或多个,finally语句最多一个。
2)try、catch、finally三个代码块中变量的作用域为代码块内部,分别独立而不能相互访问。如果要在三个块中都可以访问,则需要将变量定义到这些块的外面。
3)若有多个catch块,只会匹配其中一个异常类并执行catch块代码,而不会再执行别的catch块,并且匹配catch语句的顺序是由上到下的。
throw、throws关键字的区别如下。
throw关键字用于方法体内部,用来抛出一个Throwable类型的异常。如果抛出了检查异常,则还应该在头部声明方法可能抛出的异常类型。该方法的调用者也必须检查处理抛出的异常。如果所有的方法都层层上抛获取的异常,最终JVM会进行处理,处理也很简单,就是打印异常消息和堆栈信息。如果抛出的是Error或RuntimeException,则该方法的调用者可选择处理该异常。
throws关键字用于方法体外部的方法声明部分,用来声明方法可能会抛出某些异常。仅当抛出了检查异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣般地在catch块中打印堆栈信息来做处理。
3.Java异常和C++异常的区别
在C++异常处理模型中,它给予程序员最大的自由度和发挥空间,允许程序员抛出任何想要的异常对象,它可以是语言系统中原本所提供的各种简单数据类型(如int 、float 、double等),也可以是用户自定义的抽象数据对象(如class的object实例)。但是C++ 语言规范中并无此约束,况且由于各个子系统(基础运行库)不是一个厂商(某一个人)统一设计的,所以导致每个子系统设计出的异常对象系统彼此相差甚远。这给最终使用(重用)这些库的程序员带来了很大的不一致性,甚至是很大的麻烦,需要花费很多时间来学习和熟悉这些不同的异常对象子系统。更大的问题是,这些不同子系统之间语义上的不一致,从而造成程序员在最终处理这些异常时,将很难把它们统一起来,例如,MFC库系统中,采用CMemoryException来表示一个与内存操作相关的异常;而其他的库系统中很可能就会采用另外一个class来表示内存操作的异常错误。本质上说,缺乏规范和统一所造成的恶劣后果。所以,设计Java语言的时候,就要充分考虑到这些问题,把它们纳入语言的统一规范中,这对广大的程序员来说,无疑是一件好事。
实际的Java编程中,由于JDK平台已经为我们设计好了非常丰富和完整的异常对象分类模型。因此,Java程序员一般不需要重新定义自己的异常对象,而且即便是需要扩展自定义的异常对象,也往往会从Exception派生而来。所以,对于Java程序员而言,它一般只需要在它的顶级函数中用catch (Exception ex)就可以捕获出所有的异常对象,而不必像C++中采用catch (…) 那样的语法。
4.异常处理中常见的问题
(1)过于庞大的try块
某些程序员把大量的代码放入单个try块,试图用一个catch语句捕获所有的异常和处理所有可能出现的异常,实际上这是一个坏习惯。原因就在于为了图省事,不愿花时间分析一大块代码中哪几行代码会抛出异常、异常的具体类型是什么。把大量的语句装入单个巨大的try块就像是出门旅游时把全部家当塞入集装箱带走,虽然东西是带上了,但要找出来可不容易。
对于这种问题,可以设置多个异常抛出点来解决。异常对象从产生点产生后,到被捕捉后终止生命的全过程中,实际上是一个传值过程,所以,应根据实际来合理控制检测异常个数。catch语句表示会出现某种异常,而且希望能够处理该异常。所以在catch语句中就应该尽量指定具体异常类型,也可使用多个catch,用于分别处理不同的异常。例如,要捕获一个最明显的异常是SQLException,这是JDBC操作中常见的异常。另一个可能的异常是IOException,因为它要操作OutputStreamWriter。显然,在同一个catch块中处理这两种截然不同的异常是不合适的。如果用两个catch块分别捕获SQLException和IOException就要好多了。
(2)异常的完整性
在Java语言中,如果一个函数运行时可能会向上层调用者函数抛出一个异常,那么,它就必须在该函数的声明中显式地注明(采用throws关键字)。否则编译器会报出错误信息“must be caught or declared to be thrown”。其中“must be caught”指在Java的异常处理模型中,要求所有被抛出的异常都必须有对应的“异常处理模块”。如果你在程序中利用throw出现一个异常,那么在你的程序(函数)中就必须要用catch处理这个异常。例如下面的例子中,抛出了一个Exception类型的异常,所以在该函数中,就必须有一个catch,并处理此异常。如果没有这个catch,Java语言在编译时就直接拦住这种可能出现错误的情况,不让程序通过。
try { ...... // throw Exception } catch(Exception ex) { // find Exception // hand of it }
“declared to be thrown”指的是“必须显式地声明某个函数可能会向外部抛出一个异常”,也即是说,如果一个函数内部,它可能抛出了一种类型的异常,但该函数内部又不想用catch处理这种类型的异常,此时,它就必须(强制性)使用throws关键字来显式地声明该函数可能会向外部抛出一个异常,以便该函数的调用者知晓并能够及时处理这种类型的异常。如下列代码:
class MyException extends Exception { MyException() { } } class My1Exception extends Exception { My1Exception() { } } class A { public int format(String str) { int i = Integer.valueof(str); // throw new MyException(); return i; } public static void testTester() throws MyException, My1Exception { new A().format("S"); } } public class Test { public static void main(String[] args) throws MyException, My1Exception { A.testTester(); } }
5.RuntimeException异常
在Java异常处理中,一般有两类异常:其一,就是通过throw语句,程序员在代码中人为抛出的异常(由于运行时动态地监测到了一个错误);另外一个是系统运行时异常,例如,“被零除”、“空字符串”、“无效句柄”等,对于这类异常,程序员实际上完全可以避免它,只要我们写代码时足够小心严谨。因此,为了彻底解决这种隐患,提高程序整体可靠性(不至于因为编码时考虑不周或一个小疏忽导致系统运行时崩溃),使用RuntimeException异常就是为了实现这样的功能。
Java语言中的这两种异常中,前者叫checked exception,它是从java.lang. Exception类衍生出来的;后者叫runtime exception,它是从java.lang.Runtime Exception类衍生出来的。
下面就是一个被零除的例子:
public class Test { public static void main(String[] args) { test(); } static void test() { int i = 4; int j = 0; //运行时,这里将触发了一个ArithmeticException //ArithmeticException从RuntimeException派生而来 System.out.println("i / j = " + i / j); } }
运行结果如下:
java.lang.ArithmeticException: / by zero at Test.test(Test.java:16) at Test.main(Test.java:8) Exception in thread "main"
下面是一个空String的例子:
import java.io.*; public class Test { public static void main(String[] args) { test(); } static void test() { String str = null; str.compareTo("abc"); // 运行时,这里将触发了一个NullPointerException // NullPointerException从RuntimeException派生而来 } }
针对RuntimeException类型的异常,javac是无法通过编译时的静态语法检测来判断到底哪些函数(或哪些区域的代码)可能抛出这类异常(这完全取决于运行时状态,或者说运行态所决定的),也正因为如此,Java异常处理模型中的“must be caught or declared to be thrown”规则也不适用于RuntimeException。但是Java虚拟机却需要有效地捕获并处理此类异常。当然,RuntimeException也可以被程序员显式地抛出,而且为了程序的可靠性,对一些可能出现“运行时异常(RuntimeException)”的代码区域,程序员最好能够及时地处理这些意外的异常,即通过catch(RuntimeExcetion)或catch(Exception)来捕获它们。如下面的示例程序,代码如下:
import java.io.*; public class Test { public static void main(String[] args) { try { test(); } catch (Exception e) { System.out.println("A Exception!"); e.printStackTrace(); } } static void test() throws RuntimeException { String str = null; str.compareTo("abc"); } }
面试例题3:谈谈final、finally、finalize的区别。
解析:
1.final
final可以用于控制成员、方法,或者是一个类是否可被覆写或继承等功能,这些特点使final在Java中拥有了一个不可或缺的地位,也是学习Java时必须要知道和掌握的关键字之一。
(1)final成员
当在类中定义变量时,若在其前面加上final关键字,那就是说,这个变量一旦被初始化,便不可改变,这里不可改变的意思对基本类型来说是其值不可变,而对于对象变量来说是其引用不可变。其初始化可以在两个地方,一是其定义处,二是在构造函数中,两者只能选其一。
还有一种用法是定义方法中的参数为final。对于基本类型的变量,这样做并没有什么实际意义,因为基本类型的变量在调用方法时是传值的,也就是说,你可以在方法中更改这个参数变量而不会影响到调用语句,然而对于对象变量,却显得很实用,因为对象变量在传递时是传递其引用的,这样,你在方法中对对象变量的修改也会影响到调用语句中的对象变量。当你在方法中不需要改变作为参数的对象变量时,明确使用final进行声明,会防止你无意地修改而影响到调用方法。
(2)final方法
将方法声明为final有两个原因,第一就是说明已经知道这个方法提供的功能满足要求,不需要进行扩展,并且也不允许任何从此类继承的类来覆写这个方法,但是仍然可以继承这个方法,也就是说,可以直接使用。第二就是允许编译器将所有对此方法的调用转化为inline(行内)调用的机制,它会在调用final方法时,直接将方法主体插入到调用处,而不是进行例行的方法调用,例如,保存断点、压栈等,这样可能会使程序效率有所提高。然而当方法主体非常庞大时,或在多处调用此方法时,调用主体代码便会迅速膨胀,可能反而会影响效率,所以要慎用final进行方法定义。
(3)final类
当将final用于类时,就需要仔细考虑,因为一个final类是无法被任何人继承的,那也就意味着此类在一个继承树中是一个叶子类,并且此类的设计已被认为很完美,不需要进行修改或扩展。对于final类中的成员,可以定义其为final,也可以不是final。而对于方法,由于所属类为final的关系,自然也就成了final型的。也可以明确地给final类中的方法加上一个final,但这显然没有意义。
2.finally
finally关键字是对Java异常处理模型的最佳补充。finally结构使代码总会执行,而不管有无异常发生。使用finally可以维护对象的内部状态,并可以清理非内存资源。如果没有finally,你的代码就会很费解。
3.finalize
根据Java语言规范,JVM保证调用finalize函数之前,这个对象是不可达的,但是JVM不保证这个函数一定会被调用。另外,规范还保证finalize函数最多运行一次。
通常,finalize用于一些不容易控制,并且非常重要的资源的释放,例如,一些I/O的操作、数据的连接。这些资源的释放对整个应用程序是非常关键的。在这种情况下,程序员应该以通过程序本身管理(包括释放)这些资源为主,以finalize函数释放资源方式为辅,形成一种双保险的管理机制,而不应该仅仅依靠finalize来释放资源。
答案:
1.final修饰符(关键字)
如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此,一个类不能既被声明为abstract,又被声明为final。将变量或方法声明为final,可以保证它们在使用中不被改变。其初始化可以在两个地方:一是其定义处,也就是说,在final变量定义时直接给其赋值;二是在构造函数中。这两个地方只能选其一,要么在定义时给值,要么在构造函数中给值,不能同时既在定义时给了值,又在构造函数中给另外的值,而在以后的引用中只能读取,不可修改。被声明为final的方法也同样只能使用,不能重写(override)。
2.finally
在异常处理时提供finally块来执行任何清除操作。如果抛出一个异常,那么相匹配的catch子句就会执行,然后控制就会进入finally块(如果有的话)。
3.finalize
finalize是方法名。Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在Object类中定义的,因此,所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
面试例题4:try{}里有一个return语句,那么紧跟在这个try后的finally{}里的code会不会被执行,什么时候被执行?在return前还是后?[中国大陆著名杀毒软件公司JS2009年10月笔试题]
A.会执行,在return前执行
C.不会执行
B.会执行,在return后执行
D.会抛出异常
解析:finally结构在Java、C++、C#中,代码总会执行,而Java、C++、C#里很多回收机制代码就在finally里,而一个函数return后就会销毁,因此,要在return前执行。
答案:A
面试例题5:下面四段Java程序中哪些不能被编译通过?[加拿大著名通信软件公司N2009年12月笔试题]
程序1:
import java.io.*; public class Test { public static void main(String[] args) { try { test(); } catch (Exception ex) { ex.printStackTrace(); } } static void test() { try { throw new Exception("test"); } catch (Exception ex) { ex.printStackTrace(); } } }
程序2:
import java.io.*; public class Test { public static void main(String[] args) { try { test(); } catch (Exception ex) { ex.printStackTrace(); } } static void test() { throw new Exception("test"); } }
程序3:
import java.io.*; public class Test { public static void main(String[] args) { try { test(); } catch (Exception ex) { ex.printStackTrace(); } } static void test() throws Exception { throw new Exception("test"); } }
程序4:
import java.io.*; public class Test { public static void main(String[] args) { try { test(); } catch (IoException ex) { ex.printStackTrace(); } } static void test() throws Exception { } }
A.程序1和程序2
C.程序1和程序3
B.程序3和程序4
D.程序2和程序4
解析:程序1是很经典的异常捕捉方法,可以通过编译;程序2虽然在这里能够捕获到Exception类型的异常,但是test函数中单独throw(抛)一个异常却不加捕获是错误的,所以不能通过编译;程序3由于函数显式地声明了可能抛出Exception类型的异常,所以这种写法又能够被编译通过;程序4中,虽然test() 函数并没有真正抛出一个Exception类型的异常,但是由于函数在声明时,表示它可能抛出一个Exception类型的异常。所以这里必须catch一个Exception类型的异常,否则不能被编译通过。
答案:D
面试例题6:下列输出结果是什么?[中国杭州著名B2B软件公司T2009年10月笔试题]
import java.io.*; public class Test { public static void main(String[] args) { try{ test(); System.out.println("condition 1"); }catch (ArrayIndexoutofBoundsException e){ System.out.println("condition 2"); }catch (Exception e){ System.out.println("condition 3"); }finally{ System.out.println("finally"); } //test(); } static void test() { String str = "cc"; str.compareTo("abc"); } }
解析:因为test()方法运行是正常的,所以不会抛出异常。
答案:
condition 1
finally
5.7 反射
面试例题:什么是Reflection(反射)?其他语言有这种特点吗?
解析:反射主要是指程序可以访问、检测和修改它本身的状态或行为的一种能力。这一概念的提出很快引发了计算机科学领域关于应用反射性的研究。它首先被程序语言的设计领域所采用,并在LISP和面向对象方面取得了成绩,其中LEAD/LEAD++、OpenC++、MetaXa和OpenJava等就是基于反射机制的语言。最近,反射机制也被应用到了视窗系统、操作系统和文件系统中。
反射本身并不是一个新概念,它可能会被联想到光学中的反射概念,尽管计算机科学赋予了反射概念新的含义,但是,从现象上来说,它们确实有某些相通之处。在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。可以看出,与一般的反射概念相比,计算机科学领域的反射不单单指反射本身,还包括对反射结果所采取的措施。所有采用反射机制的系统(即反射系统)都希望使系统的实现更开放。可以说,实现了反射机制的系统都具有开放性,但具有开放性的系统并不一定采用了反射机制,开放性是反射系统的必要条件。一般来说,反射系统除了满足开放性条件外,还必须满足原因连接(Causally-connected)。所谓原因连接,是指对反射系统自描述的改变能够立即反映到系统底层的实际状态和行为上的情况,反之亦然。开放性和原因连接是反射系统的两大基本要素。
答案:Java中的反射是一种强大的工具,它能够创建灵活的代码,这些代码可以在运行时装配,无须在组件之间进行链接。反射允许在编写与执行时,使程序代码能够接入装载到JVM中的类的内部信息,而不是源代码中选定的类协作的代码。这使反射成为构建灵活应用的主要工具。需注意的是,如果使用不当,反射的成本会很高。
Java中的类反射Reflection是Java程序开发语言的特征之一,它允许运行中的Java程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性。Java的这一能力在实际应用中也许用得不是很多,但是在其他的程序设计语言中根本就不存在这一特性。例如,Pascal、C或者C++中就没有办法在程序中获得与函数定义相关的信息。