文章目录
- Day09-面向对象-多态
- 学习目标
- 1对象数组
- 1.1 对象数组的声明和使用
- 1.2 对象数组的内存图分析
- 2. 多态
- 2.1 前提
- 2.2 多态的意义
- 2.3 多态的特点
- 2.3.1 成员方法的特点
- 2.3.2 成员变量的特点
- 2.4 引用数据类型转换
- 2.4.1 类型转换的意义
- 2.4.2 转型的异常
- 3. final关键字的使用
- 3.1 修饰类
- 3.2 修饰方法
- 3.3 修饰局部变量
- 3.4 修饰成员变量
Day09-面向对象-多态
学习目标
- 对象数组
- 能够说出final关键字的使用
- 能够说出什么是多态
- 能够说出多态的特点以及作用
- 能够说出引用数类型转换方式作用以及注意事项
- 能够使用instanceof关键字判断一个对象是否属于某一个类
1对象数组
数组是用来存储一组数据的容器,一组基本数据类型的数据可以用数组装,那么一组对象也可以使用数组来装。
即数组的元素可以是基本数据类型,也可以是引用数据类型。当元素是引用数据类型是,我们称为对象数组。
注意:对象数组,首先要创建数组对象本身,即确定数组的长度,然后再创建每一个元素对象,如果不创建,数组的元素的默认值就是null,所以很容易出现空指针异常NullPointerException。
1.1 对象数组的声明和使用
案例:
(1)定义矩形类,包含长、宽属性,area()求面积方法,perimeter()求周长方法,String getInfo()返回圆对象的详细信息的方法
(2)在测试类中创建长度为5的Rectangle[]数组,用来装3个矩形对象,并给3个矩形对象的长分别赋值为10,20,30,宽分别赋值为5,15,25,遍历输出
package com.atguigu.test08.array;
public class Rectangle {
double length;
double width;
double area(){//面积
return length * width;
}
double perimeter(){//周长
return 2 * (length + width);
}
String getInfo(){
return "长:" + length +
",宽:" + width +
",面积:" + area() +
",周长:" + perimeter();
}
}
package com.atguigu.test08.array;
public class ObjectArrayTest {
public static void main(String[] args) {
//声明并创建一个长度为3的矩形对象数组
Rectangle[] array = new Rectangle[3];
//创建3个矩形对象,并为对象的实例变量赋值,
//3个矩形对象的长分别是10,20,30
//3个矩形对象的宽分别是5,15,25
//调用矩形对象的getInfo()返回对象信息后输出
for (int i = 0; i < array.length; i++) {
//创建矩形对象
array[i] = new Rectangle();
//为矩形对象的成员变量赋值
array[i].length = (i+1) * 10;
array[i].width = (2*i+1) * 5;
//获取并输出对象对象的信息
System.out.println(array[i].getInfo());
}
}
}
1.2 对象数组的内存图分析
对象数组中数组元素存储的是元素对象的地址。
2. 多态
多态是继封装、继承之后,面向对象的第三大特性。
生活中,比如一条叫旺财的狗,我们可以说它是一条狗,也可以说它是一个动物;一个叫张三的男性,我们可以称它为男人,也可以称它为人,同时还能称呼他是一个动物。
多态: 就是指同一事物,具有多个不同表现形式。
2.1 前提
- 继承或者实现【二选一】
- 方法的重写【意义体现:不重写,无意义】
- 父类引用指向子类对象【格式体现】
class Test {
public static void main(String[] args) {
// 父类的引用 stu 指向了一个 new Student() 子类对象
Person stu = new Student();
stu.sleep(); // 子类重写了父类的方法,执行的是子类自己的方法
}
}
class Person {
void sleep() {
System.out.println("人正在睡觉");
}
}
class Student extends Person {
@Override
void sleep() {
System.out.println("学生正在教室里睡觉");
}
}
2.2 多态的意义
查看一下代码,思考这段代码是否可以进行优化:
class Test {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
func(dog);
func(cat);
}
private static void func(Dog dog) {
dog.eat();
}
private static void func(Cat cat) {
cat.eat();
}
}
class Dog {
public void eat() {
System.out.println("小狗正在啃骨头");
}
}
class Cat {
public void eat() {
System.out.println("小猫正在吃鱼");
}
}
上述代码中,根据不同的参数类型,我们定义了不同的func
方法,这个方法出现了重载。但是,我们从func
方法的内部实现上来看,这两个方法都是调用对象的eat
方法。
其实,在上述代码里,我们可以考虑给Dog
类和Cat
类定义一个父类,通过传递父类参数的形式,来简化代码。
实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。
class Test {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
func(dog);
func(cat);
}
private static void func(Animal animal) { // 参数只需要指定 Animal 父类即可
animal.eat();
}
}
class Animal{ // 定义一个 Animal 类,让Dog和Cat都继承自它
public void eat() {
System.out.println("动物正在吃东西");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("小狗正在啃骨头");
}
}
class Cat extends Animal {
public void eat() {
System.out.println("小猫正在吃鱼");
}
}
上述代码的简化,并不只是简单的替换,同时还是一种扩展。当我们再出现新类型(例如Pig类)的对象时,我们只需要让Pig类继承自Animal类,就可以直接把这个Pig类型的对象传给func
方法作为参数使用。这样就大大的提高了代码的灵活性,便于代码后期的扩展。
实际上,多态在我们程序中是大量存在的,因为有了多态的存在,才使得Java语言变得更加的灵活以扩展,实现了低耦合高内聚的编码思想。
2.3 多态的特点
父类引用指向子类对象的这种多态形式,在调用方法和使用属性时,有一些细节需要我们注意。
2.3.1 成员方法的特点
观察一下代码,思考代码的执行结果。
package com.atguigu.java;
public class Test {
public static void main(String[] args) {
Animal animal = new Dog();
animal.eat(); // 会调用 Dog 类的 eat 方法
// animal.watchDog(); 编译会报错
((Dog) animal).watchDog();
}
}
class Animal { // 定义一个 Animal 类,让Dog和Cat都继承自它
public void eat() {
System.out.println("动物正在吃东西");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("小狗正在啃骨头");
}
public void watchDog() {
System.out.println("小狗正在看家");
}
}
对于调用方法,记住一点编译看左边,运行看右边。
也就是说,在编译时要看等号左边声明的类型里是否有这个方法,如果有这个方法就编译通过,反之不通过。而在运行时,是看等号右边到底创建的是一个什么类型的对象,会直接调用这个对象具体的方法。
2.3.2 成员变量的特点
直接通过对象名称访问成员变量,等号左边是谁,优先用谁。
public class Test {
public static void main(String[] args) {
Animal animal = new Dog();
System.out.println(animal.type); // 动物
}
}
class Animal { // 定义一个 Animal 类,让Dog和Cat都继承自它
String type = "动物";
public void eat() {
System.out.println("动物正在吃东西");
}
}
class Dog extends Animal {
String type = "狗";
@Override
public void eat() {
System.out.println("小狗正在啃骨头");
}
}
2.4 引用数据类型转换
多态的转型分为向上转型与向下转型两种:
-
向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。当父类引用指向一个子类对象时,便是向上转型。
使用格式:
父类类型 变量名 = new 子类类型(); 如:Animal a = new Cat();
-
向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。
使用格式:
子类类型 变量名 = (子类类型) 父类变量名; 如:Cat c =(Cat) a;
2.4.1 类型转换的意义
调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做向下转型。
public class Test {
public static void main(String[] args) {
Animal dog = new Dog();
// dog.watchDoor(); 编译报错,调用方法时,编译时看左边的类型,是Animal,Animal类里没有 watchDoor方法
((Dog) dog).watchDoor();
}
}
class Animal {
String type = "动物";
}
class Dog extends Animal {
public void watchDoor() {
System.out.println("狗正在看门");
}
}
2.4.2 转型的异常
转型的过程中,一不小心就会遇到这样的问题,请看如下代码:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse 【运行报错】
}
}
这段代码可以通过编译,但是运行时,却报出了 ClassCastException
,类型转换异常!这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系,不符合类型转换的定义。
为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验,格式如下:
变量名 instanceof 数据类型
如果变量属于该数据类型,返回true。
如果变量不属于该数据类型,返回false。
所以,转换前,我们最好先做一个判断,代码如下:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
if (a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse(); // 调用的是 Cat 的 catchMouse
} else if (a instanceof Dog){
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse
}
}
}
3. final关键字的使用
学习了继承后,我们知道,子类可以在父类的基础上改写父类内容,比如,方法重写。那么我们能不能随意的继承API中提供的类,改写其内容呢?显然这是不合适的。为了避免这种随意改写的情况,Java提供了final
关键字,用于修饰不可改变内容。(最终的)
final: 不可改变。可以用于修饰类、方法和变量。
- 类:被修饰的类,不能被继承。
- 方法:被修饰的方法,不能被重写。
- 变量:被修饰的变量,不能被重新赋值。
3.1 修饰类
final class Person {}
// 报错,被 final 修饰的类不能被继承
// class Student extends Person {}
查询API发现像 public final class String
、public final class Math
、public final class Scanner
等,很多我们学习过的类,都是被final修饰的,目的就是仅供我们使用,而不让我们所以改变其内容。
3.2 修饰方法
被final修饰的方法继承后不允许重写,否则会报错。
class Person {
final void demo(){
System.out.println("我是Person类里的demo方法");
}
}
class Student extends Person {
// void demo(){} // 报错,被final修饰的方法不允许被重写
}
3.3 修饰局部变量
-
如果局部变量时基本数据类型,被final修饰后,只能赋值一次,再次赋值会报错。
public class FinalDemo1 { public static void main(String[] args) { // 声明变量,使用final修饰 final int a; // 第一次赋值 a = 10; // 第二次赋值 a = 20; // 报错,不可重新赋值 // 声明变量,直接赋值,使用final修饰 final int b = 10; // 第二次赋值 b = 20; // 报错,不可重新赋值 } }
-
如果局部变量是引用数据类型,在被final修饰后,只能指向一次某个对象,不允许再修改指向。但是不影响对象内部的成员变量值的修改。
// 被 final 修饰的变量只能指向一次内存地址 final Person p = new Person("zhangsan", 18); // p = new Person("Jack", 20); 不能够再次指向其他位置,否则会报错 p.setName("李四"); // 但是可以修改这个对象的属性
3.4 修饰成员变量
-
显式初始化,即在定义时就指定变量的值。
class Test { public static void main(String[] args) { Person p = new Person(); // p.ID = 1; 报错,ID被final修饰,不允许修改 } } class Person { final int ID = 10; }
-
构造方法初始化。
class Test { public static void main(String[] args) { Person p = new Person("张三",8); // p.ID = 3; 报错,被final修饰的ID不允许再修改 } } class Person { private String name; final int ID; Person(String name,int ID) { this.name = name; this.ID = ID; } }
被 final 修饰的常量通常都使用全大写,例如:
Math.PI,Math.E