抽象类
抽象方法 : 没有方法体的方法。
抽象类:被abstract所修饰的类。
//抽象类的语法格式:
【权限修饰符】 abstract class 类名{
}
【权限修饰符】 abstract class 类名 extends 父类{
}
//抽象方法的语法格式
【其他修饰符】 abstract 返回值类型 方法名(【形参列表】);
注意:抽象方法没有方法体
关于抽象类的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。
-
抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。
理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。
-
抽象类中,也有构造方法,是供子类创建对象时,初始化父类成员变量使用的。
理解:子类的构造方法中,有默认的super()或手动的super(实参列表),需要访问父类构造方法。
-
抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。
-
抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。
理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。
多态
例如飞的动作,昆虫、鸟类和飞机,飞起来也是不一样的。可见,同一行为,通过不同的事物,可以体现出来的不同的形态。那么此时就会出现各种子类的类型。但是Java是强类型静态语言,既每一个变量在使用之前必须声明它确切的类型,然后之后的赋值和运算时都是严格按照这个数据类型来处理的。例如:
int num = 10;
String str = "hello";
Student stu = new Student();
但是,有的时候,我们在设计一个数组、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型。这个时候,Java就引入了多态。
1、格式
父类类型 变量名 = 子类对象;
父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
2、编译时类型与运行时类型不一致问题
-
编译时,看“父类”,只能调用父类声明的方法,不能调用子类扩展的方法(即调用的方法父类中必须有);
-
运行时,看“子类”,一定是执行子类重写的方法体(即,如果子类重写了父类方法,则调用子类的方法);
多态的应用:
1、多态参数
父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。
2、多态数组
例如:家里养了两只猫,两条狗,想要统一管理他们的对象,可以使用多态动物数组
//多态数组
public class TestAnimal {
public static void main(String[] args) {
Animal[] all = new Animal[4];//可以存储各种Animal子类的对象
all[0] = new Cat();
all[1] = new Cat();
all[2] = new Dog();
all[3] = new Dog();
for (int i = 0; i < all.length; i++) {
all[i].eat();//all[i]编译时是Animal类型,运行时看存储的是什么对象
}
}
}
父子类之间的类型转换
1、向上转型
-
当父类引用指向一个子类对象时,便是向上转型。这个过程是自动完成的
使用格式:
父类类型 变量名 = new 子类类型();
如:Animal a = new Cat();
注意:此时通过父类的变量就只能调用父类中有的方法,不能调用子类特有的方法了
多态引用时关于成员变量与成员方法引用的原则:
1、成员变量:只看编译时类型
如果直接访问成员变量,那么只看编译时类型
public class TestField {
public static void main(String[] args) {
Father f = new Son();
System.out.println(f.x);//只看编译时类型,结果为1
}
}
class Father{
int x = 1;
}
class Son extends Father{
int x = 2;
}
//结果为调用父类的实例变量x,因此输出1.
2、非虚方法:只看编译时类型
如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法。其他方法称为虚方法。
-
静态方法、私有方法、final方法、实例构造器、通过super调用的父类方法都是非虚方法。
-
静态方法与类型直接关联
-
私有方法在外部不可访问
-
final不可被继承
-
public class TestField {
public static void main(String[] args) {
Father f = new Son();
f.test();//只看编译时类型,结果为“father”,因为调用的是静态方法,静态方法是非虚方法,在编译期就确定了具体的调用版本,只看编译时类型,如果该方法不是静态的,那么结果为“son”
}
}
class Father{
public static void test(){
System.out.println("father");
}
}
class Son extends Father{
public static void test(){
System.out.println("son");
}
}
//因为该方法为静态方法属于非虚方法,在编译器就确定了,因为引用是Father对象,因此调用的是father中的成员方法,因此输出"father"
小贴士:静态方法不能被重写调用静态方法最好使用“类名.”
3、虚方法:静态分派与动态绑定
(1)示例一:重写
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 a = new Cat();
a.eat();
}
}
//结果为:“吃鱼”
如上代码在编译期间先进行静态分派:即确定是调用Animal类中的public void eat()方法,如果Animal类或它的父类中没有这个方法,将会报错。
而在运行期间动态的在进行动态分派:即确定执行的是Cat类中的public void eat()方法,因为子类重写了eat()方法,如果没有重写,那么还是执行Animal类在的eat()方法
(2)示例二:重载
class MyClass{
public void method(Father f) {
System.out.println("father");
}
public void method(Son s) {
System.out.println("son");
}
public void method(Daughter f) {
System.out.println("daughter");
}
}
class Father{
}
class Son extends Father{
}
class Daughter extends Father{
}
public class TestOverload {
public static void main(String[] args) {
MyClass my = new MyClass();
Father f = new Father();
Father s = new Son();
Father d = new Daughter();
my.method(f);//father
my.method(s);//father
my.method(d);//father
}
}
/**结果为:
father
father
father
*/
如上代码在编译期间先进行静态分派:即确定是调用MyClass类中的method(Father f)方法,如果Animal类或它的父类中没有这个方法,将会报错。
而在运行期间动态的在进行动态分派:即确定执行的是MyClass类中的method(Father f)方法,因为my对象的运行时类型还是MyClass类型。
有些同学会疑问,不是应该分别执行method(Father f)、method(Son s)、method(Daughter d)吗?
因为此时f,s,d编译时类型都是Father类型,因此method(Father f)是最合适的。
(3)示例三:重载
class MyClass{
public void method(Father f) {
System.out.println("father");
}
public void method(Son s) {
System.out.println("son");
}
}
class Father{
}
class Son extends Father{
}
class Daughter extends Father{
}
public class TestOverload {
public static void main(String[] args) {
MyClass my = new MyClass();
Father f = new Father();
Son s = new Son();
Daughter d = new Daughter();
my.method(f);//father
my.method(s);//son
my.method(d);//father
}
}
如上代码在编译期间先进行静态分派:即确定是分别调用MyClass类中的method(Father f),method(Son s),method(Father f)方法。
而在运行期间动态的在进行动态分派:即确定执行的是MyClass类中的method(Father f),method(Son s),method(Father f)方法,因为my对象的运行时类型还是MyClass类型。
有些同学会疑问,这次为什么分别执行method(Father f)、method(Son s)?
因为此时f,s,d编译时类型分别是Father、Son、Daughter,而Daughter只能与Father参数类型匹配
(4)示例四:重载与重写
class MyClass{
public void method(Father f) {
System.out.println("father");
}
public void method(Son s) {
System.out.println("son");
}
}
class MySub extends MyClass{
public void method(Daughter d) {
System.out.println("daughter");
}
}
class Father{
}
class Son extends Father{
}
class Daughter extends Father{
}
public class TestOverload {
public static void main(String[] args) {
MyClass my = new MySub();
Father f = new Father();
Son s = new Son();
Daughter d = new Daughter();
my.method(f);//father
my.method(s);//son
my.method(d);//father
}
}
如上代码在编译期间先进行静态分派:即确定是分别调用MyClass类中的method(Father f),method(Son s),method(Father f)方法。
而在运行期间动态的在进行动态分派:即确定执行的是MyClass类中的method(Father f),method(Son s),method(Father f)方法。
有些同学会疑问,my对象不是MySub类型吗,而MySub类型中有method(Daughter d)方法,那么my.method(d)语句应该执行MySub类型中的method(Daughter d)方法?
-
my变量在编译时类型是MyClass类型,那么在MyClass类中,只有method(Father f),method(Son s)方法,
-
f,s,d变量编译时类型分别是Father、Son、Daughter,而Daughter只能与Father参数类型匹配
-
而在MySub类中并没有重写method(Father f)方法,所以仍然执行MyClass类中的method(Father f)方法
(5)结论
方法的分派有两个需要考虑的因素:
-
方法的参数:在编译期间确定,根据编译器时类型,找最匹配的
-
方法的所有者:
-
如果没有重写,就按照编译时类型处理
-
如果有重写,就按照运行时类型处理
-