编写高质量代码: 改善Java程序的151个建议 | 第三章 类、对象及方法


在这里插入图片描述

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

第三章 类、对象及方法

建议31:在接口中不要存在实现代码
  • 可以通过在接口中声明一个静态常量s,其值是一个匿名内部类的实例对象,可以实现接口中存在实现代码

    public class Suggest31 {
    
        interface S {
            public void doSomething();
        }
    
        interface B {
            public static final S s = new S() {
                @Override
                public void doSomething() {
                    System.out.println("我在接口中实现了");
                }
            };
        }
    
        public static void main(String[] args) {
            B.s.doSomething();
        }
    }
    

    这是一种不好的编码习惯, 接口是用来干嘛的? 接口是一个契约, 不仅仅约束着实现者, 同时也是一个保证,保证提供的服务(常量、方法)是稳定、可靠的。如果把实现代码写到接口中, 那接口就绑定了可能变化的因素, 这就会导致实现不再稳定和可靠, 是随时都可能被抛弃、被更改、被重构的。

建议32:静态变量一定要先声明后赋值
  • 也可以先使用后声明,因为静态变量是类初始化时首先被加载,JVM会去查找类中所有的静态声明,然后分配空间,分配到数据区(Data Area)的,它在内存中只有一个拷贝,不会被分配多次,注意这时候只是完成了地址空间的分配还没有赋值,之后JVM会根据类中静态赋值(包括静态类赋值和静态块赋值)的先后顺序来执行,后面的操作都是地址不变,值改变。

    public class Suggest32 {
    
        // 这样子写符合先使用后赋值,但结果是1
        static {
            i = 100;
        }
        public static int i= 1;
    
    
    
        public static void main(String[] args) {
            System.out.println(i);
        }
    }
    
建议33:不要覆写静态方法
  • 一个实例对象有两个类型:表面类型实际类型,表面类型是声明时的类型,实际类型是对象产生时的类型。对于非静态方法,它是根据对象的实际类型来执行的,即执行了覆写方法。

    而对于静态方法,首先静态方法不依赖实例对象,通过类名访问;其次,可以通过对象访问静态方法,如果通过对象访问,JVM则会通过对象的表面类型查找到静态方法的入口,继而执行。

    public class Suggest33 {
    
        public static void main(String[] args) {
            Base base = new Sub();
            base.doAnything();
            base.doSomething();
        }
        
    }
    
    // 基类
    class Base {
        public static void doSomething() {
            System.out.println("我是父类静态方法");
        }
    
        public void doAnything() {
            System.out.println("我是父类非静态方法");
        }
    }
    
    // 子类, 覆写父类方法
    class Sub extends Base {
    
        public static void doSomething() {
            System.out.println("我是子类静态方法");
        }
    
        @Override
        public void doAnything() {
            System.out.println("我是子类非静态方法");
        }
    }
    
    我是子类非静态方法
    我是父类静态方法
    

    表现形式不同。隐藏用于静态方法,覆写用于非静态方法。在代码上的表现是﹔@Override注解可以用于覆写,不能用于隐藏。
    职责不同。隐藏的目的是为了抛弃父类静态方法,重现子类方法,例如我们的例子,Sub.doSomething 的出现是为了遮盖父类的Base.doSomething方法,也就是期望父类的静态方法不要破坏子类的业务行为﹔而覆写则是将父类的行为增强或减弱,延续父类的职责。

建议34:构造函数尽量简化
  • 通过new关键字生成对象时必然会调用构造函数。构造函数的简繁情况会直接影响实例对象的创建是否繁琐。

    子类实例化时,首先会初始化父类(注意这里是初始化,可不是生成父类对象),也就是初始化父类的变量,调用父类的构造函数,然后才会初始化子类的变量,调用子类自己的构造函数,最后生成一个实例对象。构造函数太复杂有可能造成,对象使用时还没完成初始化。

    public class Suggest34 {
        public static void main(String[] args) {
            SimpleServer s = new SimpleServer(1000);
        }
    }
    
    abstract class Server {
    
        public final static int DEFAULT_PORT = 4000;
    
        public Server() {
            int port = getPort();
            System.out.println("端口号: " + port);
        }
    
        protected abstract int getPort();
    }
    
    class SimpleServer extends Server {
    
        private int port = 100;
    
        public SimpleServer(int _port) {
            port = _port;
        }
    
        @Override
        protected int getPort() {
            return Math.random() > 0.5 ? port : DEFAULT_PORT;
        }
    }
    
    
建议35:避免在构造函数中初始化其他类
  • 有可能造成不断的new新对象的死循环,直到栈内存被消耗完抛出StackOverflowError异常为止

    public class Suggest35 {
        public static void main(String[] args) {
            Son s = new Son();
            s.doSomething();
        }
    }
    
    class Father {
        Father() {
            new Other();
        }
    }
    
    class Son extends Father {
        public void doSomething() {
            System.out.println("do something");
        }
    }
    
    class Other {
        public Other() {
            new Son();
        }
    }
    
    

    声明s变量时,调用了Son的无参构造函数,JVM又默认调用了父类Father的无参构造函数,接着Father类又初始化了Other类,而Other类又调用了Son类,于是一个死循环就诞生了,直到栈内存被消耗完毕为止。

建议36:使用构造代码块精炼程序
  • 四种类型的代码块:

    • **1、普通代码块:**在方法后面使用“{}”括起来的代码片段, 不能单独执行, 必须通过方法名调用执行;

    • **2、静态代码块:**在类中使用static修饰,并使用“{}”括起来的代码片段, 用于静态变量的初始化或对象创建前的环境初始化;

    • **3、同步代码块:**使用synchronized关键字修饰,并使用“{}”括起来的代码片段,表示同一时间只能有一个线程进入到该方法,是一种多线程保护机制;

    • **4、构造代码块:**在类中没有任何的前缀或后缀,并使用“{}”括起来的代码片段。编译器会把构造代码块插入到每个构造函数的最前端。

      构造代码块的两个特性

      1、在每个构造函数中都运行;

      2、在构造函数中它会首先运行

    构造代码块应用如下场景:

    1. 初始化实例变量
    2. 初始化实例环境
    public class Suggest36 {
    
        {
            System.out.println("执行构造代码块");
        }
    
        public Suggest36() {
            System.out.println("执行无参构造");
        }
    
        public Suggest36(String str) {
            System.out.println("执行有参构造");
        }
    
        public static void main(String[] args) {
            Suggest36 suggest36 = new Suggest36();
        }
    }
    
    
建议37:构造代码块会想你所想
  • 编译器会把构造代码块插入到每一个构造函数中,有一个特殊情况:如果遇到this关键字(也就是构造函数调用自身其他的构造函数时)则不插入构造代码块。如果遇到super关键字,编译器会把构造代码块插入到super方法之后执行。

    public class Suggest37 {
        public static void main(String[] args) {
            new BaseOne();
            new BaseOne("");
            new BaseOne(0);
            System.out.println("实例对象数量: " + BaseOne.getNumOfObjects());
        }
    }
    
    class BaseOne {
    
        private static int numOfObjects = 0;
    
        {
            // 构造代码块, 计算产生对象数量
            numOfObjects++;
        }
    
        public BaseOne() {
    
        }
    
        public BaseOne(String str) {
    
        }
    
        // 有参构造调用无参构造
        public BaseOne(int i) {
    
        }
    
        public static int getNumOfObjects() {
            return numOfObjects;
        }
    }
    

    上一个建议是说编译器会把构造代码块插入到每一个构造函数中,但是有一个例外的情况没有说明:如果遇到this关键字(也就是构造函数调用自身其他的构造函数时)则不插入构造代码块,对于我们的例子来说,编译器在编译时发现String形参的构造函数调用了无参构造,于是放弃插人构造代码块,所以只执行了一次构造代码块。

建议38:使用静态内部类提高封装性
  • Java嵌套内分为两种:

    1、静态内部类

    2、内部类;静态内部类两个优点:加强了类的封装性和提高了代码的可读性

    静态内部类与普通内部类的区别

    1、静态内部类不持有外部类的引用,在普通内部类中,我们可以直接访问外部类的属性、方法,即使是private类型也可以访问,这是因为内部类持有一个外部类的引用,可以自由访问。而静态内部类,则只可以访问外部类的静态方法和静态属性,其他则不能访问。

    2、静态内部类不依赖外部类,普通内部类与外部类之间是相互依赖的关系,内部类不能脱离外部类实例,同声同死,一起声明,一起被垃圾回收器回收。而静态内部类可以独立存在,即使外部类消亡了;

    3、普通内部类不能声明static的方法和变量,注意这里说的是变量,常量(也就是final static修饰的属性)还是可以的,而静态内部类形似外部类,没有任何限制。

    public class Suggest38 {
        public static void main(String[] args) {
            Person person = new Person("Rocky编程日记");
            person.setHome(new Person.Home("广州","123"));
        }
    }
    
    class Person {
        // 姓名
        private String name;
        // 家庭
        private Home home;
        // 构造函数设置属性值
        public Person(String _name) {
            name = _name;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public void setHome(Home home) {
            this.home = home;
        }
    
        public Home getHome() {
            return home;
        }
    
        public static class Home {
            // 家庭地址
            private String address;
            // 家庭电话
            private String tel;
    
            public Home(String _address, String _tel) {
                address = _address;
                tel = _tel;
            }
        }
    }
    
建议39:使用匿名类的构造函数
  • List l2 = new ArrayList(){}; //定义了一个继承于ArrayList的匿名类,只是没有任何的覆写方法而已。

    List l3 = new ArrayList(){{}}; 
    //定义了一个继承于ArrayList的匿名类,并且包含一个初始化块,类似于构造代码块)
    
    public class Suggest39 {
        public static void main(String[] args) {
            List l1 = new ArrayList();
            // l2 代表的 是一个匿名类的 声明 和赋值, 它定义了一个继承于 ArrayList 的匿名类,
            // 只是没有任何的 覆写方法而已
            List l2 = new ArrayList(){}; // 类似于 List l2 = new L2();
            // 匿名内部类的代码, 类似于 List l3 = new L3();
            List l3 = new ArrayList(){{}};
            System.out.println(l1.getClass() == l2.getClass());
            System.out.println(l1.getClass() == l3.getClass());
            System.out.println(l2.getClass() == l3.getClass());
        }
    }
    
    class L2 extends  ArrayList {}
    class L3 extends  ArrayList {
        {
            // 初始化块
        }
    }
    
    // 输出结果
    false
    false
    false
    

    匿名函数虽然没有名字, 但也是可以有构造函数的, 它用构造函数块来代替,那上面的3个输出就很清楚了:虽然父类相同,但是类还是不同的。

建议40:匿名类的构造函数很特殊
  • 匿名类初始化时直接调用了父类的同参数构造器,然后再调用自己的构造代码块。

    public class Suggest40 {
    
        public static void main(String[] args) {
            // 接收两个参数1和2,然后设置一个操作符号,计算其值,结果是3
            // 带有参数的匿名类声明时到底是调用的哪一个构造函数
            Calculator c1 = new Calculator(1, 2) {
                {
                    setOperator(Ops.ADD);
                }
            };
            System.out.println(c1.getResult());
    
            Calculator c2 = new ADD(1, 2) {
                {
                    setOperator(Ops.ADD);
                }
            };
            System.out.println(c2.getResult());
    
            Calculator c3 = new ADD1(1, 2) {
                {
                    setOperator(Ops.ADD);
                }
            };
            System.out.println(c3.getResult());
        }
    }
    
    enum Ops {
        ADD,
        SUB
    }
    class Calculator {
        private int i, j, result;
    
        public Calculator() {}
    
        public Calculator(int _i, int _j) {
            i = _i;
            j = _j;
        }
    
        protected void setOperator(Ops _op) {
            System.out.println("Calculator");
            result = _op.equals(Ops.ADD) ? i + j : i - j;
        }
    
        public int getResult() {
            return result;
        }
    }
    
    class ADD extends Calculator {
        {
            System.out.println("ADD");
            setOperator(Ops.ADD);
        }
        // 覆写父类的构造方法
        public ADD(int _i, int _j) {
            System.out.println("ADD(int _i, int _j)");
        }
    }
    
    class ADD1 extends Calculator {
        {
            System.out.println("ADD1");
            setOperator(Ops.ADD);
        }
        // 覆写父类的构造方法
        public ADD1(int _i, int _j) {
            super(_i, _j);
            System.out.println("ADD1(int _i, int _j)");
        }
    }
    
    
    
    Calculator
    3
    ADD
    Calculator
    ADD(int _i, int _j)
    Calculator
    0 // 思考?为啥是0
    ADD1
    Calculator
    ADD1(int _i, int _j)
    Calculator
    3
    

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

建议41:让多重继承成为现实
  • Java中一个类可以多种实现,但不能多重继承。使用成员内部类实现多重继承。内部类一个重要特性:内部类可以继承一个与外部类无关的类,保证了内部类的独立性,正是基于这一点,多重继承才会成为可能。

    public class Suggest41 {
    
        interface Father {
            public int strong();
        }
    
        interface Mother {
            public int kind();
        }
    
        class FatherImpl implements Father {
            // 父亲的强壮指数是 8
            @Override
            public int strong() {
                return 8;
            }
        }
    
        class MotherImpl implements Mother {
            // 母亲的强壮指数是 8
            @Override
            public int kind() {
                return 8;
            }
        }
        private Son son = new Son();
        private Daughter daughter = new Daughter();
    
        class Son extends FatherImpl implements Mother {
    
            @Override
            public int strong() {
                return  super.strong() + 1;
            }
    
            @Override
            public int kind() {
                return new MotherSpecial().kind();
            }
    
            private class MotherSpecial extends MotherImpl {
                public int kind() {
                    return super.kind() - 1;
                }
            }
        }
    
        class Daughter extends MotherImpl implements Father {
    
            @Override
            public int strong() {
                // 匿名内部类 来覆写父类的方法, 以完成继承父亲行为的功能
                return new FatherImpl() {
                    public int strong() {
                        // 女儿强壮指数降低了
                        return super.strong() - 2;
                    }
                }.strong();
            }
        }
    
        public static void main(String[] args) {
            Suggest41 suggest41 = new Suggest41();
            Son son = suggest41.son;
            System.out.println("son kind " +  son.kind());
            System.out.println("son strong " +  son.strong());
    
            Daughter daughter = suggest41.daughter;
            System.out.println("daughter kind " +  daughter.kind());
            System.out.println("daughter strong " +  daughter.strong());
        }
    }
    
    son kind 7
    son strong 9
    daughter kind 8
    daughter strong 6
    
建议42:让工具类不可实例化
  • 工具类的方法和属性都是静态的,不需要实例即可访问。且工具类最好不要做继承的打算。

    **实现方式:**将构造函数设置为private,并且在构造函数中抛出Error错误异常。

    public class Suggest42 {
    
        /**
         * Don't let anyone instantiate this class.
         * java反射修改权限。改成 private + 抛异常
         */
        // private Suggest42() {}
    
        public Suggest42() {throw new Error("不要实例化我!");}
    
        public static void main(String[] args) {
            Suggest42 suggest42 = new Suggest42();
        }
    }
    
建议43:避免对象的浅拷贝
  • 浅拷贝存在对象属性拷贝不彻底的问题。

    对于只包含基本数据类型的类可以使用浅拷贝

    而包含有对象变量的类需要使用序列化与反序列化机制实现深拷贝

    public class Suggest43 {
        public static void main(String[] args) {
            Man man = new Man("父亲");
    
            Man m1 = new Man("大儿子", man);
            Man m2 = m1.clone();
            m2.setName("二儿子");
            m1.getMan().setName("干爹");
            System.out.println(m1.getName() + " 的父亲是 " + m1.getMan().getName());
            System.out.println(m2.getName() + " 的父亲是 " + m1.getMan().getName());
        }
    }
    
    class Man implements Cloneable {
    
        private String name;
    
        private Man father;
    
        public Man(String _name) {
            name = _name;
        }
    
        public Man(String _name,Man _father) {
            name = _name;
            father = _father;
        }
    
        @Override
        public Man clone() {
            Man man = null;
            try {
                man =(Man) super.clone();
            }
            catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return man;
        }
    
        public void setName(String name) {
            this.name  = name;
        }
    
        public Man getMan() {
            return father;
        }
    
        public String getName() {
            return name;
        }
    }
    
    
    

    拷贝规则:

    1. 基本类型: 如果变量是基本类型,则拷贝其值,比如int、float 等。
    2. 对象: 如果变量是一个实例对象,则拷贝地址引用,也就是说此时新拷贝出的对象与原有对象共享该实例变量,不受访问权限的限制。
    3. String 字符串: 这个比较特殊,拷贝的也是一个地址,是个引用,但是在修改时,它会从字符串池 ( String Pool)中重新生成新的字符串,原有的字符串对象保持不变,在此处我们可以认为String是一个基本类型。
建议44:推荐使用序列化实现对象的拷贝
  • 通过序列化方式来处理,在内存中通过字节流的拷贝来实现深拷贝。使用此方法进行对象拷贝时需注意两点

    1. 对象的内部属性都是可序列化的;
    2. 注意方法和属性的特殊修饰符,比如final、static、transient变量的序列化问题都会影响拷贝效果。一个简单办法,使用Apache下的commons工具包中的SerializationUtils类,直接使用更加简洁方便
    public class Suggest44 {
    
        @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;
        }
    }
    
