读改善java程序的151个建议(4)

31.在接口中不要存在实现代码
接口是一个契约,不仅仅约束着实现者,同时也是一个保证,保证提供的服务(常量、方法)是稳定、可靠的,如果把实现代码写在接口中,那接口就绑定了可能变化的因素,这就会导致 实现不再稳定和可靠,是随时都可能被抛弃、被更改、被重构的。所以,接口中虽然可以有实现,但应该避免使用。

32.静态变量一定要先声明后再赋值
下面的代码,输出结果为1
public  class  OtherClient  {
       static  {
               a=100;
       }
         public  static  int  a =1;
public  static  void  main ( String []  args ) {
              
                System .out. println  (a);
}
}
下面的代码,输出结果为100
public  class   OtherClient  {
  public   static  int  a =1;
       static  {
               a=100;
       }
       
public   static  void   main (  String []  args ) {
              
                System  .out. println  (a);
}
}
这是为什么呢?
这要从静态变量的诞生说起了,静态变量是类加载时被分配到数据区(Data Area)的,它在内存中只有一个拷贝,不会被分配多次 ,其后的所有赋值操作都是值改变,地址则保持不变。jvm初始化变量是先声明空间,然后再赋值的,也就是说: int a=100; 在jvm中是分开执行的,等价于int a; a=100; 静态变量是在类初始化时首先被加载的,JVM会去查找类中所有的静态声明,然后分配空间,注意这时只是完成了地址空间的分配,还没有赋值,之后jvm会根据类中静态赋值(包括静态赋值和静态块赋值)的先后顺序来执行。

33.不要覆写静态方法
在java中可以通过覆写(Override)来增加或减弱父类的方法和行为,但覆写是针对非静态方法(也叫实例方法,只有生成实例才能调用的方法)的,不能针对静态方法(static修饰的方法,也叫类方法),why?
我们知道一个实例对象有两个类型:表面类型(Apparent Type)和实际类型(Actual Type),表面类型是声明时的类型,实际类型是对象产生时的类型。
对于非静态方法,它是根据对象的实际类型来执行的。而对于静态方法来说比较特殊,首先静态方法不依赖实例对象,它是通过类名访问的;其次,可以通过对象访问静态方法,如果是通过对象调用静态方法,JVM则会通过对象的表面类型查找到静态方法的入口,继而执行之。
静态方法不能覆写,但是可以隐藏。隐藏的目的是为了抛弃父类静态方法,重现子类方法,也就是期望父类的静态方法不要破坏子类的业务行为;而覆写则是将父类的行为增加或减弱,延续父类的职责。
需要说明的是:实例对象访问静态方法或静态属性不是好习惯。

34.构造函数尽量简化
子类实例化时 ,会首先初始化父类(注意这里是初始化,可不是生成父类对象),也就是初始化父类的变量,调用父类的构造函数,然后才会初始化子类的变量,调用子类自己的构造函数,最后生成一个实例对象。
执行父类无参构造函数,也就是子类的有参构造函数中默认包含了super()方法

35.避免在构造函数中初始化其他类

36.使用构造代码块精炼程序
在java中一共有四种类型的代码块:
(1)普通代码块:就是在方法后面使用{}括起来的代码片段,它不能单独执行,必须通过方法名调用执行
(2)静态代码块:static修饰,并使用{}括起,用于静态变量的初始化或对象创建前的环境初始化。
(3)同步代码块:synchronized修饰,并使用{}括起,它表同一时间只能有一个线程进入到该方法块中,是一种多线程保护机制。
(4)构造代码块:在类中没有任何的前缀或后缀,并使用{}括起来的代码片段。关于构造代码块,我们知道代码块不具有独立执行的能力,那么编译器是如何处理构造代码块的呢?很简单,编译器会把构造代码块插入到每个构造函数的最前端。构造代码块会在每个构造函数内首先执行(需要注意的是:构造代码块不是在构造函数之前运行,它依托于构造函数的执行)
可将构造代码块应用到如下场景中:1)初始化实例变量 2)初始化实例环境

37.构造代码块会想你所想
在上一个建议是编译器会把构造代码块插入到每一个构造函数中,但有一个例外的 情况没有说明:如果遇到this关键字(也就是构造函数调用自身其他的构造函数时)则不插入构造代码块。还有一点需要说明,super不会这样处理,编译器把构造代码块插入到super方法之后执行。

38.使用静态内部类提高封装性
java中的嵌套类(nested class)分为两种:静态内部类(也叫静态嵌套类,static nested class)和内部类(inner class),只有在是静态内部类的情况下才能把static修复符放在类前,其他任何时候static都是不能修饰类的。
静态内部类:内部类,并且是静态(static修饰)的即为静态内部类
静态内部类主要有两个优点:加强了类的封装性和提高了代码的可读性
静态内部类与普通内部类有什么区别?
(1)静态内部类不持有外部类的引用(静态内部类只可以访问外部类的静态方法和静态属性,普通内部类可以自由访问)
(2)静态内部类不依赖外部类(普通内部类与外部类之间是相互依赖关系,内部类实例不能脱离外部类实例,也就是说它们是同生同死,而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类还是可以存在的)
(3)普通内部类不能声明 static的方法和变量 

39.使用匿名类的构造函数

40.匿名类的构造函数很特殊
匿名类的构造函数特殊处理机制:一般类(也就是具有显示名字的类)的所有构造函数默认都是调用父类的无参构造函数的,而匿名类因为没有名字,只能由构造函数代码块代替,也就无所谓的有参和无参构造函数,它在初始化时直接调用父类的同参构造函数,然后再调用了自己的构造代码块。

41.让多重继承成为现实
其实说的是通过内部类曲折解决问题

42.让工具类不可实例化
一般是通过设置构造函数为private访问权限,但是如果用户仍然要生成一个实例来访问(通过反射修改构造函数权限),隐藏问题可能爆发。那有没有更好的限制办法呢?有,即不仅仅设置成private访问权限,还抛异常,代码如下:
public class UtilsClass{
private UtilsClass(){
throw new Error("不要实例化我!");
}
}
本建议主要说的是,如果一个类不允许实例化,就要保证“平常”渠道都不能实例化它。

43.避免对象的浅拷贝
我们知道一个类实现了Cloneable接口就表示它具备了被拷贝的能力,如果再覆写clone()方法就完全具体拷贝能力。拷贝是在内存中进行的,所以在性能方面比直接通过new生成对象要快很多,特别是大对象的生成上,这会使性能提升非常显著。但要注意一个问题:浅拷贝(Shwdow Clone)存在对象属性拷贝不彻底的问题。
例:
public  class  Person  implements  Cloneable  {
         private  String  name;
         private  Person  father;
         public  Person  ( String  _name ){
       name= _name  ;
       }
       
         public  Person  ( String  _name , Person  _parent ){
               name= _name ;
               father= _parent ;
       }

         public  String  getName  () {
                return  name;
       }

         public  void  setName  ( String  name ) {
                this .name =  name ;
       }

         public  Person  getFather  () {
                return  father;
       }

         public  void  setFather  ( Person  father  ) {
                this .father =  father ;
       }

         @Override
         public  Person  clone  (){
                Person  person=  null ;
                try {
                     person=(  Person )  super .clone();
                     
              }
                catch ( CloneNotSupportedException  e){
                     e.  printStackTrace ();
              }
                return  person;
       }
}

