黑马程序员——面向对象的三大特性(3)

------<ahref="http://www.itheima.com" target="blank">Java培训、Android培训、iOS培训、.Net培训</a>、期待与您交流! -------

多态

所谓多态,可以理解为事物存在的多种体现形态。

人分为男人,女人,等等,那某个具体的人,如马云既是一个男人,也是一个人。(用子类或者父类描述都可以)

同理,我家邻居养了只猫,那叫它猫也可以,叫它动物也可以。

这就是多态最基本的概念:父类引用指向子类对象。

猫 x = new 猫();

动物 x = new 猫();

1.      多态的体现。

父类的引用指向了自己的子类对象。换句话说:

父类的引用也可以接收自己的子类对象。

2.      多态的前提。

必须是类与类之间有关系,要么继承,要么实现。

存在覆盖,调用父类中已出现但没具体化的而由子类定义的具体功能。

3.      多态的好处。

大大提高了程序扩展性。

4.      多态的弊端:

只能使用父类的引用访问父类中的成员。

5.      多态的应用。

通过父类引用调用子类中具体的功能,即使后期添加了新的子类,只要是符合规则还是可以调用,原代码不用改动,提升了程序的扩展性。


上图其实是传入参数Animal类接收其子类Cat,Dog, Pig的对象,也是多态。在函数里面直接调用三者从父类那里继承并改写的方法就可以了,代码很简练。

6.      多态的出现代码中的特点(多态使用的注意事项)

多态中成员函数的特点:

在编译时:参阅引用型变量所属的类中是否有调用的方法。若没有编译失败。因为父类引用指向子类对象,当在编译的时候子类对象还没有创建出来,这时虚拟机只判断父类里面是否有所写的那些方法,没有的话编译无法通过。

在运行时:参阅对象所属的类中是否有调用的方法。有的话直接调用,根本不管父类。因为具体调用方法的还是对象,因此直接走堆内存了,与用什么变量指向没有关系。

简单总结就是:成员函数在多态调用时,编译看左边,运行看右边。

例如下:

class Fu
{
	int num = 0;
	void method1()
	{
		System.out.println("fu method_1");
	}
	void method2()
	{
		System.out.println("fu method_2");
	}
	static void method4()
	{
		System.out.println("fu method_4");
	}
}
class Zi extends Fu
{
	int num = 1;
	void method1()
	{
		System.out.println("zi method_1");
	}
	void method3()
	{
		System.out.println("zi method_3");
	}
	static void method4()
	{
		System.out.println("zi method_4");
	}
}
class  DuotaiDemo4
{
	public static void main(String[] args) 
	{
		Fu s = new Zi();
		s.method1();//父类与子类都有这个方法,但是在调用的时候直接通过堆内存中的子类对象,所以是子类的方法被调用。
		s.method2();//子类继承自父类的方法,没有争议。
//	s.method3();父类中没有method3这个方法,因此如果写了编译不会通过。
	}
}

打印结果为

多态中成员变量的特点:

无论编译和运行,都参考左边(引用型变量所属的类)的变量。而不是那个对象的变量。

上例中如果主函数再加入System.out.println(s.num);打印结果是父类的num变量0。

 

多态中,静态成员函数的特点:

无论编译和运行,都参考左边。(引用型变量所属的类)。静态方法一进内存就已经绑定在其所属的类上了,所以不通过堆内存的对象访问。因此没有对象特有的覆盖性质,引用类型是谁,就调用谁的方法,有点类似于类名调用。

上例中如果主函数再加入a.method4();打印结果是父类的method4方法。


Animal a= new Cat(); 本质是进行类型提升,也就是向上转型

a.eat();使用共性的方法。

而如果想要调用子类的特有方法时,必须先强制将父类引用转成子类类型,叫做向下转型(否则无法直接使用子类特有方法,编译不能通过)。

写法如:

Cat c =(Cat)a;

c.CatchMouse();调用子类特有功能,抓老鼠。

这里需要注意的是,千万不要出现这样的操作:将父类对象(如果它可以被创建出来的话)转换成子类类型。不可能创建一个动物,当需要让它抓老鼠的时候让它变成猫,需要看家的时候让它变成狗。如:

Animal a= new Animal();

Cat c =(Cat)a;

这么写是错误的。会报ClassCastException类型转换异常Animalcannot be cast to Cat,动物类型的对象不能向下转为猫类型的。

我们能转换的是父类引用指向了自己的子类对象这种情况,该引用可以被提升,也可以被向下转型。需要牢记:多态自始至终都是子类对象在做变化。

 

instanceof 关键字判断是否某一类型的对象。

上例中,

Animal a = new Cat();
Cat c = (Cat)a;

