每天10个编码坑(《编写高质量代码 改善Java程序的151个建议》)

本文探讨了Java编程中的几个关键原则,包括接口不应包含实现代码、静态变量的声明和赋值顺序、避免覆盖静态方法、构造函数的简洁性以及在构造函数中初始化其他类的风险。此外,介绍了构造代码块的用途、静态内部类的优势以及匿名类构造函数的特性。这些最佳实践有助于提升代码的可读性、稳定性和维护性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

NO.31 在接口中不要存在实现代码

接口中可以声明常量,声明抽象方法,也可以继承父类接口,但就是不能有实现,这只是对于一般程序来讲,还有一种特殊的程序:

public class Client {
    public static void main(String[] args) {
        B.s.doSomething();
    }

    interface B {
        public static final S s = new S() {
            @Override
            public void doSomething() {
                System.out.println("在接口中实现了");
            }
        };
    }

    interface S {
        public void doSomething();
    }
}

在B接口中声明了一个静态常量s,其值是一个匿名内部类的实例,在创建S实例的时候实现了S接口。
此类代码是严禁出现的,接口是一个契约,不仅仅约束着实现者,同时也是一个保证,保证提供的服务(常量、方法)是稳定、可靠的,如果把实现代码写到接口中,那接口就绑定了可能变化的因素,这就会导致实现不在稳定和可靠,是随时都有可能被抛弃、被更改、被重构的,接口中虽然可以有实现,但应避免使用。

接口中不能存在实现代码

NO.32 静态变量一定要先声明后赋值

public class Client {
    public static int i=1;
    static {
        i=100;
    }
    public static void main(String[] args) {
        System.out.println(i);
    }
}

运行结果:
在这里插入图片描述
如果此时将静态变量和静态代码块调换位置,代码如下:

public class Client {
    static {
        i=100;
    }
    public static int i=1;
    public static void main(String[] args) {
        System.out.println(i);
    }
}

这段代码依然是可以编译且执行的,结果为1,由此可以看出静态变量是可以先使用后声明的。
这要从静态变量的诞生说起,静态变量是类加载时被分配到数据区的,他在内存中只有一个,不会被分配多次,其后的所有赋值都是值改变,地址则保持不变。JVM初始化变量是先声明空间,再进行赋值,也就是说:
int i=100;
会被分成两步执行:
int i;
i=100;

静态变量是在类初始化时首先被加载的,JVM会去查找类中的所有静态声明,然后分配空间,但是这时候只是完成了地址空间的分配,还没有赋值,之后JVM会根据类中静态赋值(包括静态类赋值和静态块赋值)的先后顺序来执行,对于程序来说,就是先声明了int类型的地址空间,并把地址传给了i,然后按照类中的先后顺序进行赋值,最后赋值的是1,那么i的值就是1。谁的位置最靠后谁就有最终的决定权。

变量要先声明后使用,否则结果可能会和你的期望值不一样

NO.33 不要覆写静态方法

在Java中可以通过覆写增强或减弱父类的方法或行为,但覆写只是针对非静态方法(实例方法)的,不能针对静态方法(类方法)。

public class Client {
    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("我是子类非静态方法");
    }
}

Base是父类,Sub是Base的子类,子类的doAnything()覆写了父类,而doSomething()方法与父类的方法名、参数、返回值也都相同,按理来说也是覆写,但是运行结果如下:
在这里插入图片描述
同样调用子类的方法,但是一个执行了子类方法,一个执行了父类方法。两者的差别仅仅是有无static修饰。
我们知道一个实例对象有两个类型,表面类型和实际类型,对于上面的例子而言,base的表面类型就是Base,实际类型就是Sub,对于非静态方法,他是根据对象的实际类型来执行的,也就是执行了Sub的doAnything()方法,而对于静态方法而言,首先静态方法不依赖于实例对象,是可以通过类名直接调用的,但其实也可以通过实例对象调用,如果是通过实例对象访问的静态方法,JVM就会通过对象的表面类型找到方法的入口,继而执行,因此上面是执行了父类的静态方法。

