【左京淳的JAVA学习笔记】第六章 继承与多型

在创造各式各样的物件时,有很多时候会发现怎麽重复的代码很多。
为了解决这个问题,可以采用继承与介面的方式。

继承的文法

class Employee {}
class sales extends Employee {}

想像一间公司里有许多员工,有业务有後勤有管理人员。这些职位的人员虽然都有各自的特性,但也都是员工的一种,他们应该会有员工编号之类的共用属性。
例如sales类继承了Employee类,就可以获得里面所有的变数与方法,不用重新写一次。而且还可以追加新的方法,或是覆写掉不喜欢的方法!

介面的文法

class Employee implements MyInterface {}

介面有点类似继承,一样可以获得来自其他类里面的变数和方法。不过两者概念上有点不一样。
继承只能继承一个类,概念像是一个Employee类可以升级为业务、後勤或是管理人员,拥有进阶的能力。
而介面可以引用很多个,类似装备或工具,可以随时装上或拆卸。

接下来介绍一下关於继承的细节
被继承的类又被称为Super类或是父类,继承类则被称为Sub类或子类。
请看以下关於覆写(override)的范例:

class Super {
  public void print(String s){
    System.out.println("Super class : " + s );
  }
  public void method(){}
}

class Sub extends Super {
  public void print(String s){
    System.out.println("Sub class : " + s );
  }
  //void method(){}  
}

class Sample6_1 {
  public static void main(String[] args) {
    Super s1 = new Super();
    s1.print("text");  //会调用Super类的方法
    Sub s2 = new Sub();
    s2.print("text");  //会调用Sub类的方法
  }
}

执行结果

Super class : text
Sub class : text

可以发现Sub class里面的print方法被复写掉了。与Super class的执行结果不同。
要注意的是,覆写Super类的方法时,使用一样的名字和返回值类型,修饰词则需一样或者权限更开放。
(权限开放程度: public > protected > 不指定 > private)
例如本案例的method()在Super class里面的修饰词是public,则Sub class里面的method()也必须是public,如果没写或写其他修饰词就会报错。

final修饰词

final修饰词可以用在变数前面,让其成为无法被修改的定数。也可以放在方法前面,让其无法被覆写。如果是放在类的前面,则此类无法被继承。如下例:

class Super {
  final void method(){}
}

final class Super {}

this指令

this用来代称本物件,用来解决变数代称的问题。
例如有个有名的对话故事如下
「谁打我?」
谁:「我没打人」
人:「我知道」
我:「知道什麽?」

程序范例如下

int id;
void setId(int id){
  //id = id;  //左右两个id都是同一个变数(第二行的id)。
  this.id = id;  //this.id是指这个物件的id(第一行的id)。
}

this也可以用来减少重复写代码的问题,例如应用在建构式中,范例如下:

class Foo {
  String s; int i;
  public Foo(){
    this("no_data");
  }
  public Foo(String s){
    this(s,1);
  }
  public Foo(String s, int i){
    this.s = s;this.i = i;
    System.out.println("String : " + this.s);
    System.out.println("int : " + this.i);
  }
}

class Sample6_2 {
  public static void main(String[] args) {
    System.out.println("调用Foo()------");
    Foo f1 = new Foo();
    System.out.println("调用Foo(String s)------");
    Foo f2 = new Foo("Tom");
    System.out.println("调用Foo(String s, int i)------");
    Foo f3 = new Foo("Mary",30);
  }
}

执行结果

调用Foo()------
String : no_data
int : 1
调用Foo(String s)------
String : Tom
int : 1
调用Foo(String s, int i)------
String : Mary
int : 30

可以看到Foo里面有三个建构式,分别对应无资料、只有文字、有文字及数值资料等三种状况。
利用this指令,当无String资料时,将预设值("no_data")填入後,传给第二个建构式。
在第二个建构式中,当无int值时,将预设值(1)填入後,然後传给第三个建构式。
在第三个建构式中进行完整的处理。

利用这样的结构,就不需要重复写很多个类似的建构式了。

super指令

super表示父类,利用这个指令可以调用父类的方法或变数。
虽然子类一般都继承了父类的方法,但是如果有覆写的状况,则可以利用super调用原本的父类方法。

class Super {
  int num;
  public void methodA(){num += 100;}
  public void print(){System.out.println("num = " + num);}
}

class Sub extends Super {
  public void methodA(){num += 500;}
  public void methodB(int num){
    methodA();
    print();
    super.methodA();
    print();
  }
}

class Sample6_3 {
  public static void main(String[] args) {
    Sub s = new Sub();
    s.methodB(0);
  }
}

执行结果

num = 500
num = 600

调用覆写後的方法让num增加了500
再调用父类原本的方法,让num增加了100

利用super指令呼叫建构式

当子类被实例化时,会先执行父类的建构式,再执行子类的建构式。
但是有个跟直觉不太一样的地方,需要透过super指令来修正。看看以下范例:

class Super {
  public Super(){System.out.println("Super()");}
  public Super(int a){System.out.println("Super(int a)");}
}

class Sub extends Super {
  public Sub(){System.out.println("Sub()");}
  public Sub(int a){System.out.println("Sub(int a)");}
}

class Sample6_4 {
  public static void main(String[] args) {
    Sub s1 = new Sub();
    Sub s2 = new Sub(10);
  }
}

执行结果

Super()
Sub()
Super()
Sub(int a)

由结果的第一行及第二行可以发现,实例化物件时,确实先执行了父类的建构式,然後执行子类的建构式。
但是第三行为什麽不是super(int a)呢?
这是因为当子类被实例化时,预设会执行父类的无参数建构式(因为参数并未传递给父类)。
如果要避免这种现象,需要使用super指令,把参数丢给父类。如下例:

class Super {
  public Super(){System.out.println("Super()");}
  public Super(int a){System.out.println("Super(int a)");}
}

class Sub extends Super {
  public Sub(){System.out.println("Sub()");}
  public Sub(int a){
    super(a);
    System.out.println("Sub(int a)");
  }
}

class Sample6_5 {
  public static void main(String[] args) {
    Sub s1 = new Sub();
    Sub s2 = new Sub(10);
  }
}

执行结果

Super()
Sub()
Super(int a)
Sub(int a)

abstract class(抽象类)

在JAVA里面,处理内容被详细记载,可以实例化并使用的类称为实例类。相对的,只记载了方法名称,却未填写内容的类,被称为抽象类。
想像一下,假如我们想设计一台吸尘器,但形式和内容都还不确定,只知道它需要110V的电,能够吸灰尘。
那麽首先我们就先设计一个具有110V插头、具有吸灰尘方法的class,至於详细的内容,就等物件实例化时再填写就好。

抽象类具有以下特点:
无法被实例化。要实例化需新增一个实例类,继承了此抽象类之後,把所有抽象方法都覆写成实例方法(记入内容)。
抽象类里面可以写抽象方法以及实例方法
抽象类也可以继承抽象类。

抽象类和抽象方法的写法,就是在最前面加上一个abstract修饰词即可。
abstract class 类名{}
abstract 返回值 方法名(引数);

范例

abstract class Employee{}
class abstract Employee{}  //报错,abstract须放在最前面

abstract void funcA();
abstract void funcA(){}; //报错,abstract方法不需要{}及其内容。
void abstract funcA(); //报错,abstract须放在最前面

interface(介面)

介面说起来有点像抽象类,里面也放了一些抽象方法。不过不一样的是,介面使用implements来引用,而非extends继承。
请看以下范例:

interface MyInter1 {
  double methodA(int num);
  default void methodB(){System.out.println("methodB()");}
}

interface MyInter2 {
  int methodC(int val1, int val2);
  static void methodD(){System.out.println("methodD()");}
}

class MyClass implements MyInter1,MyInter2 {
  public double methodA(int num){return num * 0.3;}
  public int methodC(int val1, int val2){return val1 * val2;}
}

class Sample6_6 {
  public static void main(String[] args) {
    MyClass obj = new MyClass();
    System.out.println("methodA() " + obj.methodA(10));
    System.out.println("methodC() " + obj.methodC(10,20));
    obj.methodB();
    //obj.methodD();  //error
    MyInter2.methodD();
  }
}

执行结果

methodA() 3.0
methodC() 200
methodB()
methodD()

说明
methodA和methodC是抽象方法,在MyClass里面被覆写为实例方法而能执行。(注意覆写时使用了public修饰词,这是因为覆写时的权限必须宽於抽象方法。)
methodB前面加了default修饰词,这是因为interface类原本是只能写抽象方法的,但JAVA SE8之後利用default修饰词也可以写实例方法。
methodD是static方法,看起来没有传递给实例化物件使用,只能由interface呼叫。

interface的定数和方法

在interface里面,储存值会自动加上public static final修饰词。

在interface里面,方法会自动加上public和abstract修饰词

interface的继承
interface简称为IF,也可以使用继承,范例如下:

interface XIF {
  void methodA();
}
interface YIF {
  void methodB();
}
interface SubIF extends XIF, YIF{
  void methodC();
}

class MyClass implements XIF,YIF {
  public void methodA(){System.out.println("methodA()");}
  public void methodB(){System.out.println("methodB()");}
  public void methodC(){System.out.println("methodC()");}
}

class Sample6_7 {
  public static void main(String[] args) {
    MyClass c = new MyClass();
    c.methodA();c.methodB();c.methodC();
  }
}

以下两点请注意:
interface可以继承复数的interface(还记得class只能继承一个class吗?)
将抽象方法覆写时,需使用pbulic修饰词。

基本Data型的变换

为了方便,有时JAVA会自动进行资料型态的变换。
由左至右,比较小的资料储存型态可以转换成比较大的资料储存型态。
byte -> short -> int -> long -> float -> double
char -> int
如果要反向转换的话,需使用()符号进行强制转换。

范例
变数的自动转换

short s = 10;
int i = s;

变数s会从short型自动转换为int型态

引数的自动转换

int i = 100;
method(i);

void method(double b){};

i虽然是int型态的资料,但是作为引数被丢入method时,可以自动转换为double型。

返回值的自动转换

double d = method();
int method(){int i = 100; return i;}

返回的int型资料,可以自动被转换为double型资料填入变数d中。

变数的强制转换

int i = 100;
short s = (short)i;

由大至小的转换需要使用()符号进行强制转换。

引数的强制转换

double d = 10.5
method((int)d);
void method(int i){}

返回值的强制转换

int i = method();
int method(){double d = 10.5; return (int)d;}

计算时的注意点:
两个数值进行计算时,JAVA会自动把双方变为一样的资料型态。
如果原本的资料型态是byte,short等较小的资料型态,会被转换成int型态之後计算。
范例

short s1 = 10;
s1 = ++s1;  //可顺利执行。因为没有使用计算符,因此资料型态未被转换。
s1 = s1 + 1; //NG,因为s1会被转换为int型後+1,无法再放回short型的变数里面。

解决例

s1 = short(s1 + 1);

物件的型态变换

除了数值外,物件也可以自动变换。
子类可以自动变换为父类。
实例类可以自动变换为抽象类。

这是什麽意思呢?
想像员工是一个父类,业务是一个子类。
由於业务保有员工的各种属性,所以可以被当成员工来处理。
但如果是相反方向的强制转换,则须使用()。
例:

class Super {}
class Sub extends Super {}
class Test {
  Super super = new Sub();  //子类转父类可自动转换
  Sub sub = (Sub)super;  //父类转子类须强制转换
}

物件转换的使用时机

请看以下范例

class Super {
  void methodA(){}
}

class Sub extends Super {
  void methodA(){}
  void methodB(){}
}

class Test {
  Super super = new Sub();
  super.methodA();
  //super.methodB();
  Sub sub = (Sub)super;
  sub.methodB();
}

此范例中实例化了一个Sub物件,并转存成Super形式。
super.methodA(); 这行执行的methodA,会是Sub class里覆写後的。
super.methodB(); 这行会报错,因为super class里面没有methodB方法。
须将其转回Sub形式,才能呼叫methodB方法。

举个容易了解的例子,假设员工拥有「沟通(基础)」这个技能,业务也有,但是是升级版的「沟通(进阶)」。
这时你请这位员工执行此技能,就会执行出「沟通(进阶)」(回不去了)。
但基於JAVA的保护机制,你无法让一个人以员工身分执行业务专有的技能,例如「开发市场」。(即便他会)
必须要明白的告诉JAVA说,这个员工是个业务,才能够使用业务的技能。

关於物件的转换,有一些需要注意的点,请看下例:

class Super {}
class Sub extends Super {}
class Foo {}

Super obj1 = new Sub();
Sub sub1 = (Sub)obj1;  //可正常执行。

Foo obj2 = new Foo();
Sub sub2 = (Sub)obj2;  //NG,无继承关系的物件无法转换,编译时会报错。

Super obj3 = new Super();
Sub sub3 = (Sub)obj3;  //NG,本质是Super的物件无法转换为Sub物件,执行时会报错。

以上案例说明了强制转换只是一种声明,并不能改变物件的本质。
所以本来是Sub类的物件,即使转为Super,也可再转回Sub。
但原本是Super的物件,无法转换为Sub。

概念说明如下:
Sub(拥有100%能力) ->Super(被当成Super看待,部分能力受限)->Sub(拥有100%能力)
Super(拥有100%能力) ->Sub(被当成Sub看待,超出自己的能力范围,因此NG)

原来只有超人才能变身成超人呀..

利用instanceof演算符判断实例化物件的类型

instance是实例的意思,可以判断目标物件是否为特定的class(也包含父类或interface)
来看看范例:

interface A {}
interface B {}
class C {}
class D extends C implements B {}
class E {}

class Sample6_8 {
  public static void main(String[] args) {
    D d = new D();
    System.out.println(d instanceof A);
    System.out.println(d instanceof B);
    System.out.println(d instanceof C);
    System.out.println(d instanceof D);
    //System.out.println(d instanceof E);
  }
}

执行结果

false
true
true
true

以上是第六章 继承与多型的学习心得,下一章会介绍API的使用。

参考教材: JAVAプログラマSilver SE8 - 山本 道子


<<:  SEO:关於 Custom Campaign Tracking

>>:  [Android Studio] -- Day 5 主题变换Theme02

D-15 过滤器 ? filter ? attribute

filter 眼尖的小光在昨日的内容中看到了一个有趣的东西,就是MiddlewareFilter,所...

[DAY 22] 试题反映理论

试题反映理论 在试题反映理论(Item Response Theory, IRT)中 能用作因素来解...

Day3 什麽是Git?

大家好,我是乌木白,今天我们开始讲我们这次铁人赛的第一个技能,就是Git啦!先和大家声明我是把我自...

[Day04] CH03:各式运算子(上)

今天要介绍的是运算子(Operator),在程序语言中有分为: 指定运算子 「=」可以把右侧的东西指...

Day29-台湾菜鸟工程师除错之卷四

不过让我印象最深刻的面试就是yahoo所委托的专案 那时候委托者是需要做出一个yahoo news...