JavaSE基础知识(十七)--Java复用代码之组合与继承

news/2024/7/6 23:28:13

Java SE 是什么,包括哪些内容(十七)?

本文内容参考自Java8标准
再次感谢Java编程思想对本文的启发!
复用代码是Java众多引人注目的功能之一,但要想成为极具革命性的语言,仅仅能够复制代码并对之加以改变是不够的(也就是仅仅只能复制黏贴是不行的),它还必须能够做更多的事情(这里所谓更多的事情,就是你即将看到的组合,继承,代理这三种复用代码的机制)!
复制黏贴在C语言这种过程型语言中是经常使用的,但收效并不是很好,正如Java中所有事务一样,问题解决都是围绕着类展开的,可以通过创建新类来复用代码,而不必再从头开始写,可以使用别人业已开发并调试好的类(说白了就是在Java中可以实现不用复制黏贴就能直接实现复用代码的目的)。
要达到上述的目的的窍门在于使用类而不破坏现有程序的代码。
共有两种方法:
**⑴、组合:**组合非常直观,只需要在新的类中产生现有类的对象,因为新的类是由现有类的对象组成的,所以称之为组合。组合只是复用了现有程序代码的功能,而非它的形式。
**⑵、继承:**继承稍微更细致一些,它按照现有类的类型来创建新的类,无需改变现有类的形式,而是继续保持它原有的形式,并且在它的基础上添加新的代码,这是一种非常神奇的方式,继承是面向对象设计的基石之一(编译器对继承的支持也是相当给力的)。
就组合与继承而言,实际上,它们的语法和行为大多数是相似的,都是利用现有的类型生成新的类型!
由于代理的知识还需要其他的内容支撑,我现在的博文暂时还未涉及,所以,在这里,只会提及组合和继承两种代码复用的机制!有兴趣的可以持续关注后续博文的更新!

组合

迄今为止我的博文中已多次使用到了组合技术,实际上,只需将对象的引用置于新类中即可。
例如,假设你需要某个对象,它又需要多个String对象,几个基本类型数据,以及另一个类的对象。(对于非基本类型的对象,必须将其引用置于新类中,但是基本数据类型可以直接定义。)。
代码示例:

// 组合代码示例
   //类WaterSource
   class WaterSource{
      //String类型的私有类变量s
      private String s;
      //构造方法
      WaterSource(){
         //打印字符串"WaterSource()"
         System.out.println("WaterSource()");
         //为私有变量s赋值"Constructed";
         s = "Constructed";
      }
      //方法toString()
      public String toString(){
         //返回私有类变量s的值。
         return s;
      }
   }
   //类SprinklerSystem
   public class SprinklerSystem{
      //定义String类型的私有类变量,分别是value1,value2,value3,value4;
      private String value1,value2,value3,value4;
      //定义WaterSource类型的私有类变量,这里就是组合机制的体现。
      //类SprinklerSystem里组合了类WaterSource
      private WaterSource source = new WaterSource();
      //定义基本类型int的类变量。
      private int i;
      //定义基本类型float的类变量。
      private float f;
      //toString()方法
      public String toString(){
         //返回一个String类型。
         //包含了所有的类变量的值。
         return 
            "value1 = " + value1 + " "+
            "value2 = " + value2 + " "+
            "value3 = " + value3 + " "+
            "value4 = " + value4 + "\n"+
            "i = " + i + " " + "f = " + f + " " +
            "source = " + source;
      }
      //程序执行入口main方法
      public static void main(String[] args){
          //创建类SprinklerSystem的对象。
          SprinklerSystem sprinkler = new SprinklerSystem();
          //输出对象的内容。这里会自动调用toString()方法。
          System.out.println(sprinkler);
      }
   }

这里有个问题需要重点强调:上面两个类所定义的方法中,有一个方法很特殊:toString(),实际上,每一个非基本类型的对象都有一个toString()方法,当编译器需要一个String,但是你却只有一个对象的时候,它就会被调用(比如你用System.out.println()直接打印一个对象的引用,这个时候实际调用的就是toString()方法!)
落实到代码中,比如在类SprinklerSystem的toString()方法中:
直接打印一个对象的引用,实际调用的就是这个对象的toString()方法!
"source = " + source;
通过这句代码,编译器将会知道,你是想要将一个String对象(“source”)与WaterSource对象相加,但是String类有一个强制性的规定(String对象只能与String对象相加),因此,编译器的选择是统一将其他任何的对象转换成String对象,关键就在于toString()方法(每个对象都有这个方法),而且它的主要功能就是将任何对象都转换成String对象,所以,针对代码" + source",编译器调用类WaterSource的toString()方法,将对象source转换成了一个String对象,这样做之后,编译器就能将两个String对象拼接在一起同时将结果传递给System.out.print()输出(当你想要使所创建的类都具备这种行为的时候,编写一个toString()方法即可,同时在方法内部拼接好打印输出内容的格式)。
我们来看下上面代码运行后得到的结果:
示例代码的运行结果!
结果印证了我之前有关初始化的博文中提到的结论,Java的默认初始化功能会将基本类型初始化为0,引用类型初始化为null,而且如果你试图在这个时候为它们调用任何的方法,都会得到一个异常-----运行时错误,但是很方便的是,在不抛出异常的情况下仍旧可以打印一个null引用。
编译器并不是简单地为每一个引用都创建默认对象(默认初始化),这一点是很有意义的,因为如果真的是那样的话,会在很多情况下增加不必要的负担,如果想初始化这些引用,可以在代码中的下列位置进行:
、在定义对象的地方,这意味着它们总是能在构造方法被调用之前被初始化。
、在类的构造方法中。
、就在即将使用这些对象之前,这种方式称为惰性初始化(一直拖到了要用的时候才进行初始化,而Java中提倡的是声明时就初始化),在没有必要立即生成对象或者不必每次都生成对象的时候,这种方式可以减少额外的负担。
、使用实例初始化。
代码示例:

// 以上四种初始化方式的代码示例
   //类Soap
   class Soap{
      //String类型的私有类变量s
      private String s;
      //构造方法
      Soap(){
         //打印字符串"Soap()"
         System.out.println("Soap()");
         //为String类型变量s赋值
         s = "Constructed";
      }
      //toString()方法
      public String toString(){
         //返回字符串s的值
         return s;
      }
   } 
   //类Bath
   public class Bath{
      //声明String类型的私有变量。
      //其中s1,s2在声明的时候就初始化了。
      //s3,s4没有初始化。
      private String s1 = "Happy",s2 = "Happy",s3,s4;
      //声明Soap引用类型的变量,未初始化
      private Soap castille;
      //声明int基本类型的变量i
      private int i;
      //声明float基本类型的变量i
      private float toy;
      //构造方法
      public Bath(){
         //打印字符串"Inside Bath()"
         System.out.println("Inside Bath()");
         //注意,这是在构造方法内部
         //初始化变量s3.
         s3 = "Joy";
         //初始化变量toy.
         toy = 3.14f;
         //初始化变量castille.
         castille = new Soap();
      }
      //实例初始化,一定要带上{ }
      { i = 47; } 
      //toString方法
      public String toString(){
         //if判断句
         if(s4 == null){
             //这就是延迟初始化
             s4 = "Joy";
         }
         //返回字符串
         return 
           "s1 = " + s1 + "\n" + 
           "s2 = " + s2 + "\n" + 
           "s3 = " + s3 + "\n" + 
           "s4 = " + s4 + "\n" + 
           "i = " + i + "\n" + 
           "toy = " + toy + "\n" + 
           "castille = " + castille;
      }
      //程序执行入口main方法
      public static void main(String[] args){
          //创建类Bath的对象。
          Bath b = new Bath();
          //输出对象的内容。这里会自动调用toString()方法。
          System.out.println(b);
      }
   }

结果示例:
结果示例!
这里有个问题需要强调一下:初始化意味着这个变量的存储空间存储了值,但是有的人在看到这个代码的执行结果时,会和初始化的时机混淆,认为这个打印出来的执行结果就是初始化的顺序。实际上,上面示例代码的执行结果是不能反映真实的初始化顺序的!
为了更清楚了解以上代码示例的执行结果,我们具体来看一下执行过程。
首先从main方法开始:
Bath b = new Bath();
这句代码创建了Bath对象,那么首先调用的是Bath的构造方法,
来看下它里面的代码:
System.out.println(“Inside Bath()”);------------------------输出1
执行到这句代码的时候,就直接输出了字符串"Inside Bath()",所以在结果中的第一行,你会看到这行字符串。
s3 = “Joy”;
这句代码没有任何输出,仅仅是初始化。
toy = 3.14f;
这句代码没有任何输出,仅仅是初始化。
castille = new Soap();
执行到这句代码的时候,创建的是Soap对象,调用的是Soap的构造方法。
Soap的构造方法里面的代码:
System.out.println(“Soap()”);------------------------输出2
执行到这句代码的时候,就直接输出了字符串"Soap()",所以在结果中的第二行,你会看到这行字符串。
s = “Constructed”;
这句代码没有任何输出,仅仅是初始化。
//-------------------------------------执行到这里 Bath b = new Bath(); 就算是执行完毕了!
下一句代码:
System.out.println(b);
前面说了,b是Bath对象的引用,在直接System.out.println()一个引用对象的时候,默认调用它的toString()方法。所以,我们找到Bath类的toString()方法:
public String toString(){
if(s4 == null) {
s4 = “Joy”;

这句代码没有任何输出,仅仅是初始化。
}
这句代码是主要的输出内容:
return
"s1 = " + s1 + “\n” + ------------------------输出3
这句代码输出s1的值,由于它已经被初始化,值是"Happy",
执行到这句代码的时候,就直接输出了字符串"Happy",所以在结果中的第三行,你会看到这行字符串。
"s2 = " + s2 + “\n” + ------------------------输出4
这句代码输出s2的值,由于它已经被初始化,值是"Happy",
执行到这句代码的时候,就直接输出了字符串"Happy",所以在结果中的第四行,你会看到这行字符串。
"s3 = " + s3 + “\n” + ------------------------输出5
这句代码输出s3的值,由于它已经被初始化,值是"Joy",
执行到这句代码的时候,就直接输出了字符串"Joy",所以在结果中的第五行,你会看到这行字符串。
"s4 = " + s4 + “\n” + ------------------------输出6
这句代码输出s4的值,由于它已经被初始化,值是"Joy",
执行到这句代码的时候,就直接输出了字符串"Joy",所以在结果中的第六行,你会看到这行字符串。
"i = " + i + “\n” + ------------------------输出7
这句代码输出i的值,由于它已经被初始化,值是47,
执行到这句代码的时候,就直接输出了字符串47,所以在结果中的第七行,你会看到这个值。
"toy = " + toy + “\n” + ------------------------输出8
这句代码输出toy的值,由于它已经被初始化,值是3.14f,
执行到这句代码的时候,就直接输出了字符串3.14,所以在结果中的第八行,你会看到这个值。
"castille = " + castille;------------------------输出9
这句代码输出castille的值,又因为castille是对象引用,所以默认调用它的toString()方法,找到类Soap的toString()方法,
public String toString(){
return s;
}

它直接返回了变量s的值。
又因为s在类Soap的构造方法中已经被初始
化了,值是"Constructed"(s = “Constructed”;),所以相当于返回的是字符串"Constructed",所以这里输出字符串"Constructed"。
将上面备注的输出1~9连起来就是上面示例代码的输出了!

继承

继承是所有OOP语言以及Java必不可少的,当你在Java中创建一个类的时候,总是在继承,因此,除非已明确指出要从其它类继承,否则就是在隐式地从Java的根类Object类继承(Object类是Java中所有类的祖先类,Java中所有的类都是直接或者间接继承了它!)。
组合的语法比较平实,没有什么特殊的关键字之类的,但是继承使用的是一种特殊的语法,在继承过程中,需要先声明"新类与旧类相似",这种声明是通过在类主体的左边花括号之前,书写后面紧随基类名称的关键字extends实现的,当你这么做的时候,在新类中会自动得到基类的所有方法和域:
代码示例

// 通过继承的语法,新类会自动获得基类的所有方法和域,但是使用的时候还需要看
// 具体的权限修饰词!private修饰的是不能使用的。
   //基类Cleanser
   class Cleanser{
      //私有的String类型类变量s,已初始化,值为"Cleanser";
      //子类中也拥有这个变量,但是子类不能访问它。
      private String s = "Cleanser";
      //方法append(String a),带一个String类型的形式参数。
      //这个方法是拼接字符串s和参数a.然后把拼接的结果继续赋值给变量s.
      //要充分理解操作符"+=",这实际上是一个累加的操作。
      public void append(String a){ s += a; }
      //方法dilute(),这个方法是拼接变量s和字符串"dilute()"
      public void dilute(){ append("dilute()"); }
      //方法dilute(),这个方法是拼接变量s和字符串"apply()"
      public void apply(){ append("apply()"); }
      //方法dilute(),这个方法是拼接变量s和字符串"scrub()"
      public void scrub(){ append("scrub()"); }
      //方法toString(),返回拼接最后的结果字符串,也就是变量s。
      //从结果上能看出累加的效果。
      public String toString(){ return s; } 
      //程序执行入口main方法
      public static void main(String[] args){
         //创建类Cleanser的对象。
         Cleanser c = new Cleanser();
         //调用方法dilute()
         c.dilute();
         //同上。
         c.apply();
         //同上。
         c.scrub();
         //打印输出引用c的值,这里默认会调用类Cleanser的toString()方法
         System.out.println(c);
      }
   }
   //-------------------------------------------------------------下面通过一个子类来继承基类Cleanser
   //子类Detergent,通过extends关键字继承了基类Cleanser。
   public class Detergent extends Cleanser{
      //实际上在子类的类里,你甚至可以不需要声明任何的变量以及方法,因为通过
      //关键字extends,子类自动拥有了父类的一切(包括父类的所有变量以及方法)。
      //你可以在子类里面直接使用父类的变量和方法,但是仅限权限为
      //public、protected以及包访问权限修饰的变量和方法。
      //权限为private是无法访问的。
     
      //甚至你还能改写父类中方法的实现代码。
         //这种操作在Java中有一个专门的名称:方法重写!
         //你需要与"方法重载"区分开来。
         //子类中重写了方法scrub()
         public void scrub(){
            //调用append(String a)方法进行字符串的拼接(注意,这个append()方法
            //还是父类中的append()方法,实现也还是父类中的实现,因为你并没有在
            //子类中对它进行重写!)。
            append("Datergent.scrub()");
            //通过"super"关键字明确调用父类中的那个同名方法(scrub())
            super.scrub();
            //原先父类中的scrub()方法实现的代码是:
            //append("scrub()");
            //现在既然你在子类中已经改写了它的实现代码,那么当你在子类中再次调用
            //scrub()方法的时候,就会
            //按照你最新的代码来执行(这里需要明确区别的是,通过关键字"super"
            //再次调用了父类中的scrub()方法,所以除了执行子类中的新代码外,
            //父类中原先的代码也会执行!)。
         }
         //在子类中你还能继续按照你的想法新增内容,比如新增一个方法foam()
         //这个方法在父类中是没有的。
         //方法foam(),这个方法是拼接变量s和字符串"foam()"
         //新增一个方法相当于为子类增加了新的接口。
         public void foam(){ append("foam()"); }
      //程序执行入口main方法
      public static void main(String[] args){
         //创建子类对象d
         Detergent d = new Detergent();
         //比如你想在这里使用父类中的变量s,
         //System.out.println(d.s);
         //这个时候编译器会报错。
         //因为变量s的权限修饰词是private,在子类中不能被访问。
         //但是你可以直接调用父类中的方法(因为这些方法的权限修饰词是public)
         //比如
         //直接调用父类的方法append(String a)
         d.append("abc");
         //直接调用父类的方法dilute()
         d.dilute();
         //直接调用父类的方法apply()
         d.apply();
         //直接调用子类的方法scrub(),为什么说是子类,因为重写了它
         d.scrub();
         //直接调用子类的方法foam()
         d.foam();
         //直接打印输出引用d,这个时候调用的是父类的toString()方法。
         //但是返回的结果是与子类方法调用轨迹相关的。
         //因为子类也继承了变量s,这个字符串s拼接的内容都是根据子类方法的
         //调用轨迹生成的,与父类的方法调用轨迹无关。
         System.out.println(d);
         //调用基类的main方法,也就是执行基类中main方法的代码。
         //因为main方法是static的,所以可以直接通过类名调用,不用创建对象。
         //通过它,你能看到父类main方法的执行结果。
         Cleanser.main(args);
       }
   }

上面的示例程序示范了Java的许多特性,首先是Cleanser类里面通过"+=“操作符拼接字符串,同时再次赋值,这个操作符是被Java设计者重载用以处理String(字符串)对象的操作符之一(另一个是"+","+"仅仅是拼接字符串,并没有赋值的功能!)。
其次,类Cleanser和类Detergent均有main方法,可以为每个类都创建main方法,这种在每个类中都设置main方法的技术可使每个类的测试都变得简单易行,而且在完成单元测试之后,也无需删除main方法,可以留着下次测试使用。
即使是一个程序中含有多个类,也只有命令行所调用的那个类的main方法会被调用,因此,在上面的代码示例中,如果命令行是java Detergent(这里指的是Windows经典黑窗口的命令行,在IDE中,你是看不到命令行的!),那么Detergent.main()会被调用。
命令行图示!
即使类Cleanser不是一个public类,如果命令行是java Cleanser,那么Cleanser.main()仍然会被调用,即使这个类只具有包访问权限,其public main()仍然是可以访问的(也就是可执行的!)。
在上面的代码示例中Detergent.main()明确调用了Cleanser.main(),并将从命令行获取的参数传递给了它(String[] args的args代表的就是命令行参数,这个参数将作为一个String数组存在,并传递给main方法,当你用Windows经典黑窗口执行程序的时候,有时直接就是java Detergent,这个命令代表直接执行这个类,但是有的时候会在java Detergent 后面再加上一些参数。这些参数将作为String数组传递进main方法中。),当然,也可以向其传递任意的String数组。
父类Cleanser中的方法必须是public的,这很重要,如果没有加任何的访问权限修饰词,那么成员默认的访问权限是包访问权限,它仅允许包内的成员访问,因此,在此包中,如果没有任何的访问权限修饰词,任何人都可以使用这些方法,例如Detergent就不成问题,但是,其它包中的类若要从Cleanser中继承,则只能访问public成员,所以,为了继承,一般将所有的数据成员都设置成private,将所有的方法指定为public(protected成员在子类中也可以访问),但是针对特殊的情况可以进行调整,但是上述方法确实是一个实用的规则。
命令行加参数图示!
父类Cleanser的接口中有以下方法:append(),dilute(),apply(),scrub(),toString(),由于子类Detergent是通过关键字extends从父类Cleanser导出的,所以它的接口能自动获得这些方法(尽管没有在子类Detergent里面看到这些方法的显示定义),并且可以直接使用,所以,继承就达到了代码复用的目的(因为你压根没定义就直接能使用)!
正如在子类Detergent中看到的,它对方法scrub()进行了重新实现,所以,你能发现,可以直接使用从父类继承而来的方法,也可以选择修改它,让它拥有自己的实现。
在实现继承的过程中你可能会有这种需求:你希望在子类的方法中直接调用父类的方法(以达到进一步的代码复用),这个时候,你需要使用关键字"super"来明确表达对父类中方法的调用,如果省略了关键字"super”,而是直接写方法名称,这样仅能实现同一个类中的方法相互调用,更有甚者,如果在一个方法中调用自己,则会产生递归的效果,这可能是你不希望看到的。
通过super关键字避免产生递归!
一开始接触继承的时候,很多人会产生一定程度的疑惑以及不适应,往往父类子类的代码要来来回回看好多遍,实际上,你只要明白一点就够:继承只是你通过关键字extends通知编译器将父类中的所有代码(注意是所有代码,也就是类{ }内的所有内容)都复制给了子类,只是没有体现在子类的代码中而已,所以,以后你一旦遇到了继承的代码,你干脆直接将父类的代码复制黏贴到子类中,然后完全脱离父类的代码,单独研究子类的代码,这样就能清晰很多,如果最后你要运行代码,一定要记得将你复制黏贴到子类中的代码原原本本删除,如果没有删除,编译器会认为你在子类中重写了父类的方法,也违背了编译器"只需你声明一个extends关键字就自动为你复制黏贴所有代码的初衷了!",另外,继承虽然将父类中的所有代码都复制到了子类中,但是子类还是要根据父类具体的权限修饰词来使用(private修饰的子类不能用,public、protected修饰的可以使用,在详细描述继承的博文中,我会有更加细致的解释!)。
在继承的过程中,子类并不一定非得使用父类的方法,也可以在子类中添加新的方法(直接正常添加就行),就像上面代码示例中的foam()方法一样。
剩下还有两个有关继承的问题提一下:
⑴、初始化父类(也叫基类)
由于继承涉及父类和子类两个类,而不是只有一个类,所以要试着想象子类所产生的对象,会有点困惑,从外部来看,它像是一个和父类具有共同接口的新类,可能还会有一些额外的方法和域。
实际上在Java编译器的内部,继承并不像表面看上去的那么简单,不仅仅是将父类的接口复制到了子类中,当你创建了一个子类的对象时,该对象包含了一个父类的子对象,这个对象与你用父类直接创建的对象是一样的,二者的区别在于,后者来自于外部,没有任何约束,而前者是被包装在子类的内部。
所以,在正常运行子类对象之前,你需要将包装在内部的父类子对象进行正常的初始化!而且仅有一种方法来保证这一点:在构造方法中调用基类的构造方法来执行初始化。父类构造方法具有父类对象执行初始化的所有知识和能力,Java会自动在子类的构造方法中插入对父类构造方法的调用,下面通过代码来展示上述机制在三层继承关系上是如何工作的。
代码示例:

// Java三层继承的示例
   //类Art
   class Art{
      //不带参数的构造方法
      Art(){
         //打印字符串"Art constructor"
         System.out.println("Art constructor");
      }
   }
   //类Drawing继承类Art
   class Drawing extends Art{
      //不带参数的构造方法
      Drawing(){
         //打印字符串"Drawing constructor"
         System.out.println("Drawing constructor");
      }
   }
   //类Cartoon继承类Drawing
   public class Cartoon extends Drawing{
      //不带参数的构造方法
      Cartoon(){
         //打印字符串"Cartoon constructor"
         System.out.println("Cartoon constructor");
      }
      //程序执行入口main方法
      public static void main(String[] args){
         //创建子类Cartoon的对象
         Cartoon c = new Cartoon();
   }
 }

结果示例:
结果示例!
你注意到,在示例代码中,仅仅只有这一行创建对象的代码:
Cartoon c = new Cartoon();
但是在结果示例中,你会发现所有继承环节里面的类的构造方法都被调用了,说明都创建了相应的对象。
所以结论是:Java编译器在执行继承结构的时候,往往都是从上到下依次创建继承环节里面的每一个类的对象,直至最后创建继承结构中最末尾的类的对象,也就是最终的子类(首先创建Art类的对象,再创建Drawing类的对象,最后才是Cartoon类的对象),实际上,这里还有一个隐含的类,没有显示标识出来:Object类,Art类默认继承Object类,这点需要注意。
构建的过程都是向外扩散的,父类在子类的构造方法可以访问它之前就已经完成初始化。
上面的代码示例中,构造方法都是没有带形式参数的,下面来看下带形式参数的情况
上面示例中的个各类都有默认的构造方法,编译器可以轻松地调用它们是因为不需要考虑传递什么样的参数,但是,如果你想调用一个带参数的构造方法,就必需用关键字super显式地调用父类的构造方法,并且配以适当的参数列表。

// 带参数的构造方法
   //类Game
   class Game{
      //带一个int类型形式参数的构造方法
      Game(int i){
         //打印字符串"Game constructor"
         System.out.println("Game constructor");
      }
   }
   //类BoardGame继承类Game
   class BoardGame extends Game{
      //带一个int类型形式参数的构造方法
      BoardGame(int i){
         //显式调用父类Game中带int类型形式参数的构造方法
         //如果是默认构造方法(不带形式参数),就不用显式调用。
         super(i);
         //打印字符串"BoardGame constructor"
         System.out.println("BoardGame constructor");
      }
   }
   //类Chess继承类BoardGame
   public class Chess extends BoardGame{
      //不带参数的构造方法
      Chess(){
         //显式调用父类BoardGame中带int类型形式参数的构造方法
         //并传入了实际的参数
         super(11);
         //打印字符串"Chess constructor"
         System.out.println("Chess constructor");
      }
      //程序执行入口main方法
      public static void main(String[] args){
         //创建类Chess的对象
         Chess c = new Chess();
      }
   }

结果示例:
一句创建对象的代码,牵动了三个类的构造方法!
如果将类BoardGame的构造方法中调用父类带形式参数构造方法的代码去除(去除代码super(i)),编译器就报错了!要看具体的报错信息,可以打开Eclipse进行尝试:
去除类BoardGame构造方法的代码super(i),编译器就报错了!
因为类BoardGame只有一个带int类型的构造方法,而没有默认的构造方法(不带任何参数的构造方法),如果你不在这个唯一的构造方法中调用父类(类Game)同样带int类型形式参数的构造方法(这个时候必须调用类Game中同样带int类型形式参数的构造方法),编译器将会报错,提示你找不到合适的(只有带int类型形式参数的构造方法才合适)构造方法。
你必须要注意的是,调用父类带形式参数构造方法的代码必须是子类构造方法代码的第一句(也就是说super(i)必须是类BoardGame带参数构造方法的第一句代码),否则编译器同样会报错!
如果子类中只有唯一一个构造方法,并且带了形式参数(默认构造方法除外),那么就需要使用关键字super在它这个唯一的构造方法的第一句代码中明确调用父类中具有相同类型形式参数的构造方法(还有一点需要特别注意,如果子类的这个唯一的构造方法有多个形式参数,那么父类中也需要有与子类这个构造方法形式参数数量、类型以及顺序完全一样的构造方法,并且通过super关键字在子类的构造方法代码第一句进行调用),如果子类中唯一的构造方法是默认的构造方法,编译器会自动调用,就不需要你在代码中通过super关键字特别调用了。
下一篇博文,我将介绍组合与继承的中庸之道:代理。
PS:时间有限,有关Java SE的内容会持续更新!今天就先写这么多,如果有疑问或者有兴趣,可以加QQ:2649160693,并注明CSDN,我会就博文中有疑义的问题做出解答。同时希望博文中不正确的地方各位加以指正!


http://www.niftyadmin.cn/n/1982549.html

相关文章

eclipse HttpServlet 类会报错

2019独角兽企业重金招聘Python工程师标准>>> 以前的eclipse因为更换电脑的位数,启动时会出现Failed to load the JNI shared library的错误,好像是和jdk位数(32位或64)有关系 重新copy了一个eclipse导进去项目遇到以下…

JavaSE基础知识(十七)--Java复用代码之静态代理

Java SE 是什么,包括哪些内容(十七)? 本文内容参考自Java8标准 再次感谢Java编程思想对本文的启发! Java中复用代码的方式,除了之前博文中提到过的组合和继承之外,还有第三种:代理,而代理又分为…

JavaSE基础知识(十七)--Java复用代码之动态代理

Java SE 是什么,包括哪些内容(十七)? 本文内容参考自Java8标准 再次感谢Java编程思想对本文的启发! 上一篇博文中详细说明了静态代理的内容,也指出了静态代理只适合小范围的使用(使用和维护都很麻烦),真正强大的是动态…

JavaSE基础知识(十七)--Java动态代理中InvocationHandler中Object类型参数proxy的作用

Java SE 是什么,包括哪些内容(十七)? 本文内容参考自Java8标准 再次感谢Java编程思想对本文的启发! 上一篇博文中详细说明了动态代理的内容,但是在说到调用处理器InvocationHandler的时候,有一个Object类型的参数prox…

JavaSE基础知识(十七)--Java复用代码之结合使用组合与继承(正确的初始化与清理)

Java SE 是什么,包括哪些内容(十七)? 本文内容参考自Java8标准 再次感谢Java编程思想对本文的启发! 在我们的日常编程工作中,同时使用组合与继承是很常见的事情,下面通过一个例子来说明: PS:同…

基于HTML5实现3D热图Heatmap应用

为什么80%的码农都做不了架构师?>>> Heatmap热图通过众多数据点信息,汇聚成直观可视化颜色效果,热图已广泛被应用于气象预报、医疗成像、机房温度监控等行业,甚至应用于竞技体育领域的数据分析。 http://www.hightopo…

JavaSE基础知识(十七)--Java复用代码之在组合与继承之间选择

Java SE 是什么,包括哪些内容(十七)? 本文内容参考自Java8标准 再次感谢Java编程思想对本文的启发! 通过前面的博文,我们了解到,组合与继承都允许在新的类中放置子对象,组合是显式地这么做,继承…

使用Hexo搭建自己的博客

2019独角兽企业重金招聘Python工程师标准>>> 参考文章 使用GitHub和Hexo搭建免费静态Blog http://wsgzao.github.io/post/hexo-guide 我的主题 https://github.com/ppoffice/hexo-theme-icarus hexo博客换主题--icarus http://hexo.trity.cc/2015/08/24/hexo%…