public  static  void  main ( String []  args ) {

                //test1
                Person  f=  new  Person  ( "Father A"  );
                Person  s1=  new  Person  ( "大儿子"  ,f);
                Person  s2=s1.  clone ();
              s2.  setName (  "小儿子" );
                System .out. println  (s1. getName  ()+ "的父亲"  +s1. getFather  (). getName  ());
                System .out. println  (s2. getName  ()+ "的父亲"  +s2. getFather  (). getName  ());
              
              
                //test2
                Person  fathPerson=  new  Person  ( "Father B"  );
                Person  son1=  new  Person  ( "big son"  ,fathPerson);
                Person  son2=son1.  clone ();
              son2.  setName (  "little son" );
                //son1认干爹
              son1.  getFather ().  setName (  "gan die C" );
                System .out. println  (son1. getName  ()+ "的父亲"  +son1. getFather  (). getName  ());
                //结果显示son2的父亲也是 gan die c了,Father B两个儿子都没了
                System .out. println  (son2. getName  ()+ "的父亲"  +son2. getFather  (). getName  ());
       }

分析:我们知道所有类都继承自Object,Object提供了一个对象拷贝的默认方法 ,即上面代码中的super.clone方法 ,但是该方法是有缺陷的,它提供的是一种浅拷贝方式,也就是说它并不会把对象的所有属性全部拷贝一份,而是有选择性的拷贝,它的拷贝规则如下:
(1)基本类型:如果变量是基本类型,则拷贝其值,比如int,float等。
(2)对象:如果变量是一个实例对象,则拷贝地址引用,也就是说此时新拷贝出的对象与原对象共享该实例变量,不受访问权限的权限。(有点变态)
(3)String字符串:这个比较特殊,拷贝的也是一个地址,是个引用,但在修改时,它会从字符串池(string pool)中重新生成新的字符串,原有的字符串对象保持不变,在此处我们可以认为String是一个基本类型。
上面示例代码的原因就是第(2)点所致。
修正:
@Override
         public  Person  clone  (){
                Person  p=  null ;
                try {
                     p=(  Person )  super .clone();
                       //基于拷贝出的对象p,获取其父亲的姓名,生成新的对象new Person,做为p的父亲
                     p.  setFather (  new  Person  (p. getFather  (). getName  ()));
              }
                catch ( CloneNotSupportedException  e){
                     e.  printStackTrace ();
              }
                return  p;
       }

44.推荐使用序列化实现对象的拷贝
被拷贝的类只要实现Serializable接口即可,不需要任何实现,当然serialVersionUID常量还是要加上去的。
例如:
@SuppressWarnings ( "unchecked"  )
         public  static  < T  extends  Serializable >  T  clone  ( T  obj ){
                T  cloneObj=  null ;
                try {
                       //读取对象字节数据
                       ByteArrayOutputStream  baos= new  ByteArrayOutputStream  ();
                       ObjectOutputStream  oos= new  ObjectOutputStream  (baos);
                     oos.  writeObject ( obj  );
                     oos.  close ();
                     
                       //分配内存空间,写入原始对象,生成新对象
                       ByteArrayInputStream  bais= new  ByteArrayInputStream (baos. toByteArray  ());
                       ObjectInputStream  ois= new  ObjectInputStream  (bais);
                     cloneObj=(  T )ois.  readObject ();
                     ois.  close ();
                     
                     
              }
                catch ( Exception  e){
                     e.  printStackTrace ();
              }
                return  cloneObj;
       }
用此方法进行对象拷贝时需要注意两点:
(1)对象的内部属性都是可序列化的
如果有内部属性不可序列化,则会抛出序列化异常,这会让调试者很纳闷:生成一个对象怎么会出现序列化异常呢?从这一点考虑,还需要将上面的这个工具方法的异常进行细化处理
(2)注意方法和属性的特殊修饰符。在建议11,12点中有提到
final、static、transient(瞬时变量,不进行序列化的变量)
当然还有一个更简单的办法,使用Apache下的commons工具包中的SerializationUtils类

45.覆写equals方法时不要识别不出自己
在这里此点主要是用来说明覆写equals方法时,不要违背equals方法的自反性原则:对于任何非空引用x,x.equals(x) 应该返回true

46.equals应该考虑null值情景

47.在equals中使用getClass进行类型判断
总之这里就一句话,覆写equals时,建议使用getClass进行类型判断,而不要使用instance of 

48.覆写equals方法必须覆写hashCode方法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值