sharajava

2006年7月27日 #

类的动态加载

: 调用 Class.forName() ClassLoader.loadClass() 的区别在什么地方 ?

: 这两方法都是通过一个给定的类名去定位和加载这个类名对应的 java.long.Class 类对象 . 尽管如此 , 它们的在行为方式上还是有区别的 .

Ø         用哪个 java.lang.ClassLoader 进行加载

Ø         返回的 Class 对象是否被初始化

Class.forName(String) 方法(只有一个参数), 使用调用者的类加载器来加载, 也就是用加载了调用forName方法的代码的那个类加载器. 相应的, ClassLoader.loadClass()方法是一个实例方法(非静态方法), 调用时需要自己指定类加载器, 那么这个类加载器就可能是也可能不是加载调用代码的类加载器. 如果用特定的类加载器来加载类在你的设计中占有比较重要的地位, 你就应该调用ClassLoader.loadClass(String)方法或Class.forName(String, boolean, ClassLoader)方法.

    另外, Class.forName()方法对加载的类对象进行初始化. 可见的效果就是类中静态初始化段及字节码中对所有静态成员的初始工作的执行(这个过程在类的所有父类中递归地调用). 这点就与ClassLoader.loadClass()不同. ClassLoader.loadClass()加载的类对象是在第一次被调用时才进行初始化的.

    你可以利用上述的差异. 比如,要加载一个静态初始化开销很大的类, 你就可以选择提前加载该类(以确保它在classpath), 但不进行初始化, 直到第一次使用该类的域或方法时才进行初始化.

    最常用的是Class.forName(String, boolean, ClassLoader). 设置第二个参数为false即推迟初始化, 第三个参数指定要用来进行加载的类加载器. 我建议为了最大的灵活性使用这个方法.

类初始化错误是难处理的

    成功地加载了类, 并不意味着就不会有其它问题. 静态初始化代码可以抛出异常, 异常被包装到java.long.ExceptionInInitializerError的实例中. 异常抛出后, 这个类将不可用. 这样, 如果你需要在代码中处理这些错误, 你就应该调用进行初始化的Class.forName()方法.

    但进一步说, 如果你要处理ExceptionInInitializerError并试图从错误中恢复, 很可能不如你想象的那样正常工作. 请看下面的示例代码:


public   class  Main
{
    
public   static   void  main (String [] args)  throws  Exception
    {
        
for  ( int  repeat = 0; repeat < 3; ++ repeat)
        {
            
try
            {
                
// "Real" name for X is outer class name+$+nested class name:
                Class.forName ("Main$X");
            }
            
catch  (Throwable t)
            {
                System.out.println ("load attempt #" + repeat + ":");
                t.printStackTrace (System.out);
            }
        }
    }

    
private   static   class  X
    {
        
static
        {
            
if  (++ s_count == 1)
                
throw   new  RuntimeException ("failing static initializer");
        }
        
    } 
// End of nested class

    
private   static   int  s_count;

// End of class

    上面的代码3次尝试加载一个内部类X, 即便是X的静态初始化只在每一次加载时失败, 3次加载都抛出了异常.

>java Main
load attempt #0:
java.lang.ExceptionInInitializerError
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:140)
        at Main.main(Main.java:17)
Caused by: java.lang.RuntimeException: failing static initializer...
        at Main$X.<clinit>(Main.java:40)
        ... 3 more
load attempt #1:
java.lang.NoClassDefFoundError
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:140)
        at Main.main(Main.java:17)
load attempt #2:
java.lang.NoClassDefFoundError
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:140)
        at Main.main(Main.java:17)

    有点令人吃惊的时, 在第2, 3次进行类加载时, 抛出的异常竟然是java.lang.NoClassDefFoundError. 这里发生的事情是, 第一次加载后(在进行初始化之前), JVM发现X已经被加载, 而这个X的类实例在加载它的类加载器被垃圾回收之前是不会被卸载的. 所以这之后的对Class.forName()的调用时, JVM不会再尝试进行初始化的工作, 但是, 更令人不解的是, 抛出一个NoClassDefFoundError.

    卸载这样的类的方法是丢弃原来加载该类的类加载器实例并重新创建一个. 当然, 这只能是在你使用了Class.forName(String, boolean, ClassLoader)这个3参数的方法的时候才能办到.

隐藏的 Class.forName() 方法

    你一定用过JavaX.class的语法去获取一个在编译器就知道名字的类对象实例. 在字节码的层次上, 这一点是如何实现的就不被人熟知了. 不同的编译器有不同的实例细节, 但共同点是, 所有编译器所相应产生的代码都是调用的Class.forName(String)这一个参数的方法. 比如J2SE 1.4.1javac就把Class cls = X.class; 翻译成如下等价的形式:

 
        
// This is how "Class cls = X.class" is transformed:
         if  ( class $Main$X ==  null )
        {
            
class $Main$X =  class $ ("Main$X");
        }
        Class cls = 
class $Main$X;

    

    
static  Class  class $ (String s)
    {
        
try
        {
            
return  Class.forName (s);
        }
        
catch  (ClassNotFoundException e)
        {
            
throw   new  NoClassDefFoundError (e.getMessage());
        }
    }

    
static  Class  class $Main$X;  // A synthetic field created by the compiler

Sun javac 开个玩笑

从上面的例子你可以看到, 编译器调用Class.forName()方法加载类对象, 并将其缓存到一个包内可见的静态变量中. 这种令人费解的实现方式的可能是因为在早期版本的Java, 这种X.class的语法还未被支持, so the feature was added on top of the Java 1.0 byte-code instruction set.(???)

利用这一点, 你可以在编译器的开销上做一些有趣的事情. J2SE 1.3.1编译下面的代码片段:

public   class  Main
{
    
public   static   void  main (String [] args)  throws  Exception
    {
        System.out.println ("String class: " + String.
class );
        
class $java$lang$String =  int . class ;
        System.out.println ("String class: " + String.
class );
    }
    
    
static  Class  class $java$lang$String;

// End of class

运行它, 你会得到下面这个很荒谬的输出结果:

>java Main

String class: class java.lang.String

String class: int

J2SE 1.4.1, 上面的代码将不能被编译通过, 但你仍然可以用反射的方式戏弄它:

public   static   void  main (String [] args)  throws  Exception
    {
        System.out.println ("String class: " + String.
class );
        Main.
class .getDeclaredField ("class$java$lang$String").set ( null int . class );
        System.out.println ("String class: " + String.
class );
    }

 

    综上所述, 下次你再调用Class.forName()方法时, 你应该知道它的局限性可选的替代方案了.

 

 

posted @ 2006-07-27 09:06 sharajava 阅读(2128) | 评论 (0)编辑 收藏