建议45:覆写equals方法时不要识别不出自己
  • 需要满足p.equals§返回为真,自反性

    public class Suggest45 {
        public static void main(String[] args) {
            D1 d1 = new D1("Rocky编程日记");
            D1 d2 = new D1("Rocky编程日记 ");
            List<D1> list = new ArrayList<>();
            list.add(d1);
            list.add(d2);
    
            System.out.println("列表中是否包含Rocky编程日记: " + list.contains(d1));
            System.out.println("列表中是否包含Rocky编程日记 : " + list.contains(d2));
        }
    }
    
    
    class D1 implements Cloneable {
    
        private String name;
    
        public D1(String _name) {
            name = _name;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        @Override
        public boolean equals(Object obj) {
            if (obj instanceof D1) {
                D1 d = (D1) obj;
                return name.equalsIgnoreCase(d.getName().trim());
            }
            return false;
        }
    }
    
建议46:equals应该考虑null值情景
  • 覆写equals方法时需要判一下null,否则可能产生NullPointerException异常

    			// 如下改造            
    			if (d.getName() == null || name == null) {
                    return false;
                }
    
建议47:在equals中使用getClass进行类型判断
  • 使用getClass方法来代替instanceof进行类型判断

    public class Suggest47 {
        public static void main(String[] args) {
                E2 e1 = new E2("Rocky编程日记",100);
                E2 e2 = new E2("Rocky编程日记",1001);
                E1 e3 = new E1("Rocky编程日记");
                System.out.println(e3.equals(e1));
                System.out.println(e3.equals(e2));
                System.out.println(e1.equals(e2));
        }
    }
    
    class E2 extends E1 {
        private int id;
    
        public E2(String _name, int _id) {
            super(_name);
            id = _id;
        }
    
        public boolean equals(Object obj) {
            if (obj instanceof E2) {
                E2 e = (E2) obj;
                return super.equals(obj) && e.getId() == id;
            }
            return false;
        }
    
        private int getId() {
            return id;
        }
    }
    
    class E1 {
    
        private String name;
    
        public E1(String name) {
            this.name = name;
        }
    
        @Override
        public boolean equals(Object obj) {
            // 推荐使用 getClass()
            if (obj != null && obj.getClass() == this.getClass()) {
    //        if (obj instanceof E1) {
                E1 d = (E1) obj;
                if (d.getName() == null || name == null) {
                    return false;
                }
                return name.equalsIgnoreCase(d.getName());
            }
            return false;
        }
        
        private String getName() {
            return name;
        }
    }
    
建议48:覆写equals方法必须覆写hashCode方法
  • 需要两个相同对象的hashCode方法返回值相同,所以需要覆写hashCode方法,如果不覆写,两个不同对象的hashCode肯定不一样,简单实现hashCode方法,调用org.apache.commons.lang.builder包下的Hash码生成工具HashCodeBuilder。

    public class Suggest48 {
        public static void main(String[] args) {
            Map<E1, Object> map = new HashMap<E1, Object>() {
                {
                    put(new E1("Rocky编程日记"), new Object());
                }
            };
            List<E1> list = new ArrayList<E1>() {
                {
                    add(new E1("Rocky编程日记"));
                }
            };
            System.out.println(list.contains(new E1("Rocky编程日记")));  // true
            System.out.println(map.containsKey(new E1("Rocky编程日记")));// false
        }
    }
    
    // 增加
        @Override
        public int hashCode() {
            return new HashCodeBuilder().append(name).toHashCode();
        }
    
建议49:推荐覆写toString方法
  • 原始toString方法显示不人性化。格式都是 类名 + @ + hashCode, 不方便调试。
建议50:使用package-info类为包服务
  • package-info类是专门为本包服务的,是一个特殊性主要体现在3个方面:
    1. 它不能随便被创建;
    2. 它服务的对象很特殊;
    3. package-info类不能有实现代码;package-info类的作用
      • 声明友好类和包内访问常量;
      • 为在包上标注注解提供便利;
      • 提供包的整体注释说明
建议51:不要主动进行垃圾回收
  • 主动进行垃圾回收是一个非常危险的动作,因为System.gc要停止所有的响应(Stop the world),才能检查内存中是否有可回收的对象,所有的请求都会暂停。
  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值