在子类中构建与父类相同的方法名、参数、返回值,访问权限(权限可以扩大),并且父类、子类都是静态方法,这种行为叫做隐藏,它与覆写有两点不同:
(1)表现形式不同:隐藏用于静态方法,覆写用于非静态方法。在代码上的表现是:@override注解可以用于覆写,不可以用于隐藏。
(2)职责不同:隐藏的目的是为了抛弃父类静态方法,重现子类静态方法,也就是期望父类的静态方法不要破坏子类的业务行为,而覆写则是将父类的行为增强或减弱,延续父类的职责。

静态方法不能覆写,虽然不能覆写,但是可以隐藏,然而通过实例对象访问静态方法或静态属性不是一个好习惯,给代码带来了坏味道,建议远之

NO.34 构造函数尽量简化

通过new创建对象时必然会调用构造函数,构造函数的繁琐情况会直接影响实例对象的创建是否繁琐。
以下模拟了一个监听端口号的服务:

public class Client {
    public static void main(String[] args) {
        Server server = new SimpleServer(1000);
    }
}

abstract class Server {
    public static final 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) {
        this.port = port;
    }

    @Override
    protected int getPort() {
        return Math.random() > 0.5 ? port : DEFAULT_PORT;
    }
}

代码意图如下:
(1)通过SimpleServer的构造函数接收port;
(2)子类的构造函数默认调用父类的构造函数;
(3)父类的构造函数调用子类的getPort()方法(getPort方法模拟了检查端口号是否有效的过程,有效就使用,无效就使用默认的端口)获得端口号;
(4)父类构造函数建立端口监听机制;
该程序运行结果不是0就是40000,永远不会出现100或者1000,要解释这个问题就要知道子类时如何实例化的:
子类在实例化时会首先初始化父类(这里的初始化不是生成父类的实例对象),也就是初始化父类的变量调用父类的构造函数,然后才会初始化子类的变量,调用子类的构造函数,最后生成一个实例对象。
上面的程序的执行过程如下:

  • 子类SimpleServer的构造函数接收int类型的参数:1000;
  • 父类初始化常量,也就是DEFAULT_PORT初始化,并设置为40000;
  • 执行父类的构造函数,其实是子类的有参构造中默认包含了父类的构造函数;
  • 父类的构造函数执行到int port = getPort()时,调用子类的getPort()方法;
  • 子类的getPort()方法中的port还没有初始化,那就默认是0,而DEFAULT_PORT已经初始化为了40000;
  • 父类初始化完毕,开始初始化子类的变量,port赋值为100;
  • 执行子类的构造函数,port被重新赋值为1000;
  • 子类的实例化过程结束,对象创建完成。
    产生这个结果的主要原因是在初始化getPort()方法时,port还没有被初始化,那么返回的只能是port的默认值0。
    这个问题的产生从浅出说是由类元素初始化顺序导致的从深处说是构造函数太复杂引起的,构造函数用作初始化变量,声明实例的上下文,这都是简单实现,而我们的例子中却实现了一个复杂的逻辑,放在构造函数中就不合适了,会跟我们的预期相悖。
    构造函数简化,应该达到“一眼洞穿”的效果

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

public class Client {
    public static void main(String[] args) {
        Son son = new Son();
        son.doSomething();
    }
}
//父类
class Father{
    public Father() {
        new Other();
    }
}
//子类
class Son extends Father{
    public void doSomething(){
        System.out.println("Hi,show me something");
    }
}
//相关类
class Other{
    public Other(){
        new Son();
    }
}

这是一个在构造函数中实例化其他类的代码,执行结果如下:
在这里插入图片描述
这是因为声明son变量时,调用了父类的构造方法,接着父类又初始化了Other类,而Other类又初始化了Son类,于是陷入了死循环,直至栈内存被消耗完为止。
解决此类问题的最好办法就是:不要在构造函数中声明初始化其他的类

NO.36 使用构造代码块精炼程序

