本文目录:【蓝色部分为本章的目录】
1.基本概念 2.Java变量相关 1)Java变量分类 2)Java中变量的初始化 3)Java变量修饰符和访问域 4)Java类修饰符[不包含内部类] 3.Java涉及OO的关键知识点【主体】 1)继承的基本概念 2)抽象类、接口、final类:3)重载和重写:4)对象的拷贝[深拷贝和浅拷贝]:5)关键字this、super6)Java中的inlining[内联]7)带继承的构造函数以及构造顺序8)谈谈Object中的方法:equals、hashCode、toString9)带继承的类型转换以及转换中关于成员变量和成员函数的调用10)Java语言中的反射11)按引用传递和值传递原理12)Java中的包和导入13)匿名类和内部类4.Java编程OO设计技巧1)对象创建以及周期2)对象属性设置3)垃圾回收4)继承、接口、抽象类5.总结 6)Java中的inlining[内联] i.JIT介绍: JIT是Just in time,即时编译技术的缩写,使用该技术,能够加速Java程序的执行速度。一般情况下,我们写出来的Java源代码通常使用javac编译称为java字节码,JVM通过解释字节码将其翻译成机器可以识别的机器指令,然后逐条读取和翻译。但是使用解释执行的执行速度比可执行的二进制字节码要缓慢,为了提高其执行速度,就引入了JIT技术。JIT在运行的时候,会把翻译过的机器码进行指令缓存,以准备下次调用,理论上讲,使用JIT技术,速度可以接近于以前的纯编译技术。 JIT执行原理:当JIT编译启用的时候【默认是启用的】,JVM会读取.class文件,将该文件发给JIT编译器,JIT编译器会将该字节码编译称为本机机器码,然后将编译好的机器码进行指令缓存,当下一次遇到二进制字节码的时候,JIT运行时会去判断该指令是否已经缓存,如果缓存过该指令,就直接从指令缓存里面读取对应的机器码,然后进行翻译执行,若没有缓存过的机器指令还是按照原来的执行步骤进行。根据上边的原理,JIT编译器会针对每条字节码进行编译,有时候会出现编译过程负载过重,为了避免这样的情况,JVM会选择性地去编译字节码,目前的JDK里面实现的JIT技术仅仅针对经常执行的字节码进行编译。 *:虽然JIT可以提高代码执行的速度,但是程序员在写程序的过程不能依赖JIT,最终执行的速度不仅仅由JVM本身决定,大部分情况还是取决于代码本身的结构,如果代码本身结构不够好,有可能JIT会减低代码执行速度。 ii.使用Java语言特性让JIT实现运行时inlining: 写过程序的程序员都知道,内联在编译器编译代码过程的速度是很快的,而Java语言本身不像C++一样直接可以使用inline关键字来进行内联操作,但是Java有个技巧可以实现对应的内联候选操作。如果要让Java里面的函数称为inlining的候选者,必须声明为private、static或final。这种函数的速度很快原因根JVM编译Java代码的过程有关,这样的函数在JVM里面可以在编译期被静态决议,而不是动态决议。使用函数体替换函数调用会使得代码执行更快,但是如果使用了大量的这种inlining方式,会造成代码的体积膨胀,所以一般情况下我们可以在写代码的过程考虑针对小函数使用inlining。Java编译器的JIT原始版本是没有inlining的能力的,但是针对新版本的JVM,如果一个函数定义为了static、final或private,则函数本体将会inlining化,这样就可以在不明显体积的情况下改善执行效率。 这样的代码针对Java编译器本身而言是不会出现inlining化操作的,但是针对目前大部分JITs而言,就可以在运行时被inlining化,并且可以显著提高代码的性能,其效率倍率估计在3倍左右。 inline函数仅仅在这个函数被多次调用的时候,才能感觉到性能的显著提升,这个道理很简单,因为inlining化的函数被inline过后,就不需要再负担函数调用的开销,调用次数多了实际上不是没有消耗系统资源,只是节省了更多的系统资源,整体上讲性能会有显著提升。 需要注意的是,JIT技术的引入,某个函数是否实现inlining的决定在于两个时间,一个是由编译时Java编译器决定的,另外一个是由运行时JIT决定的,不同的Java编译器和JITs产品可能使用不同的规则来inlining化Java里面的函数,甚至有可能有些编译器关闭了inlining。使用编译器inline函数和使用JITinline函数各有利弊,如果是编译器inline了一个函数,JIT的负载就会降低,代码执行效率显著提高,但是在这种情况下,一旦这个函数有了变化,所有调用了该函数的.class文件内的代码是无法感知的,这种情况需要将所有调用该函数的代码重编译才行。想法,如果是JIT在运行时inline了一个函数,任何时候发生了函数的本质变化,所有class都可以感知,效率虽然不如编译时inline,但是灵活性会增强,反而减少了重编译的次数。 7)带继承的构造函数以及构造顺序 i.构造函数概述: 构造函数是对象被创建的时候提供的一种特殊的数据结构,这里需要明白的是构造函数本身不是函数。它具有和它所在的类完全一样的名字。一旦定义好一个构造函数,该对象在被实例化的过程会自动调用它,而构造函数本身没有任何返回类型,不能错误地认为构造函数的返回类型是void,构造函数返回值的类型就是该类本身。在初始化过程,构造函数的任务就是将一个对象的内部状态进行初始化,从内存结构上讲,一旦一个对象通过构造函数初始化结束过后,我们就通过构造函数拿到了一个清楚、可用的对象,该对象的数据结构以及存储模型在这个时候是可以被JVM清晰识别的。 构造函数在源代码级别我们可以认为是一个特殊的方法,构造方法有时候我们又称为一个类的构造子,其特征如下: [1]构造函数的方法名必须与类名相同 [2]构造函数没有返回类型,也不能使用void作为返回类型,在方法名前面不能生命方法类型 [3]构造函数的作用是为了完成对象初始化,它能够把定义对象时的参数传给对象 [4]构造函数不能由代码本身像其他方法一样进行调用,只能JVM本身在初始化一个对象的时候自行调用 [5]一个类可以有多个构造函数,构造函数同样支持重载,重载法则遵循函数的重载法则 [6]如果没有定义任何构造函数系统会默认一个无参数的构造函数,此函数什么也不做 ii.JVM类加载器如何加载.class文件: 从底层看来,JVM本身就是以Java字节码为指令组成的抽象CPU,在服务端或者本地由开发人员先编译出.class文件,然后放在服务器上或者本地,然后在客户端远程调用或者直接从本地系统进行调用。可以这样理解,当一个Java程序启动的时候,JVM本身的实例就诞生了,而该JVM实例的运行起点就是我们经常写的程序的入口: public static void main(String args[]) 这里有一点需要理解的概念是,虽然我们经常将上边这句代码写入一个public class的类里面,实际上这个程序入口和代码里面包含它的类没有直接的关系,这个程序入口放入某个类里面的目的只是为了JVM能够找到该程序的入口,并不是说这个函数也属于某个,这点我们可以用最简单的代码来证明,也就是说,即使类不存在,这个函数入口也是有效的,一旦JVM找到了某个类里面的函数入口,就会将该class作为JVM实例运行的起点来对待,这个起点也就是我们平时所说的程序入口。所以当我们用java命令输入:java Helloworld的时候,实际上JVM是在寻找Helloworld.class文件里面是否存在一个函数入口,入口定义如上边这段代码所写。 JVM实例运行起来过后:main会作为该程序初始线程的起点,也就是说任何其他的线程都必须由该线程来启动。 JVM内部有两种线程:守护线程和非守护线程【这里不做这方面详细介绍,只需要理解的是main属于非守护线程,守护线程一般是由JVM自己使用,java程序本身也可以标明自己创建的线程是守护线程。当程序中所有非守护线程都终止的时候,JVM实例就消失了,然后JVM才会退出。在JVM的安全管理允许的范围内,我们也可以使用System.exit()来退出,只是这样的退出方式是要受JVM安全管理器的控制的,我们平时写代码一般都是在本机运行,调用的代码都是本地代码,所以不会存在安全问题,如果.class文件是存在于服务端,在执行该代码的时候需要签名】 当JVM实例在寻找函数入口过程,会自己启动内部的ClassLoader,也就是平时我们所说的类加载器,类加载器会按照类加载原理把运行环境下所有编译好的类按需要加载进来【*:有些类会延迟加载】。 然后JVM开始按照main函数里面的代码顺序进行.class文件的执行 iii.对象初始化流程: 我们根据一段代码来分析对象初始化流程:/** *基类包含一静态变量、包含一实例变量 *包含一个静态初始化块以及一个构造子 */class Base{ public static int a = 10;public int b = 20;
static
{
System.out.println("Static Init Base " + a);
//System.out.println("Null Init " + b);
}
public Base()
{
System.out.println("Init Base " + this.b);
}
}/** *一级子类和基类包含的内容一样 **/class SuperClass extends Base{ public static int a1 = getSuperStaticNumber();
public int b1 = getSuperInstanceNumber();
public SuperClass()
{
System.out.println("Init SuperClass" + this.b1);
}
static
{
System.out.println("Static Init SuperClass" + a1);
}
public static int getSuperStaticNumber() {
System.out.println("Static member init");
return 100;
}
public int getSuperInstanceNumber()
{
System.out.println("Instance member init");
return 200;
}}/** *二级子类为测试该代码的驱动类 */public class SubClass extends SuperClass{ public static int a2 = getStaticNumber();
public int b2 = getInstanceNumber();
public SubClass()
{
System.out.println("Init SubClass " + this.b2);
}
public static int getStaticNumber()
{
System.out.println("Static member init Sub");
return 1000;
}
public int getInstanceNumber()
{
System.out.println("Instance member init Sub");
return 2000;
}
public static void main(String args[])
{
new SubClass();
}
static
{ System.out.println("Static Init " + a2);
}
} 这段代码会有以下输出:Static Init Base 10Static member initStatic Init SuperClass 100Static member init SubStatic Init 1000Init Base 20Instance member initInit SuperClass 200Instance member init SubInit SubClass 2000 [1]对象在初始化过程,JVM会先去搜索该类的*父类,直到搜索到我们所定义的SubClass继承树上直接继承于Object类的子类,在这里就是Base类; [2]然后JVM会先加载Base类,然后初始化Base类的静态变量a,然后执行Base类的静态初始化块,按照这样第一句话会输出:Static Init Base 10【*:此时该类还未调用构造函数,构造函数是实例化的时候调用的】 [3]然后JVM按照继承树往下搜索,继续加载Base类的子类,按照静态成员函数->静态成员变量->静态初始化块的顺序往下递归,直到加载完我们使用的对象所在的类。 [4]类加载完了过后开始对类进行实例化操作,这个过程还是会先搜索到直接继承于Object类的子类,在这里就是Base类; [5]JVM会实例化Base类的成员函数,然后实例化成员变量,最后调用Base类的构造函数; [6]之后,JVM会递归往继承树下边进行调用,顺序还是遵循:成员函数->成员变量->构造函数; [7]最后直到SubClass类的构造函数调用完成 按照上边书写的逻辑,我们就很清楚了上边源代码的执行结果,而整个JVM初始化某个类的流程就是按照以上逻辑进行 在构造函数调用过程,有几点是需要我们留意的,这里就不提供代码实例,有兴趣的朋友可以自己去试试 [1]如果一个类的父类没有无参数构造函数,也就是说父类自定义了一个带参数的构造函数,那么系统不会提供无参数构造函数,此时子类在调用构造函数的时候必须最开始显示调用super(param),因为在构造函数调用之前系统总会先去调用父类的构造函数 [2]若一个类定义的时候没有提供构造函数,JVM会自动为该类定义一个无参数的构造函数 [3]一个类在调用构造函数的时候,JVM隐藏了一句代码super(),前提是父类未定义构造函数或者显示定义了无参构造函数;其含义就是调用父类的构造函数,如果父类的无参数构造函数被覆盖的话需要在子类构造函数中显示调用父类带参数的构造函数 [4]当类中的成员函数遇到变量的时候,会先根据变量名在函数域即局部变量范围内去寻找该变量,如果找不到才会去寻找实例变量或者静态变量,其意思可以理解为局部变量可以和实例变量或者静态变量同名,而且会在函数调用过程优先使用,这个原因在于在函数范围内,如果调用的变量是实例变量,其中前缀this.被隐藏了。 以上的流程和法则需要多写点代码来进行检测,因为还会出现疑惑的地方,这个在后边我会慢慢讲解。 8)谈谈Object中的方法:equals、hashCode、toString i.equals方法: 改写equals方法表面上看起来很简单,但是如果改写不好有可能会导致错误,并且后果可能不堪设想。在Java语言里面,容易混淆的就是Object的一些方法以及对应的特性以及相关原理,而针对概念上讲,对于Object的实例而言,以下几种条件是我们在操作过程期望的结果: [1]一个类的每个实例应该是唯一的而且是独立的 [2]不应该去关心一个类是否提供了“逻辑相等”的测试功能 [3]超类改写过equals方法的情况下,考虑继承过来的行为对子类本身是否合适 [4]一个类是私有的,或者是包内私有,可以确定的是它对应的equals方法永远不应该被调用 针对对象来说,等价意味着两个对象必须满足的逻辑等价性,从数学的角度来讲包括以下几个点: [1]自反性:可以这样理解:a.equals(a)这句代码应该返回true,也就是说一个对象必须等于其本身,这里的对象指代的是对象的内容。这条在我们改写equals方法的时候是不能违背的。 [2]对称性:对称性理解为:a.equals(b)返回为true那么b.equals(a)也会返回true,同样如果前者是false的话后者应该也是false。这条在我们改写equals方法的时候也是不能违背的。 [3]传递性:传递性理解为:若a.equals(b)返回为true,b.equals(c)为true,那么a.equals(c)也应该返回true。 [4]一致性:任何时候,如果a.equals(b)返回为true,在a和b两个对象内部的属性不改变的情况下,任何一个时候都应该让a.equals(b)返回为true,这就是对象内容相等的一致性。 [5]非空性:对于任何一个对象a,a.equals(null)应该永久返回为false。 区别==和equals方法: 先看一段简单的代码:public class TestEquals{ public static void main(String args[])
{
int a = 10;
int b = 10;
System.out.println("a == b is " + (a==b));
Integer a1 = new Integer(10);
Integer b1 = new Integer(10);
System.out.println("a1 == b1 is " + (a1==b1)); System.out.println("a1 equals b1 is " + a1.equals(b1));
}
} 运行上边的代码我们将会得到以下的输出:a == b is truea1 == b1 is falsea1 equals b1 is true 接下来我们分析一下上边的代码来找出==和equals的一些区别: 从概念上讲: 1]针对基本数据类型而言,==比较的是两个变量的值,而针对基本数据类型,不存在equals方法来比较两个变量的值; 2]针对符合数据类型而言,==比较的是两个引用是否指向同一个对象,而equals方法比较的是两个对象的内容是否真正相等; 用我们平时学习数学的逻辑而言,输出的第一行和第三行是很好理解的,它们所表示的就是逻辑上的内容相等,针对原始数据和复合数据类型比较的都是内容等价性,唯独很难理解的是第二行输出。对于复合数据类型而言,==比较的是两个引用是否指向同一个对象,简单地讲==比较的是内存栈上的两个引用是否指向同一个内存堆上的对象,可以这样讲:如果两个引用a==b,那么调用a.equals(b)一定会返回true,而如果a.equals(b)返回为true,而a==b不一定是true,有可能返回false。下图体现了==操作和equals操作: 上图有三个引用a、b、c,上边是Integer对象1,它的内容为Integer实例,它的值为10,下边还有一个Integer对象2,它的内容也为Integer实例,值为10,左边的方格就是有序的内存栈,右边的椭圆就是内存堆,同样可以认为是JVM分配的对象池 [1]上边三个引用a==b将返回false,b==c将返回true,因为a引用指向的是对象1,而b引用和c引用指向的是对象2,所以==比较的是两个引用是否指向同一对象 [2]而上边三个引用a.equals(b)、b.equals(c)、a.equals(c)返回都是true,因为对象1和对象2的内容都是10,equals比较的是两个对象里面的值是否相等 【*:Integer调用equals方法比较的是对象的内容是因为Integer重写过Object的equals类,如果一个自定义的类,在没有重写equals类的时候,调用的是Object原始equals方法,而Object原始的equals方法和==是等价的,也就是说在自定义类没有覆盖equals方法值钱,调用equals方法和使用==是等价的,也就是说equals方法的缺省实现就是==】 所以在实现高质量的equals方法的时候,这里提供一个原则: [1]使用==操作符检查“实参是否为指向对象的一个引用” [2]使用instanceof操作符检查“实参是否为正确的类型” [3]把实参转换到正确的类型,因为比较的是对象,引用指向的对象的类型就是最终比较的目标 [4]对于该类中每一个“关键”域,检查实参中的域与当前对象中对应的域值是否匹配 [5]当我们写完了equals方法过后,需要遵循上边的不可违背的原则 在改写equals方法的时候,还有一些保证高质量equals方法的条件: [1]当我们改写equals方法的时候,总是要改写hashCode方法 [2]不要企图让equals方法去追求过度的等价关系,这种等价关系只是从逻辑上等价就可以,有些附加的对象属性是不需要相等的比如两个同样的文件内容,创建时间不一样,不能比较创建时间这个属性 [3]不能让equals方法去依赖不可靠资源,这种实现在网络访问的时候常见,比如每次拿到的某个路由器的IP地址,虽然是同样的URI,但是返回的地址不一定每次都一样 [4]不要将equals生命中的Object对象替换为其他类型,equals最好使用重写的方式重写Object的equals方法 ii.hashCode方法 Java集合里面很多结合是基于散列值进行的,这样的集合包括HashMap、HashSet、Hashtable。按照上边改写equals的法则,虽然自定义对象改写了equals方法,但是有可能调用的时候返回的还是false,原因就在于在改写equals方法的时候需要两条作为约束。我们先看看Java规范里面关于hashCode方法的约定【以下为摘录翻译】: [1]在一个应用程序执行期间,如果使用一个对象的equals方法进行比较,如果两个对象的相关属性没有进行任何修改的话,该对象多次调用hashCode方法的时候,必须始终如一返回一个整数,而在同一个应用程序多次执行过程中,这个整数却可以不一样,即应用程序每次执行的时候,这个整数可以是不相同的。JVM会在每次返回该整数的时候返回一个散列值,这个散列值在同一次执行的时候返回的是同一个散列值,但是在执行不同的次数的时候,整数可能会变化 [2]如果两个对象调用equals方法是相等的,那么调用这两个对象的任一个对象的hashCode返回的散列整数值是相等的,也就是说,如果两个对象的equals返回true,那么两个对象的hashCode返回的散列整数值是相等的 [3]如果两个对象调用equals方法返回的值是不相等的,那么调用两个对象中任意一个的hashCode方法,不要求必须产生不同的整数结果。但是程序员本人应该知道,对于不相等的对象产生不同的散列整数,是有可能提高散列表的性能的 这样就可以理解的是,为什么仅仅满足改写了equals方法,还是没有办法使得两个调用对象是内容相等的,这两条约束就是: [1]在改写每个equals方法的类中,必须对应改写hashCode方法 [2]相等的对象必须具有相等的散列值hashCode。 从这里可以明白的是JVM内部判断两个对象内容是否相等,主要在于它们的散列整数值是否相等,即使两个对象属性不同,比较的时候主要是依靠hashCode法则来进行最终的判断,也就是说,两个内容相等的对象返回的hashCode必须是相等的。 iii.toString方法 在自定义类的时候,总是要改写toString方法,如果toString方法直接从String继承过来,它的输出格式为:“类名@一个散列码的无符号十六进制表示” 注意这里的用词,自定义类的时候,总是要改写toString方法,这种约定和equals和hashCode的约定不一样,不像那样严格,但是提供一个良好的toString实现,可以使得该类提供了对应的信息,使用起来更加方便Debug以及各种需要的格式化信息输出,而我们在实现toString方法的时候尽量保证toString里面包含了该对象中的所有有用的信息,而且从规范的角度上讲最好提供一个良好的文档来描述重写的toString方法。下边就是一个简单的例子:/** *返回一个用户的相关信息 *里面的格式为: *“(用户编号) *User Name:用户名 *Password:用户的密码 **/public String toString(){ return "(" + userSerial+ ")" + "/nUser Name:" + userName + "/nPassword:" + password;
} 9)带继承的类型转换以及关于成员变量和成员函数的调用 Java中的复合类型的转型是一个比较复杂的课题,这种类型转化和我们用到的原始类型的向上向下转型不一样,而且在转型过程对方法的调用以及变量的调用都是一个需要理解的核心内容: i.理解引用类型和对象类型 在编写Java代码的时候,如果写了这样一句话:A a = new B();在这样一句话里面,其实引用和对象都是包含了类型的,引用的类型是A类的引用,而对象的类型是B类初始化的对象,所以根据这样一句话,必须了解的是引用和对象都存在类型这样的概念,而且在我们初始化一个对象并且将引用指向该对象的过程中,有可能对象的类型和引用的类型是不一样的。【当然上边代码满足的条件是A和B存在继承关系,而且A是B的同级类或者父类以及父类以上】 ii.理解Java的继承树 我们把定义好的类、抽象类、接口相互之间的结构描述出来的抽象结构成为继承树,先看下边的代码:interface A{}interface B extends A{}class B1 implements B{}class B2 extends B1{}class B3 extends B1{}interface C{}class C1 extends B2 implements C{}class C2 extends C1{}class C3 extends C1{}class C4 extends C2{} 根据上边的定义,我们可以绘制出对应的继承树 上边的图就是上边定义的类和接口的一个继承树,箭头从树叶指向树根,这里有几个需要认识的是关于“跨类引用”: [1]如果引用a是类A的一个引用,那么a可以指向类A的一个实例,或者说指向类A的一个子类的实例,这是向上转型的一种情况 [2]如果a是接口A的一个引用,那么a必须指向实现了接口A的一个类的实例,或者是实现了该接口类的子类,这是接口回调的情况 Java语言里面,向上转型是自动的,向下转型是需要强制转换的,这里需要区别的概念有: 接口回调:接口回调的意思就是可以把实现了某一个接口的类创建的对象的引用赋给该接口声明的接口变量,那么该接口变量就可以调用被类实现的实现的接口的方法。这一个过程实际上是通知相应的对象调用接口的方法,这一个过程称为对象功能的接口回调 向上转型:向上转型唯一不同的是向上转型的引用类型不是接口申明,二是类申明,而且在向上转型的过程里面,还会牵涉到父类方法的重写 接下来我们看下边的代码段:A a = new C4(); //这是接口回调,因为A是接口,C4是类,按照继承树的单向遍历可以知道C4的第四代父类B1实现了接口A的子接口B,所以这种做法是可以通过Java编译器的C c = new C1(); //这是接口回调,C是接口,而C1类直接实现了接口CB b = new B1(); //这里是接口回调,因为B是接口,B1是直接实现了接口B的类B1 b1 = new B3(); //这里是向上转型,B1是B3的父类,这里申明的引用是B1的类型,但是实例化的对象是B3类型B1 b2 = new B2(); //这里是向上转型,B1是B2的父类,这了生命的引用是B1的类型,但是实例化的对象是B2的类型C1 c1 = new C4(); //这里是向上转型,C1是C4的二代父类,申明的引用是C1的类型,实例化的对象是C4的类型C1 c2 = new C2(); //这里是向上转型,C1是C2的直接父类,申明的引用是C1类型,实例化的对象是C2的类型c1 = c2; //这里是向上转型,可以理解为将引用c1指向c2指向的对象,因为c1的引用类型是C1,c2指向的对象是C2类型所以这句话是可以通过编译的c2 = (C1)c1; //这里是向下转型,c2引用类型是C1,而c1指向的对象类型是C4,因为C1是C4的二代父类,所以这句话也是可以通过编译的c1 = (B1)b2; //这句话不能通过编译,因为引用c1的类型是C1,b2指向的对象类型是B2,而C1指向的不是B2,所以这句话是不能通过编译的b1 = (B3)b2; //这句话可以通过编译,因为引用b1的类型是B1,b2指向的对象类型是B2,而最终转换成的类型是B3,但是因为B2是B1的子类,所以可以通过编译,但是这句话会报运行时错误,因为意味着将一个B2对象转换成为一个B3对象,因为B2和B3属于兄弟类型,它们之间没有直接和间接的父子关系,所以这句话可以通过编译却在运行的时候会抛出ClassCastException 上边这些代码的结果写在注释里了,所以我们可以总结为: [1]通过子类对象引用赋值给超类对象的引用变量可以实现动态的方法调用 [2]不能把父类对象引用赋给子类对象的引用变量,这种情况需要进行强制转换 [3]记住一个简单而复杂的规则,一个类型引用只能引用自身的方法和变量 在转型过程分析需要一点:转型过程首先分析引用类型,然后分析转换的引用指向的对象类型,先判断它们之间是否存在继承关系,如果存在继承关系,可以通过编译,如果不存在继承关系,不能通过编译,最终看向下转型的最终目标类型,如果目标类型和被转换的引用指向的对象存在继承关系,是不会异常,如果二者不在继承树上,就会出现转型异常。 iii.理解Java的动态绑定: JVM在方法调用的过程中会做以下几个事情: 编译器查看对象声明的类型和方法(包括对象变量的声明类型),通过声明类型找到方法列表 编译器查看调用方法提供的参数类型 如果方法是private、static、final或者构造子,编译器就可以确定那个方法了,实现完了这几个步骤就是JVM的静态绑定 如果执行完了过后还没有检索完类里面的其他方法,就使用动态绑定,在程序运行过程,动态绑定就意味着:JVM将调用对象实际类型所限定的方法,下边是动态绑定的过程: JVM提取对象的实际类型的方法表 JVM搜索对应的搜索签名 调用该方法 动态绑定的条件可以看以下几点 [1]是同一父类 [2]可以有抽象方法
[3]不一定,private修饰的变量和方法不可被子类继承,在子类中可以增加子类的变量和方法
动态绑定针对两个有继承或者实现关系的类而言,具体操作为: [1]编译器检查对象的声明类型和方法名,假设我们调用x.f(args)方法,并且x已经被声明为C类的对象,那么编译器会列出C类中所有的名称为f的方法和从C类的超类继承过来的f方法 [2]接着编译器检查方法调用中提供的参数类型,如果所有名为f的方法中有一个参数类型和调用提供的参数类型最匹配,就调用这个方法,这种方式称为“重载解析” [3]当程序运行并且使用动态绑定调用方法的时候,虚拟机必须调用同x所指向的对象的实际类型匹配的方法版本。假设实际类型为D(C类的子类),如果D类定义了f(String)那么该方法会被调用,否则就在D的超类中搜索f(String),依次递归 按照上边的原理,看一段代码:public class TestIterator{ public static void main(String args[])
{
Collection c = new LinkedList();
c.add("WWW");
c.add("JJJ");
c.add("KKK");
List list = (List)c; int i = 1;
System.out.println(c.remove(i));
System.out.println(list.remove(i));
}
} 以上代码的输出为falseJJJ 分析上边的代码,Collection中有boolean remove(Object o)方法,而List有E remove(int index)和boolean remove(Object o);其中E remove(int index)并没有覆盖Collection中的任何方法,它和boolean remove(Object o)知识函数相同而签名的参数类型不一样,当我们调用c.remove(1)的时候,在Collection接口中只找到了boolean remove(Object o)符合函数声明,于是实际上动态绑定了LinkedList的boolean remove(Object o),并将i自动装箱成为Object类型,所以返回的值自然是false。而调用list.remove(i)的时候,根据参数类型找到了最合适的函数就是E remove(int index),于是直接绑定了E remove(int index)方法,返回了对象的值,所以就可以理解上边为什么会有这样两行输出了。
10)Java语言中的反射 Java反射在这个章节仅仅做一个简单的介绍,反射的高级应用我会用专程的章节来进行详细说明,包括实际应用、动态定义类以及动态调用方法以及AOP相关动态代理的实现 i.概念: JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。 ii.反射的作用: [1]可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型 [2]应用程序需要在运行时从某个特定的程序集中载入一个特定的类型,以便实现某个任务时可以用到反射。 [3]反射主要应用与类库,这些类库需要知道一个类型的定义,以便提供更多的功能。 iii.应用点: [1]现实应用程序中很少有应用程序需要使用反射类型 [2]使用反射动态绑定需要牺牲性能 [3]有些元数据信息是不能通过反射获取的 [4]某些反射类型是专门为那些clr 开发编译器的开发使用的,所以你要意识到不是所有的反射类型都是适合每个人的。 iv.反射的性能: 使用反射来调用类型或者触发方法,或者访问一个字段或者属性时CLR需 要做更多的工作:校验参数,检查权限等等,所以速度是非常慢的。所以尽量不要使用反射进行编程,对于打算编写一个动态构造类型(晚绑定)的应用程序,可以采取以下的几种方式进行代替: [1]通过类的继承关系。让该类型从一个编译时可知的基础类型派生出来,在运行时生成该类 型的一个实例,将对其的引用放到其基础类型的一个变量中,然后调用该基础类型的虚方法。 [2]通过接口实现。在运行时,构建该类型的一个实例,将对其的引用放到其接口类型的一个变量中,然后调用该接口定义的虚方法。 [3]通过委托实现。让该类型实现一个方法,其名称和原型都与一个在编译时就已知的委托相符。在运行时先构造该类型的实例,然后在用该方法的对象及名称构造出该委托的实例,接着通过委托调用你想要的方法。这个方法相对与前面两个方法所作的工作要多一些,效率更低一些 11)按引用传递和值传递原理 在Java里面,一般普遍存在的误解是:Java中的参数以引用(by reference)方式传递,其实这不是真的,应该确切地说:参数其实是以by value方式传递!其实Java里面,所有的参数都是以值方式传递的。 看下边的代码:import java.awt.Point;public class PassByValue{ public static void modifyPoint(Point pt,int j)
{
pt.setLocation(5,5);
j = 15;
System.out.println("During modifyPoint " + " pt = " + pt + " and j = " + j);
}
public static void main(String args[])
{
Point p = new Point(0,0);
int i = 10;
System.out.println("Before modifyPoint " + " p = " + p + " and i = " + i);
modifyPoint(p,i);
System.out.println("After modifyPoint " + " p = " + p + " and i = " + i);
}
} 运行这段代码可以知道输出:Before modifyPoint p = java.awt.Point[x=0,y=0] and i = 10During modifyPoint pt = java.awt.Point[x=5,y=5] and j = 15After modifyPoint p = java.awt.Point[x=5,y=5] and i = 10 这个结果现实了modifyPoint方法改变了Point对象,但是没有改变int i。在main()中,i被赋值为10.由于参数是By Value方式传递,所以modifyPoint()收到的i的一个副本,然后将这个副本改成15,并且返回,所以main内原值i没有受到影响。对比之下,如果按照值传递,应该不会修改Point里面的值,但是刚好想法,程序修改掉了Point里面的x和y值。关键的原因在于,实际上Point对象在modifyPoint方法里面是在和Point对象的引用副本打交道,而不是在和Point对象的副本打交道。在这里p是一个对象引用,这个地方Java以值传递的方式传递了这个对象引用。当p从main()被传入modifyPoint()时,传入的p的副本。也就是说Java在函数传入的时候,值传递的意思就是传入原始变量的值的副本值以及复合变量的引用的副本而非对象副本,这就是Java里面按照值传递的真正含义。 12)Java中的包和导入 i.package概念 Java中的“包”是一个比较重要的概念,package定义的是:一个包就是一些提供访问保护和命名空间管理的相关类与接口的集合,使用包的目的就是使类容易查找使用,防止命名冲突以及访问控制。Java中的package关键字必须是Java源文件除去注释以外的第一条语句,导入包的语句可以有很多,但是必须位于package语句之后,以及在类定义之前,一个源文件中有多个类,但最多只能有一个是public的。 ii.静态导入 静态导入的使用,在于可以使用其他类中定义的类方法和类变量,而且这些方法变量就像在本地定义一样。也就是说,静态导入允许调用其他类中定义的静态成员时,可以忽略类名 这里用一段代码来演示静态导入的使用,先定义一个类:package com.test;
public class StaticValue { public static final int A = 10; public static final double B = 10.10;
public static double add(double first, double second) { return (first + second); }} 然后使用驱动程序类制造Java程序运行的入口:package com.test;
import static com.test.StaticValue.A;import static com.test.StaticValue.B;import static com.test.StaticValue.add;
public class StaticImport { public static void main(String args[]){ System.out.println("Value A is " + A); System.out.println("Value B is " + B); double f = add(1.1, 2.2); System.out.println("Value f is " + f); }} 该程序的输出为:Value A is 10Value B is 10.1Value f is 3.3000000000000003 静态导入没有其他的语法,只是在import的时候不能使用普通的import语法,必须使用import static; 13)匿名类和内部类 i.内部类: Java的内部类又成为InnerClass,内部类表面上看,就是类中定义了一个类,但是实际上并不像我们想象中那样简单,咋一看内部类有些多余,它的用处对于初学者来说可能并不是那么显著,但是随着对它的深入了解,就知道内部类的设计是很巧妙的过程。 [1]内部类的代码段:public class TestOuter{ protected class InnerOne
{
public void writeLine()
{
System.out.println("Hello");
}
}
public static void main(String args[])
{
TestOuter outer = new TestOuter(); TestOuter.InnerOne innerOne = outer.new InnerOne(); TestOuter.InnerOne innerOne2 = new TestOuter().new InnerOne(); innerOne.writeLine(); innerOne2.writeLine(); }
} 以上是内部类初始化的语句,初始化的时候有两种方法, 一种方法是: OuterClass.InnerClass class1 = new OuterClass().new InnerClass(); 另外一种方法是: OuterClass class2 = new OuterClass(); OuterClass.InnerClass class1 = class2.new InnerClass(); 在上边的初始化的语句里面,InnerClass如果是使用类可以访问的话,可以将OuterClass.InnerClass简化写成InnerClass; [2]非静态内部类对象有着指向外部累的引用 将上边的代码段修改为:public class TestOuter{ private int a = 0; protected class InnerOne { private int a = 1; public void writeLine() { System.out.println("Hello + " + this.a); System.out.println("Hello + " + TestOuter.this.a); } } public static void main(String args[]) { TestOuter outer = new TestOuter(); InnerOne innerOne = outer.new InnerOne(); innerOne.writeLine(); }} 针对原来的代码做了以下修改,我们给TestOuter类增加了一个私有成员变量a,然后在内部类里面可以直接访问a。其意思就是说一个内部类对象可以访问创建它的外部类对象的内容,甚至包括直接访问私有变量!这个特性很有用,为我们设计时提供了更多的思路和捷径,如果要实现这个功能,内部类对象就必须有指向外部类对象的引用。Java编译器在创建内部类的时候,隐式地把外类对象的引用也传了进去并且保存下来。这样就使得内部类对象始终可以访问其外部对象,这也是为什么在外部类作用范围之外要创建内部类对象必须先要创建一个外部类对象的原因。根据上边的代码可以知道,当外部类和内部类都存在一个同名的变量的时候,如果不带任何变量前缀,调用的是内部的变量,如果要调用外部类的变量需要使用下边格式:OuterClass.this.VAR 所以第二段程序的输出为:Hello + 1Hello + 0 [3]静态内部类 其实和普通的类一样,内部类也可以有静态的,不过和非静态类想比,区别就在于静态内部类没有了指向外部的引用,这个就和C++中嵌套类的概念是一样的,其实Java的内部类本质上和C++里面的嵌套类很相似,其区别在于是否拥有外部类的引用。除此之外,在任何静态内部类中,都不能有静态数据,静态方法或者又有一个静态内部类。不过静态内部类中却可以拥有这一切。 [4]局部内部类 Java里面的内部类可以是局部的,它可以定义在一个方法内或者一个语句块内,只是局部内部类不能带有修饰符:package com.test;
public class TestOuter{ public void fun(){ class Hell{ private int a = 0; public void test(){ System.out.println("Hello World"); } } Hell hell = new Hell(); hell.test(); } public static void main(String args[]) { TestOuter outer = new TestOuter(); outer.fun(); }} 从上边的的代码可以知道,Class本身的没有任何修饰符,而且使用域和局部变量的域相同。 ii.匿名内部类: Java的匿名内部类语法规则看起来古怪点,不过和匿名数组一样,可以使得代码变得更加简洁,最常见的例子就是Java Swing的ActionListener,如下边代码:public class TestOuter{ public Good cont(){ return new Good()
{
private int i = 11;
public void writeLine()
{ System.out.println("Write Line");
}
}
}
} 以上就是匿名内部类的使用方法,不过有一点需要注意的是,匿名内部类因为没有名字,所以它没有构造函数,如果这个匿名内部类继承了一个含有参数构造函数的父类,创建它的时候必须带上参数,并在实现的过程中使用super关键字调用相应的内容,如果要初始化成员变量,有以下几种方法: [1]如果在一个方法的匿名内部类,可以利用这个方法传入想要的参数,不过有一点,这些参数必须被声明为final。 [2]将匿名内部类改造成有名字的局部内部类,这样就可以使用构造函数了 [3]在这个匿名内部类中使用初始化代码块 iii.内部类好处 如果实现一个接口,但是接口中的一个方法和构想中的类中的一个方法名称或者参数相同,这种时候可以建一个内部类实现这个接口,由于内部类对外部类的所有内容都是可访问的,所以这样做可以完成直接实现这个接口的功能。其实Java语言中的内部类和接口可以完成C++里面的多继承设计,而且可以更好实现多继承的效果。