`
shangjava
  • 浏览: 1188878 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

3.Java中的类和对象【第二章节草案】

阅读更多

本文目录:【蓝色部分为本章的目录】
1.基本概念
2.Java变量相关
1)Java变量分类
2)Java中变量的初始化
3)Java变量修饰符和访问域
4)Java类修饰符[不包含内部类]
3.Java涉及OO的关键知识点【主体】
1)继承的基本概念
2)抽象类、接口、final类:
3)重载和重写:
4)对象的拷贝[深拷贝和浅拷贝]:
5)关键字this、super
6)Java中的inlining[内联]
7)带继承的构造函数以及构造顺序
8)谈谈Object中的方法:equals、hashCode、toString
9)带继承的类型转换以及转换中关于成员变量和成员函数的调用
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的候选者,必须声明为privatestaticfinal。这种函数的速度很快原因根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 voidmain(Stringargs[])
  这里有一点需要理解的概念是,虽然我们经常将上边这句代码写入一个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.对象初始化流程:
  我们根据一段代码来分析对象初始化流程:
/**
*基类包含一静态变量、包含一实例变量
*包含一个静态初始化块以及一个构造子
*/
classBase{
public static inta = 10;
public intb = 20;
static
{
System.out.println("Static Init Base "+ a);
//System.out.println("Null Init " + b);
}
publicBase()
{
System.out.println("Init Base "+this.b);
}
}
/**
*一级子类和基类包含的内容一样
**/
classSuperClassextendsBase{
public static inta1 =getSuperStaticNumber();
public intb1 =getSuperInstanceNumber();
publicSuperClass()
{
System.out.println("Init SuperClass"+this.b1);
}
static
{
System.out.println("Static Init SuperClass"+ a1);
}
public static intgetSuperStaticNumber()
{
System.out.println("Static member init");
return100;
}
public intgetSuperInstanceNumber()
{
System.out.println("Instance member init");
return200;
}
}
/**
*二级子类为测试该代码的驱动类
*/
public classSubClassextendsSuperClass{
public static inta2 =getStaticNumber();
public intb2 =getInstanceNumber();
publicSubClass()
{
System.out.println("Init SubClass "+this.b2);
}
public static intgetStaticNumber()
{
System.out.println("Static member init Sub");
return1000;
}
public intgetInstanceNumber()
{
System.out.println("Instance member init Sub");
return2000;
}
public static voidmain(Stringargs[])
{
newSubClass();
}
static
{
System.out.println("Static Init "+ a2);
}
}
  这段代码会有以下输出:
Static Init Base 10
Static member init
Static Init SuperClass 100
Static member init Sub
Static Init 1000
Init Base 20
Instance member init
Init SuperClass 200
Instance member init Sub
Init 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 classTestEquals
{
public static voidmain(Stringargs[])
{
inta = 10;
intb = 10;
System.out.println("a == b is "+ (a==b));

Integer a1 =newInteger(10);
Integer b1 =newInteger(10);
System.out.println("a1 == b1 is "+ (a1==b1));
System.out.println("a1 equals b1 is "+ a1.equals(b1));
}
}
  运行上边的代码我们将会得到以下的输出:
a == b is true
a1 == b1 is false
a1 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:用户的密码
**/
publicString toString()
{
return"("+ userSerial+")"+"\nUser Name:"+ userName +"\nPassword:"+ password;
}
  9)带继承的类型转换以及关于成员变量和成员函数的调用
  Java中的复合类型的转型是一个比较复杂的课题,这种类型转化和我们用到的原始类型的向上向下转型不一样,而且在转型过程对方法的调用以及变量的调用都是一个需要理解的核心内容:
  i.理解引用类型和对象类型
  在编写Java代码的时候,如果写了这样一句话:A a = new B();在这样一句话里面,其实引用和对象都是包含了类型的,引用的类型是A类的引用,而对象的类型是B类初始化的对象,所以根据这样一句话,必须了解的是引用和对象都存在类型这样的概念,而且在我们初始化一个对象并且将引用指向该对象的过程中,有可能对象的类型和引用的类型是不一样的。【当然上边代码满足的条件是A和B存在继承关系,而且A是B的同级类或者父类以及父类以上】
  ii.理解Java的继承树
  我们把定义好的类、抽象类、接口相互之间的结构描述出来的抽象结构成为继承树,先看下边的代码:
interfaceA{}
interfaceBextendsA{}
classB1implementsB{}
classB2extendsB1{}
classB3extendsB1{}
interfaceC{}
classC1extendsB2implementsC{}
classC2extendsC1{}
classC3extendsC1{}
classC4extendsC2{}
  根据上边的定义,我们可以绘制出对应的继承树
  上边的图就是上边定义的类和接口的一个继承树,箭头从树叶指向树根,这里有几个需要认识的是关于“跨类引用”:
  [1]如果引用a是类A的一个引用,那么a可以指向类A的一个实例,或者说指向类A的一个子类的实例,这是向上转型的一种情况
  [2]如果a是接口A的一个引用,那么a必须指向实现了接口A的一个类的实例,或者是实现了该接口类的子类,这是接口回调的情况
  Java语言里面,向上转型是自动的,向下转型是需要强制转换的,这里需要区别的概念有:
  接口回调:接口回调的意思就是可以把实现了某一个接口的类创建的对象的引用赋给该接口声明的接口变量,那么该接口变量就可以调用被类实现的实现的接口的方法。这一个过程实际上是通知相应的对象调用接口的方法,这一个过程称为对象功能的接口回调
  向上转型:向上转型唯一不同的是向上转型的引用类型不是接口申明,二是类申明,而且在向上转型的过程里面,还会牵涉到父类方法的重写
  接下来我们看下边的代码段:
A a =newC4();//这是接口回调,因为A是接口,C4是类,按照继承树的单向遍历可以知道C4的第四代父类B1实现了接口A的子接口B,所以这种做法是可以通过Java编译器的
C c =newC1();//这是接口回调,C是接口,而C1类直接实现了接口C
B b =newB1();//这里是接口回调,因为B是接口,B1是直接实现了接口B的类
B1 b1 =newB3();//这里是向上转型,B1是B3的父类,这里申明的引用是B1的类型,但是实例化的对象是B3类型
B1 b2 =newB2();//这里是向上转型,B1是B2的父类,这了生命的引用是B1的类型,但是实例化的对象是B2的类型
C1 c1 =newC4();//这里是向上转型,C1是C4的二代父类,申明的引用是C1的类型,实例
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics