本文目录:【蓝色部分为本章的目录】
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技术,速度可以接近于以前的纯编译技术。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操作:
} 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{} 根据上边的定义,我们可以绘制出对应的继承树
[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++里面的多继承设计,而且可以更好实现多继承的效果。