改善Java程序的151个建议 30 - 35

改善Java程序的151个建议 30 - 35

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

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

静态变量的诞生

静态变量是类加载时被分配到数据区(DataArea)的,它在内存中只有一个拷贝,不会被分配多次,其后的所有赋值操作都是值改变,地址则保持不变。我们知道JVM初始化变量是先声明空间,然后再赋值的,也就是说:

image-20210804174314078

在JVM中是分开执行,等价于:

image-20210804174521347

静态变量是在类初始化时首先被加载的,JVM会去查找类中所有的静态声明,然后分配空间,注意这时候只是完成了地址空间的分配,还没有赋值,之后JVM会根据类中静态赋值(包括静态类赋值和静态块赋值)的先后顺序来执行。对于程序来说,就是先声明了int类型的地址空间,并把地址传递给了i,然后按照类中的先后顺序执行赋值动作,首先执行静态块中i=100,接着执行i=1,那最后的结果就是i=1了。哦,如此而已,那再问一个问题:如果有多个静态块对i继续赋值呢?i当然还是等于1了,谁的位置最靠后谁有最终的决定权。

33. 不要覆写静态方法

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

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

解释了这么多,我们回头看一下本建议的标题:静态方法不能覆写,可以再续上一句话,虽然不能覆写,但是可以隐藏。顺便说一下,通过实例对象访问静态方法或静态属性不是好习惯,它给代码带来了“坏味道”,建议读者阅之戒之。

34. 构造函数尽量简化

子类初始化过程理论

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

建议

构造函数简化,再简化,应该达到“一眼洞穿”的境界。

问题代码案例

package ad30to35;

/**
 * @author luxiaoyang
 * @create 2021-08-04-17:59
 */
public class Client {

    public static void main(String[] args) {

        Base base = new Sub();

        SimpleServer simpleServer = new SimpleServer(1000);
        simpleServer.start(simpleServer.getPort());

    }
}


/**
 * 定义一个服务
 */
abstract class Server {

    public final static int DEFAULT_PORT = 40000;

    public Server() {
        //获得子类提供的端口号
        int port = getPort();
    }

    // 由子类提供端口号,并做可用性检查
    protected abstract int getPort();

    void start(int port) {
        System.out.println("端口号: " + port);
        /*进行监听动作*/
    }

}

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;
    }
}
该代码的意图如下:
  1. 通过SimpleServer的构造函数接收端口参数。
  2. 子类的构造函数默认调用父类的构造函数。
  3. 父类构造函数调用子类的getPort方法获得端口号。
  4. 父类构造函数建立端口监听机制。
  5. 对象创建完毕,服务监听启动,正常运行。
问题

输出结果要么是“端口号:40000”,要么是“端口号:0”,永远不会出现“端口号:100”或是“端口号:1000”

回顾子类如何实例化
  1. 子类实例化时,会首先初始化父类(注意这里是初始化,可不是生成父类对象),也就是初始化父类的变量
  2. 调用父类的构造函数
  3. 初始化子类的变量
  4. 调用子类自己的构造函数
  5. 生成一个实例对象

看上面的程序,其执行过程如下:

  1. 子类SimpleServer的构造函数接收int类型的参数:1000。
  2. 父类初始化常变量,也就是DEFAULT_PORT初始化,并设置为40000。
  3. 执行父类无参构造函数,也就是子类的有参构造中默认包含了super()方法
  4. 父类无参构造函数执行到“int port=getPort()”方法,调用子类的getPort方法实现。
  5. 子类的getPort方法返回port值(注意,此时port变量还没有赋值,是0)或DEFAULT_PORT(此时已经是40000)了。
  6. 父类初始化完毕,开始初始化子类的实例变量,port赋值100。
  7. 执行子类构造函数,port被重新赋值为1000。
  8. 子类SimpleServer实例化结束,对象创建完毕。

终于清楚了,在类初始化时getPort方法返回的port值还没有赋值,port只是获得了默认初始值(int类的实例变量默认初始值是0),因此Server永远监听的是40000端口了(0端口是没有意义的)。这个问题的产生从浅处说是由类元素初始化顺序导致的,从深处说是因为构造函数太复杂而引起的。构造函数用作初始化变量,声明实例的上下文,这都是简单的实现,没有任何问题,但我们的例子却实现了一个复杂的逻辑,而这放在构造函数里就不合适了。

解决办法

问题知道了,修改也很简单,把父类的无参构造函数中的所有实现都移动到一个叫做start的方法中,将SimpleServer类初始化完毕,再调用其start方法即可实现服务器的启动工作,简洁而又直观,这也是大部分JEE服务器的实现方式。

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

声明

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

package ad30to35;

/**
 * @author luxiaoyang
 * @create 2021-08-04-21:30
 */
public class Client2 {

    public static void main(String[] args) {
        Son son = new Son();
        son.doSomething();
    }

}


// 父类
class Father{
    Father() {
        new Other();
    }
}

class Son extends Father {
    public void doSomething() {
        System.out.println("Hi,show me something");
    }
}

// 相关类
class Other {
    public Other() {
        new Son();
    }
}

不要在构造函数中声明初始化其他类,养成良好的习惯

ing() {
System.out.println(“Hi,show me something”);
}
}

// 相关类
class Other {
public Other() {
new Son();
}
}


不要在构造函数中声明初始化其他类,养成良好的习惯

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赞一下鼓励

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值