cinstanceof Cat和c instanceof Animal的结果都为真。a instanceof Cat和 a instanceof Animal结果也都为真。因为它们所指向的对象都是子类对象,不管用不用多态,它们都必然是子类的实例,也是父类的实例,因为他们继承了父类。如果子类还实现了某个接口那么它也是该接口的实例,instanceof的结果也为真。

instanceof这个关键字一般只在两种情况下使用:1.子类型有限,需要知道是哪种类型从而进行有针对性的操作,所有类型的参数都要考虑到,因此种类不能太多否则扩展性差。2.针对不同传入类型,调用那个类型的特有方法。

另外,接口型引用指向自己的子类对象也是多态。

 

内部类

所谓内部类很好理解,就是定义在类里面的类(注意不是子类,子类是一个类继承或者实现另一个类)。这种一个复杂事物内部还有一个复杂事物的现象在现实生活中也很常见比如说汽车是一个类,汽车里面的发动机也是一个类,而再细看,发动机里面的喷油嘴因为有自己的属性和方法也是一个复杂事物也是一个类,而这些类是有关联的,是层层嵌套的,小的类存在于大的类里面的;而继承不是这样:父类往往定义了所有子类的共性内容,子类不存在于父类之中。就像猎豹属于猫科动物,但它却不是猫科动物的内部类,它生活在非洲大草原上,即使是内部类的话也是非洲生态系统的内部类。


内部类定义原则(后面讲集合时会细说)

描述事物时,事物的内部还有事物,该事物用内部类来描述。因为内部事务在使用外部事物的内容。

内部类的访问规则:

1.      内部类可以直接访问外部类中的成员,包括私有。

之所以可以直接访问外部类中的成员,是因为内部类中默认持有了一个外部类的引用,格式:外部类名.this.成员名

2.      外部类要访问内部类,必须建立内部类对象。

外部类中的方法可以访问在成员位置的内部类。


当内部类定义在外部类成员位置上时,而且非私有,可以在外部其他类中,先建立内部类所在的外部类对象,再建立内部类对象。创建方法如下:

Outer.Innerxxx = new Outer().new Inner();

当内部类在成员位置上,就可以被成员修饰符所修饰。如private:将内部类在外部类中进行封装。

还有静态static:当内部类被static修饰后,只能直接访问外部类中static成员,出现了访问局限。因为静态加载的时候还没有创建对象,此时只有静态。(当然创建了对象,静态再访问里面的成员是可以的,但这就不算直接访问了)

在外部其他类中,要想直接访问被static修饰的内部类中的非静态成员,不用再创建外部类对象,而仅仅需要创建内部类对象,就可以使用内部类中的属性和方法了。

格式如下:

newOuter.Inner().function();

因为成员如果是静态的,想要获得这个成员我们不用再创建其所属的类的对象直接就可以获得,而如果成员不是静态的,我们必须创建它所属类的对象才能获得,这里面Inner是静态的,静态的主函数不用创建Outer的对象直接可以访问,但是function是非静态的,必须创建Inner的对象才能访问。

 

在外部其他类中,要想直接访问static的内部类的静态成员,此时不用创建对象,直接调用内部类类名即可访问,格式如下:

Outer.Inner.function();

全都是静态的,直接调用。

 

注意:当内部类中定义了静态成员,该内部类必须是静态的。因为内部类是一个类的同时也是一个成员,如果它不是静态的话,想要访问它必须先建立外部类的对象,而它里面的静态内容却先于对象加载,这是不符合逻辑的;更何况外部类中的静态方法也只能直接访问静态的内部类。

 

内部类虽然是成员,但是它和外部类一样,在编译的时候也会生成一个字节码文件,在硬盘中的格式如:

Outer$Inner.class,它与外部类的Outer.class共同存在。

这很好理解,因为也只有这样我们才能访问内部类中的静态资源,因为字节码文件先于静态加载。

 

另外内部类支持多层嵌套,即内部类中还有内部类,那么它在硬盘中的字节码文件格式如:

Outer$Inner$Innerr.class


局部内部类

内部类可以写在类的任意位置上,既可以是成员也可以是局部。

内部类定义在局部时

1.      不能被成员修饰符如static private等所修饰,而此时内部类中也不能有静态成员,因为当内部类中定义了静态成员,该内部类必须是静态的。

2.      可以直接访问外部类中的成员,因为还持有外部类中的引用。但是不可以访问它所在的局部中的变量。只能访问被final修饰的局部变量或者被final修饰的传入参数。可以这样理解:外部类创建完了对象,方法等局部内容进了栈内存,而其中的内部类,就算是定义在局部位置的,也不可能跟着进栈内存,而是进了堆内存中。这样就牵扯到了生命周期的问题。我们知道内部类也是类,只有当对该类的引用消失时,内部类才会被垃圾回收器回收并消亡。而局部位置的变量(诸如方法或者for循环等的)都存储在栈内存里,当调用完这个方法或者这个代码块就会退栈,那这个局部位置上的变量也就消失了。这样,内部类的生命周期就比局部位置的变量的生命周期要长,内部类就有可能调用到已经消失的局部变量,因此内部类不能访问方法中的局部变量。而如果在局部变量前面加上final修饰符,那么这个局部变量就会进入堆内存中,与整个对象共存亡,这时同在堆内存中的内部类就可以正常访问方法中的局部变量了。

这里需要注意的是,虽然被final修饰的局部变量是一个常量,不可以被重复赋值,但局部变量只存在于栈内存中,运行完即释放,因此主函数下一条语句再进来执行时,又是新的局部变量,可以被重新赋值。如下:



3.      局部的内部类依然会生成字节码文件,格式与在成员位置时略有不同:Outer$1Innerr.class,在外部类名$与内部类名之间加了个1,不管这个内部类外部套了多少层局部(函数,for循环等)都是1

匿名内部类

现实中有这样一种情况:人这个类里面有心脏等器官的内部类,而这些器官与其他动物体内的器官有一定关联,但是又有一定区别,所以这些器官也有它们对应的父类,我们把每个器官共性的内容抽取出来,让每个具体的物种体内的器官类直接继承或者实现总比在每个物种类内部定义单独的内部类要简单方便而且提高复用性,降低重复。

但有的时候,这么做也会很麻烦,比如说我们仅仅想使用这个内部类中的某个功能,也要先定义并命名这个内部类让它继承一个外部的类(1),再进行复写父类的内容(2),然后创建对象(3),最后调用方法(4)。

代码如图:



匿名内部类其实就是对调用内部类里面资源的简写格式。是把上面所说的4步融合为1步完成。

我们想要使用内部类里面的资源,但是它没有名字,如何创建对象呢?这就是匿名内部类的前提:内部类必须继承某个类或者实现某个接口。

匿名内部类的格式: new父类或者接口(){定义子类的内容},这个东西叫做匿名子类对象。要调用里面的某个方法,直接在后面加上.方法就可以,这是匿名内部类与匿名对象的结合,不需要父类引用;也可以用多态的方式,用父类引用指向匿名子类对象。上例用匿名内部类改写的代码如下:


匿名内部类本质就是一个匿名子类对象,这个对象比较胖。可以理解为带内容的类生成对象以一条语句表现出来,正常的内部类里面写什么,这个匿名内部类的大括号里就可以写什么。

 

匿名内部类一个比较常见的应用情景就是一个函数的参数是一个接口,接口里面只有不超过3个的抽象函数,这时如果再定义子类并创建其子类对象太麻烦。直接在主函数里给这个函数传参数时用匿名内部类方法直接创建接口的“子类”对象。代码如下:

interface Inter
{
	void method();
}
class Demo
{
	public static void show(Inter in)
	{
		in.method();
	}
}
class Practice 
{
	public static void main(String[] args) 
	{
		Demo.show(new Inter()
		{
			public void method()
			{
				System.out.println("method");
			}
		});
	}
}

当某个类既没有父类也没有接口时候,也可以用匿名内部类。因为所有类都有上帝Object所以可以直接利用。如下图:


但是如果要使用多态,让父类引用指向匿名子类对象,再调该对象中的特有方法那就不行了(下图),因为父类引用中没有function方法,编译不能通过。而上图之所以成功原因是它只是一个子类对象,没有父类引用。


总结

匿名内部类的出现是为了简化代码书写,缺点是需要父类或者接口,并且如果是多态的话无法调用子类特有内容(因为父类中不存在该内容,无法编译,多态特性)。但如果是用匿名对象的方法创建匿名内部类的话,是可以直接使用子类的特有方法的。

另外上面所说的多态指向匿名子类对象若想调用子类特有内容必须强制转换,但是强转要求有类名,必须定义一个子类并继承父类或者接口,而我们匿名内部类的目的就是不用定义类直接建立对象。那样做的话就不是匿名内部类了。

其他弊端:当父类中抽象方法很多时,匿名内部类会非常臃肿,每一条都要复写,阅读性极差。因此匿名内部类中定义的方法最好不超过三个。

拔高

匿名内部类的基类(父类)如果需要带参数的非默认构造函数,可以在建立匿名对象的时候,通过传递参数选择相应构造函数。

         匿名内部类没有名字,因此无法有构造函数,但是可以用构造代码块的形式完成实例初始化。当然这种情况下无法重载,只有这么一个构造代码块。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值