20191128-编写高质量代码:3-类、对象及方法

  1. 在接口中不要存在实现代码
  2. 静态变量一定要先声明后赋值
static {
    i = 100;
}
public static int i=1;
public static void main(String[] args){
    System.out.println("i="+i);
}

这段代码是可以编译的,输出结果为:i=1

静态变量是类加载时被分配到数据区(Data Area)的,它在内存中只有一个拷贝,不会被分配多次,其后的所有赋值操作都是值改变,地址保持不变。

静态变量是在类初始化时首先被加载的,JVM会去查找类中所有的静态声明,然后分配空间,注意这时候只是完成了地址空间的分配,还没有赋值,之后jvm会根据类中静态赋值(包括静态类赋值和静态块赋值)的先后顺序来执行。

对于程序来说,先声明来int类型的地址空间,并把地址传递给了i,然后按照类中的先后顺序执行赋值动作,首先执行静态块中的i=100,接着执行i=1,那最后结果就是i=1了。
33. 不要覆写静态方法

public class Base {
    public static void doSomething(){
        System.out.println("父类-静态方法");
    }
    public  void doAnything(){
        System.out.println("父类-非静态方法");
    }
}
public class Sub extends Base{

    public static void doSomething(){
        System.out.println("子类-静态方法");
    }

    @Override
    public void doAnything() {
        System.out.println("子类-非静态方法");
    }
}
public static void main(String[] args){
    Base base = new Sub();
    base.doAnything();
    base.doSomething();
}

运行结果:
子类-非静态方法
父类-静态方法

一个实例对象有两个类型:表面类型(Apparent Type) 和 实际类型(Actual Type),表面类型是声明时的类型,实际类型是对象产生时的类型。比如上面的代码中,变量base的表面类型是Base,实际类型是Sub。
对于非静态方法,它是根据对象的实际类型来执行的,也就是了Sub类的doAnything();
而对于静态方法,首先静态方法不依赖实例对象,它是通过类名访问的;其次可以通过对象访问静态方法,如果是通过对象调用静态方法,jvm会通过对象的表面类型查找到静态方法的入口,继而执行之。

在子类中构建与父类相同的方法名、输入参数、输出参数、访问权限(权限可以扩大),并且父类、子类都是静态方法,此种行为叫做隐藏(Hide),它与覆写有两点不同:

  • 表现形式不同。隐藏用于静态方法,覆写用于非静态方法。
  • 职责不同。隐藏的目的是为了抛弃父类静态方法,重现子类方法。而覆写则是将父类的行为增强或减弱,延续父类的职责。
  1. 构造函数尽量简化
    构造函数简化,再简化,应该达到“一眼洞穿”的境界

  2. 避免在构造函数中初始化其他类
    构造函数是一个初始化必须执行的代码,它决定着类的初始化效率,如果构造函数比较复杂,而且还关联了其他类,则可能产生意想不到的问题。

  3. 使用构造代码库精炼程序
    什么叫代码块(Code Block)?
    用大括号把多行代码封装在一起,形成一个独立的数据体,实现特定算法的代码集合即为代码块,一般来说代码块不能单独运行,必须要有运行主体。
    在Java中一共有四种类型的代码块:
    1、普通代码块
    方法后面使用“{}”括起来的代码片段,不能单独运行,必须通过方法名调用执行。
    2、静态代码块
    在类中使用static修饰,并使用“{}”括起来的代码片段,用于静态变量的初始化或对象创建前的环境初始化。
    3、同步代码块
    使用synchronized修饰,并使用“{}”括起来的代码片段,它表示同一时间只能有一个线程进入到该方法块中,是一种多线程保护机制。
    4、构造代码块
    在类中没有任何的前缀或后缀,并使用“{}”括起来的代码片段

public class Client {

    {
        System.out.println("构造代码块");
    }

    public Client(){
        System.out.println("无参构造函数");
    }

    public Client(String str){
        System.out.println("有参构造函数");
    }
}

编译器会把构造代码块插入到每个构造函数的最前端,上面的代码等价于:

public class Client {
    public Client(){
        System.out.println("构造代码块");
        System.out.println("无参构造函数");
    }

    public Client(String str){
        System.out.println("构造代码块");
        System.out.println("有参构造函数");
    }
}

