Java——类、对象及方法(一)

类、对象及方法

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

看到这样的标题读者可能会纳闷:接口中有实现代码?这怎么可能呢?确实,接口中可以声明常量,声明抽象方法,也可以继承父接口,但就是不能有具体实现,因为接口是一种契约(Contract)​,是一种框架性协议,这表明它的实现类都是同一种类型,或者是具备相似特征的一个集合体。对于一般程序,接口确实没有任何实现,但是在那些特殊的程序中就例外了,阅读如下代码:

java 复制代码
public class Client {
    public static void main(String[] args) {
            //调用接口的实现
            B.s.doSomething();
    }
}
//在接口中存在实现代码
interface B{
    public static final S s = new S(){
            public void doSomething(){
                    System.out.println("我在接口中实现了");
            }
    };
}
//被实现的接口
interface S{
    public void doSomething();
}

仔细看main方法,注意那个B接口。它调用了接口常量,在没有任何显式实现类的情况下,它竟然打印出了结果,那B接口中的s常量(接口是S)是在什么地方被实现的呢?答案是在B接口中。

在B接口中声明了一个静态常量s,其值是一个匿名内部类(Anonymous Inner Class)的实例对象,就是该匿名内部类(当然,可以不用匿名,直接在接口中实现内部类也是允许的)实现了S接口。你看,在接口中存在着实现代码吧!

这确实很好,很强大,但是在一般的项目中,此类代码是严禁出现的,原因很简单:这是一种不好的编码习惯,接口是用来干什么的?接口是一个契约,不仅仅约束着实现者,同时也是一个保证,保证提供的服务(常量、方法)是稳定、可靠的,如果把实现代码写到接口中,那接口就绑定了可能变化的因素,这就会导致实现不再稳定和可靠,是随时都可能被抛弃、被更改、被重构的。所以,接口中虽然可以有实现,但应避免使用。

注意接口中不能存在实现代码。

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

这标题看着让人很纳闷,什么叫做变量一定要先声明后赋值?Java中的变量不都是先声明后使用的吗?难道还能先使用后声明?能不能暂且不说,我们先来看一个例子,代码如下:

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

这段程序很简单,输出100嘛!对,确实是100,我们再稍稍修改一下,代码如下:

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

注意,变量i的声明和赋值调换了位置,现在的问题是:这段程序能否编译?如果可以编译那输出是多少?还要注意:这个变量i可是先使用(也就是赋值)后声明的。

答案是:可以编译,没有任何问题,输出是1。对,你没有看错,输出确实是1,而不是100。仅仅调换了一下位置,输出就变了,而且变量i还真是先使用后声明的,难道这世界真的颠倒了?

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

java 复制代码
int i=100;

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

java 复制代码
int i; //分配地址空间
i=100; //赋值

静态变量是在类初始化时首先被加载的,JVM会去查找类中所有的静态声明,然后分配空间,注意这时候只是完成了地址空间的分配,还没有赋值,之后JVM会根据类中静态赋值(包括静态类赋值和静态块赋值)的先后顺序来执行。对于程序来说,就是先声明了int类型的地址空间,并把地址传递给了i,然后按照类中的先后顺序执行赋值动作,首先执行静态块中i=100,接着执行i=1,那最后的结果就是i=1了。

哦,如此而已,那再问一个问题:如果有多个静态块对i继续赋值呢?i当然还是等于1了,谁的位置最靠后谁有最终的决定权。

有些程序员喜欢把变量定义放到类的底部,如果这是实例变量还好说,没有任何问题,但如果是静态变量,而且还在静态块中进行了赋值,那这结果可就和你期望的不一样了,所以遵循Java通用的开发规范"变量先声明后使用"是一个良好的编码风格。

注意再次重申变量要先声明后使用,这不是一句废话。

33、不要覆写静态方法

我们知道在Java中可以通过覆写(Override)来增强或减弱父类的方法和行为,但覆写是针对非静态方法(也叫做实例方法,只有生成实例才能调用的方法)的,不能针对静态方法(static修饰的方法,也叫做类方法)​,为什么呢?我们先看一个例子,代码如下:

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

