目录
前言:
多态:
多态的定义:
向上转型:
方法重写:
再看toString方法:
动态绑定:
向下转型:
小练习:
抽象类:
什么是抽象类?
抽象方法:
抽象类:
抽象类的使用:
小总结:
接口:
接口是什么?
接口中的方法修饰符:
接口中的成员修饰符:
接口的使用:
接口的定义格式:
接口中的代码块使用:
类使用多个接口:
接口的继承:
Comparable接口:
小练习一:
小练习二:
小总结:
克隆:
浅拷贝:
深拷贝:
经过之前的学习,我们都已经了解了什么是继承和封装,那么今天我们就来学习面向对象的最后一个特性,多态。
当然,了解了多态也就需要知道和它相关的知识,抽象类和接口。
多态:多种形态,去完成某个行为,不同对象去完成时产生不同形态。有一种看人说话的感觉,都是说同一种事物,但是根据不同的对象说话的方式不同。
要想实现多态,需要满足几个条件:
1.继承关系上:向上转型
2.子类和父类有同名 覆盖/重写 方法
3.通过父类对象的引用,去调用这个重写的方法。我们我们来逐一介绍。
我们先来看一段代码:
class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(this.name + "正在吃饭");
}
}
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
public void bark() {
System.out.println(this.name + "汪汪叫");
}
}
public class New {
public static void main(String[] args) {
Dog a = new Dog("hehe",12);
a.eat();
a.bark();
System.out.println("=========");
Animal lala = new Animal("lala", 19);
lala.eat();
lala.bark();//这是子类特有方法,只能调用父类自己特有的成员方法或者成员变量
}
}
这里我们用Dog类继承了Animal类,我们定义了一个Animal类的lala对象,去调用了Dog类中的特定的bark方法,因为bark是Dog的方法,毫无疑问,会报错。
之前说过,多态涉及3个定义,此时我们就来讲解向上转型和向下转型。
Dog gege = new Dog("gege", 10);
Animal animal = gege;
//animal这个引用对象 指向了 dog 这个引用对象
我们把gege这个Dog类的对象转换为其父类的类型,此时就发生了向上转型。我们将这两句代码合并:
Animal animal = new Dog("gege",11);
可以看到,先去调用了Dog的构造方法。 向上转型有三个情况:
此时就完成了向上转型。接下来就要讲解方法重写和动态绑定。
还是否记得我们之前学到的方法重载?对没错,它和方法重写是两个概念。我们先来讲解方法重写,之后再给出区别。
此时我们在Dog的类里面写入eat方法(Animal中也有eat方法),并让发生向上转型对象调用。
class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(this.name + "正在吃饭");
}
}
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
public void bark() {
System.out.println(this.name + "汪汪叫");
}
public void eat() {
System.out.println(this.name + "正在吃狗粮");
}
}
public class New {
public static void main(String[] args) {
Animal animal = new Dog("lele", 10);
animal.eat();
}
}
重写也称为覆盖。是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写,返回值和形参都不能改变。
tips:我们进行方法重写,IDE编译器会出现一个图标来表明发生了方法重写:
为了更好的的区分方法进行重写,我们一般重写方法时,都会在上面加上@Override.
@Override//这个注解即发生了方法重写 public void eat() { System.out.println(this.name + "正在吃狗粮"); }
在Java中,有一种术语叫做注解,比如@Override就是其中的一种,起到提示的作用(有点像C的assert函数)。
这里也需要注意他们的权限,被重写的方法权限修饰符必须大于等于继承的方法。
实现重写:
toString方法是Object类中的方法,因为Java中所有的类都默认继承于Object类,所以当Dog类中没有写toString方法,就会调用Object的toString方法。所以我们每次调用时toString时都会发生向上转型。这就是方法重写。
对于已经投入使用的类,尽量不要进行修改。最好的方式是:当重新定义了一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。
此时在通过父类的引用进行调用的时候,是调用子类的方法,把这个过程就叫做 动态绑定。
方法的重载就是静态绑定;方法的重写就是动态绑定。
我们执行进行方法重写的文件(我上方的代码,最长的那个),之后我们看编译时是如何发生动态绑定的。
我们直接在IDEA右击,之后点击explorer就会打开当前文件的目录,之后回退到out目录,之后点进去(用手机看的可以不用操作,不用纠结)。找到编辑的类生成的字节码文件(后缀为.class),之后在当前目录中输入cmd打开控制台。之后再控制台中输入javap -c 文件名:
我们可以看到在编译时,调用的方法确实是调用的Animal的eat方法,但是运行期间调用的是Dog的eat方法。这就叫做动态绑定,是在运行时绑定;而静态绑定在编译的时候就确定调用谁。
静态绑定:也称前期绑定(早绑定)。
动态绑定:也称后期绑定(晚绑定)。需要在程序运行时,才能确定具体调用哪个类的方法。
当父类引用,引用的子类对象不一样的时候,调用这个重写的方法,所表现出来的行为不一样时,我们把这种思想就叫做多态。
向上转型的优点是让代码更加灵活;缺点是不能调用子类特有的方法。
此时再多定义一个Cat类,并写入方法。
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
public void miaomiao() {
System.out.println(this.name + "喵喵叫");
}
}
此时是类型转换异常,所以向下转换是非常不安全的,此时只能这样运行:
Animal animal = new Dog("yuanyuan", 10);
if (animal instanceof Cat) {
Cat cat = (Cat)animal;
}else {
System.out.println("理解了!");
}
instanceof是判断是否是其子类(包括该类),所以我们判断一下即可。
我们来看一个代码:
class B {
public B() {
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
//private int num = 1;
@Override
public void func() {
System.out.println("D.func()");
}
}
public class Again {
public static void main(String[] args) {
D d = new D();
}
}
这里先去调用父类的构造方法,父类中调用了它的方法,但是子类中有相同的方法,因为动态绑定的关系,所以会去调用子类重写方法。
此时我们再打印num的值(放开那条注释):
public void func() {
System.out.println("D.func() " + num);
}
会发现并不是1,而是0。此时子类中有该变量,但是还没来得及赋值为1。
顾名思义,就是抽象的类。哈哈,确实,但是也确实抽象。再讲抽象类之前,我们还是先来看代码。此时我们来打印一些图形,在不使用多态的情况下:
class Shape {
public void draw() {
System.out.println("画一个图形:");
}
}
class Rect extends Shape {
public void draw() {
System.out.println("□!");
}
}
class Triangle extends Shape {
public void draw() {
System.out.println("△!");
}
}
class Cycle extends Shape {
public void draw() {
System.out.println("○!");
}
}
public class Test {
public static void main(String[] args) {
Cycle cycle = new Cycle();
Rect rect = new Rect();
Triangle triangle = new Triangle();
String[] strings = {"cycle","rect","cycle","rect","triangle"};
for (String x : strings) {
if (x.equals("cycle")) {
cycle.draw();
}else if (x.equals("rect")) {
rect.draw();
}else if (x.equals("triangle")) {
triangle.draw();
}
}
}
}
这样写就是不知道多态才会这样写,如果利用多态来写,就可以省去很多代码:
public class Test {
public static void main(String[] args) {
/*Shape shape1 = new Cycle();
Shape shape2 = new Triangle();
Shape shape3 = new Rect();*/
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), new Rect(), new Triangle()};
for (Shape shape : shapes) {
shape.draw();
}
}
}
我们发现父类中的draw方法有些累赘,但是又不能不写,因为是其他形状继承的,否则完成不了多态。所以就有了抽象方法。
因为父类中的方法是被重写的,不会被调用,所以我们我们可以什么都不写:
class Shape {
public void draw() {
}
}
所以这不是一个方法,所以只能是抽象方法,使用抽象关键字abstract修饰。
但是依旧报错,如果此方法为抽象方法,那么这个类也必须是抽象类。
抽象方法必须在抽象类中使用,此时我们使用了抽象方法,所以要把类描述为抽象类:
abstract class Shape {
public abstract void draw();
}
因为所有对象都是通过类来描述的,但是反过来,并不是所有类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个对象,这样的类就是抽象类。
因为抽象类中缺少一些关键信息,所以抽象类是不可以实例化的。
抽象类当中,可以和普通类一样,定义成员变量和成员方法。
abstract class Shape {
public String name;
public abstract void draw();
}
既然把方法抽象就要声明这个类是抽象类,那么抽象类肯定是继承时才会使用。此时直接继承会报错:
必须实现抽象类中的抽象方法才可以被继承。
abstract class Shape {
public String name;
public abstract void draw();
}
class Flower extends Shape {
@Override
public void draw() {
System.out.println("❀!");
}
}
所以抽象类的出现本身就是为了被继承。而且抽象方法和成员不能被final修饰(书写顺序无所谓)。
public static void drawMap(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
Shape shape = new Cycle();
//因为抽象类不能实例化,所以只能向上转型
drawMap(shape);
}
抽象类也可以被继承,可以被抽象类继承,也可以被正常的类继承,但是不是 抽象类 继承的抽象类必须实现抽象类中的实例方法(就是正常的类继承了抽象类,就必须实现之前所有抽象类中的方法)。
所以出来混,迟早还是要还的。
抽象类也是类,内部可以包含普通的方法和属性,甚至构造方法。
抽象类不能被private修饰;抽象方法不能被final和static修饰,因为抽象方法要被子类重写。因为可以通过子类来调用父类的构造方法。
一个类只能继承一个抽象类。
实际生活中,接口就是设备上的(en……编不出来了)。接口属于一种标准,使用时只要符合规范,就可以使用。
Java中接口可以看成多个类的公共规范,是一种引用数据类型。
接口是使用interface方法来修饰的,接口当中不能有被实现的方法,意味着只能有抽象方法:
但是两个方法除外(JDK8引入的):一个是被static修饰的方法,一个是被default修饰的方法。否则只是普通的抽象方法。
之后我们来观察接口中的方法修饰符:
所以接口中的方法默认都是public abstract修饰的,即使前面没写,也是默认加上的。
接口中的成员都是 public static final 修饰的,即使前面没写,也是默认加上的,也意味着它是常量。
也就是说,接口是对抽象类的抽象,抽象的抽象更不能实例化。
说了这么多,那么接口到底是如何使用的?比抽象还抽象。
类和接口之间的关系,可以使用implements来进行关联。
但是此时为什么报错,是因为接口中的方法没有被重写,和抽象类类似。
所以此时我们再来实现之前的打印形状,就可以不使用抽象类了,可以直接使用接口了。
interface S {
/*public int a = 1;
public static int b = 2;
public static final int c =3;*/
void draw();
}
class R implements S {
@Override
public void draw() {
System.out.println("矩形!");
}
}
class F implements S {
@Override
public void draw() {
System.out.println("❀!");
}
}
public class Test3 {
public static void func(S shape)
{
shape.draw();
}
public static void main(String[] args) {
S shape1 = new R();
S shape2 = new F();
S[] shapes = {shape1, shape2};
for (S sh : shapes) {
func(sh);
}
}
}
既然接口无法进行实例化,那么我们就可以利用向上转型和动态绑定,通过多态的方法对其进行使用。
接口的定义格式与定义类的格式基本相同,将class关键字换成了interface关键字,就定义了一个接口。
命名规则即规范:创建接口时,接口命名一般使用大写字母I开头。接口命名一般使用“形容词”词性单词。接口中的方法和属性不要添加任何修饰符号(因为默认也会有),保持代码简洁性。
还是和之前一样,接口中的方法,都是要重写的,因为出来混都是要还的。
我们可以在一个类中使用多个接口。当我们在一个类中要使用多个接口时,可以使用“,”隔开。
abstract class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
interface IFly {
void fly();
}
interface IRun {
void Run();
}
interface ISwim {
void Swim();
}
class Dog extends Animal implements IRun {
public Dog(String name, int age){
super(name, age);
}
@Override
public void Run() {
System.out.println(this.name + "正在跑!");
}
}
class Frog extends Animal implements ISwim,IRun {
public Frog(String name, int age) {
super(name, age);
}
@Override
public void Swim() {
System.out.println(this.name + "蛙泳");
}
@Override
public void Run() {
System.out.println(this.name + "跳跳");
}
}
class Duck extends Animal implements ISwim,IRun,IFly {
public Duck(String name, int age) {
super(name, age);
}
@Override
public void fly() {
System.out.println(this.name + "在飞");
}
@Override
public void Run() {
System.out.println(this.name + "双脚跑");
}
@Override
public void Swim() {
System.out.println(this.name + "鸭泳");
}
}
public class Test {
public static void running(IRun iRun) {
iRun.Run();
}
public static void flying(IFly iFly) {
iFly.fly();
}
public static void main(String[] args) {
running(new Dog("二狗",11));
flying(new Duck("唐老鸭",10));
}
}
接口之间,也可以实现继承。如果一个类使用的是继承的接口,则需要重写父类接口和子类接口的所有方法。
interface A {
void testA();
}
interface B extends A {
void testB();
}
class TestDemo1 implements B {
@Override
public void testA() {
}
@Override
public void testB() {
}
}
所以还是那句话,出来混还是要还的。
我们如果对一个类进行比较时,需要明确类型。否则就会报错。
class Student {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Test {
public static void main(String[] args) {
Student student1 = new Student("zhangsan", 10);
Student student2 = new Student("lisi", 11);
System.out.println(student1 > student2);
//报错,因为没有指定类型
}
}
为了实现比较,我们需要使用接口。我们导入一个包,之后利用这个接口(Comparable)进行比较。
我们点进去这个接口并看里面的内容:
这里面有一个<>,代表泛型的意思,泛型我们以后再了解。当前代表我们要比较的类型,此时我们传入Student.
class Student implements Comparable
因为接口中有一个抽象方法,所以我们使用这个接口要去重写这个compareTo方法。
public int compareTo(Student o) {
if (this.age > o.age) {
return 1;
}else if (this.age == this.age) {
return 0;
}else {
return -1;
}
}
//主方法调用
System.out.println(student1.compareTo(student2));
我们目前重写了该方法,但是有弊端,此时再次调用该方法只能比较年龄了。所以我们可以改进。但是我们要根据姓名进行比较时就没办法了。
此时我们可以进行改进,我们可以使用比较器Comparator这个接口:
其实就是实现一个类,之后重写compare方法,之后创建这个类的对象之后调用方法。
class AgeCompare implements Comparator {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
public static void main(String[] args) {
Student student1 = new Student("zhangsan", 10);
Student student2 = new Student("lisi", 11);
AgeCompare ageCompare = new AgeCompare();
System.out.println(ageCompare.compare(student1, student2));
}
此时我们再比较姓名。创建一个类(比较器),之后实现compare方法,因为是字符串比较,所以用调用compareTo方法。
class NameCompare implements Comparator {
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);
}
}
NameCompare nameCompare = new NameCompare();
System.out.println(nameCompare.compare(student1, student2));
此时是String类型调用的compareTo方法,所以我们先进入该方法。
我们可以发现,String类实现了Comparable接口,所以是String类重写了compareTo方法。
此时两种方法都可以写,但是可以根据实际情况来修改,利用比较器灵活性更强。
我们定义一个接口,这个接口时USB,它里面有打开设备方法和关闭方法,之后定义三个类,分别是Computer类和Mouse类和KeyBoard类,
package demo2;
public class Test {
public static void main(String[] args) {
Computer computer1 = new Computer();
Mouse mouse = new Mouse();
computer1.useService(mouse);
System.out.println("==========");
KeyBoard keyBoard = new KeyBoard();
computer1.useService(keyBoard);
}
}
package demo2;
public class Computer {
public void powerOn() {
System.out.println("打开电脑");
}
public void powerOff() {
System.out.println("关闭电脑");
}
public void useService(USB usb) {
usb.openDevice();//先打开
if (usb instanceof Mouse) {
Mouse mouse = (Mouse)usb;
mouse.click();
}else if (usb instanceof KeyBoard) {
KeyBoard keyBoard = (KeyBoard)usb;
keyBoard.flap();
}
usb.closeDevice();//后关闭
}
}
public class Mouse implements USB{
@Override
public void openDevice() {
System.out.println("鼠标开始工作");
}
public void click() {
System.out.println("疯狂点击鼠标……");
}
@Override
public void closeDevice() {
System.out.println("鼠标结束工作");
}
}
public class KeyBoard implements USB{
@Override
public void openDevice() {
System.out.println("插上键盘设备");
}
public void flap() {
System.out.println("疯狂敲击键盘……");
}
@Override
public void closeDevice() {
System.out.println("关闭键盘设备");
}
}
package demo2;
public interface USB {
void openDevice();
void closeDevice();
}
了解了Comparable接口后,我们再来对一组数据进行排序,也是一个类。
class Student {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Test {
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("zhangsan", 11);
students[1] = new Student("lisi", 5);
students[2] = new Student("wangwu", 7);
System.out.println("排序前:" + Arrays.toString(students));
Arrays.sort(students);
System.out.println("排序后:" + Arrays.toString(students));
}
}
执行此代码会发生错误,那么此时我们就点击这个报错,看是哪里出现了错误:
和之前举的例子一样,是因为Arrays.sort排序必须指定类型,而且发现了向上转型。Comparable是一个接口,但是此时我们的类和这个接口没有任何关系,所以我们要去使用这个接口。
使用接口以后,我们发现里面调用了compareTo方法,这个方法是接口的抽象方法,所以我们要重写一遍,并得知是排序的是哪个类型。
class Student implements Comparable {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Student o) {
return this.age - o.age;
}
}
两个特例,静态的方法直接通过接口名调用即可;default方法需要实例化一个对象,通过向上转型才能调用成功。
和重写抽象类方法一样,重写权限修饰符必须大于等于父类,但是由于接口的方法默认都是 public abstract static 修饰的,所以重写方法只能用public修饰。
接口相较于抽象类,可以更好的让其他类使用更加灵活。书写顺序不能错。
2个关系:
- 类和接口之间的关系 ->implements 实现
- 接口和接口之间的关系 -> extends 拓展
接口的修饰符可以为abstract,并且默认为其修饰。
克隆也是拷贝,一般编程中分为两种,深拷贝和浅拷贝。
Java中其实也有现成的拷贝方法,我们先来看代码:
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
此时我们有一个学生类,此时我们要创建两个对象,其中一个要去拷贝一个已经实例化的对象。
public class Test {
public static void main(String[] args) {
Person person1 = new Person("zhangsan",10);
//此时新创将一个对象,并将person1内容拷贝给新对象
Person person2 = person1;//此时需要调用克隆方法
}
}
此时就需要调用clone方法。我们要知道所有类都是继承与Object的类,所以我们搜索Object类。
我们可以看到Object类中有clone方法。但是 .(点) 不出来,这是因为protected的原因。
我们之前讲过,用protected修饰的成员或者方法,调用时就必须使用super。但是此时主方法是静态方法,不能使用this和super,所以要么再写一个方法,要么直接在子类中调用。
所以我们重写clone方法(因为默认继承Object类,这个类中有这个方法,所以进行重写),之后通过对象调用。此时我们在子类中重写clone方法。
但是可能会抛出异常,异常处理我们以后再学,就先按照提示输入即可。
还是报错,此时就需要向下转型。
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person("zhangsan",10);
//此时新创将一个对象,并将person1内容拷贝给新对象
Person person2 = (Person) person1.clone();//此时需要调用克隆方法
}
}
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person("zhangsan",10);
//此时新创将一个对象,并将person1内容拷贝给新对象
Person person2 = (Person) person1.clone();//此时需要调用克隆方法
System.out.println(person1);
System.out.println(person2);
}
}
这个方法确实重写了,但是当我们要实现克隆时,一定要使用一个Cloneable接口,才能实现克隆。
我们把它叫做空接口/标记接口,表明当前类是可以被克隆的。之前报错是因为不支持克隆,实现接口以后就可以克隆。
此时就完成了浅拷贝,那么接下来我们来了解深拷贝。
此时就需要用到继承来观察了。观察一下代码:
class Money {
public double m = 19.9;
}
class Person implements Cloneable {
public String name;
public int age;
public Money money = new Money();
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person("zhangsan",10);
//此时新创将一个对象,并将person1内容拷贝给新对象
Person person2 = (Person) person1.clone();//此时需要调用克隆方法
System.out.println(person1.money.m);
System.out.println(person2.money.m);
System.out.println("=============");
person1.money.m = 200;
System.out.println(person1.money.m);
System.out.println(person2.money.m);
}
}
注意这里我们只改变了了person1的值,如果拷贝的话不会影响person2的值。但是结果并不是想象中的。
这就是浅拷贝的弊端。对于嵌套的内容他们指向相同的地址。 此时我们就要来了解深拷贝了。 在Money里面也要使用克隆接口,拷贝时也要把父类拷贝进去(注意要先在Money实现Cloneable接口)。
class Money implements Cloneable {
//这里面也要使用克隆接口 和 重写克隆方法
public double m = 19.9;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
之后去Person类中拷贝Money。
@Override
protected Object clone() throws CloneNotSupportedException {
//为了实现深拷贝
Person tmp = (Person)super.clone();
//之后tmp中的money再拷贝一次
tmp.money = (Money)this.money.clone();
return tmp;
}
class Money implements Cloneable{
public double m = 19.9;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person implements Cloneable {
public String name;
public int age;
public Money money = new Money();
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Person tmp = (Person)super.clone();
tmp.money = (Money)this.money.clone();
return tmp;
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person("zhangsan",10);
//此时新创将一个对象,并将person1内容拷贝给新对象
Person person2 = (Person) person1.clone();//此时需要调用克隆方法
System.out.println(person1.money.m);
System.out.println(person2.money.m);
System.out.println("============");
person1.money.m = 200;
System.out.println(person1.money.m);
System.out.println(person2.money.m);
}
}
完结了这一篇,希望大佬点点赞。