构造代码块不是在构造函数之前运行的,它依托于构造函数执行。
37. 构造代码块会想你所想
构造代码块是为了提取构造函数的共同量,减少各个构造函数的代码而产生的,因此,Java就很聪明地认为把代码块插入到没有this方法的构造函数中即可,而调用其他构造函数的则不插入,确保每个构造函数只执行一次构造代码块。
this是特殊情况,但super没有特殊的处理,编译器只是把构造代码块插入到super方法之后执行而已,仅此不同。

  1. 使用静态内部类提高封装性
    静态内部类和普通内部类的区别:
  • 静态内部类不持有外部类的引用
    静态内部类只可以访问外部类的静态方法和静态属性
  • 静态内部类不依赖外部类
    普通内部类与外部类之间是相互依赖的关系,内部类实例不能脱离外部类实例,也就是说它们会同生死,一起声明,一起被GC回收。
    而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类还是可以存在的。
  • 普通内部类不能声明static的方法和变量
  1. 使用匿名类的构造函数
List list1 = new ArrayList();
List list2 = new ArrayList(){};
List list3 = new ArrayList(){{}};

System.out.println(list1.getClass() == list2.getClass());
System.out.println(list2.getClass() == list3.getClass());
System.out.println(list1.getClass() == list3.getClass());

输出结果:
false
false
false

list1声明了一个ArrayList的实例对象;
list2代表的是一个匿名类的声明和赋值,它定义了一个继承于ArrayList的匿名类,只是没有覆写方法而已。其代码类似于:

class Sub extends ArrayList {
}
List list2 = new Sub;

list3其代码类似于:

class Sub extends ArrayList {
    {
        // 初始化块
    }
}
List list3 = new Sub;

匿名函数虽然没有名字,但可以有构造函数,它用构造函数块来代替。
40. 匿名类的构造函数很特殊
一般类(具有显示名字的类)的所有构造函数默认都是调用父类的无参构造,而匿名类因为没有名字,它在初始化时直接调用了父类的同参数构造,然后再调用了自己的构造代码块。
41. 让多重继承成为现实
内部类可以继承一个与外部类无关的类,保证了内部类的独立性
42. 让工具类不可实例化
工具类的方法和属性都是静态的,不需要生成实例即可访问,jdk不希望被初始化,设置了构造函数为private,如:

public final class Math {

    /**
     * Don't let anyone instantiate this class.
     */
    private Math() {}
}
  1. 避免对象的浅拷贝
    Object的clone(),提供的是一种浅拷贝,拷贝规则如下:
  • 基本类型 :拷贝其值
  • 对象:拷贝地址引用
    即此时新拷贝出的对象与原有对象共享该实例变量,不受访问权限的限制
  • String 字符串:拷贝地址引用,但在修改时,会从字符串池中重新生成新的字符串,原有的字符串对象保持不变,在此处可以认为String是一个基本类型
    浅拷贝只是Java提供的一种简单拷贝机制,不便于直接使用
  1. 推荐使用序列化实现对象的拷贝
    在内存中通过字节流的拷贝来实现,即把母对象写到一个字节流中,再从字节流中将其读出来,这样就可以重建一个新对象了,该新对象与母对象之间不存在引用共享问题,相当于深拷贝了一个新对象。
public class CloneUtils {

    public static <T extends Serializable> T clone(T obj){

        T clonedObj = 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);
            clonedObj =(T)ois.readObject();
            bais.close();
        }catch (Exception e){
            e.printStackTrace();
        }
        return clonedObj;
    }
}

此工具类要求被拷贝的对象必须实现Serializable接口。用此方法需要注意两点:

  • 对象的内部属性都是可序列化的
  • 注意方法和属性的特殊修饰符

直接使用Apache下commons工具包中的SerializationUtils类,更加简洁方便

  1. 覆写equals方法时不要识别不出自己
  2. equals应考虑null值情景
  3. 在equals中使用getClass进行类型判断
    建议使用getClass类型判断,而不要使用instanceof(这个很容易让子类钻空子)。
  4. 覆写equals方法必须覆写hashCode方法
    HashCodeBuilder是org.apache.commons.lang.builder包下的一个哈希码生成工具,可以直接在项目中集成,毕竟方便使用
  5. 推荐覆写toString方法
  6. 使用package-info类为包服务
    package-info类专门为本包服务
  • 不能随便被创建
  • 服务对象很特殊
  • package-info类不能有实现代码

package-info类的主要作用:
1、声明友好类和包内访问常量
比如一个包中有很多内部访问的类或常量,可以统一放到package-info类中,便于集中管理,可以减少友好类到处游走的情况。
2、为在包上标注注解提供便利
比如我们要写一个注解,查看一个包下的所有对象,只要把注解标注到package-info文件中即可。
3、提供包的整体注释说明

  1. 不要主动进行垃圾回收

《编写高质量代码:改善Java程序的151个建议》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值