注意看程序,子类的doAnything方法覆写了父类方法,这没有任何问题,那doSomething方法呢?它与父类的方法名相同,输入、输出也相同,按道理来说应该是覆写,不过到底是不是覆写呢?我们先看输出结果:

java 复制代码
我是子类非静态方法
我是父类静态方法

这个结果很让人困惑,同样是调用子类方法,一个执行了子类方法,一个执行了父类方法,两者的差别仅仅是有无static修饰,却得到不同的输出结果,原因何在呢?

我们知道一个实例对象有两个类型:表面类型(Apparent Type)和实际类型(Actual Type)​,表面类型是声明时的类型,实际类型是对象产生时的类型,比如我们例子,变量base的表面类型是Base,实际类型是Sub。对于非静态方法,它是根据对象的实际类型来执行的,也就是执行了Sub类中的doAnything方法。而对于静态方法来说就比较特殊了,首先静态方法不依赖实例对象,它是通过类名访问的;其次,可以通过对象访问静态方法,如果是通过对象调用静态方法,JVM则会通过对象的表面类型查找到静态方法的入口,继而执行之。因此上面的程序打印出"我是父类静态方法"​,也就不足为奇了。

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

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

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

34、构造函数尽量简化

我们知道在通过new关键字生成对象时必然会调用构造函数,构造函数的简繁情况会直接影响实例对象的创建是否繁琐。在项目开发中,我们一般都会制订构造函数尽量简单,尽可能不抛异常,尽量不做复杂算法等规范,那如果一个构造函数确实复杂了会怎么样?我们来看一段代码:

java 复制代码
public class Client {
    public static void main(String[] args) {
           Server s = new SimpleServer(1000);
    }
}
//定义一个服务
abstract class Server{
    public final static int DEFAULT_PORT = 40000;
    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;
    }
}

该代码是一个服务类的简单模拟程序,Server类实现了服务器的创建逻辑,子类只要在生成实例对象时传递一个端口号即可创建一个监听该端口的服务,该代码的意图如下:

  • 通过SimpleServer的构造函数接收端口参数。
  • 子类的构造函数默认调用父类的构造函数。
  • 父类构造函数调用子类的getPort方法获得端口号。
  • 父类构造函数建立端口监听机制。
  • 对象创建完毕,服务监听启动,正常运行。

貌似很合理,再仔细看看代码,确实也和我们的意图相吻合,那我们尝试多次运行看看,输出结果要么是"端口号:40000"​,要么是"端口号:0"​,永远不会出现"端口号:100"或是"端口号:1000"​,这就奇怪了,40000还好说,但那个0是怎么冒出来的呢?代码在什么地方出现问题了?

要解释这个问题,我们首先要说说子类是如何实例化的。子类实例化时,会首先初始化父类(注意这里是初始化,可不是生成父类对象)​,也就是初始化父类的变量,调用父类的构造函数,然后才会初始化子类的变量,调用子类自己的构造函数,最后生成一个实例对象。了解了相关知识,我们再来看上面的程序,其执行过程如下:

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

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

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

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

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

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

java 复制代码
public class Client {
    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("Hi,show me something");
    }
}
//相关类
class Other{
    public Other(){
           new Son();
   }
}

这段代码并不复杂,只是在构造函数中初始化了其他类,想想看这段代码的运行结果是什么?是打印"Hi,showme something"吗?

答案是这段代码不能运行,报StackOverflowError异常,栈(Stack)内存溢出。这是因为声明s变量时,调用了Son的无参构造函数,JVM又默认调用了父类Father的无 参构造函数,接着Father类又初始化了Other类,而Other类又调用了Son类,于是一个死循环就诞生了,直到栈内存被消耗完毕为止。

可能有读者会觉得这样的场景不可能在开发中出现,那我们来思考这样的场景:Father是由框架提供的,Son类是我们自己编写的扩展代码,而Other类则是框架要求的拦截类(Interceptor类或者Handle类或者Hook方法)​,再来看看该问题,这种场景不可能出现吗?

