多态、final、权限、内部类
1 多态
1.1 概述
引⼊
多态是继封装、继承之后,⾯向对象的第三⼤特性。同⼀⾏为,通过不同的事物,可以体现出来的不同的形
态。多态,描述的就是这样的状态。
定义
多态:是指同⼀⾏为,具有多个不同表现形式 。
前提【重点】
- 继承或者实现【⼆选⼀】
- ⽅法的重写【意义体现:不重写,⽆意义】
- ⽗类引⽤指向⼦类对象【格式体现】
1.2 多态的实现
多态体现的格式:
⽗类类型 变量名 = new ⼦类对象;
变量名.⽅法名();
⽗类类型:指⼦类对象继承的⽗类类型,或者实现的⽗接⼝类型。
代码如下:
Fu f = new Zi();
f.method();
当使⽤多态⽅式调⽤⽅法时,⾸先检查⽗类中是否有该⽅法,如果没有,则编译错误;如果有,执⾏的是⼦类重写后⽅法。代码如下:
定义⽗类:
public abstract class Animal {
public abstract void eat();
}
定义⼦类:
class Cat extends Animal {
public void eat() {
System.out.println("吃⻥");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃⻣头");
}
}
定义测试类:
public class Test {
public static void main(String[] args) {
// 多态形式,创建对象
Animal a1 = new Cat();
// 调⽤的是 Cat 的 eat
a1.eat();
// 多态形式,创建对象
Animal a2 = new Dog();
// 调⽤的是 Dog 的 eat
a2.eat();
}
}
1.3 多态的好处
实际开发的过程中,⽗类类型作为⽅法形式参数,传递⼦类对象给⽅法,进⾏⽅法的调⽤,更能体现出多态的扩展性与便利。代码如下:
定义⽗类:
abstract class Animal {
public abstract void eat();
}
定义⼦类:
class Cat extends Animal {
public void eat() {
System.out.println("吃⻥");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃⻣头");
}
}
定义测试类:
public class Test01 {
public static void main(String[] args) {
// 多态形式,创建对象
Cat c = new Cat();
Dog d = new Dog();
// 调⽤showCatEat
showCatEat(c);
// 调⽤showDogEat
showDogEat(d);
/*
* 以上两个⽅法,均可以被showAnimalEat(Animal a)⽅法所替代
* ⽽执⾏效果⼀致
*/
showAnimalEat(c);
showAnimalEat(d);
}
public static void showCatEat(Cat c) {
c.eat();
}
public static void showDogEat(Dog d) {
d.eat();
}
public static void showAnimalEat(Animal a) {
a.eat();
}
}
由于多态特性的⽀持, showAnimalEat ⽅法的 Animal 类型,是 Cat 和 Dog 的⽗类类型,⽗类类型接收⼦类对象,当然可以把 Cat 对象和 Dog 对象,传递给⽅法。当 eat ⽅法执⾏时,多态规定,执⾏的是⼦类重写的⽅法,那么效果⾃然与 showCatEa t、 showDogEat ⽅法⼀致, 所以 showAnimalEat 完全可以替代以上两⽅法。不仅仅是替代,在扩展性⽅⾯,⽆论之后再多的⼦类出现,我们都不需要编写showXxxEat ⽅法了,直接使⽤ showAnimalEat 都可以完成。所以,多态的好处,体现在,可以使程序编写的更简单,并有良好的扩展。
1.4 引⽤类型转换
多态的转型分为向上转型与向下转型两种:
向上转型
向上转型:多态本身是⼦类类型向⽗类类型向上转换的过程,这个过程是默认的。当⽗类引⽤指向⼀个⼦类对象时,便是向上转型。
使⽤格式:
⽗类类型 变量名 = new ⼦类类型();
如:Animal a = new Cat();
向下转型
- 向下转型:⽗类类型向⼦类类型向下转换的过程,这个过程是强制的。
⼀个已经向上转型的⼦类对象,将⽗类引⽤转为⼦类引⽤,可以使⽤强制类型转换的格式,便是向下转型。
使⽤格式:
⼦类类型 变量名 = (⼦类类型) ⽗类变量名; 如: Cat c =(Cat) a;
为什么要转型
当使⽤多态⽅式调⽤⽅法时,⾸先检查⽗类中是否有该⽅法,如果没有,则编译错误。也就是说,不能调⽤⼦类拥有,⽽⽗类没有的⽅法。编译都错误,更别说运⾏了。这也是多态给我们带来的⼀点"⼩麻烦"。所以,想要调⽤⼦类特有的⽅法,必须做向下转型。
转型演示,代码如下:
定义类:
abstract class Animal {
abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃⻥");
}
public void catchMouse() {
System.out.println("抓⽼⿏");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃⻣头");
}
public void watchHouse() {
System.out.println("看家");
}
}
定义测试类:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调⽤的是 Cat 的 eat
// 向下转型
Cat c = (Cat) a;
c.catchMouse(); // 调⽤的是 Cat 的 catchMouse
}
}
转型的异常
转型的过程中,⼀不⼩⼼就会遇到这样的问题,请看如下代码:
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
}
}
}
2 final关键字
2.1概述
学习了继承后,我们知道,⼦类可以在⽗类的基础上改写⽗类内容,⽐如,⽅法重写。那么我们能不能随意的继承API中提供的类,改写其内容呢?显然这是不合适的。为了避免这种随意改写的情况,Java提供了 final 关键字, ⽤于修饰不可改变内容。
- final:不可改变。可以⽤于修饰类、⽅法和变量。
- 类:被修饰的类,不能被继承。
- ⽅法:被修饰的⽅法,不能被重写。
- 变量:被修饰的变量,不能被重新赋值。
2.2 使⽤⽅式
修饰类
格式如下:
final class 类名 {
}
查询API发现像 public final class String 、 public final class Math 、 public final class Scanner 等,很多我们学习过的类,都是被 final 修饰的,⽬的就是供我们使⽤,⽽不让我们所以改变其内容。
修饰⽅法
格式如下:
修饰符 final 返回值类型 ⽅法名(参数列表) {
//⽅法体
}
重写被 final 修饰的⽅法,编译时就会报错。
修饰变量
1.局部变量 – 基本类型
基本类型的局部变量,被 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; // 报错,不可重新赋值
}
}
思考,如下两种写法,哪种可以通过编译?
写法1:
final int c = 0;
for (int i = 0; i < 10; i++) {
c = i;
System.out.println(c);
}
写法2:
for (int i = 0; i < 10; i++) {
final int c = i;
System.out.println(c);
}
根据 final 的定义,写法1报错!写法2,为什么通过编译呢?因为每次循环,都是⼀次新的变量c。这也是⼤家需要注意的地⽅。
2.局部变量 – 引⽤类型
引⽤类型的局部变量,被 final 修饰后,只能指向⼀个对象,地址不能再更改。但是不影响对象内部的成员变量值的修改,代码如下:
public class FinalDemo2 {
public static void main(String[] args) {
// 创建 User 对象
final User u = new User();
// 创建 另⼀个 User对象
u = new User(); // 报错,指向了新的对象,地址值改变。
// 调⽤setName⽅法
u.setName("张三"); // 可以修改
}
}
3.成员变量
成员变量涉及到初始化的问题,初始化⽅式有两种,只能⼆选⼀:
- 显示初始化:
public class User {
final String USERNAME = "张三";
private int age;
}
- 构造⽅法初始化:
public class User {
final String USERNAME ;
private int age;
public User(String username, int age) {
this.USERNAME = username;
this.age = age;
}
}
被 final 修饰的常量名称,⼀般都有书写规范,所有字⺟都⼤写。
3 权限修饰符
3.1概述
在Java中提供了四种访问权限,使⽤不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限:
- public:公共的
- protected:受保护的
- friendly(不填,也称为default):默认的
- private:私有的
3.2 不同权限的访问能⼒
public | protected | default | private | |
同一个类中 | ☑️ | ☑️ | ☑️ | ☑️ |
同一个包中(子类与无关类) | ☑️ | ☑️ | ☑️ | |
不同包的子类 | ☑️ | ☑️ | ||
不同包中的五官类 | ☑️ |
- 成员变量使⽤ private ,隐藏细节。
- 构造⽅法使⽤ public ,⽅便创建对象。
- 成员⽅法使⽤ public ,⽅便调⽤⽅法。
4 内部类
4.1 概述
什么是内部类
将⼀个类A定义在另⼀个类B⾥⾯,⾥⾯的那个类A就称为内部类,B则称为外部类。
成员内部类
- 成员内部类:定义在类中⽅法外的类。
定义格式:
class 外部类 {
class 内部类 {
}
}
在描述事物时,若⼀个事物内部还包含其他事物,就可以使⽤内部类这种结构。⽐如,汽⻋类 Car 中包含发动机类 Engine ,这时,Engine 就可以使⽤内部类来描述,定义在成员位置。
代码举例:
class Car { //外部类
class Engine { //内部类
}
}
访问特点
内部类可以直接访问外部类的成员,包括私有成员。
- 外部类要访问内部类的成员,必须要建⽴内部类的对象。
- 创建内部类对象格式:
外部类名.内部类名 对象名 = new 外部类型().new 内部类型();
访问演示,代码如下:
定义类:
public class Person {
private boolean live = true;
class Heart {
public void jump() {
// 直接访问外部类成员
if (live) {
System.out.println("⼼脏在跳动");
} else {
System.out.println("⼼脏不跳了");
}
}
}
public boolean isLive() {
return live;
}
public void setLive(boolean live) {
this.live = live;
}
}
定义测试类:
public class InnerDemo {
public static void main(String[] args) {
// 创建外部类对象
Person p = new Person(); // 创建内部类对象
Heart heart = p.new Heart();
// 调⽤内部类⽅法
heart.jump();
// 调⽤外部类⽅法
p.setLive(false);
// 调⽤内部类⽅法
heart.jump();
}
}
输出结果:
⼼脏在跳动
⼼脏不跳了
内部类仍然是⼀个独⽴的类,在编译之后会内部类会被编译成独⽴的.class⽂件,但是前⾯冠以外部类的类名和 $ 符号 。⽐如,Person$Heart.class
4.2 匿名内部类【重点】
- 匿名内部类:是内部类的简化写法。它的本质是⼀个带具体实现的⽗类或者⽗接⼝的匿名的⼦类对象。
开发中,最常⽤到的内部类就是匿名内部类了。以接⼝举例,当你使⽤⼀个接⼝时,似乎得做如下⼏步操作:
1.定义⼦类
2.重写接⼝中的⽅法
3.创建⼦类对象
4.调⽤重写后的⽅法
我们的⽬的,最终只是为了调⽤⽅法,那么能不能简化⼀下,把以上四步合成⼀步呢?匿名内部类就是做这样的快捷⽅式。
前提
匿名内部类必须继承⼀个⽗类或者实现⼀个⽗接⼝。
格式
new ⽗类名或者接⼝名() {
// ⽅法重写
@Override
public void method() {
// 执⾏语句
}
};
使⽤⽅式
以接⼝为例,匿名内部类的使⽤,代码如下:
定义接⼝:
public abstract class FlyAble {
public abstract void fly();
}
创建匿名内部类,并调⽤:
public class InnerDemo {
public static void main(String[] args) {
/*
* 1.等号右边:是匿名内部类,定义并创建该接⼝的⼦类对象
* 2.等号左边:是多态赋值,接⼝类型引⽤指向⼦类对象
*/
FlyAble f = new FlyAble() {
public void fly() {
System.out.println("我⻜了~~~");
}
};
// 调⽤fly⽅法,执⾏重写后的⽅法
f.fly();
}
}
以上两步,也可以简化为⼀步,代码如下:
public class InnerDemo3 {
public static void main(String[] args) {
/*
* 创建匿名内部类,直接传递给showFly(FlyAble f)
*/
showFly(new FlyAble() {
public void fly() {
System.out.println("我⻜了~~~");
}
});
}
public static void showFly(FlyAble f) {
f.fly();
}
}
5 引⽤类型⽤法总结
实际的开发中,引⽤类型的使⽤⾮常重要,也是⾮常普遍的。我们可以在理解基本类型的使⽤⽅式基础上,进⼀步去掌握引⽤类型的使⽤⽅式。基本类型可以作为成员变量、作为⽅法的参数、作为⽅法的返回值,那么当然引⽤类型也是可以的。
5.1 class作为成员变量
类作为成员变量时,对它进⾏赋值的操作,实际上,是赋给它该类的⼀个对象。
5.2 interface作为成员变量
我们使⽤⼀个接⼝,作为成员变量,以便随时更换技能,这样的设计更为灵活,增强了程序的扩展性。
接⼝作为成员变量时,对它进⾏赋值的操作,实际上,是赋给它该接⼝的⼀个⼦类对象。