1.多态的形式
多态是继封装、继承之后,面向对象的第三大特性。
多态是出现在继承或者实现关系中的。
多态体现的格式:
父类类型 变量名 = new 子类/实现类构造器;
变量名.方法名();
多态的前提 :有继承关系,子类对象是可以赋值给父类类型的变量。例如Animal是一个动物类型,而Cat是一个猫类型。Cat继承了Animal,Cat对象也是Animal类型,自然可以赋值给父类类型的变量。
- 必须有继承或者实现关系。
2.必须存在父类类型的变量引用子类类型的对象。
3.存在方法重写。
下面举个例子:
定义父类:
public class Animal {
public void eat(){
System.out.println("动物吃东西!")
}
}
定义子类:
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
test:
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();
}
}
- 从上面案例可以看出,Cat和Dog都是动物,都是吃这一行为,但是出现的效果(表现形式)是不一样的。
2. 多态的好处
实际开发的过程中,父类类型作为方法 形式参数 ,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。代码如下:
定义父类:
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("吃骨头");
}
}
test类:
public class Test {
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方法执行时,多态规定,执行的是子类重写的方法,那么效果自然与showCatEat、showDogEat方法一致,所以showAnimalEat完全可以替代以上两方法。
- 不仅仅是替代,在扩展性方面,无论之后再多的子类出现,我们都不需要编写showXxxEat方法了,直接使用showAnimalEat都可以完成。 从而实现了实现类的自动切换。
所以,多态的好处,体现在,可以使程序编写的更简单,并有良好的扩展。
3. 多态的劣势
我们已经知道多态编译阶段是看左边父类类型的,如果子类有些独有的功能,此时 多态的写法就无法访问子类独有功能了 。
4.重载(Overload)与重写(Override)
多态中重写的基本规则
- 参数必须要一样,且返回类型必须兼容重写是派生类对基类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。 即外壳不变,核心重写! 基类定义出其他的程序代码要如何使用方法。不管基类使用了哪种参数,覆盖此方法的派生类也一定要使用相同的参数。同样,不论基类声明的返回类型是什么,派生类必须要声明返回一样的类型或该类型的派生类。要记得, 派生类对象必须保证可以执行基类的一切。
- 不能降低方法存取的极限简而言之,方法和变量的存取权必须相同或者更为开放。例如不能把public的方法降低为private。
- 基类的成员方法只能被它的派生类重写。
- 声明为 final 的方法不能被重写。
- 声明为 static 的方法不能被重写,但是能够被再次声明。
- 构造方法 不能被重写。
- 如果不能继承一个方法,那么它一定不能被重写
- 当需要在派生类中调用基类的被重写方法时,要使用 super 关键字。
class Animal{
public void move(){
System.out.println("动物可以移动");
}
}
class Dog extends Animal{
public void move(){
super.move(); // 应用super类的方法
System.out.println("狗可以跑和走");
}
}
public class TestDog{
public static void main(String args[]){
Animal b = new Dog(); // Dog 对象
b.move(); //执行 Dog类的方法
}
}
//编译结果如下
>>>动物可以移动
>>>狗可以跑和走
重载
重载的意义时同一个类里,两个方法的名称相同,但参数不同。
所以,重载和多态 没有半毛钱关系!
重载可以有同一方法的多个不同参数版本以便调用。比如某个方法需要int,调用方就需要把double转成int然后才能调用。如果有个重载版的方法取用double参数,这样调用就简单多了。
因为 重载 方法不需要满足定义在基类的多态合约,所以扩展起来比较方便。
重载最常用的地方就是 构造器 的重载。
原则
- 返回类型可以不同可以任意改变重载方法的返回类型,只要所有的覆盖使用不同参数即可。
- 不能只改变返回类型如果只有返回类型改变但参数一样,编译器不会通过编译。也就是说, 被重载的方法必须改变参数列表(参数个数或类型不一样); 结合前两条,无法以返回值类型作为重载函数的区分标准。
- 可以更改存储权限可以任意地设定重载方法的存储权限。
- 方法能够在同一个类中或者在一个派生类中被重载。
public class Overloading {
public int test(){
System.out.println("test1");
return 1;
}
public void test(int a){
System.out.println("test2");
}
//以下两个参数类型顺序不同
public String test(int a,String s){
System.out.println("test3");
return "returntest3";
}
public String test(String s,int a){
System.out.println("test4");
return "returntest4";
}
public static void main(String[] args){
Overloading o = new Overloading();
System.out.println(o.test());
o.test(1);
System.out.println(o.test(1,"test3"));
System.out.println(o.test("test4",1));
}
}
重写与重载之间的区别
区别点 | 重载方法 | 重写方法 |
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可以修改 | 一定不能修改 |
异常 | 可以修改 | 可以减少或删除,一定不能抛出新的或者更广的异常 |
访问 | 可以修改 | 一定不能做更严格的限制(可以降低限制) |
总结
方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是基类与派生类之间多态性的一种表现,重载可以理解成多态的具体表现形式。
- (1) 方法重载 是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
- (2)方法重写是在派生类存在方法与基类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
- (3)方法重载是一个类的多态性表现,而方法重写是派生类与基类的一种多态性表现。
多态成员访问的特点
Parent p = new Child();
成员变量
编译看左边(基类),运行看左边(基类);无论如何都是访问基类的成员变量。
成员方法
编译看左边(基类),运行看右边(派生类),动态绑定。
Static方法
编译看左边(基类),运行看左边(基类)。
只有非静态的成员方法,编译看左边,运行看右边。
这样,我们也可以得出多态的局限:
不能使用派生类特有的成员属性和派生类特有的成员方法。
5.引用类型转换
为什么要转换 ?
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说, 不能调用 子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点”小麻烦”。所以,想要调用子类特有的方法,必须做向下转型。
回顾基本数据类型转换
- 自动转换: 范围小的赋值给范围大的.自动完成:double d = 5;
- 强制转换: 范围大的赋值给范围小的,强制转换:int i = (int)3.14
多态的转型分为向上转型(自动转换)与向下转型(强制转换)两种。
<1> 向上转型(自动转型)
- 向上转型 :多态本身是子类类型向父类类型向上转换(自动转换)的过程,这个过程是默认的。当父类引用指向一个子类对象时,便是向上转型。使用格式:
父类类型 变量名 = new 子类类型();
如:Animal a = new Cat();
原因是:父类类型相对与子类来说是大范围的类型,Animal是动物类,是父类类型。Cat是猫类,是子类类型。Animal类型的范围当然很大,包含一切动物。 所以子类范围小可以直接自动转型给父类类型的变量
<1> 向下转型(强制转换)
- 向下转型 :父类类型向子类类型向下转换的过程,这个过程是强制的。一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。
使用格式:
子类类型 变量名 = (子类类型) 父类变量名;
如:Aniaml 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
}
}
这段代码可以通过编译,但是运行时,却报出了 ClassCast Exception ,类型转换异常!这是因为,明明创建了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.1 概述
2.1.1 什么是内部类
将一个类A定义在另一个类B里面,里面的那个类A就称为 内部类 ,B则称为 外部类 。可以把内部类理解成寄生,外部类理解成宿主。
内部类是Java类的五大成份之一,也是我们最后一个需要学习的成份。
2.1.2 什么时候使用内部类
一个事物内部还有一个独立的事物,内部的事物脱离外部的事物无法独立使用
- 人里面有一颗心脏。
- 汽车内部有一个发动机。
- 为了实现更好的封装性。
2.2 内部类的分类
按定义的位置来分
- 静态内部类 ,类定义在了成员位置 (类中方法外称为成员位置,有static修饰的内部类)
- 实例内部内 ,类定义在了成员位置 (类中方法外称为成员位置,无static修饰的内部类)
- 局部内部类 ,类定义在方法内
- 匿名内部类 。一般定义在方法中,或者可执行代码中
2.3 静态内部类
静态内部类特点 :
- 有static修饰的内部类,属于外部类本身的。
- 总结:静态内部类与其他类的用法完全一样。只是访问的时候需要加上外部类.内部类。
- 拓展 :静态内部类可以直接访问外部类的静态成员。
内部类的使用格式 :
外部类.内部类。
静态内部类对象的创建格式 :
外部类.内部类 变量 = new 外部类.内部类构造器;
案例演示 :
// 外部类:Outer01
class Outer01{
private static String sc_name = "小不点";
// 内部类: Inner01
public static class Inner01{
// 这里面的东西与类是完全一样的。
private String name;
public Inner01(String name) {
this.name = name;
}
public void showName(){
System.out.println(this.name);
// 拓展:静态内部类可以直接访问外部类的静态成员。
System.out.println(sc_name);
}
}
}
public class InnerClassDemo01 {
public static void main(String[] args) {
// 创建静态内部类对象。
// 外部类.内部类 变量 = new 外部类.内部类构造器;
Outer01.Inner01 in = new Outer01.Inner01("张三");
in.showName();
}
}
2.4 实例内部类
实例内部类特点 :
- 无static修饰的内部类,属于外部类对象的。
- 宿主:外部类对象。
内部类的使用格式 :
外部类.内部类。 // 访问内部类的类型都是用 外部类.内部类
实例内部类创建对象格式 :
外部类.内部类 变量 = new 外部类构造器.new 内部类构造器;
- 拓展1:实例内部类不能定义静态成员。
- 拓展2:实例内部类可以直接访问外部类的私有和静态成员。 案例演示
public class InnerClassDemo02 {
public static void main(String[] args) {
// 宿主:外部类对象。
// Outer02 out = new Outer02();
// 创建内部类对象。
Outer02.Inner02 in = new Outer02().new Inner02("张三");
in.showName();
}
}
class Outer02{
// 实例内部类,属于外部类对象的。
// 拓展:实例内部类不能定义静态成员。
public class Inner02{
// 这里面的东西与类是完全一样的。
private String name;
public Inner02(String name) {
this.name = name;
}
public void showName(){
System.out.println(this.name);
}
}
}
2.5 局部内部类
- 局部内部类 :定义在 方法中 的类。
定义格式:
class 外部类名 {
数据类型 变量名;
修饰符 返回值类型 方法名(参数列表) {
// …
class 内部类 {
// 成员变量
// 成员方法
}
}
}
2.6 匿名内部类
2.6.1 概述
匿名内部类 :是内部类的简化写法。它的本质是一个 带具体实现的 父类或者父接口的 匿名的 子类对象 。开发中,最常用到的内部类就是匿名内部类了。
2.6.2 引入
实际上,如果我们希望定义一个只要使用一次的类,就可考虑使用匿名内部类。匿名内部类的本质作用
是为了简化代码 。
之前我們使用接口时,似乎得做如下几步操作:
- 定义子类
- 重写接口中的方法
- 创建子类对象
- 调用重写后的方法
interface Swim {
public abstract void swimming();
}
// 1. 定义接口的实现类
class Student implements Swim {
// 2. 重写抽象方法
@ Override
public void swimming() {
System.out.println("狗刨式...");
}
}
public class Demo07 {
public static void main(String[] args) {
// 3. 创建实现类对象
Student s = new Student();
// 4. 调用方法
s.swimming();
}
}
我们的目的,最终只是为了调用方法,那么能不能简化一下,把以上四步合成一步呢?匿名内部类就是做这样的快捷方式。
2.6.3 匿名内部类前提和格式
匿名内部类必须 继承一个父类 或者 实现一个父接口 。
匿名内部类格式
new 父类名或者接口名(){
// 方法重写
@Override
public void method() {
// 执行语句
}
};
2.6.4 使用方式
以接口为例,匿名内部类的使用,代码如下:
创建匿名内部类,并调用:GUI做界面
interface Swim {
public abstract void swimming();
}
public class Demo07 {
public static void main(String[] args) {
// 使用匿名内部类
new Swim() {
@Override
public void swimming() {
System.out.println("自由泳...");
}
}.swimming();
// 接口 变量 = new 实现类(); // 多态,走子类的重写方法
Swim s2 = new Swim() {
@Override
public void swimming() {
System.out.println("蛙泳...");
}
};
s2.swimming();
s2.swimming();
}
}
2.6.5 匿名内部类的特点
- 定义一个没有名字的内部类
- 这个类实现了父类,或者父类接口
- 匿名内部类会创建这个没有名字的类的对象
2.6.6 匿名内部类的使用场景
通常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递。代码如下:
interface Swim {
public abstract void swimming();
}
public class Demo07 {
public static void main(String[] args) {
// 普通方式传入对象
// 创建实现类对象
Student s = new Student();
goSwimming(s);
// 匿名内部类使用场景:作为方法参数传递
Swim s3 = new Swim() {
@Override
public void swimming() {
System.out.println("蝶泳...");
}
};
// 传入匿名内部类
goSwimming(s3);
// 完美方案: 一步到位
goSwimming(new Swim() {
public void swimming() {
System.out.println("大学生, 蛙泳...");
}
});
goSwimming(new Swim() {
public void swimming() {
System.out.println("小学生, 自由泳...");
}
});
}
// 定义一个方法,模拟请一些人去游泳
public static void goSwimming(Swim s) {
s.swimming();
}
}