class对象及类的初始化过程
第⼗四章类型信息
14.2 加载,链接,初始化
Java字节代码的表现形式是字节数组(byte[]),⽽Java类在JVM中的表现形式是java.lang.Class类的对象。
这三个步骤中,对开发⼈员直接可见的是Java类的加载,通过使⽤Java类加载器(class loader)可以在运⾏时刻动态的加载⼀个Java类;⽽链接和初始化则是在使⽤Java类之前会发⽣的动作。
所有的类都是在对其第⼀次使⽤时,动态加载到JVM中的。当程序创建第⼀个对类的静态成员的引⽤时,就会加载这个类。这个证明构造⽅法也是类的静态⽅法,及时在构造器之前并没有使⽤statis关键字。因此,使⽤new操作符创建类的新对象也会被当做对类的静态成员的引⽤。
JVM和类
⾸先编译:javac将.java⽂件编译为.class字节码⽂件。然后运⾏:调⽤Java命令运⾏Java程序时,该命令将会启动⼀条Java虚拟机进程,进⾏类的初始化。不管该Java程序启动了多少条线程,创建了多少个
变量,它们都处于该Java虚拟机进程⾥,共享该JVM进程的内存区。当系统出现以下⼏种情况时,JVM进程将被终⽌:
程序运⾏到最后正常结束;
程序运⾏到使⽤it()或it()代码结束程序;
程序运⾏过程中遇到未捕获的异常或错误⽽结束;
程序所在的平台强制结束了JVM进程。
符号引⽤和直接引⽤
在java中,⼀个java类将会编译成⼀个class⽂件。在编译时,java类并不知道引⽤类的实际内存地址,因此只能使⽤符号引⽤来代替。⽐如org.simple.People类引⽤org.simple.Tool类,在编译时People类并不知道Tool类的实际内存地址,因此只能使⽤符号org.simple.Tool(假设)来表⽰Tool类的地址。⽽在类装载器装载People类时,此时可以通过虚拟机获取Tool类 的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,即直接引⽤地址。
1. 加载
类加载器分成两类:启动类加载器(bootstrap)和⽤户⾃定义的类加载器(user-defined)。两者的区别在于启动类加载器是由JVM 的原⽣代码实现的,⽽⽤户⾃定义的类加载器都继承⾃Java中的java.lang.ClassLoader类
类加载器需要完成的最终功能是定义⼀个Java类,即把Java字节代码转换成JVM中的java.lang.Class类的对象。
Java类加载器有两个⽐较重要的特征:层次组织结构和代理模式。层次组织结构指的是每个类加载器都有⼀个⽗类加载器,通过getParent()⽅法可以获取到。类加载器通过这种⽗亲-后代的⽅式组织在⼀起,形成树状层次结构。代理模式则指的是⼀个类加载器既可以⾃⼰完成Java类的定义⼯作,也可以代理给其它的类加载器来完成。由于代理模式的存在,启动⼀个类的加载过程的类加载器和最终定义这个类的类加载器可能并不是⼀个。前者称为初始类加载器,⽽后者称为定义类加载器。两者的关联在于:⼀个Java类的定义类加载器是该类所导⼊的其它Java类的初始类加载器。⽐如类A通过import导⼊了类 B,那么由类A的定义类加载器负责启动类B的加载过程。
⼀般的类加载器在尝试⾃⼰去加载某个Java类之前,会⾸先代理给其⽗类加载器。当⽗类加载器不到的时候,才会尝试⾃⼰加载。这个逻辑是封装在java.lang.ClassLoader类的loadClass()⽅法中的。⼀般来说,⽗类优先的策略就⾜够好了。在某些情况下,可能需要采取相反的策略,即先尝试⾃⼰加
载,不到的时候再代理给⽗类加载器。这种做法在Java的Web容器中⽐较常见,也是Servlet规范推荐的做法。⽐如,Apache Tomcat为每个Web应⽤都提供⼀个独⽴的类加载器,使⽤的就是⾃⼰优先加载的策略。IBM
WebSphere Application Server则允许Web应⽤选择类加载器使⽤的策略。
类加载器的⼀个重要⽤途是在JVM中为相同名称的Java类创建隔离空间。在JVM中,判断两个类是否相同,不仅是根据该类的⼆进制名称,还需要根据两个类的定义类加载器。只有两者完全⼀样,才认为两个类的是相同的。因此,即便是同样的Java字节代码,被两个不同的类加载器定义之后,所得到的Java类也是不同的。如果试图在两个类的对象之间进⾏赋值操作,会抛出
java.lang.ClassCastException。这个特性为同样名称的Java类在JVM中共存创造了条件。
2. 链接
Java类的链接指的是将Java类的⼆进制代码合并到JVM的运⾏状态之中的过程。在链接之前,这个类必须被成功加载。类的链接包括验证、准备和解析等⼏个步骤。1.验证是⽤来确保Java类的⼆进制表⽰在结构上是完全正确的。如果验证过程出现错误的话,会抛出java.lang.VerifyError错误。2.准备过程则是创建Java类中的静态域,并将这些域的值设为默认值。准备过程并不会执⾏代码。3.在⼀个Jav
a类中会包含对其它类或接⼝的形式引⽤,包括它的⽗类、所实现的接⼝、⽅法的形式参数和返回值的Java类等。解析的过程就是确保这些被引⽤的类能被正确的到。解析的过程可能会导致其它的Java类被加载【当程序主动使⽤某个类时,如果该类还没有被加载到内存中,系统会通过加载、连接、初始化这三个步骤来对该类进⾏初始化(如果没有意外,JVM将会连续完成这三个步骤,所以有时也会把这三个步骤统称为类的加载或初始化)】。
不同的JVM实现可能选择不同的解析策略。⼀种做法是在链接的时候,就递归的把所有依赖的形式引⽤都进⾏解析。⽽另外的做法则可能是只在⼀个形式引⽤真正需要的时候才进⾏解析。也就是说如果⼀个Java类只是被引⽤了,但是并没有被真正⽤到,那么这个类有可能就不会被解析。考虑下⾯的代码:
public class LinkTest {
public static void main(String[] args) {
ToBeLinked toBeLinked = null;
System.out.println("Test link.");
}
}
类 LinkTest引⽤了类ToBeLinked,但是并没有真正使⽤它,只是声明了⼀个变量,并没有创建该类的实例或是访问其中的静态域。在Oracle的JDK 6中,如果把编译好的ToBeLinked的Java字节代码删除之后,再运⾏LinkTest,程序不会抛出错误。这是因为ToBeLinked 类没有被真正⽤到,⽽Oracle的JDK 6所采⽤的链接策略使得ToBeLinked类不会被加载,因此也不会发现ToBeLinked的Java字节代码实际上是不存在的。如果把代码改成ToBeLinked toBeLinked = new ToBeLinked();之后,再按照相同的⽅法运⾏,就会抛出异常了。因为这个时候ToBeLinked这个类被真正使⽤到了,会需要加载这个类。
3. 初始化
当⼀个Java类第⼀次被真正使⽤到的时候,JVM会进⾏该类的初始化操作。
初始化过程的主要操作是执⾏静态代码块和初始化静态域。
在⼀个类被初始化之前,它的直接⽗类也需要被初始化。但是,⼀个接⼝的初始化,不会引起其⽗接⼝的初始化。
在初始化的时候,会按照源代码中从上到下的顺序依次执⾏静态代码块和初始化静态域
hh
public class StaticTest {
public static int X = 10;
public static void main(String[] args) {
System.out.println(Y); //输出60
}
static {
X = 30;
}
public static int Y = X * 2;
}
在上⾯的代码中,在初始化的时候,静态域的初始化和静态代码块的执⾏会从上到下依次执⾏。因此变量X的值⾸先初始化成10,后来⼜被赋值成30;⽽变量Y的值则被初始化成60。main最后执⾏。
Java类和接⼝的初始化只有在特定的时机才会发⽣,这些时机包括:
1. 创建⼀个Java类的实例。如MyClass obj = new MyClass()
2. 调⽤⼀个Java类中的静态⽅法。如MyClass.sayHello()
3. 给Java类或接⼝中声明的静态域赋值。如MyClass.value = 10
4. 访问Java类或接⼝中声明的静态域,并且该域不是常值变量。如 int value = MyClass.value
5. 在顶层Java类中执⾏assert语句。
6. 通过Java反射API也可能造成类和接⼝的初始化。需要注意的是,当访问⼀个Java类或接⼝中的静态域的时候,只有真正声
明这个域的类或接⼝才会被初始化。考虑下⾯的代码:
class B {
static int value = 100;
java源代码加密
static {
System.out.println("Class B is initialized."); //输出
}
}
class A extends B {
static {
System.out.println("Class A is initialized."); //不会输出
}
}
public class InitTest {
public static void main(String[] args) {
System.out.println(A.value); //输出100
}
}
在上述代码中,类InitTest通过A.value引⽤了类B中声明的静态域value。由于value是在类B中声明的,只有类B会被初始化,⽽类A则不会被初始化。
⾃定义类加载器
在 Java应⽤开发过程中,可能会需要创建应⽤⾃⼰的类加载器。典型的场景包括实现特定的Java字节代码查⽅式、对字节代码进⾏加密/解密以及实现同名 Java类的隔离等。创建⾃⼰的类加载器并不是⼀件复杂的事情,只需要继承⾃java.lang.ClassLoader类并覆写对应的⽅法即可。 java.lang.ClassLoader中提供的⽅法有不少,下⾯介绍⼏个创建类加载器时需要考虑的:
defineClass():这个⽅法⽤来完成从Java字节代码的字节数组到java.lang.Class的转换。这个⽅法是不能被覆写的,⼀般是⽤原⽣代码来实现的。
findLoadedClass():这个⽅法⽤来根据名称查已经加载过的Java类。⼀个类加载器不会重复加载同⼀名称的类。
findClass():这个⽅法⽤来根据名称查并加载Java类。
loadClass():这个⽅法⽤来根据名称加载Java类。
resolveClass():这个⽅法⽤来链接⼀个Java类。
这⾥⽐较 容易混淆的是findClass()⽅法和loadClass()⽅法的作⽤。前⾯提到过,在Java类的链接过程中,会需要对Java类进⾏解析,⽽解析可能会导致当前Java类所引⽤的其它Java类被加载。在这个时候,JVM就是通过调⽤当前类的定义类加载器的loadClass()⽅法来加载其它类的。findClass()⽅法则是应⽤创建的类加载器的扩展点。应⽤⾃⼰的类加载器应该覆写findClass()⽅法来添加⾃定义的类加载逻辑。 loadClass()⽅法的默认实现会负责调⽤findClass()⽅法。
前⾯提到,类加载器的代理模式默认使⽤的是⽗类优先的策略。这个策略的实现是封装在loadClass()⽅法中的。如果希望修改此策略,就需要覆写loadClass()⽅法。
下⾯的代码给出了⾃定义的类加载的常见实现模式:
public class MyClassLoader extends ClassLoader {
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = null; //查或⽣成Java类的字节代码
return defineClass(name, b, 0, b.length);
}
}
编译期常量
如果⼀个static final值是编译期常量,如 static final age =10,(个⼈觉得如果没有设初始值,也算编译期常量吧,0)那么这个值不需要对其所在类进⾏初始化就可以被读取。但是,如果只是⼀个static final,还不⾜以确保这种⾏为例如,static final age =10+3;因为他不是⼀个编译期常量,他的值在运⾏期,代码运⾏后才能确定。
java中静态属性,静态⽅法和静态初始化器(静态态代码段)
⾮static的⽅法是属于某个对象的⽅法,在这个对象创建时对象的⽅法在内存中拥有⾃⼰专⽤的代码段;
⽽static的⽅法是属于整个类的,它在内存中的代码段将随着类的定义⽽分配和装载,不被任何⼀个对象专有:
构造函数是在⽤new运算符产⽣新对象时由系统⾃动执⾏,⽽静态初始化器则是在它所属的类加载⼊内存时由系统调⽤运⾏的,当然是先有类,再有对象;⽽且静态块在给类分配内存的时候就会被执⾏,和静态变量⼀样。所以静态初始化器先于构造函数。;
static 与实例⽆关,哪怕你创建实例后,你在销毁实例后,static⾥⾯的东西依然存在,直到线程over。
JAVA在执⾏程序的时候⾸先装载类,然后main()
所以,在装载类的时候,遇到static马上执⾏,
因此,
14.3 强制类型转换
⾸先我们先构造⼀个Son对象,然后⽤⼀个Father类型变量引⽤它:
Father father = new Son();
在这⾥Son 对象实例被向上转型为father了,但是请注意这个Son对象实例在内存中的本质还是Son类型的,只不过它的能⼒临时被消弱了⽽已,如果我们想变强怎么办?将其对象类型还原!
Son son = (Son)father;
这条语句是可⾏的,其实father引⽤仍然是Father类型的,只不过是将它的能⼒加强了,将其加强后转交给son引⽤了,Son对象实例在son的变量的引⽤下,恢复真⾝,可以使⽤全部功能了。
前⾯提到⽗类强制转换成⼦类并不是总是成功,那么在什么情况下它会失效呢?
当引⽤类型的真实⾝份是⽗类本⾝的类型时,强制类型转换就会产⽣错误。例如:
Father father = new Father();
Son son = (Son) father;
这个系统会抛出ClassCastException异常信息。
14.5 Class类型信息
每⼀个Class类的对象就代表了⼀种被加载进⼊JVM的类,他代表了该类的⼀种信息映射。
开发者可以通过三种途径获得⼀个类的Class对象:
1. Class类的ForName()⽅法的返回值。
2. 访问所有类都⽤的静态属性—class
3. 调⽤⽗类Obejct的⽅法getClass(),并不是静态的。
在Class类中,定义许多关于⼀个类详细信息的⽅法。例如,getName(),getMethod(),getConstructoe(),newInstance()等可以⽤于反射开发,还有isInstance()和isInterface()等⼀些关于类的功能⽅法。
Class > Methods > isInstance(obj) 判断⼀个对象属于不属于这个类型。⼦类也算
public boolean isInstance(Object obj)
Determines if the specified Object is assignment-compatible with the object represented by this Class. This
method is the dynamic equivalent of the Java language instanceof operator. The method returns true if the
specified Object argument is non-null and can be cast to the reference type represented by this Class
object without raising a ClassCastException. It returns false otherwise.
isAssignableFrom(Class
public boolean isAssignableFrom(Class<?> cls)
Determines if the class or interface represented by this Class object is either the same as, or is a superclass or superinterface of, the class or interface 14.6 反射:运⾏时的类信息

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。