那有读者可能要说了,这种问题只要系统一运行就会发现,不可能对项目产生影响。

那是因为我们在这里展示的代码比较简单,很容易一眼洞穿,一个项目中的构造函数可不止一两个,类之间的关系也不会这么简单的,要想瞥一眼就能明白是否有缺陷这对所有人员来说都是不可能完成的任务,解决此类问题的最好办法就是:不要在构造函数中声明初始化其他类,养成良好的习惯。

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

什么叫代码块(Code Block)​?用大括号把多行代码封装在一起,形成一个独立的数据体,实现特定算法的代码集合即为代码块,一般来说代码块是不能单独运行的,必须要有运行主体。在Java中一共有四种类型的代码块:

  • (1)普通代码块
    就是在方法后面使用"{}"括起来的代码片段,它不能单独执行,必须通过方法名调用执行。
  • (2)静态代码块
    在类中使用static修饰,并使用"{}"括起来的代码片段,用于静态变量的初始化或对象创建前的环境初始化。
  • (3)同步代码块
    使用synchronized关键字修饰,并使用"{}"括起来的代码片段,它表示同一时间只能有一个线程进入到该方法块中,是一种多线程保护机制。
  • (4)构造代码块
    在类中没有任何的前缀或后缀,并使用"{}"括起来的代码片段。

我们知道,一个类至少有一个构造函数(如果没有,编译器会无私地为其创建一个无参构造函数)​,构造函数是在对象生成时调用的,那现在的问题来了:构造函数和构造代码块是什么关系?构造代码块是在什么时候执行的?在回答这个问题之前,我们先来看看编译器是如何处理构造代码块的,看如下代码:

java 复制代码
public class Client {
    {
            //构造代码块
            System.out.println("执行构造代码块");
    }
    public Client(){
            System.out.println("执行无参构造");
    }
    public Client(String _str){
            System.out.println("执行有参构造");
    }
}

这是一段非常简单的代码,它包含了构造代码块、无参构造、有参构造,我们知道代码块不具有独立执行的能力,那么编译器是如何处理构造代码块呢?很简单,编译器会把构造代码块插入到每个构造函数的最前端,上面的代码与如下代码等价:

java 复制代码
public class Client {
    public Client(){
           System.out.println("执行构造代码块");
           System.out.println("执行无参构造");
    }
    public Client(String _str){
           System.out.println("执行构造代码块");
           System.out.println("执行有参构造");
    }
}

每个构造函数的最前端都被插入了构造代码块,很显然,在通过new关键字生成一个实例时会先执行构造代码块,然后再执行其他代码,也就是说:构造代码块会在每个构造函数内首先执行(需要注意的是:构造代码块不是在构造函数之前运行的,它依托于构造函数的执行)​,明白了这一点,我们就可以把构造代码块应用到如下场景中:

  • (1)初始化实例变量(Instance Variable)
    如果每个构造函数都要初始化变量,可以通过构造代码块来实现。当然也可以通过定义一个方法,然后在每个构造函数中调用该方法来实现,没错,可以解决,但是要在每个构造函数中都调用该方法,而这就是其缺点,若采用构造代码块的方式则不用定义和调用,会直接由编译器写入到每个构造函数中,这才是解决此类问题的绝佳方式。
  • (2)初始化实例环境
    一个对象必须在适当的场景下才能存在,如果没有适当的场景,则就需要在创建对象时创建此场景,例如在JEE开发中,要产生HTTP Request必须首先建立HTTPSession,在创建HTTP Request时就可以通过构造代码块来检查HTTP Session是否已经存在,不存在则创建之。

以上两个场景利用了构造代码块的两个特性:在每个构造函数中都运行和在构造函数中它会首先运行。很好地利用构造代码块的这两个特性不仅可以减少代码量,还可以让程序更容易阅读,特别是当所有的构造函数都要实现逻辑,而且这部分逻辑又很复杂时,这时就可以通过编写多个构造代码块来实现。每个代码块完成不同的业务逻辑(当然了,构造函数尽量简单,这是基本原则)​,按照业务顺序依次存放,这样在创建实例对象时JVM也就会按照 顺序依次执行,实现复杂对象的模块化创建。

