java中的多态与C++中的类似。
一、向上转型及向下转型
1、向上转型
向上转型指的是将子类对象赋给父类引用(父类引用引用子类对象),例如实现:
class Animal{
protected String name;
public Animal(String name){
this.name = name;
}
public void eat(){
System.out.println(name+"eat");
}
public void sleep(){
System.out.println(name+"sleep");
}
}
class Dog extends Animal{
public Dog(String name){
super(name);
}
public void func(){
System.out.println(this.name + "在游戏");
}
}
class Cat extends Animal{
public Cat(String name){
super(name);
}
public void func(){
System.out.println(this.name + "在猫砂中");
}
}
public class TestDemo1 {
public static void main(String[] args) {
Dog dog = new Dog("哈士奇");
Animal dog2 = dog;
dog2.eat();
dog2.sleep();
dog2.func();//无法访问子类的方法或者属性,只能访问父类自己的方法或者属性
}
}
当然也可以直接:
Animal dog = new Dog("哈士奇");
dog.sleep();
dog.eat();
注意:向上转型之后,通过父类的引用只能访问父类自己的方法或者属性,因此dog2.func()是无法实现的,会抛异常。
向上转型发生的时机:
(1)直接赋值:上面已经示范
(2)传参,例如:
class Animal{
protected String name;
public Animal(String name){
this.name = name;
}
public void eat(){
System.out.println(name+"eat");
}
public void sleep(){
System.out.println(name+"sleep");
}
}
class Dog extends Animal{
public Dog(String name){
super(name);
}
public void func(){
System.out.println(this.name + "在游戏");
}
}
class Cat extends Animal{
public Cat(String name){
super(name);
}
public void func(){
System.out.println(this.name + "在猫砂中");
}
}
public class TestDemo1 {
public static void func(Animal animal){
animal.eat();
animal.sleep();
}
public static void main(String[] args) {
Animal dog = new Dog("哈士奇");
func(dog);
}
}
结果为:
(3)返回值,例如:
class Animal{
protected String name;
public Animal(String name){
this.name = name;
}
public void eat(){
System.out.println(name+"eat");
}
public void sleep(){
System.out.println(name+"sleep");
}
}
class Dog extends Animal{
public Dog(String name){
super(name);
}
public void func(){
System.out.println(this.name + "在游戏");
}
}
class Cat extends Animal{
public Cat(String name){
super(name);
}
public void func(){
System.out.println(this.name + "在猫砂中");
}
}
public class TestDemo1 {
public static Animal func(){
Dog dog = new Dog("哈士奇");
return dog;
}
public static void main(String[] args) {
Animal animal = func();
animal.eat();
animal.sleep();
}
}
结果也为:
2、向下转型
向下转型就是父类对象转为子类对象,上面向上转型是子类对象转为父类对象,那么父类引用是没有办法访问子类的方法和属性的,我们如果想要dog访问dog类的func方法,可以使用向下转型,例如:
class Animal{
protected String name;
public Animal(String name){
this.name = name;
}
public void eat(){
System.out.println(name+"eat");
}
public void sleep(){
System.out.println(name+"sleep");
}
}
class Dog extends Animal{
public Dog(String name){
super(name);
}
public void func(){
System.out.println(this.name + "在游戏");
}
}
class Cat extends Animal{
public Cat(String name){
super(name);
}
}
public class TestDemo1 {
public static Animal func(){
Dog dog = new Dog("哈士奇");
return dog;
}
public static void main(String[] args) {
Animal animal = new Dog("哈士奇");
Dog dog = (Dog)animal;
dog.func();
}
}
结果为:
但是向下转型是非常不安全的,例如:
public static void main(String[] args) {
Animal animal = new Dog("哈士奇");
Cat cat = (Cat)animal;
cat.func();
}
运行就会抛异常:
因为animal本质上引用的是一个Dog对象,但是却将其转为
二、多态(运行时绑定或者动态绑定)
1、重写
(1)方法名相同
(2)返回值相同
(3)参数列表相同
(4)不同的类—>继承关系
例如:
class Animal{
protected String name;
public Animal(String name){
this.name = name;
}
public void eat(){
System.out.println("Animal():eat()");
}
public void sleep(){
System.out.println("Animal():sleep()");
}
}
class Dog extends Animal{
public Dog(String name){
super(name);
}
public void eat(){
System.out.println("Dog():eat()");
}
}
class Cat extends Animal{
public Cat(String name){
super(name);
}
public void sleep(){
System.out.println("Cat():sleep()");
}
}
public class TestDemo1 {
public static void main(String[] args) {
Animal dog = new Dog("哈士奇");
dog.eat();
Animal cat = new Cat("喵喵");
cat.sleep();
}
}
我们想要调用Dog自己的eat方法和Cat自己的sleep方法,此时在Dog类中对Animal类的eat方法进行重写,运行结果为:
这种情况就是多态,体现在父类的引用引用子类的对象,同时通过父类引用调用同名的覆盖方法,就会发生运行时绑定,也就是动态绑定(多态)。
重写的注意事项:
(1)需要重写的方法不能被final修饰,因为final修饰的方法密封方法,不可以修改。
(2)被重写的方法访问修饰限定符不能是private的(原因在继承篇已经讲过)。
(3)被重写的方法子类的访问修饰限定符要大于父类的访问修饰限定符。
(4)static方法不能被重写。
注意:
在构造方法中调用重写方法,例如:
class B{
public B(){
func();
}
public void func(){
System.out.println("B::func()");
}
}
class D extends B{
private int count = 1;
@Override
public void func() {
System.out.println("D::func()" + count);
}
}
public class TestDemo2 {
public static void main(String[] args) {
D d = new D();
}
}
父类B中的构造方法调用了func()方法,此时的运行结果为:
发现调用了子类D重写的func()方法,且count为0,这是因为在创建子类D的对象时,会调用父类B的构造方法,可以发现父类B的构造方法调用的是子类重写的func()方法,但是此时D对象本身还没有构造,因此count还没有初始化,因此为0。
总结:
在继承的条件下,子类重写父类的同名方法,父类的引用引用子类的对象,调用同名方法调用的是子类的同名方法。
使用多态的好处是:
(1)类调用者对类的使用成本降低,能够让类的调用者不用知道这个类的类型,只需要知道这个对象具有某个方法;
(2)降低代码的圈复杂度,避免使用大量的if-else,例如实现代码:
class Shape{
public void draw(){
}
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("○");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("□");
}
}
class Flower extends Shape {
@Override
public void draw() {
System.out.println("♣");
}
}
public class TestDemo {
public static void drawMap(Shape shape){
shape.draw();
}
public static void main(String[] args) {
Shape shape1 = new Flower();
Shape shape2 = new Cycle();
Shape shape3 = new Rect();
//多态作为参数的情况
drawMap(shape1);
drawMap(shape2);
drawMap(shape3);
}
}
代码实现打印多个图形的功能,使用多态避免了使用多个if-else语句,降低了圈复杂度。
(3)可扩展能力更强,例如上述代码如果我们要多增加一种新的形状,使用多态只需要添加一个子类,对draw方法进行重写即可,代码改动成本比较低。
三、抽象类
在多态总结中,打印多个图形的例子,父类Shape的draw方法好都是由它的子类的draw方法好像并没有做什么实际的工作,打印图形主要是由它的子类的draw方法来完成的,对于这种没有实际工作的方法,可以将其设计为一个抽象方法,包含抽象方法的类称为抽象类,即:
abstract class Shape{
abstract public void draw();
}
在draw方法前加上abstract关键字表示它是一个抽象方法,抽象方法没有方法体({}),包含抽象方法的类,必须加上abstract关键字,表示它是一个抽象类。
注意:
(1)抽象类不能实例化,例如:
Shape shape = new Shape();
显示编译出错,提醒Shape是抽象的,无法实例化。
(2)抽象方法不能是private的,例如:
abstract class Shape{
abstract private void draw();
}
显示编译出错,提醒非法的修饰符组合:abstr和private;
(3)抽象类主要是用来被继承的:注意如果一个类继承了抽象类,这个类中一定要实现抽象类中的抽象方法,例如:
发现Rect类继承了抽象类Shape,但是出现了错误,因为没有重写Shape类中的抽象方法draw,此时应该为:
abstract class Shape {
abstract public void draw();
public int age;//定义字段
//定义方法
void func() {
System.out.println("func");
}
}
class Rect extends Shape {
public void draw(){
System.out.println("□");
}
}
public class TestDemo {
public static void main(String[] args) {
Shape shape = new Rect();
shape.func();
}
}
此时的运行结果就是:
抽象类的最大意义就是为了被继承,它本身不能被实例化,要想使用,只能创建该抽象类的子类,然后让子类重写抽象类中的抽象方法,它相当于多了一重编译器的检验,实际工作不应该由父类完成,而应由子类完成,如果此时不小心误用成父类了,使用普通类编译器不会报错,但是父类是抽象类就会在实例化的时候提示错误,尽早发现问题。
因此抽象类或者抽象方法一定不能被final修饰。
四、接口
接口是抽象类的更进一步,抽象类中可以包含非抽象方法和字段,而接口中包含的方法都是抽象方法,字段只能包含静态常量。
例如在打印图形的例子中,父类Shape没有包含其他非抽象方法,可以设计为一个接口,例如:
interface Shape {
//省略abstract和public
void draw();
}
class Rect implements Shape {
public void draw(){
System.out.println("□");
}
}
public class TestDemo {
public static void main(String[] args) {
Shape shape = new Rect();
shape.draw();
}
}
(1)使用interface定义一个接口;
(2)接口中的方法一定是抽象方法,因此可以省略abstract;
(3)接口中的方法一定是public,因此可以省略public;
(4)子类Rect使用implements继承接口,此时表达的含义是“实现”而不是“扩展”;
(5)在调用时同样可以创建一个接口的引用,对应一个子类的实例;
(6)接口不能被单独实例化
(7)接口中其实可以有具体实现的方法,这个方法被default修饰(jdk1.8中加入的);
(8)接口中的成员变量一定是静态常量,即:
interface Shape {
void draw();
public static final int num = 0;//静态常量
}
注意:
这个出现了错误,发现Rect中的draw()无法实现Shape中的draw(),正在尝试分配更低的访问权限; 以前为public,也就是说,子类中重写的抽象方法draw必须是public的,即:
(9)接口是为了解决单继承的问题出现的,有时候需要一个类同时集成自多个父类,java中只支持单继承,一个类只能extends一个父类,但是可以同时实现多个接口,达到多继承类似的作用,例如实现一个动物类:
class Animal{
protected String name;
public Animal(String name){
this.name = name;
}
}
//会飞的
interface IFlying{
void Fly();
}
//会跑的
interface IRunning{
void Run();
}
//会在水里游的
interface ISwimming{
void Swim();
}
//创建几个具体的动物
//狗是会跑的
class Dog extends Animal implements IRunning{
public Dog(String name){
super(name);
}
@Override
public void Run() {
System.out.println(this.name + "Run()");
}
}
//鸟是会飞的
class Bird extends Animal implements IFlying{
public Bird(String name){
super(name);
}
@Override
public void Fly() {
System.out.println(this.name +"Fly()");
}
}
//鱼会游
class Fish extends Animal implements ISwimming{
public Fish(String name){
super(name);
}
@Override
public void Swim() {
System.out.println(this.name +"Swim()");
}
}
//青蛙是会跑也会在水里游的
class Frog extends Animal implements IRunning,ISwimming{
public Frog(String name){
super(name);
}
@Override
public void Run() {
System.out.println(this.name +"Run()");
}
@Override
public void Swim() {
System.out.println(this.name +"Swim()");
}
}
//鸭子既会跑,也会飞,也会游
class Buck extends Animal implements IRunning,IFlying,ISwimming{
Buck(String name){
super(name);
}
@Override
public void Run() {
System.out.println(this.name +"Run()");
}
@Override
public void Fly() {
System.out.println(this.name +"Fly()");
}
@Override
public void Swim() {
System.out.println(this.name +"Swim()");
}
}
public class TestDemo {
public static void main(String[] args) {
IRunning animal1 = new Dog("wangwang");
animal1.Run();
IFlying animal2 = new Bird("haha");
animal2.Fly();
ISwimming animal3 = new Fish("Tom");
animal3.Swim();
IRunning animal4 = new Frog("Jerri");
animal4.Run();
ISwimming animal5 = new Frog("Jim");
animal5.Swim();
ISwimming animal6 = new Buck("gaga");
animal6.Swim();
IRunning animal7 = new Buck("lala");
animal7.Run();
IFlying animal8 = new Buck("jiejie");
animal8.Fly();
}
}
它的编译运行结果为:
也就是一个类继承一个父类,同时实现多种接口,继承表达的含义时is-a语义,而接口表达的含义时含有某种特性,上述表达的就是:
狗是一种动物,具有会跑的特性;
青蛙是一种动物,既可以跑,也可以游;
鸭子是一种动物,既能跑,也能飞,也可以游;
这样的设计就是体现多态的好处,让程序员不用关心类型,有了接口之后,类使用者不用关心具体类型,只需要关心某个类是都具有某种能力,例如实现一个方法:
public class TestDemo {
public static void Walk(IRunning irunning){
System.out.println("走路");
irunning.Run();
}
public static void main(String[] args) {
IRunning animal1 = new Dog("wangwang");
/*animal1.Run();*/
Walk(animal1);
}
}
在这个walk方法内部,并不关注是哪种动物,只要参数是会跑的就可以,甚至参数不是动物,例如:
class Robot implements IRunning{
private String name;
public Robot(String name){
this.name = name;
}
@Override
public void Run() {
System.out.println(this.name + "正在用轮子跑");
}
}
public class TestDemo {
public static void Walk(IRunning animal){
System.out.println("走路");
animal.Run();
}
public static void main(String[] args) {
/*IRunning animal1 = new Dog("wangwang");
*//*animal1.Run();*//*
Walk(animal1);*/
IRunning robot = new Robot("celin");
Walk(robot);
}
}
此时运行结果为:
Comparable接口
例如实现一个学生类,创建一个对象数组,对学生的成绩进行排序:
class Student{
private String name;
private int score;
public Student (String name,int score){
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
}
public class TestDemo {
public static void main(String[] args) {
//创建一个对象数组
Student[] students = new Student[] {
new Student("张三", 90),
new Student("李四", 86),
new Student("王五", 91),
new Student("赵六", 50),
};
Arrays.sort(students);
System.out.println(Arrays.toString(students));
}
}
此时运行就会抛异常,可见使用现成的sort方法是不可以的,比较对象的大小需要额外来指定,我们的Student类来实现Comparable接口,并实现其中的compareTo方法,例如:
class Student implements Comparable{
private String name;
private int score;
public Student (String name,int score){
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
@Override
public int compareTo(Object o) {
Student s = (Student) o;
if(this.score > s.score){
return -1;
} else if(this.score < s.score){
return 1;
} else {
return 0;
}
}
}
public class TestDemo {
public static void main(String[] args) {
//创建一个对象数组
Student[] students = new Student[] {
new Student("张三", 90),
new Student("李四", 86),
new Student("王五", 91),
new Student("赵六", 50),
};
Arrays.sort(students);
System.out.println(Arrays.toString(students));
}
}
注意此时我们如果Student类实现Comparable< Student> 接口,此时的compareTo的参数就是Student类型的。
sort方法中会自动调用compareTo方法,它的参数是Object,其实传入的是Student对象,compareTo方法的对象大小关系为:
如果当前对象应排在参数对象之前, 返回小于 0 的数字;
如果当前对象应排在参数对象之后, 返回大于 0 的数字;
如果当前对象和参数对象不分先后, 返回0
此时的运行结果就是:
注意对于sort方法来讲,传入的对象都要是可比较的,要具有compareTo能力。
接口间的继承
接口可以继承一个接口,达到复用的作用,例如:
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {
}
class Frog implements IAmphibious {
private String name;
public Frog(String name){
this.name = name;
}
@Override
public void run(){
System.out.println(this.name + "正在跑");
}
@Override
public void swim() {
System.out.println(this.name + "正在游");
}
}
public class TestDemo {
public static void main(String[] args) {
IRunning frog1 = new Frog("haha");
frog1.run();
ISwimming frog2 = new Frog("heihei");
frog2.swim();
}
}
此时的运行结果就是:
接口间的继承就相当于将多个接口合并在一起。
Clonable接口
Object类中存在一个clone方法,调用这个方法可以创建一个对象的拷贝,要合法使用clone接口,就要先实现Clonable接口,否则会跑出CloneNotSupportedException异常,例如实现一个Person类,要进行克隆这个人,实现:
class Person implements Cloneable{
private String name;
public Person clone(){
Person p = null;
try{
p = (Person)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return p;
}
}
public class TestDemo {
public static void main(String[] args) {
Person p1 = new Person();
Person p2 = p1.clone();
System.out.println(p1==p2);
}
}
此时运行结果是false,当然我们可以直接写成:
class Person implements Cloneable{
private String name;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class TestDemo {
public static void main(String[] args) throws CloneNotSupportedException{
Person p1 = new Person();
Person p2 = (Person)p1.clone();//一定要转为Person类型,因此此时实现的clone方法返回的是Object类型
System.out.println(p1.age);
System.out.println(p2.age);
//修改
System.out.println("修改后");
p2.age = 12;
System.out.println(p1.age);
System.out.println(p2.age);
}
}
此时运行结果是:
可以发现p2是p1的克隆,改变p2的age并不会改变p1的age,也就是说通过 clone 拷贝出的 p2对象只是拷贝了 p1自身, 而没有拷贝p1内部包含的对象,也就是clone方法是一种浅拷贝。
又比如:
class Money{
double money = 12.6;
}
class Person implements Cloneable{
protected int age;
Money m = new Money();//创建一个Money对象
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class TestDemo {
public static void main(String[] args) throws CloneNotSupportedException{
Person p1 = new Person();
Person p2 = (Person)p1.clone();
System.out.println(p1.m.money);
System.out.println(p2.m.money);
//修改
System.out.println("修改后");
p2.m.money = 99.9;
System.out.println(p1.m.money);
System.out.println(p2.m.money);
}
}
此时的运行结果为:
可以发现p2克隆p1,对p2的m引用指向的money进行改变,p1的m引用指向的money也改变了,因为在Person类中创建m引用,创建p1对象,p2对象克隆p1,也会克隆p1中的m引用,p2中的m引用与p1中的m引用指向的内容是同一个,因此改变p2中m引用指向的money,p1中m引用指向的money也会改变。
如果也想将money实现浅拷贝,也就是改变p2中m引用指向的money不会改变p1中m引用指向的money,此时可以这样实现:
class Money implements Cloneable{
double money = 12.6;
//要将money的内容也克隆一份,首先先在Money实现克隆
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person implements Cloneable{
protected int age;
Money m = new Money();//创建一个Money对象
@Override
protected Object clone() throws CloneNotSupportedException {
//这里实现先将p指向的内容克隆一份,然后将当前p对象指向的m引用的money克隆一份
Person p = (Person) super.clone();
p.m = (Money) this.m.clone();
return p;
}
}
public class TestDemo {
public static void main(String[] args) throws CloneNotSupportedException{
Person p1 = new Person();
Person p2 = (Person)p1.clone();
System.out.println(p1.m.money);
System.out.println(p2.m.money);
//修改
System.out.println("修改后");
p2.m.money = 99.9;
System.out.println(p1.m.money);
System.out.println(p2.m.money);
}
}
此时运行结果就达到了我们想要的效果,如图:
改变p2的m引用指向的money不会改变p1的m引用指向的money,实现了m对象的浅拷贝。
总结:
抽象类和接口的区别:
核心区别:是:抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法。