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

4.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.总结
4.Java编程OO设计技巧:
  以下这些内容牵涉到开发过程中的一些开发经验,以及个人整理的一些OO设计心得和项目实践内容,仅做参考,而且文字量比较大!我们在学习Java的时候,初学一般不会考虑到很多程序性能以及内存管理上的问题,但是我们在开发过程就会遇到很多偏向这个方面的问题,这些问题往往不是因为别的原因,就是因为我们本身写的代码质量的问题,这种情况在大型项目以及嵌入式系统开发的时候尤为突出。所以养成一个良好的开发习惯以及一个比较规范的代码习惯对自己本身是一个不错的学习语言的方式,如果能够针对语言基础掌握一些更加良好的编程技巧,就会使得开发的系统更加美丽。
  1)对象创建以及周期
  i.理解编译期(compile-time)优化
  ——◆编程心得[1]:不能依赖编译期优化技术——
  我们编程的时候已经习惯了[编译器优化能力],通常都是在开发过程关闭代码的优化选项,一旦等程序调试通过了,就打开编译器优化,让编译器产生优化代码。从优化的效率而言,现代编译器优化技术本身还是蛮先进的,而编译器优化过程会使得代码执行更加高效。正因为如此,很多程序员在编程的时候就过于依赖编译器的优化特性来清理自己写得很差的代码,但是从开发角度上讲,这是个坏习惯。其实我们在编写代码过程中,应该从计算机的思维来书写程序,因为这些代码最终会经过计算机编译器进行编译以及优化操作。
  Sun公司的javac编译器和其他的公司的编译器仅仅支持少量的优化技术,包括最简单的一些优化技术,如常量合并无用码删除
  [1]常量合并是编译器针对常量表达式的预处理过程,也可以称为预先运算,先看下边代码段:
static final intlength =12;
static final intheight =2;
intvalue = length * height;
  上边的代码将会在编译时运算,也就是当这段代码被编译成class的字节码过后,会转换为以下形式:
intvalue =24;
  这种情况就是编译器优化过程中的常量合并,这里需要提的是:常量合并不仅仅是javac编译器里面会用到,在其他语言编译器的优化技术中有时候也会碰到。
  [2]无用码删除:这种技术的主要目的,是为了让编译器不去为[绝对不会执行]的区段产生相关字节码(bytecode),这种技术不影响代码的执行,但是可以减少生成的class文件的体积,如下代码段:
public classTestNoExecuteCode
{
public static finalbooleantestCondition = false;
publicvoidtestMethod()
{
if(TestNoExecuteCode.testCondition)
{
//……
}
}
}
  以上代码里面if语句的语句块就称为[绝对不会执行],但是有一点,javac编译器判断这段代码是否绝对不会执行的规则是:运行时才会判断为false的表达式,还是会生成字节码(bytecode)的,不生成字节码(bytecode)的情况必须是——表达式在编译期间就已经被判断为false了。javac命令若打开优化代码使用javac -o,但是Sun的Java 2 SDK中-o编译器选项对生成的bytecode毫无作用,如今没有太大的必要使用这个选项,若开发过程我们真的需要将代码优化,有三个选择:【以下参考E文的翻译】

  优化Java源代码最好的方式是手工优化,如果要获得更好的性能,可以掌握一些手工优化方法;
  使用第三方的编译器,将源码编译为优化的字节码(bytecode)
  依赖JIT或者Hotspot运行时优化策略
  ii.理解运行时(runtime)代码优化
  ——编程心得[2]:善用运行时代码优化技术——
  虽然编译器在编译时不会产生大量优化过的字节码(bytecode),但是JITs却可以做各种优化工作,JITs的概念,可以参考第三章第六节:Java中的inlining,这里就不做重复介绍。JIT本身的运行目的就在于它会将字节码(bytecode)在运行时转换为本机二进制码(native binary code),某些JITs在转化之前,会先分析编译器生成的字节码,然后进行优化转换。JIT本身的目的在于将字节码转化称为本机二进制码,本机执行的方式通常比解释执行的方式速度快很多,从这点思考,如果被编译的字节码如果被操作系统执行的次数很多,这种转化是相当合算的。
  但是JIT的使用前提在于:JITs必须确保[收集数据和执行优化]的时间,不能超过优化节省的时间。
  一般情况下,JIT执行我们编译好的代码都会使程序更加快捷,但是开发过程可能会忽略一点,JIT本身的运行也是需要时间的,JITs是针对相对较少的运行时间设计,它的存在是为了加速代码而并非使代码缓慢下来,为了收集[充分的、为执行优化技术而必要的]数据,必须花额外的开销,而基于这些考虑,JITs又不得不去忽略一些优化技术。单纯依赖运行时优化的一个问题就是程序的大小,这一点主要体现在嵌入式系统开发以及实时编程中,因为这些程序对大小是有一定的要求的,而且存在特殊性;很多嵌入式系统本身没有内存去启动JIT或者Hotspot执行层,对需要Java快速运转的实时编程,JIT或者Hotspot会有不确定性。这点可以参考rtj技术,即Real Time Java。
  上边两点可以知道,绝佳组合就是:优化过后的bytecode和JIT或Hotspot执行层结合!
  [*: 编译时和运行时的相关内容,在Java异常部分同样会提及,如果有必要到时候我会写一份关于这种小知识点的总结性内容]
  iii.对象创建和使用
  ——编程心得[3]:减小对象创建的成本——
  我们写一段Java代码的时候,如果定义了一个类A,往往创建A的对象写的语句就是:
A a =newA();
  当然上边这种做法是我们都使用的标准做法,既然是如此,一个复杂对象在创建的时候有可能就会牵涉到对象的成本开销了,而这份成本开销是我们最容易忽略的。对象的构造过程往往不像我们想象中那样简单,我们印象中的创建对象就是:分配内存+初始化值域,这里我们再谈谈对象的创建。因为我们将需要创建的对象的数量和体积最小化,就可以一定程度上提升程序的性能,这种做法将称为任何系统本身的“福音”。所以我们必须彻底了解:一个对象创建过程。以下是对象创建过程(*:需要提及的是这里只是对象的创建,我们在学习Java的对象和类的时候一定要区分类加载对象创建两个过程针对源代码定义的变量以及成员产生的影响。)
  [1]从内存堆(heap)之中分配内存,用以存放全部的实例变量以及这个对象连同其父类以及整个继承树上的父类的实现专用数据,这种数据包括类和方法的数据区以及类和方法的指针。
  [2]该对象的实例变量被初始化称为对应的缺省值
  [3]调用最底层派生类的构造函数,构造函数做的第一件事情就是调用它的父类的构造函数,这个函数会递归调用直到Ojbect根类为止,这里也说明了一点:Java里面所有的Class的基类就是java.lang.Object。
  【*:这里纠正一个小小的笔误,以防读者误解。前边有个程序我直接写了调用Base类的构造函数,实际上对JVM本身而言在对象初始化的时候,确实是最先调用了Object的构造函数,也就是说从父类往下递归调用,但是这个递归的过程不是JVM直接调用的父类的,它是先调用的子类的构造,因为子类的构造函数的第一句话总是super()或者super(param),所以它会一直递归到Object类,每个类的实例化过程都是如此,所以希望读者能够理解这个初始化过程,同样理解在构造函数里面什么都不写的时候,它使用super关键字的隐藏语句,同样也可以解释为什么当父类定义了含参数的构造函数的时候,子类一定要显示调用父类的构造函数并且使用super(param)的方式】
  [4]在构造函数本体执行的时候,所有的实例变量都会设置初始值和初始化区段先执行,然后再执行构造函数本体。所以基类的构造函数先执行,然后才会执行子类的构造函数,这就使得任何构造函数都可以放心大胆使用父类的任何实例变量。
  【*:这里可以从概念上来理解这个设计过程,如果一个子类继承了父类,那么子类理所当然是可以使用父类的变量的,但是假设子类的构造在父类之前,那么就会出现子类在调用构造函数的时候,父类的变量还未进行初始化,而子类本身又不可能对父类的变量进行初始化,所以这种构造函数的递归调用在所有OO编程语言里面几乎都是如此的设计法则。八卦一句:我们在学习一门语言的时候尽可能去参透语言设计的为什么,这样更加容易辅助我们理解语言的一些特性。】
  既然一个对象的创建过程牵涉到了这么多过程,那么创建的时候创建一个轻量级对象比创建一个重量级对象的效率要快很多。而轻量级对象的含义可以理解为:不具有长继承链、同样不包含了太多其他对象的对象。这样就牵涉到如何使用OO设计使得对象变得更加轻量级,这里引入一个新概念POJO,POJO的概念在很多框架中都经常使用,其设计以及相关内容我可以推荐读者一本书《POJOs in Action》,POJO属于轻量级的对象,在很多框架诸如Hibernate、JDO中有时候都经常涉及到,而JPA规范里面所设计的领域模型对象大部分也属于POJO对象,而且将POJO规范化了,它的中文翻译可以为:简单Java对象。
  这里我们考虑一种情况,比如一个类A,里面有两个变量属于对象类型的,一个为B类的实例、一个为C类的实例,然后由假设A、B、C三个类都是Object之下的第三代子类,那么在初始化一个A的过程是如何做呢,这里留给读者自己去思考,这个时候A就属于重量级对象。这种对象初始化的做法和一个单纯的A类的初始化过程本质上一样,但是开销却是大相径庭,这里提供一个轻量级对象A的初始化过程:
classA{
private intval;
private booleanhasData =true;
publicA(inta)
{
val = a; //这里隐藏了一个this.只要形参变量和实例变量不重名,这种省略写法是合法的。
}
//……
}
  如果写入了这样一句代码来调用该对象:A a =newA(12);
  它的建立步骤如下:
  [1]从内存堆分配内存,用来存放A类的实例变量,以及一份[实现专属数据]
  【*:以防概念混淆,这里举个例子,实现专属数据可以这样理解,A的直接父类是Object,如果Object存在实例变量,那么内存堆分配内存的时候,同样包括A类以上整个继承链里面其他超类的数据区以及方法和类的指针】
  [2]A类的实例变量val和hasData被初始化为对应的缺省值,val的缺省值为0,hasData赋值为缺省值false
  [3]调用A类的构造函数,传入值5
  [4]A类调用父类的构造函数,这里父类为(java.lang.Object)
  [5]父类构造函数调用完成过后返回,A类构造函数执行实例化的初始值设定过程,这个时候hasData赋值为true
  [6]然后赋值将val设置为5,A的构造函数调用结束
  [7]然后将引用a指向刚刚内存堆里面完成的对象A
  以上就是一个轻量级对象的整个初始化过程,结合前边讲的对象的构造顺序来理解更加容易理解Java里面对象的构造流程,如果分析一个重量级对象的构造流程,你就会发现合理设计系统里面的对象是很重要的。一般情况下,如果我们在软件性能评测的时候可以确定性能问题是由于中型对象创建而成,那么我们可以采取一定的策略来回避:1]使用延迟加载;2]重新设计该类使之合理或者更加轻量级;当然本小节的书写目的不是为了让读者在开发过程不使用重量级对象,合理设计对象的量级是OO里面的一个难点,因为业务复杂度,我们在建模过程会牵涉到很多对象的设计,所以如何设计一个对象的量级是我们开发过程要去仔细思考的内容。而class以下特征将会增加对象的创建成本:
  ●构造函数中包含过量代码
  ●内含数量众多或者庞大的对象
  ●太深的继承层次,而且有些类不是它的直接父类
  ——编程心得[4]:尽可能复用对象——
  上边提及了对象创建的开销,所以我们在设计过程需要尽可能减少创建的次数,这种情况下,创建的对象越少,意味着代码执行效率会越高。但是我们在实际开发中,有可能会针对某个相同对象进行重复的创建工作,这种情况下,我们尽量采取对象复用技术,而不去重新创建该对象。这里提供一个代码的例子来说明:
/**
*定义一个用户类,里面包含了username,password,email,salary四个字段
**/
classUserInfo
{
privateStringuserName;
privateStringpassword;
privateStringemail;
private intsalary;
public voidsetUserName(String userName)
{
this.userName= userName;
}
public voidsetPassword(String password)
{
this.password= password;
}
public voidsetEmail(String email)
{
this.email= email;
}
publicString getUserName()
{
return this.userName;
}
publicString getPassword()
{
returnthis.password;
}
publicString getEmail()
{
return this.email;
}
public voidsetSalary(intsalary)
{
this.salary= salary;
}
public intgetSalary()
{
returndbprocess(this);//这个地方dbprocess(this)是伪代码,也可以算作伪代码,它所表示的业务含义就是
}
}
/**
*该类为一个用户的服务接口,这个版本是低效代码版本
**/
public classUserService
{
public intcomputePayroll(String[] username,String[] password)
{
//TODO:断言注释部分,为Debug用,位置:UserService.computePayroll
//这个是我自己写代码的常用写法assert:username.length == password.length;
intsize = username.length;
inttotalPayroll = 0;
for(inti = 0; i < size; i++ )
{
UserInfo user =newUserInfo();//这里是迭代创建对象
user.setUserName(username[i]);
user.setPassword(password[i]);
totalPayroll += user.getSalary();
}
return totalPayroll;
}
}
  分析上边的代码,其缺陷在于,每一次迭代的时候都会去创建一次UserInfo对象,这种情况实际上是没有必要的,没有必要在循环迭代中每迭代一次都去创建该对象,这笔开销会随着对象的复杂度有所提升,所以改成以下版本:
/**
*该类为一个用户的服务接口,这个版本是高效代码版本
**/
public classUserService
{
public intcomputePayroll(String[] username,String[] password)
{
//TODO:断言注释部分,为Debug用,位置:UserService.computePayroll
//这个是我自己写代码的常用写法assert:username.length == password.length;
intsize = username.length;
inttotalPayroll = 0;
UserInfo user =newUserInfo();//在迭代外创建对象
for(inti = 0; i < size; i++ )
{
user.setUserName(username[i]);
user.setPassword(password[i]);
totalPayroll += user.getSalary();
}
return totalPayroll;
}
}
  用了以上的代码版本过后,你的代码运行比起原始版本要快大概4到5倍左右,但是这样有可能会引入一个误区,典型的使用就是对于集合Vector或者ArrayList的时候,使用这样技术是不现实的。在Java里面,集合保存的是对象引用而不是对象本身,而我们在使用以上高效代码版本的过程里并不会创建对象,这样就使得对象的拷贝不存在,在对象添加到集合里面称为集合元素的时候需要的是一个对象拷贝,否则会使得集合里面所有元素的引用指向一个对象。这种解决最好的办法就是创建对象的拷贝或者直接对对象进行clone操作,这样就可以复用该对象了,所以这种情况使用高效代码版本反而是不合适的做法。
  iv.对象的销毁
  ——编程心得[5]:消除过期的对象引用——
  我们在C++语言中可以知道,如果要消除内存分配,比如消除某个指针分配的内存要使用关键字delete操作,也就是需要自己手工管理内存。而Java具有垃圾回收功能,它会自动回收过期的对象引用以及不使用的内存,当我们用完了对象过后,系统会自动回收该对象使用期间的分配的内存。但是有一点需要说明的是:我们一旦听说系统能够自动回收内存,所以自己在编程的时候往往觉得没有必要再考虑内存管理的事情了,但实际上这是一个很不好的习惯。这里提供一个完整的例子来说明这中情况:
public classCustomStack
{
privateObject[] elements;
private intsize = 0;
publicCustomStack(intsize)
{
this.elements =newObject[size];
}
//……
publicObject pop()
{
if( size == 0 )
throw newEmptyStackException();
returnelements[--size];
}
}
  这段代码本质上讲没有太大的错误,而且不论怎么测试,它的运行都是正常的,但是却存在一个潜在的“内存泄漏问题”,严格点讲,随着垃圾回收活动的增加,不断占用了使用的内存,程序的性能会逐渐体现出上边这段代码的问题。而且这个潜在的问题有可能还会引起OutOfMemoryError的错误,只是这种问题是潜在的,有可能对于普通应用失败的几率很小很小。
  【*:这里提及一个额外的心得,我们开发程序的最初,都不可能拥有庞大的程序数据量,但是我们最好在最初设计系统的时候就考虑到系统在遇到大量数据以及复杂业务逻辑的时候的效率以及其灵活性,虽然开始感觉可能有点复杂或者繁琐,但是这样的设计会使得后期的很多开发变得异常轻松。】
  而上边这句话的错误在于:存在过期引用没有进行消除工作,在pop()方法内部我们返回的时候是直接返回的这个堆栈之前的引用,所以就存在了原来的引用指向的对象实际上是没有被清空的。可以看看修改过的版本来对比【仅提供pop函数代码】:
publicObject pop()
{
if( size == 0 )
throw newEmptyStackException();
Object obj = elements[--size];
elements[size] =null;
returnobj;
}
  上边代码有一句elements[size] = null,其实道理很简单,该方法的调用目的是返回Stack的移除对象,原始版本里面,返回是对的,但是并没有从Stack中移除,而在返回过后,有一个引用并没有设置为null,使得该对象被保留下来了,这样垃圾回收器在回收的时候不会注意到该对象,所以该对象不会被清除,反复多次调用过后,就使得Stack的操作保留了很多无用的对象下来,这样程序执行时间长了就会OutOfMemoryError。但是有一点就是不能在开发过程过于极端,不能每次遇到这样的问题的时候都去考虑设置为null的情况,本小节的目的是:消除过期的对象引用,这种做法也是尽可能消除,不是所有的内容都依靠程序手动消除,在一定情况下可以依赖Java本身的垃圾回收,也是比较标准的做法。
  2)对象属性设置
  i.函数参数:
  [1]了解Java里面的形参和实参:我们在Java
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics