一 . 面向接口编程. 不要面向类编程.
二 . 关于异常:
如果父类的一个方法抛出了异常,子类在重写此方法时可以不抛出异常而直接处理,也可以抛出父类异常的子异
常,但是不能抛出比父类方法抛出的异常级别更高的异常.
三 . Java的类装载器(Class Loader)和命名空间(NameSpace)
1.摘要:
Java的类装载器是Java动态性的核心,本文将向大家简要介绍Java的类装载器,及相关的双亲委派模型,命名
空间,运行时包等概念,同时讨论一些在学习中容易混淆的问题。
2.类装载器的功能及分类:
顾名思义,类装载器是用来把类(class)装载进JVM的。JVM规范定义了两种类型的类装载器:启动内装载器
(bootstrap)和用户自定义装载器(user-defined class loader)。
bootstrap是JVM自带的类装载器,用来装载核心类库,如java.lang.*等。由例1可以看出,java.lang.Object
是由bootstrap装载的。
Java提供了抽象类ClassLoader,所有用户自定义类装载器都实例化自ClassLoader的子类。
System Class Loader是一个特殊的用户自定义类装载器,由JVM的实现者提供,在编程者不特别指定装载器的
情况下默认装载用户类。系统类装载器可以通过ClassLoader.getSystemClassLoader() 方法得到。
例1,测试你所使用的JVM的ClassLoader
/*LoaderSample1.java*/
public class LoaderSample1
{
public static void main(String[] args)
{
Class c;
ClassLoader cl;
cl = ClassLoader.getSystemClassLoader();
System.out.println(cl);
while (cl != null)
{
cl = cl.getParent();
System.out.println(cl);
}
try
{
c = Class.forName(“java.lang.Object”);
cl = c.getClassLoader();
System.out.println(“java.lang.Object’s loader is ” + cl);
c = Class.forName(“LoaderSample1”);
cl = c.getClassLoader();
System.out.println(“LoaderSample1’s loader is ” + cl);
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
在我的机器上(Sun Java 1.4.2)的运行结果
sun.misc.Launcher$AppClassLoader@1a0c10f
sun.misc.Launcher$ExtClassLoader@e2eec8
null
java.lang.Object's loader is null
LoaderSample1's loader is sun.misc.Launcher$AppClassLoader@1a0c10f
第一行表示,系统类装载器实例化自类sun.misc.Launcher$AppClassLoader
第二行表示,系统类装载器的parent实例化自类sun.misc.Launcher$ExtClassLoader
第三行表示,系统类装载器parent的parent为bootstrap
第四行表示,核心类java.lang.Object是由bootstrap装载的
第五行表示,用户类LoaderSample1是由系统类装载器装载的
3.双亲委派模型:
从1.2版本开始,Java引入了双亲委托模型,从而更好的保证Java平台的安全。在此模型下,当一个装载器被请
求装载某个类时,它首先委托自己的parent去装载,若parent能装载,则返回这个类所对应的Class对象,若
parent不能装载,则由parent的请求者去装载。
如图1所示,loader2的parent为loader1,loader1的parent为system class loader。假设loader2被要求装载
类MyClass,在双亲委派模型下,loader2首先请求loader1代为装载,loader1再请求系统类装载器去装载
MyClass。若系统装载器能成功装载,则将MyClass所对应的Class对象的reference返回给loader1,loader1再
将reference返回给loader2,从而成功将类MyClass装载进虚拟机。若系统类装载器不能装载MyClass,loader1
会尝试装载MyClass,若loader1也不能成功装载,loader2会尝试装载。若所有的parent及loader2本身都不能
装载,则装载失败。
若有一个能成功装载,实际装载的类装载器被称为定义类装载器,所有能成功返回Class对象的装载器(包括定
义类装载器)被称为初始类装载器。如图1所示,假设loader1实际装载了MyClass,则loader1为MyClass的定义
类装载器,loader2和loader1为MyClass的初始类装载器。
需要指出的是,Class Loader是对象,它的父子关系和类的父子关系没有任何关系。一对父子loader可能实例
化自同一个Class,也可能不是,甚至父loader实例化自子类,子loader实例化自父类。假设MyClassLoader继
承自ParentClassLoader,我们可以有如下父子loader:
ClassLoader loader1 = new MyClassLoader();
ClassLoader loader2 = new ParentClassLoader(loader1); //参数 loader1为parent
那么双亲委托模型为什么更安全了?因为在此模型下用户自定义的类装载器不可能装载应该由父亲装载器装载
的可靠类,从而防止不可靠甚至恶意的代码代替由父亲装载器装载的可靠代码。实际上,类装载器的编写者可
以自由选择不用把请求委托给parent,但正如上所说,会带来安全的问题。
4.命名空间及其作用:
每个类装载器有自己的命名空间,命名空间由所有以此装载器为创始类装载器的类组成。不同命名空间的两个
类是不可见的,但只要得到类所对应的Class对象的reference,还是可以访问另一命名空间的类。
例2演示了一个命名空间的类如何使用另一命名空间的类。在例子中,LoaderSample2由系统类装载器装载,
LoaderSample3由自定义的装载器loader负责装载,两个类不在同一命名空间,但LoaderSample2得到了
LoaderSample3所对应的Class对象的reference,所以它可以访问LoaderSampl3中公共的成员(如age)。
例2不同命名空间的类的访问
/*LoaderSample2.java*/
import java.net.*;
import java.lang.reflect.*;
public class LoaderSample2
{
public static void main(String[] args)
{
try
{
String path = System.getProperty("user.dir");
URL[] us = {new URL("file://" + path + "/sub/")};
ClassLoader loader = new URLClassLoader(us);
Class c = loader.loadClass("LoaderSample3");
Object o = c.newInstance();
Field f = c.getField("age");
int age = f.getInt(o);
System.out.println("age is " + age);
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
/*sub/Loadersample3.java*/
public class LoaderSample3
{
static
{
System.out.println("LoaderSample3 loaded");
}
public int age = 30;
}
编译:javac LoaderSample2.java; javac sub/LoaderSample3.java
运行:java LoaderSample2
LoaderSample3 loaded
age is 30
从运行结果中可以看出,在类LoaderSample2中可以创建处于另一命名空间的类LoaderSample3中的对象并可以
访问其公共成员age。
5.运行时包(runtime package):
由同一类装载器定义装载的属于相同包的类组成了运行时包,决定两个类是不是属于同一个运行时包,不仅要
看它们的包名是否相同,还要看的定义类装载器是否相同。只有属于同一运行时包的类才能互相访问包可见的
类和成员。这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。假设用户自
己定义了一个类java.lang.Yes,并用用户自定义的类装载器装载,由于java.lang.Yes和核心类库java.lang.*
由不同的装载器装载,它们属于不同的运行时包,所以java.lang.Yes不能访问核心类库java.lang中类的包可
见的成员。
6.总结:
在简单讨论了类装载器,双亲委派模型,命名空间,运行时包后,相信大家已经对它们的作用有了一定的了解
。命名空间并没有完全禁止属于不同空间的类的互相访问,双亲委托模型加强了Java的安全,运行时包增加了
对包可见成员的保护。
7.参考资料:
<<Inside The Java Virtual Machine>> by Bill Venners
<<The Java Virtual Machine Specification>> by Sun
四 . 再谈类装载器
作为程序的一部分,每个类都有一个class对象.换言之,每当你编写并且编译了一个新类,就会产生一个
Class对象(更恰当的说,是被保存在一个同名的.class文件中).在运行期,一旦我们想生成这个类的一个对象,运
行这个程序的java虚拟机(JVM)首先检查这个类的Class对象是否已经加载,如果尚未加载,JVM就会根据类名查找
.class文件,并将期载入,所以Java程序并不是一开始执行就被完全加载的,这一点与许多传统语言不同,一旦某
个类的Class对象被载入内存,它就被用来创建这个类的所有对象,请看下面的例子:
class Candy
{
static
{
System.out.println ("Loading Candy");
}
}
class Gum
{
static
{
System.out.println ("Loading Gum");
}
}
class Cookie
{
static
{
System.out.println ("Loading Cookie");
}
}
public class ClassTest
{
public static void main(String[] args)
{
System.out.println ("inside main");
new Candy();
System.out.println ("After creating Candy");
try
{
Class.forName("Gum");
}
catch(ClassNotFoundException e)
{
System.out.println ("Couldn't find Gum");
}
System.out.println ("After Class.forName(\"Gum\")");
new Cookie();
System.out.println ("After creating Cookie");
}
}
打印结果如下:
inside main
Loading Candy
After creating Candy
Couldn't find Gum
After Class.forName("Gum")
Loading Cookie
After creating Cookie
这里的每个类Candy,Gum,Cookie中,都有一个static语句,在类第一次被加载时执行,这时会有相应结果打印出来
告诉我们这个类什么时候被加载了.在main()中,创建对象的代码被置于打印语句之间,以帮助我们判断加载的时
间点.你可以从输出中看出,Class对象仅在需要的时候才被加载,static语句块是在类加载时被执行的.看这行
:Class.forName("Gum"),这是Class类(所有Class对象都属于这个类型)的一个static成员,Class对象和其他对
象一样,我们可以获取并操作它的引用(这就是类加载器的工作),forName()时取得Class对象的引用一个方法,它
时用一个包含目标类的文件名的String作为输入参数,返回一个Class对象的引用,上面的代码忽略了返回值,对
forName()的调用是为了它产生的"副作用":如果类Gum还没有被加载就加载它.在加载的过程中,Gum的static语
句被执行.在前面的例子中,如果Class.forName()找不到你要加载的类.它会抛出异常ClassNotFoundException