37、构造代码块会想你所想

上一个建议中我们提议使用构造代码块来简化代码,并且也了解到编译器会自动把构造代码块插入到各个构造函数中,那我们接下来看看编译器是不是足够聪明,能够为我们解决真实的开发问题。有这样一个案例:统计一个类的实例数量。可能你要说了,这很简单,在每个构造函数中加入一个对象计数器不就解决问题了吗?或者使用我们上一个建议介绍的,使用构造代码块也可以。确实如此,我们来看如下代码是否可行:

java 复制代码
public class Client {
    public static void main(String[] args) {
           new Base();
           new Base("");
           new Base(0);
           System.out.println("实例对象数量:" + Base.getNumOfObjects());
    }
}
class Base{
    //对象计数器
    private static int numOfObjects = 0;
    {
           //构造代码块,计算产生对象数量
           numOfObjects++;
    }
    public Base(){
    }
    //有参构造调用无参构造
    public Base(String _str){
           this();
    }
    //有参构造不调用其他构造
    public Base(int _i){
    }
    //返回在一个JVM中,创建了多少个实例对象
    public static int getNumOfObjects(){
           return numOfObjects;
    }
}

这段代码是可行的吗?能计算出实例对象的数量吗?哎,好像不对呀,如果编译器把构造代码块插入到各个构造函数中,那带有String形参的构造函数可就有问题,它会调用无参构造,那通过它生成Base对象时就会执行两次构造代码块:一次是由无参构造函数调用构造代码块,一次是执行自身的构造代码块,这样的话计算可就不准确了,main函数实际在内存中产生了3个对象,但结果却会是4。不过真是这样的吗?Are you sure?我们运行一下看看结果:

java 复制代码
实例对象数量:3

非常遗憾,你错了,实例对象的数量还是3,程序没有任何问题。奇怪吗?不奇怪,上一个建议是说编译器会把构造代码块插入到每一个构造函数中,但是有一个例外的情况没有说明:如果遇到this关键字(也就是构造函数调用自身其他的构造函数时)则不插入构造代码块,对于我们的例子来说,编译器在编译时发现String形参的构造函数调用了无参构造,于是放弃插入构造代码块,所以只执行了一次构造代码块---结果就是如此。

那Java编译器为什么会这么聪明呢?这还要从构造代码块的诞生说起,构造代码块是为了提取构造函数的共同量,减少各个构造函数的代码而产生的,因此,Java就很聪明地认为把代码块插入到没有this方法的构造函数中即可,而调用其他构造函数的则不插入,确保每个构造函数只执行一次构造代码块。

还有一点需要说明,读者千万不要以为this是特殊情况,那super也会类似处理了。其实不会,在构造代码块的处理上,super方法没有任何特殊的地方,编译器只是把构造代码块插入到super方法之后执行而已,仅此不同。

注意放心地使用构造代码块吧,Java已经想你所想了。

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

Java中的嵌套类(Nested Class)分为两种:静态内部类(也叫静态嵌套类,Static Nested Class)和内部类(Inner Class)​。内部类我们介绍过很多了,现在来看看静态内部类。什么是静态内部类呢?是内部类,并且是静态(static修饰)的即为静态内部类。只有在是静态内部类的情况下才能把static修复符放在类前,其他任何时候static都是不能修饰类的。

静态内部类的形式很好理解,但是为什么需要静态内部类呢?那是因为静态内部类有两个优点:加强了类的封装性和提高了代码的可读性,我们通过一段代码来解释这两个优点,如下所示:

java 复制代码
public class Person{
    //姓名
    private String name;
    //家庭
    private Home home;
    //构造函数设置属性值
    public Person(String _name){
            name = _name;
    }
    /* home、name的getter/setter方法省略 */
    public static class Home{
            //家庭地址
            private String address;
            //家庭电话
            private String tel;
            public Home(String _address,String _tel){
                   address = _address;
                   tel = _tel;
            }
            /* address、tel的getter/setter方法省略 */
    }
}

其中,Person类中定义了一个静态内部类Home,它表示的意思是"人的家庭信息"​,由于Home类封装了家庭信息,不用在Person类中再定义homeAddre、homeTel等属性,这就使封装性提高了。同时我们仅仅通过代码就可以分析出Person和Home之间的强关联关系,也就是说语义增强了,可读性提高了。所以在使用时就会非常清楚它要表达的含义:

java 复制代码
public static void main(String[] args) {
    //定义张三这个人
    Person p = new Person("张三");
    //设置张三的家庭信息
    p.setHome(new Person.Home("上海","021"));
}

定义张三这个人,然后通过Person.Home类设置张三的家庭信息,这是不是就和我们真实世界的情形相同了?先登记人的主要信息,然后登记人员的分类信息。可能你又要问了,这和我们一般定义的类有什么区别呢?又有什么吸引人的地方呢?如下所示:

  • 提高封装性。从代码位置上来讲,静态内部类放置在外部类内,其代码层意义就是:静态内部类是外部类的子行为或子属性,两者直接保持着一定的关系,比如在我们的例子中,看到Home类就知道它是Person的Home信息。
  • 提高代码的可读性。相关联的代码放在一起,可读性当然提高了。
  • 形似内部,神似外部。静态内部类虽然存在于外部类内,而且编译后的类文件名也包含外部类(格式是:外部类+$+内部类),但是它可以脱离外部类存在,也就是说我们仍然可以通过new Home()声明一个Home对象,只是需要导入"Person.Home"而已。

解释了这么多,读者可能会觉得外部类和静态内部类之间是组合关系(Composition)了,这是错误的,外部类和静态内部类之间有强关联关系,这仅仅表现在"字面"上,而深层次的抽象意义则依赖于类的设计。

那静态内部类与普通内部类有什么区别呢?问得好,区别如下:

  • (1)静态内部类不持有外部类的引用
    在普通内部类中,我们可以直接访问外部类的属性、方法,即使是private类型也可以访问,这是因为内部类持有一个外部类的引用,可以自由访问。而静态内部类,则只可以访问外部类的静态方法和静态属性(如果是private权限也能访问,这是由其代码位置所决定的),其他则不能访问。
  • (2)静态内部类不依赖外部类
    普通内部类与外部类之间是相互依赖的关系,内部类实例不能脱离外部类实例,也就是说它们会同生同死,一起声明,一起被垃圾回收器回收。而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类还是可以存在的。
  • (3)普通内部类不能声明static的方法和变量
    普通内部类不能声明static的方法和变量,注意这里说的是变量,常量(也就是final static修饰的属性)还是可以的,而静态内部类形似外部类,没有任何限制。

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

阅读如下代码,看看是否可以编译:

java 复制代码
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(l2.getClass() == l3.getClass());
    System.out.println(l1.getClass() == l3.getClass());
}

注意ArrayList后面的不同点:l1变量后面什么都没有,l2后面有一对{},l3后面有2对嵌套的{},这段程序能不能编译呢?若能编译,那输出是多少呢?

答案是能编译,输出的是3个false。l1很容易解释,就是声明了ArrayList的实例对象,那l2和l3代表的是什么呢?

  • (1)l2=new ArrayList(){}
    l2代表的是一个匿名类的声明和赋值,它定义了一个继承于ArrayList的匿名类,只是没有任何的覆写方法而已,其代码类似于:
java 复制代码
//定义一个继承ArrayList的内部类
class Sub extends ArrayList{
}
//声明和赋值
List l2 = new Sub();
  • (2)l3=new ArrayList(){{}}
    这个语句就有点怪了,还带了两对大括号,我们分开来解释就会明白了,这也是一个匿名类的定义,它的代码类似于:
java 复制代码
//定义一个继承ArrayList的内部类
class Sub extends ArrayList{
    {
            //初始化块
    }
}
//声明和赋值
List l3 = new Sub();

看到了吧,就是多了一个初始化块而已,起到构造函数的功能。我们知道一个类肯定有一个构造函数,且构造函数的名称和类名相同,那问题来了:匿名类的构造函数是什么呢?它没有名字呀!很显然,初始化块就是它的构造函数。当然,一个类中的构造函数块可以是多个,也就是说可以出现如下代码:

java 复制代码
List l3 = new ArrayList(){{}{}{}{}{}};

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

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

在上一个建议中我们讲到匿名类虽然没有名字,但可以有一个初始化块来充当构造函数,那这个构造函数是否就和普通的构造函数完全一样呢?我们来看一个例子,设计一个计算器,进行加减乘除运算,代码如下:

java 复制代码
//定义一个枚举,限定操作符
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) {
           result = _op.equals(Ops.ADD)?i+j:i-j;
    }
    //取得运算结果
    public int getResult(){
           return result;
    }
}

代码的意图是,通过构造函数输入两个int类型的数字,然后根据设置的操作符(加法还是减法)进行计算,编写一个客户端调用:

java 复制代码
public static void main(String[] args) {
      Calculator c1 = new Calculator(1,2) {
            {
                      setOperator(Ops.ADD);
            }
      };
      System.out.println(c1.getResult());
}

这段匿名类的代码非常清晰:接收两个参数1和2,然后设置一个操作符号,计算其值,结果是3,这毫无疑问,但是这中间隐藏着一个问题:带有参数的匿名类声明时到底是调用的哪一个构造函数呢?我们把这段程序模拟一下:

java 复制代码
//加法计算
class Add extends Calculator {
    {
            setOperator(Ops.ADD);
    }
    //覆写父类的构造方法
    public Add(int _i, int _j) {
    }
}

匿名类和这个Add类是等价的吗?可能有人会说:上面只是把匿名类增加了一个名字,其他的都没有改动,那肯定是等价的啦!毫无疑问!那好,你再写个客户端调用Add类的方法看看。是不是输出结果为0(为什么是0?这很容易,有参构造没有赋值)​。这说明两者不等价,不过,原因何在呢?

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

java 复制代码
//加法计算
class Add extends Calculator {
    {
           setOperator(Ops.ADD);
    }
    //覆写父类的构造方法
    public Add(int _i, int _j) {
           super(_i,_j);
    }
}

它首先会调用父类有两个参数的构造函数,而不是无参构造,这是匿名类的构造函数与普通类的差别,但是这一点也确实鲜有人细细琢磨,因为它的处理机制符合习惯呀,我传递两个参数,就是希望先调用父类有两个参数的构造,然后再执行我自己的构造函数,而Java的处理机制也正是如此处理的!

相关推荐
程序员夏洛24 分钟前
Redis 的持久化机制有哪些?
java·数据库·redis
阿里嘎多学长25 分钟前
2026-08-29 GitHub 热点项目精选
开发语言·程序员·github·代码托管
凯尔萨厮30 分钟前
Java学习笔记十(注解)
java·笔记·学习
এ慕ོ冬℘゜1 小时前
JavaScript学习心得:从只会语法,到真正会写业务逻辑
开发语言·javascript·ecmascript
钱栈up1 小时前
VSCode 调试多模块 Maven + Spring Boot 后端:从启动失败到一次跑通
java·spring boot·maven
uoKent1 小时前
c++中new和malloc的区别
java·jvm·c++
2601_966949651 小时前
五档盘口数据对量化交易有什么价值?从信号判断到策略风险的完整分析
开发语言·人工智能·python·数据分析·量化·股票数据·quantdash
十五喵源码网1 小时前
基于springboot2+vue2的疾病防控综合系统
java·毕业设计·springboot·论文笔记
砚底藏山河1 小时前
【量化纯GET实战 #11】移动均线 MA:5 日 20 日 60 日怎么算怎么用
java·python·金融·maven