一、多态性 & 虚方法
1.1、什么是多态?
多态即是在程序中,一个名字代表着多种不同的含义
多态分为两种情形:(为何分为不同时期?先可思考!)
第一:编译时多态
典型实现编译时多态的方式:重载(Overload)
为何有编译时多态?为何重载是典型的实现编译时多态?
在程序中,也就是在编写程序的时候,也许某个方法我们用了相同的名字,但是我们想实现多态,必然还是会让一些同名的方法有所区别,完全无法区别是不可能实现多态的!那何时区别呢?程序执行有编译期和运行期,这两个阶段各自有区别编码时同名方法的手段。在编译期,我们的重载函数,经过编译、汇编、连接,最终执行的阶段(Java应该是生成字节码文件)。而这个重载,正是在编译的阶段完成了同名方法的区分 如何区分的呢?
比如:示例程序 & 反汇编代码(反汇编命令:javap -c 文件名)
从这里可以看出来,经过编译之后,我们的三个重载函数是完全不一样的,如此便可得知这是在编译器完成了区分!
第二:运行时多态
典型实现运行时多态的方式:重写(Override)
重写必须在继承关系中才能存在,没有继承就没有重写!由于是在继承关系中,我们子类对父类的方法进行重写,此时在子类的对象调用该方法的时候,就不会调用父类的方法了,而是调用子类的重写过后的方法!这不就是对同名称的方法在不同的运用情境下有不同的功效吗?所以重写是典型的多态表现。那么重写为何是运行期多态呢?又是如何来区分同名方法何时去调用子类的,何时调用父类的呢?
这是靠系统来决定的,系统有一种机制:动态绑定!也叫虚方法的调用,为什么叫虚方法呢?它很虚???其实不是,而是在你编码的时候,重写父类的方法,然后编译之后,系统都不晓得你用的方法到底是个什么方法,归谁所有,方法体如何,都不知道!此时是一个动态关系,只晓得每个方法属于每个类,然后按类完成编译成为字节码文件!
就按照我们上面的例子,可以看到反汇编的代码,每个方法都有这么一个标识:
invokevirtual 这就是按照虚方法处理的!但是有一个方法不同,它是invokespecial 的!那个是构造方法,它是实实在在的!而为什么这些方法是虚方法呢?因为这些方法都是对象级别的,且是对象可能调用也可能不调用的!所以还没有与实例进行绑定!需要动态绑定!(类级别的static方法不是虚方法,从此处也是可以理解的!)
这种运行时多态是在调用方法的时候,才会完成动态绑定,此时是系统来运行的,就是运行时多态!
1.2、上溯造型(进一步理解编译期和运行期)
假设有一个父类:Person
Person有一个子类:Student
这么一行代码:Person p = new Student();
或者是在函数参数里这样调用:
Person p = new Student();
void fun(Person p){
}
此时,上述两份代码,我们来解读一下!
我们把这种声明一个基类类型的变量,用子类对象来引用的方法,叫做上溯造型,在编译期间,它是Person(基类)的变量,但是在运行的期间,这个引用变量会指向子类的对象。所以实际上p的对象是子类Student的!然后在函数中,如果传参是一个Student的对象,也能传给要求传Person类的函数,因为学生也是人,这不矛盾(子类是父类的特殊具体化)。但是在方法体中使用的时候,就是运行期了,这时候就会根据实际情况,动态绑定,就会使用具体对象的方法、属性了!
1.3、虚方法 & 多态的示例Demo
public class VirtualMethod {
static void GetDarw(Shape s) {
s.darw();
}
static void GetSay(Shape s) {
Shape.Say();
// 写成 s.Say();会报警告!
}
public static void main(String[] args) {
// 运行时多态:用重写来实现,调用的是虚方法!
// 虚方法是除了static、private、final的方法,因为父类的这些修饰符的方法都是不可Override的!
Circle c = new Circle();
GetDarw(c);
Triangle t = new Triangle();
GetDarw(t);
Line l = new Line();
GetDarw(l);
// Shape shape = new Circle();
// shape.darw(); result: Circle is darwed! 因为实际shape是Circle的对象,这个是运行期决定的!
// static修饰的方法,不算虚方法,是类级别的,是取决于类的声明的(在编译器)
Circle StaticC = new Circle();
GetSay(StaticC);// 无论传入的对象是什么,static的方法,只取决于类的声明,而与实例无关!
}
}
class Shape {
void darw() {
System.out.println("Shape is darwed!");
}
static void Say() {
System.out.println("Hello, i am Shape!");
}
}
class Circle extends Shape {
void darw() {
System.out.println("Circle is darwed!");
}
static void Say() {
System.out.println("Hello, i am Circle");
}// 注意:Override不能重写父类的静态方法,这里不算重写!
}
class Triangle extends Shape {
void darw() {
System.out.println("Triangle is darwed!");
}
static void Say() {
System.out.println("Hello, i am Triangle!");
}
}
class Line extends Shape {
void darw() {
System.out.println("Line is darwed!");
}
static void Say() {
System.out.println("Hello, i am Line!");
}
}
看看运行效果:
示例Demo的分析与总结:
第一点:首先我们来看Darw这个方法:
static void GetDarw(Shape s) {
s.darw();
}
这个方法传入的变量是Shape类型的,但是由于上溯造型是允许的,所以我们传参的时候是可以传入它的类型的对象(也就是Shape类型),也可以传入Shape类型的子类对象(也就是此Demo的Circle、Triangle、Line的对象),而在这些对象中,我们都重写了父类Shape的方法,所以在编译期是无法确定调用的是谁的方法,具体实现如何,这些都不知道,所以此时是虚方法!然后在运行期,我们会明确参数的引用对象是谁,此时才会具体到实例,去调用属于具体实例的方法!所以在运行的时候,我们会具体到传入的实例对象,去调用,也就是它们重写父类的方法实现!
第二点:再来看看GetSay这个方法:
static void GetSay(Shape s) {
Shape.Say();
// 写成 s.Say();会报警告!
}
这个方法我们追溯到父类的Say方法:
static void Say() {
System.out.println("Hello, i am Shape!");
}
这是一个不支持动态绑定的方法(static、private、final方法都是不允许动态绑定的,因为它们都不可以重写!没有动态绑定的必要,绑定了反而出错!千万注意!!)
对于这种不支持动态绑定的方法,只会属于声明的类,我们声明的是Shape类,于是乎这个static方法就会调用Shape的方法!也就是说:对于不可以动态绑定的方法,我们只在乎声明,声明的是什么类就立即是什么类,不会动态绑定!
最后总结:
二、内部类 & 匿名类
2.1、内部类
什么是内部类?
内部类就是把一个类的定义放在另一个类里面,这个内部类和外部类的其他方法是同级别的,但是编译器还是会生成相关的class文件,命名为:外部类名$内部类名.class的格式
内部类的使用?
内部类最特殊的地方就在于在其他地方使用内部类,首先是实例化:
格式为:外部类名.内部类名 in(对象名) = 外部对象.new 内部类名(构造函数的参数列表);
然后是使用:使用的化其实挺正常的,不需要冠以外部类的名字
2.2、内部类的实例:
public class InnerClass {
public static void main(String[] args) {
Parcel p = new Parcel();
p.getInfo();
Parcel.Content c = p.new Content(20);
Parcel.Destination d = p.new Destination("QingHai");
p.getInfo(c, d);
System.out.println(c.Val() + " " + d.getDes());
}
}
class Parcel {
public Parcel() {
}
public void getInfo() {
Content cc = new Content(10);
Destination dd = new Destination("Hunan");
System.out.println("包裹送到 " + dd.getDes() + " ,里面装的内容是: " + cc.Val());
}
public void getInfo(Content c, Destination d) {
System.out.println("包裹送到 " + d.getDes() + " ,里面装的内容是: " + c.Val());
}
protected class Content {
private int c;
public Content(int c){
this.c = c;
}
public int Val() {
return this.c;
}
}
protected class Destination {
private String des;
public Destination(String des) {
this.des = des;
}
public String getDes() {
return this.des;
}
}
}
运行结果是:
实例解读:
我相信这个实例已经是相当明了了!
内部类在类里面定义、书写、使用的时候与其他的普通类没啥大区别,唯一的区别就是它可以加修饰符,只能加public、protected、private、abstract、final,因为内部类也是类的成员,所以和方法没啥大区别!
注意:不能用static修饰内部类,因为这样就不是内部类了,那是类级别,不再是对象级别!
为何第二点是这样的呢?
因为内部类是外部类的实例的成员,所以调用外部类的同名字段或者方法的时候,需要用外部类名.this,这样的作用是告诉系统,我们调用的是外部类的方法!原因是外部类的方法(一般方法)是对象级别的,我们先牵引到相关对象(即是this指针),然后用对象实例去访问!
如何理解内部类的实例化呢?
因为系统并没有生成内部类,我们也看到了,只会生成外部类名$内部类名.class的字节码文件!于是我们要声明内部类实例,就必须通过外部类来得到,然后实例化的时候,因为内部类是依托外部类的实例对象产生的,于是需要实例对象去new,也就是外部类实例.new(这只是个人猜想,方便记忆,是否正确,欢迎讨论!)
2.2、匿名类
匿名类是一种特殊的内部类,它没有名字,属于一次性使用的东西,经常用在函数参数实现接口的实际情景!
匿名类在定义的同时就生成了对象实例,即来即用!
有关匿名类的使用:
匿名类没有名字,于是也没有构造方法!然后常用的格式是:
new 接口名(参数){
实现、Overwrite父类/接口方法}
常常用来作为方法的参数!