多态
面向对象的三大特性是封装、继承和多态。之前解释了封装和继承,这里说明一下多态。
多态主要体现在以下两个方面:
- 方法的多态:重载和覆写
- 重载:是指同名方法,根据不同的参数类型及个数可以完成不同的功能
- 覆写:是指同名方法,根据实例化的子类对象不同,所完成的功能也不同
- 对象的多态:父子类对象的转换
- 向上转型:子类对象转型为父类对象,自动转换,格式为:父类 父类对象 = 子类实例
- 向下转型:父类对象转型为子类对象,强制转换,格式为:子类 子类对象 = (子类)父类实例
class A {
public void func() {
System.out.println("func A");
}
}
class B extends A {
public void func() {
System.out.println("func B");
}
}
public class Demo {
public static void main(String args[]) {
A tmp = new B();
tmp.func();
}
}
结果为:
func B
上面代码中,由于多态的向上转型,这种操作是合理的。而由于发生了对象的向上转型,并且最终由父类对象调用了func方法,但是最终的执行结果却是被子类覆写的func方法。而实际执行结果则是由实例化对象所在类是否覆写了父类中的方法来决定的,该代码中子类对象中覆写了func方法,因此最终调用的是覆写过的方法。
再看一下向下转型:
class A {
public void func() {
System.out.println("func A");
}
}
class B extends A {
public void func() {
System.out.println("func B");
}
}
public class Demo {
public static void main(String args[]) {
A a = new B();
B b = (B)a;
b.func();
}
}
结果为:
func B
上面的操作是正常的,首先实例化子类对象,然后向上转型为父类对象,然后向下转换为子类对象,然后调用子类对象的方法。因此直接实例化子类对象强制进行向下转换是不可行的,开发中要避免强制向下转换。
在面向对象的设计中,多态主要是为了方法调用的参数统一和个性化操作。其中向上转型用于方法传参时的参数统一,向下转型用于对象的个性化操作。
通常来讲,对于方法的参数设定一般是需要通用的类,也就是父类,这样可以接收所有的父类和子类对象作为参数,因为这样可以避免强制转型。同时在方法内部,又希望调用子类的方法,这也就是多态了。
class A {
public void func() {
System.out.println("func A");
}
}
class B extends A {
public void func() {
System.out.println("func B");
}
}
public class Demo {
public static void main(String args[]) {
fun(new A());
fun(new B());
}
public static void fun(A tmp) {
tmp.func();
}
}
结果为:
func A
func B
上面的代码中,在主类的fun方法中设定了父类A作为参数,但是实际上却可以传递子类B作为参数,而在实际中又是调用的各自的个性化操作,这也就是多态的直观体现。
同时也会存在一种情况,子类除了覆写父类的方法之外,还定义了子类的独有方法,而如果想要在父类参数的方法中调用子类的独有方法则会报错,此时就需要进行向下转型了:
class A {
public void func() {
System.out.println("func A");
}
}
class B extends A {
public void func() {
System.out.println("func B");
}
public void foo() {
System.out.println("func C");
}
}
public class Demo {
public static void main(String args[]) {
fun(new B());
}
public static void fun(A var) {
B tmp = (B)var;
tmp.foo();
}
}
结果为:
func C
上面的代码中,在主类中的fun方法中需要先进行向下转型,然后才能调用子类B的foo方法,否则直接调用tmp.foo()在编译时会由于父类A中找不到foo方法而报错。而在实际开发中,也很少出现这种操作,要尽量保持父类和子类方法的一致。
总的来说,多态具有以下特点:
- 向上转型:可以保证参数的统一,但是在向上转型中,通过子类实例化后的父类对象所能调用的方法只能是父类中定义过的方法
- 向下转型:其目的是父类对象要调用实例化子类中的特殊方法,但是向下转型需要经过强制转换
- 实际中多用向上转型
instanceof
而为了保证转型成功,可以使用instanceof关键字来判断某个对象是否是某个类的实例:
Object instanceof Class return boolean
而如果某个Object是某个类的实例,就返回true,否则返回false。
class A {
public void func() {
System.out.println("func A");
}
}
class B extends A {
public void func() {
System.out.println("func B");
}
}
public class Demo {
public static void main(String args[]) {
A tmp = new A();
if (tmp instanceof A) {
System.out.println("Result is true.");
} else {
System.out.println("Result is false.");
}
if (tmp instanceof B) {
System.out.println("Result is true.");
} else {
System.out.println("Result is false.");
}
}
}
结果为:
Result is true.
Result is false.
而如果将上面的一段代码修改为:
A tmp = new B();
则结果为:
Result is true.
Result is true.
从上面的结果也可以看出,经过向上转型之后,该类对象即为父类对象的实例,也是子类对象的实例。因此在相应的方法中可以使用instanceof判断参数传输的正确性,以保证可靠的向下转型。
抽象类
抽象类是JAVA中的重要部分,利用抽象类可以明确定义子类需要覆写的方法,这样可以在父类定义初期就明确子类的方法定义限制,减少开发的出错概率。
之前介绍过普通类,而抽象类是在抽象类的结构中增加抽象方法的组成部分,抽象方法指的是没有方法体的方法,同时抽象方法还限制必须要用abstract修饰。
而所有的普通方法都会有{}来表示方法体,有方法体的方体可以被对象直接调用,而抽象方法没有方法体,自然也不需要加{},但是必须要有abstract修饰,否则没有abstract就会被识别为普通方法,而普通方法没有{},就会在编译时出现错误。
abstract class A {
public void func() {
System.out.println("func A");
}
public abstract void foo();
}
class B extends A {
public void func() {
System.out.println("func B");
}
public void foo() {
System.out.println("func C");
}
}
public class Demo {
public static void main(String args[]) {
fun(new B());
}
public static void fun(A var) {
var.foo();
}
}
结果为:
func C
从上面的代码可以看出:
- 抽象方法没有方法体,也没有{},抽象方法需要使用abstract修饰
- 包含抽象方法的类也应该使用abstract修饰,称为抽象类
- 抽象类不能够进行实例化
- 抽象类必须有子类,但是每一个子类只能够继承一个抽象类,所以具备单继承权限
- 继承自抽象类的子类必须要全部覆写抽象方法
- 多态的向上转型也可以通过抽象类的子类完成抽象类的实例化对象进行操作
而普通类和抽象类的不同在于:
- 抽象类继承子类存在明确的方法覆写要求,而普通类没有
- 抽象类只是多了一些抽象方法,其它与普通类一样
- 普通类对象可以直接实例化,但抽象类对象则需要经过向上转型才能够得到实例化对象
抽象类限制
- 抽象类中存在一些属性,那么在抽象类中一定会存在构造方法,目的是为了属性初始化,并且在子类对象实例化时依然满足先执行父类构造再调用子类构造的情况
- 抽象类不能使用final定义,因为抽象类一定要被继承,而final定义的类不能被继承,因此抽象类不能使用final定义
- 抽象类中可以没有任何抽象方法,但是只要是抽象类,就不能直接使用new进行实例化
- 抽象类中依然可以定义内部的抽象类,而实现的子类也可以根据需要选择是或否定义内部类来继承抽象类
- 外部抽象类不允许使用static声明,而内部的抽象类允许使用static声明,使用static声明的内部抽象类就相当于是一个外部抽象类,继承的时候使用Outer.Inner的形式表示类名
- 在抽象类中,如果定义了static属性或方法,可以在没有对象的时候直接调用
abstract class A {
abstract class B {
public abstract void foo();
}
public abstract void func();
}
class C extends A {
public void func() {
System.out.println("func A");
}
class D extends B {
public void foo() {
System.out.println("func B");
}
}
}
public class Demo {
public static void main(String args[]) {
fun(new C());
C.D tmp = new C().new D();
tmp.foo();
}
public static void fun(A var) {
var.func();
}
}
结果为:
func A
func B
而如果使用static修饰内部抽象类,内部抽象类就可以认为是外部抽象类:
abstract class A {
static abstract class B {
public abstract void func();
}
}
class C extends A.B {
public void func() {
System.out.println("func A");
}
}
public class Demo {
public static void main(String args[]) {
A.B tmp = new C();
tmp.func();
}
}
结果为:
func A
而如果在抽象类中用static修饰属性或方法:
abstract class A {
public static void func() {
System.out.println("func A");
}
}
public class Demo {
public static void main(String args[]) {
A.func();
}
}
结果为:
func A
static修饰的抽象类属性或方法与普通类中static修饰的属性或方法用法相似。虽然不能用static直接修饰类,也不能修饰在外部继承的子类。而如果用static修饰继承类,则可以用下面的格式实例化:
abstract class A {
public abstract void func();
static class B extends A {
public void func() {
System.out.println("func A");
}
}
}
public class Demo {
public static void main(String args[]) {
A tmp = new A.B();
tmp.func();
}
}
结果为:
func A
而若用private继续修饰,则可以在父类中返回子类的实例化对象,这种设计比较常见,目的是隐藏用户不需要直到的子类。
abstract class A {
public abstract void func();
private static class B extends A {
public void func() {
System.out.println("func A");
}
}
public static A getInstance() {
return new B();
}
}
public class Demo {
public static void main(String args[]) {
A tmp = A.getInstance();
tmp.func();
}
}
结果为:
func A
而关于构造方法,也存在一个问题:
abstract class A {
public A() {
this.func();
}
public abstract void func();
}
class B extends A {
private int num = 10;
public B(int num) {
this.num = num;
}
public void func() {
System.out.println("Num is " + num);
}
}
public class Demo {
public static void main(String args[]) {
B tmp = new B(20);
tmp.func();
}
}
结果为:
Num is 0
Num is 20
上面的代码中,实例化子类对象会先调用父类构造,在父类构造中,调用了func方法,但是该方法被子类覆写过了,因此实际调用的是子类的func方法,而此时子类中的num尚未被赋值,因此为默认值。而父类构造结束,会调用子类构造结束,会给num进行赋值,此时打印num为20。
抽象类应用——模板设计模式
由于多态的这种特性,可以用于进行模板设计。首先定义一个带有抽象方法的模板类,抽象方法不必实现:
abstract class Action {
public static final int EAT = 1;
public static final int SLEEP = 3;
public static final int WORK = 5;
public void command(int flag) {
switch(flag) {
case EAT:
this.eat();
break;
case SLEEP:
this.sleep();
break;
case WORK:
this.work();
break;
}
}
public abstract void eat();
public abstract void sleep();
public abstract void work();
}
然后用不同的普通类继承模板类,这样继承类就具有了模板类所有的属性和方法,包括抽象方法:
class Robot extends Action {
public void eat() {
System.out.println("Robot eat.");
}
public void sleep() {
}
public void work() {
System.out.println("Robot work.");
}
}
class Person extends Action {
public void eat() {
System.out.println("Person eat.");
}
public void sleep() {
System.out.println("Person sleep.");
}
public void work() {
System.out.println("Person work.");
}
}
class Pet extends Action {
public void eat() {
System.out.println("Pet eat.");
}
public void sleep() {
System.out.println("Pet sleep.");
}
public void work() {
System.out.println("Pet work.");
}
}
继承类定义之后,然后在方法中用抽象类作为接口,调用抽象方法:
public class Demo {
public static void main(String args[]) {
func(new Robot());
func(new Person());
func(new Pet());
}
public static void func(Action var) {
var.eat();
var.sleep();
var.work();
}
}
结果为:
Robot eat.
Robot work.
Person eat.
Person sleep.
Person work.
Pet eat.
Pet sleep.
Pet work.
这样就实现了模板的操作,也就是说在实际调用时,模板类事先定义了子类的相关操作,子类只需要实现这些操作就可以了,所以才叫模板设计模式。
接口
之前提到了抽象类可以实现对子类覆写方法的控制,但是抽象类的子类只能进行单继承,而为了避免这个限制,就需要使用JAVA接口。
基本定义
JAVA中的接口其实也是类的一种,不过接口通常设计为只有抽象方法和全局常量的类,同时使用interface进行接口定义,而不是abstract class。可以理解为JAVA接口为用interface修饰的特殊抽象类。
之前提到所谓的全局常量就是使用“public”、“static”、“final”联合修饰的变量。因此如果想要定义一个JAVA接口,就可能是这样的:
interface A {
public static final String VAR = "VAL";
public abstract void func();
}
从上面的定义可以推断:
- 接口不能使用new直接进行实例化,因为存在抽象方法,因此用new实例化是不合理的
- 接口必须要有子类,因为存在抽象方法,因此必须要被继承才合理,同时接口子类也必须要实现所有的抽象方法
- 接口实现的关键字不是extends,而是implements,不过子类可以实现多个接口
- 接口对象也可以使用子类对象进行向上转型进行实例化操作
从上面的描述来看,接口可以理解为特殊的抽象类的。
虽然接口定义中说明只包括抽象方法和全局常量,但是这只是给实际开发说明了标准。实际中因为接口只能够使用public访问权限,同时接口中的变量默认是static final,方法默认是abstract的,因此下面的接口定义和之前是相同的:
interface A {
String VAR = "VAL";
void func();
}
但是为了代码的可读性,最好还是使用完整的写法。
接口实现
interface A {
public static final String VAR1 = "VAL1";
public abstract void func1();
}
interface B {
public static final String VAR2 = "VAL2";
public abstract void func2();
}
class C implements A,B {
public void func1() {
System.out.println("func A");
}
public void func2() {
System.out.println("func B");
}
}
上面的代码中,子类C同时继承了接口A和B,这点和抽象类不同,可以进行多继承。同时还可以使用接口类作为参数,以实现多态操作。
public class Demo {
public static void main(String args[]) {
func(new C());
foo(new C());
}
public static void func(A var) {
var.func1();
System.out.println(var.VAR1);
}
public static void foo(B var) {
var.func2();
System.out.println(var.VAR2);
}
}
结果为:
func A
VAL1
func B
VAL2
上面的代码中,就是用接口类进行了多态操作。但要注意此时func方法中的参数类型为A,因此在方法中只能够调用接口A中的方法和属性,而foo方法中的参数类型为B,在该方法中也只能够调用接口B中的方法和属性。
而既然提到多态,就不止是向上转换,还有向下转换:
public class Demo {
public static void main(String args[]) {
A a = new C();
B b = (B)a;
C c = (C)a;
a.func1();
b.func2();
c.func1();
c.func2();
}
}
结果为:
func A
func B
func A
func B
上面的代码中,子类C向上转换为了A,然后该类可以转换为类B和C,但是转换为B便只能调用B类的属性和方法,转换为C便只能调用C类的属性和方法。而这里只所以能够转换为B类,则是因为a是由C类转换而来的,那么c便是类A和B的实例化对象,因此可以转换为类A、类B和类C的对象。
而如果一个子类既要继承抽象类又要实现接口,那么应该先extends后implements:
interface A {
public static final String VAR1 = "VAL1";
public abstract void func1();
}
interface B {
public static final String VAR2 = "VAL2";
public abstract void func2();
}
abstract class C {
public static final String VAR3 = "VAL3";
public abstract void func3();
}
class D extends C implements A,B {
public void func1() {
System.out.println("func A");
}
public void func2() {
System.out.println("func B");
}
public void func3() {
System.out.println("func C");
}
}
可能执行:
public class Demo {
public static void main(String args[]) {
D tmp = new D();
System.out.println(tmp.VAR1);
System.out.println(tmp.VAR2);
System.out.println(tmp.VAR3);
tmp.func1();
tmp.func2();
tmp.func3();
}
}
结果为:
VAL1
VAL2
VAL3
func A
func B
func C
从上面的结果来看,子类可以同时继承抽象类和实现接口,而从定义来看,接口和抽象类是类似的。
抽象类可以继承抽象类或者实现若干个接口,而接口却不能继承抽象类,但是接口却可以使用extends同时继承多个父接口。
上面这段话有点绕,可以理解为接口是抽象类的抽象类,更抽象的可以派生出一般抽象的,一般抽象的不能派生出更抽象的,同级之间可以互相派生。用代码表示为:
interface A {
}
interface B {
}
interface C extends A,B{
}
abstract class D {
}
abstract class E extends D{
}
abstract class F implements A,B{
}
总结起来,同级之间继承用extends,抽象类和接口之间继承用implements。这样看起来抽象类的限制要比接口多:
- 一个抽象类只能继承一个抽象的父类,而一个接口则可以继承多个父接口
- 一个子类只能继承一个抽象类,但可以实现多个接口
虽然之前提到接口的定义,但是接口中也是可以定义普通类、抽象内部类、内部接口的。
interface A {
public abstract void func();
public abstract class B {
public abstract void func();
}
public class C {
public void func() {
System.out.println("func A");
}
}
interface D {
public abstract void func();
}
}
class E implements A {
public void func() {
System.out.println("func B");
}
class F extends B {
public void func() {
System.out.println("func C");
}
}
class G implements A.D {
public void func() {
System.out.println("func D");
}
}
}
执行代码:
public class Demo {
public static void main(String args[]) {
E e = new E();
e.func();
A.C c = new A.C();
c.func();
E.F f = new E().new F();
f.func();
E.G g = new E().new G();
g.func();
}
}
结果为:
func B
func A
func C
func D
上面的代码中同时定义了普通内部类、抽象内部类、内部接口,要注意内部类的继承关系和在外部的实例化关系。
接口应用——标准
这与使用抽象类进行模板有所不同,模板是指设定了抽象方法,然后在子类中实现该抽象方法,因此称之为模板。而虽然接口可以理解为一种特殊的抽象类,但是一般来讲接口不作为模板进行使用。在实际开发中,通常使用接口进行标准设计。
interface USB {
public abstract void start();
public abstract void stop();
}
class Flash implements USB {
public void start() {
System.out.println("Flash start");
}
public void stop() {
System.out.println("Flash stop");
}
}
class Print implements USB {
public void start() {
System.out.println("Print start");
}
public void stop() {
System.out.println("Print stop");
}
}
class Computer {
public void plugin(USB usb) {
usb.start();
usb.stop();
}
}
public class Demo {
public static void main(String args[]) {
Computer com = new Computer();
com.plugin(new Flash());
com.plugin(new Print());
}
}
结果为:
Flash start
Flash stop
Print start
Print stop
上面的代码中,定义了USB作为标准,该标准被Flash和Print实现,然后在Computer中用USB作为参数,这样就USB就成为了一种标准。不过这种使用方式用抽象类也能够实现,与抽象类作为模板也是类似的。
接口应用——工厂设计模式
interface Fruit {
public abstract void eat();
}
class Apple implements Fruit {
public void eat() {
System.out.println("Eat Apple");
}
}
class Orange implements Fruit {
public void eat() {
System.out.println("Eat Orange");
}
}
class Factory {
public static Fruit getInstance(String classname) {
if ("apple".equals(classname)) {
return new Apple();
} else if ("orange".equals(classname)) {
return new Orange();
} else {
return null;
}
}
}
public class Demo {
public static void main(String args[]) {
Fruit tmp = Factory.getInstance("apple");
tmp.eat();
}
}
结果为:
Eat Apple
上面的代码中,定义了Fruit作为接口,同时实现了Apple和Orange,并构建了Factory类,其中的getInstance方法用来获取Fruit实例对象。在实际使用中,通常采用这种设计方式,用Factory获取实际的操作对象。
接口应用——代理设计模式
代理设计是指用代理对象(Proxy)来操作真实对象,真实对象执行具体的业务操作,代理对象负责其它相关业务的处理。这有点类似上网使用代理一样,不管是代理也好,非代理也好,用户关注的只是上网,至于操作如何用户并不关心。
interface Network {
public abstract void browse();
}
class Real implements Network {
public void browse() {
System.out.println("Real browse");
}
}
class Proxy implements Network {
private Network net;
public Proxy(Network net) {
this.net = net;
}
public void check() {
System.out.println("Proxy check");
}
public void browse() {
this.check();
this.net.browse();
}
}
public class Demo {
public static void main(String args[]) {
Network net = null;
net = new Proxy(new Real());
net.browse();
}
}
结果为:
Proxy check
Real browse
上面的代码中,定义了真实对象和代理对象,并将真实对象作为参数传入代理对象,并利用代理对象进行操作来掩饰真实对象的操作,使得整个操作看起来就像是用代理进行操作一样。
虽然抽象类和接口都可以实现上面的功能,但是具体使用最好进行区分。从整体上来说,最顶级的类(动物)最好用接口定义,再次一级的类(哺乳动物)需要继承该接口,并也用接口定义,再往下一级的类(人)需要实现该接口,并用抽象类定义,而具体的类(学生)需要再继承该抽象类,这就是普通类,然后具体的对象继承该普通类就可以了。但是具体开发中是否存在这么多层级则需要根据实际进行架构。
Object
在JAVA中,存在一个特殊的类,就是Object。Object类是所有类的父类,也就是说任何一个类在定义时如果没有明确继承自一个父类,那么其就是Object类的子类。
而由于父类子类之间存在多态,那么所有类就都可以进行向上转型为Object。
同时由于Object中存在三个特殊方法toString/equals/hashCode,因此类中要根据具体情况选择覆写。这里用一段代码说明:
class Book {
private String name;
private int price;
public String toString() {
return "Book name is " + name + ",price is " + price;
}
public Book(String name, int price) {
this.name = name;
this.price = price;
}
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof Book)) {
return false;
}
Book tmp = (Book) obj;
if (name.equals(tmp.name) && price == tmp.price) {
return true;
}
return false;
}
}
public class Demo {
public static void main(String args[]) {
Book bk = new Book("English",10);
Book obj = new Book("English",10);
System.out.println(bk.equals(obj));
System.out.println(bk.toString());
System.out.println(bk.hashCode());
}
}
结果为:
true
Book name is English,price is 10
321001045
上面的代码中,使用Object接收Book对象,表示Book继承自Object,在equals方法中又进行向下转型才能够完成Book类对象的比较,hashCode可以执行则说明该方法已经在父类中实现了。
而除此之外,Object类实际上可以接收所有引用数据类型的对象,这包括数组、接口、类。
public class Demo {
public static void main(String args[]) {
Object obj = new int[] {1,2,3};
System.out.println(obj);
if (obj instanceof int[]) {
int tmp[] = (int[])obj;
for (int i = 0;i < tmp.length;++i) {
System.out.println("value = " + tmp[i]);
}
}
}
}
结果为:
[I@24d46ca6
value = 1
value = 2
value = 3
上面的代码中,就是用Object来接收数组,实现了数组的向上转型,同时进行向下转型进行遍历打印输出的。
interface A {
public void fun();
}
class B implements A {
public void fun() {
System.out.println("fun A");
}
}
public class Demo {
public static void main(String args[]) {
A a = new B();
Object obj = a;
A t = (A)obj;
t.fun();
}
}
结果为:
fun A
上面的代码中,首先是类B向上转型为类A,然后类A向上转型为Object,然后如果要调用fun方法的话,就需要向下转型为类A或类B才可以。但是这并不是因为接口继承了Object,而是因为接口属于引用数据类型。
匿名内部类
之前提到内部类是类内部定义的另外的类,而匿名内部类是没有名字的内部类,其必须在抽象类或接口基础上才可以定义。
interface A {
public void fun();
}
public class Demo {
public static void main(String args[]) {
foo(new A() {
public void fun() {
System.out.println("fun A");
}
});
}
public static void foo(A tmp) {
tmp.fun();
}
}
结果为:
fun A
上面的代码中,foo方法的参数是要接收类A对象作为参数,而实际传入的是一段代码,而这段代码就表示匿名内部类,该内部类的写法好像是数组的初始化一样(int tmp[]{1,2,3}),不过其内部是覆写了接口中的抽象方法。