代码块:用大括号把多行代码封装在一起,形成一个独立的数据体,实现特定算法的代码集合。一般来说代码块是不能单独运行的,必须要有运行主体,在Java中一共有四种类型的代码块:
(1)普通代码块
在方法后面的使用“{}”括起来的代码片段,他不能单独执行,必须通过方法名调用。
(2)静态代码块
在类中使用static修饰的使用“{}”括起来的代码片段,用于静态变量的初始化或对象创建前的环境初始化。
(3)同步代码块
使用synchronize关键字修饰的使用“{}”括起来的代码片段,表示同一时间只能有一个线程进入到该方法中,是一种多线程保护机制。
(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("执行有参构造");
    }
}

很显然在new一个实例对象时,会先执行构造代码块,然后再执行其他代码,但是并不是在构造函数之前执行的,而是构造代码块的执行依托于构造函数执行,也就是说构造代码块在构造函数内先执行。
那么构造代码块适用的场景如下:

  • 初始化实例变量
    如果每个构造函数都需要初始化变量,可以通过构造代码块来实现,当然也可以通过定义一个方法,然后再每个构造函数中调用此方法去实现,但是要在每个构造函数中调用就很麻烦,如果使用构造代码块,无需调用,在编译时期就会将构造代码块写入每个构造函数中

  • 初始化实例环境
    一个对象必须在适当的场景下才可以存在,如果没有适当的场景,则就需要在创建对象时创建此场景,例如在JEE开发中,要产生HTTP Request必须先创建HTTP Session,在创建HTTP Session时就可以通过构造代码块来检查呢HTTP Session是否存在,不存在则创建之。

很好的利用构造代码块不仅可以减少代码量,还可以让程序更容易阅读

NO.37 构造代码块会想你所想

上一个建议提议使用构造代码块来简化代码,并且也了解到编译器会自动吧构造代码块插入到各个构造方法中,那构造代码块是不是足够聪明能为我们解决开发中的问题呢?
有这样一个案例:统计一个类的实例数量。

public class Client {

    public static void main(String[] args) {
        new Base();
        new Base("");
        new Base(1);
        System.out.println("实例对象数量:"+Base.getNumOfObjects());
    }
}

class Base {
    private static int numOfObjects = 0;

    {
        numOfObjects++;
    }
    //无参构造
    public Base() {
    }
    //有参构造调用无参构造
    public Base(String str) {
        this();
    }
    //有参构造不调用无参构造
    public Base(int i) {
    }

    public static int getNumOfObjects() {
        return numOfObjects;
    }

}

Base类中列出来三个构造方法,在参数为String类型的构造中又调用了无参构造,根据上一个建议提到的,编译器会把构造代码块插入各个构造函数中,那么最终打印的结果预期是4,但是结果为:
在这里插入图片描述
这是因为还有一种例外情况:如果遇到this关键字(也就是构造函数调用自身或其他构造函数时)则不插入构造代码块,也就是说编译器发现在这个构造函数中调用了无参构造,那么就放弃插入构造代码块,所以这个构造函数只执行了一次构造代码块。
构造代码块是为了提取构造函数的共同量,减少各个构造函数的代码而产生的的,因此,Java就很聪明的认为把代码块插入到没有this()的构造函数中即可,而调用其他构造函数的则不插入,确保每个构造函数只执行一次构造代码块。
但是不要认为this 是特殊请款,super也会类似处理,super是不会这么处理的,super方法没有任何特殊的地方,编译器都只是把构造代码块插入到super()之后执行。

NO.38 使用静态内部类提高封装性

Java中的嵌套类分为两种:静态内部类和内部类,内部类很简单,静态内部类是static修饰的内部类,只有是静态内部类时才可以使用static修饰,其他任何时候都不能使用static修饰类。
静态内部类的优点:加强了类的封装性和提高了代码的可读性.

public class Person {
    private String name;
    private Home home;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

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

    public Home getHome() {
        return home;
    }

    public void setHome(Home home) {
        this.home = home;
    }

    public static class Home {
        private String address;
        private String tel;

        public Home(String address, String tel) {
            this.address = address;
            this.tel = tel;
        }

        public String getAddress() {
            return address;
        }

        public void setAddress(String address) {
            this.address = address;
        }

        public String getTel() {
            return tel;
        }

        public void setTel(String tel) {
            this.tel = tel;
        }
    }


}

以上的代码中在Person类中定义了一个静态内部类Home,表示人的家庭信息,由Home类来封装地址和电话信息,就不需要使用Person类来定义,使封装性提高了,同时通过代码就可以看出Person和Home的强关联关系,可读性提高了。

    public static void main(String[] args) {
        Person person = new Person("张三");
        person.setHome(new Person.Home("上海", "021"));
    }

使用的过程也很明确,和实际的情形相同,先定义人的主要信息,在定义家庭的信息。
静态内部类和普通内部类的区别:

  • 静态内部类不持有外部类的引用
    在普通内部类中,可以直接访问外部类的属性,方法,即使是private修饰的也可以访问这是因为内部类中有一个外部类的引用,可以自由访问,而静态内部类,只可以访问外部类的静态方法和静态属性(包括private修饰的),其他的就不能访问
  • 静态内部类不依赖外部类
    普通内部类和外部类是相互依赖的关系,内部类实例不能脱离外部类实例,也就是一起被声明,一起被垃圾回收。而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类也可以存在。
  • 普通内部类不能声明static的方法和变量
    普通内部类不能声明static的方法和变量,常量还是可以的,而静态内部类神似外部类,没有任何限制。

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

    public static void main(String[] args) {
        List l1 = new ArrayList();
        List l2 = new ArrayList(){};
        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());
    }

这段代码是编译通过的,输出结果如下:
在这里插入图片描述
l1很容易理解,就是创建了一个ArrayList的实例对象
(1)l2=new ArrayList(){};
l2代表的是一个匿名类的声明和赋值,它定义了一个继承于ArrayList的匿名类,只是没有任何的覆写方法而已。其代码类似于:

class Sub extends ArrayList{

}
List l2 = new Sub();

(2)l3=new ArrayList(){{}};
这一行代码类似于:

class Sub extends ArrayList{
    {
        //初始化块
    }
}
List l3 = new Sub();

这里的代码块起到了构造函数的作用,因为匿名类是没有名字的,而一个类的构造函数名需要和类的名字相同,很显然,初始化块就是匿名类的构造函数,并且,一个类中的构造函数可以是多个,那么
List l4 = new ArrayList(){{}{}{}{}}
这行代码是可以编译通过且运行的,所以以上三个实例虽然父类相同,但是类是不同的。

匿名类虽然没有名字,但也是可以有构造函数的,即初始化块

NO.40 匿名类的构造函数很特殊

上一建议说匿名类是有初始化块来充当构造函数的,那么这个构造函数和普通类中的构造函数是否一样呢?

enum Ops {ADD, SUB}

class Calculator {
    private int i, j, result;

    public Calculator() {
    }

    public Calculator(int i, int j) {
        this.i = i;
        this.j = j;
    }

    protected void setOperator(Ops ops) {
        result = ops.equals(Ops.ADD) ? i + j : i - j;
    }

    public int getResult() {
        return result;
    }
}

这段代码的意图很明显,通过构造函数接收两个int类型的参数,然后再设置一个运算符,最后得出运算结果。客户端代码如下:

    public static void main(String[] args) {
        Calculator calculator = new Calculator(1, 2) {
            {
                setOperator(Ops.ADD);
            }
        };
        System.out.println(calculator.getResult());
    }

运行结果:
在这里插入图片描述

这个结果是必然的,但是如果把这段代码等价为一个子类继承Calculator的方式去实现:

class Add extends Calculator{
    {
        setOperator(Ops.ADD);
    }

    public Add(int i, int j) {
    }
} 

然后写一个客户端执行这段代码会发现结果是0,这是因为匿名类的构造函数特殊处理机制,一般类的所有构造函数都是默认调用的父类的无参构造,而匿名类没有名字,只能由构造代码块代替,也就无所谓有参还是无参了,他在初始化时直接调用了父类的同参数构造,然后再调用自己的构造代码块,也就是说上面的真正等价的代码是:

class Add extends Calculator{
    {
        setOperator(Ops.ADD);
    }

    public Add(int i, int j) {
        super(i,j);
    }
}

他会先调用父类的两个参数的构造,而不是调用无参构造

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值