第10天 面向对象
第10天 面向对象
今日任务
1、接口的思想(接口的第二个作用)(掌握)
2、抽象类和接口的区别(了解)
3、多态技术(掌握)
4、Object类概述(掌握)
课堂笔记
1、接口的思想(接口的第二个作用)
接口的第一个作用:给事物体系增加额外功能(扩展功能)。
接口的第二个作用:给事物双方定义规则。一方使用规则,另一方实现规则。
说明:笔记本就是在使用这个规则,外围设备(鼠标、键盘等)在实现这个规则。
2、抽象类和接口的区别
接口和抽象类都是描述事物的共性行为,并且描述的行为一般都是抽象的。需要子类或实现类对这些行为进行实现或复写。
接口和抽象类的区别:
1、接口中只能定义抽象方法;抽象类中除了具有抽象方法外,还可以定义其它方法;
2、接口可以多实现;而抽象类只能单一继承;
3、接口用来描述事物扩展功能(额外功能);抽象类用来描述事物的共性内容(描述不清楚);
4、接口中没有构造函数;抽象类中具有构造函数;
抽象类和接口的区别代码体现如下:
//定义接口
interface Inter
{
void show();
//接口中只能有抽象方法
/*void test()
{
System.out.println("test");
}*/
}
interface InterA
{
void show();
}
//定义一个类来实现接口
class InterfaceImpl implements Inter,InterA//接口可以多实现
{
public void show()
{
System.out.println("接口show");
}
}
//定义一个抽象类
abstract class Abs
{
//抽象类中除了定义抽象方法还可以定义其他方法
void demo()
{
System.out.println("demo");
}
//抽象类中可以有构造函数,接口中没有
Abs()
{
System.out.println("抽象类中的构造函数");
}
}
class AbstractDemo extends Abs
{
}
class AbstractAndInter
{
public static void main(String[] args)
{
InterfaceImpl ip=new InterfaceImpl();
ip.show();
AbstractDemo abs=new AbstractDemo();
}
}
3、多态技术:
3.1、多态介绍
面向对象语言三大特征:封装、继承和多态。
多态:表示的是一个事物的多种表现形态。同一个事物,以不同的形态表现出来。
多态来源于生活,在生活中我们经常会对某一类事物使用它的共性统称来表示某个具体的事物,这时这个具体的事物就以其他的形式展示出来。
苹果:说苹果,说水果。
狗:说狗,说动物。
猫:说猫,说动物。
3.2、多态技术在Java中的体现
在Java中的多态代码体现:
使用父类的引用,表示自己的子类对象。
Cat c = new Cat(); 使用猫类型表示自己,这里不会发生多态现象
Animal a = new Cat(); 使用动物的类型再表示猫,这时就发生的多态的现象。
在Java中要使用多态技术:
前提:必须要有继承/实现;
好处:可以通过父类统一管理子类;
多态技术在java中的代码体现:
-
定义一个动物类Animal,在这个类中定义一个eat函数,由于每种动物吃的东西不一样,所以eat函数体不确定,这里我们可以将eat函数变成抽象函数,Animal类也会变成抽象类;
-
在分别定义Dog和Cat两个类,在这两个类中都复写Animal类中的eat函数,并在两个不同类中的eat函数体中输出不同动物吃不同东西的语句;
-
定义一个测试多态的类,在这个类中分别创建猫和狗的对象分别调用对应的函数,如果Animal父类中有多个函数,那么我们使用猫和狗的对象调用多个函数时会出现代码的复用性差的问题,所以我们定义一个函数将重复的代码写到函数中,这样就可以提高了代码的复用性差的问题;
-
在主函数中调用自定义函数,并将猫和狗的对象作为参数传递给自定义函数,而自定义函数接收的参数类型是父类的Animal类型,这里就使用了多态的现象;
//演示多态技术
abstract class Animal
{
abstract void eat();
void show()
{
System.out.println("show run .....");
}
void show2()
{
System.out.println("show2 run .....");
}
}
class Cat extends Animal
{
void eat()
{
System.out.println("猫吃鱼");
}
}
class Dog extends Animal
{
void eat()
{
System.out.println("狗啃骨头");
}
}
class DuoTaiDemo
{
public static void main(String[] args)
{
Cat c = new Cat();
demo(c);
Dog d = new Dog();
demo(d);
}
/*
在调用方法的时候发生了多态的现象
Animal a = new Cat(); 这里发生了多态
Animal a = new Dog(); 这里也是多态
Dog a=new Animal();子类引用是不可以指向父类对象的
我们在使用多态的时候,永远只能使用父类的类型接受子类的对象,而不能使用
子类的类型接受父类的对象。
*/
public static void demo( Animal a )
{
a.eat();
a.show();
a.show2();
}
}
注意:我们在使用多态的时候,永远只能使用父类的类型接受子类的对象,而不能使用子类的类型接受父类的对象。
3.3多态的弊端
//演示多态弊端
abstract class Animal
{
abstract void eat();
}
class Cat extends Animal
{
void eat()
{
System.out.println("猫吃鱼");
}
//猫有自己的特有行为 抓老鼠
void catchMouse()
{
System.out.println("猫抓老鼠");
}
}
class Dog extends Animal
{
void eat()
{
System.out.println("狗啃骨头");
}
//狗也有自己的行为 看家
void lookHome()
{
System.out.println("狗看家");
}
}
class DuoTaiDemo2
{
public static void main(String[] args)
{
Cat c = new Cat();
c.eat();
c.catchMouse();
Dog d = new Dog();
d.eat();
d.lookHome();
//使用多态调用方法
Animal a = new Dog();
a.eat();
a.lookHome();
}
}
多态的弊端:
把一个子类类型提升成了父类的类型,那么在程序编译的过程中,编译不会考虑具体是哪个子类类型,而只会根据当前的父类类型去操作,通过父类的引用在调用方法的时候,只会去父类类型所属的类中找有没有这些成员,
如果有编译通过,如果没有编译失败。
多态弊端总结:
在使用多态技术的时候,程序在编译的时候,使用多态调用成员(变量和函数),要求被调用的成员在父类中一定要存在,如果父类中没有编译就会失败。(不能使用子类特有功能或者属性)
注意:只要有多态的地方,一定发生类型的提升(肯定是把子类对象使用父类类型在表示)。
3.4、多态中的转型
在使用多态时,存在一个弊端:不能使用子类中特有的功能(函数)。
如果在多态中,必须要使用子类特有的功能,需要在多态操作时进行类型的转换。
复习下之前学习过的类型转换:
自动类型提升 例: byte b=10; int num=b;
强制类型转换 例: double d=3.14; int n=(int)d;
Animal an = new Dog();
Animal是父类类型(父引用类型) an是父引用 new Dog()是子类对象
在以上代码中,已存在了类型的转换(向上转型):父类 父引用=new子类();
如果一定要在父类引用中使用子类对象特有的功能,就需要向下转型(大类型向下转换):
说明:子类对象中特定的功能只能子类对象自己调用。
如果已经发生多态现象,但是我们还想调用子类的特有属性或者行为,这时需要使用强制类型转换,把当前父类类型转成具体的子类类型。
多态中的类型转换有两种:
1)向上转型(隐式的类型提升) 父引用指向子类对象 例:Animal an = new Dog();
2)向下转型(强制类型转换或者把父类类型转成子类类型) 把父引用强制转为子类引用 例:Dog d=(Dog) an;
强制类型转换格式:子类类型 子类引用名=(子类类型)父类引用名;
多态类型转换的代码体现:
abstract class Animal
{
abstract void eat();
}
class Cat extends Animal
{
void eat()
{
System.out.println("猫吃鱼");
}
//猫有自己的特有行为 抓老鼠
void catchMouse()
{
System.out.println("猫抓老鼠");
}
}
class Dog extends Animal
{
void eat()
{
System.out.println("狗啃骨头");
}
//狗也有自己的行为 看家
void lookHome()
{
System.out.println("狗看家");
}
}
class DuoTaiDemo3
{
public static void main(String[] args)
{
Cat c = new Cat();
c.eat();
c.catchMouse();
Dog d = new Dog();
d.eat();
d.lookHome();
//使用多态调用方法
Animal a = new Dog();
a.eat();
/*
如果已经发生多态现象,但是我们还想调用子类的特有属性或者行为,这时需要使用
强制类型转换,把当前父类类型转成具体的子类类型。
在多态中的类型转换问题:
1、隐式的类型提升。只要有多态就会发生类型提升(向上转型)。
2、把父类类型转成子类类型(强制类型转换,向下转型)。
什么时候使用向下转型:
只要在程序中我们需要使用子类的特有属性或行为(方法、函数)的时候,才会使用向下转型。
*/
//(Dog)(a).lookHome();
Dog dd = (Dog)a; //多态的转型
dd.lookHome();
注意:
1)什么时候使用向下转型:
只要在程序中我们需要使用子类的特有属性或行为(方法、函数)的时候,才会使用向下转型。
-
无论是向上还是向下转型,最终都是子类对象做着类型的变化。和父类对象没有关系。
-
为什么要使用转型
3.5多态类型转换时常见异常
在多态类型转换时经常会发生一个异常错误:ClassCastException(类型转换异常)。
多态类型转换常见异常代码演示:
abstract class Animal
{
abstract void eat();
}
class Cat extends Animal
{
void eat()
{
System.out.println("猫吃鱼");
}
//猫有自己的特有行为 抓老鼠
void catchMouse()
{
System.out.println("猫抓老鼠");
}
}
class Dog extends Animal
{
void eat()
{
System.out.println("狗啃骨头");
}
//狗也有自己的行为 看家
void lookHome()
{
System.out.println("狗看家");
}
}
class DuoTaiDemo3
{
public static void main(String[] args)
{
Animal a= new Cat();
demo(a);//传递的是猫的对象
}
public static void demo( Animal a )
{
a.eat();
//把Animal类型的a转成 Dog类的d
Dog d=(Dog)a;//将传递过来的猫的对象强制转换为狗是不可以的,会发生转换异常
/*
向下转型有风险,使用需谨慎。
在Java中要使用向下转型,必须先做类型的判断,然后在转型
Java中的类型判断 需要使用关键字 instanceof
格式:
被转的引用变量名 instanceof 被转成的类型
如果引用变量所在的那个对象 和被转成的类型一致,这个表达式返回的是true,否则是false
在多态中使用转型的时候,一定要判断,防止类型转换异常的发生:
如果在程序发生ClassCastException,一定是把不是这种类型的对象转成了这种类型。
*/
if( a instanceof Dog )
{
Dog d = (Dog)a;
d.lookHome();
}
else if( a instanceof Cat )
{
Cat c = (Cat)a;
c.catchMouse();
}
}
}
向下转型有风险,使用需谨慎。在Java中要使用向下转型,必须先做类型的判断,然后在转型Java中的类型判断 需要使用关键字 instanceof。
格式:
被转的引用变量名 instanceof 被转成的类型
如果引用变量所在的那个对象 和被转成的类型一致,这个表达式返回的是true,否则是false。
在多态中使用转型的时候,一定要判断,防止类型转换异常的发生:
如果在程序发生ClassCastException,一定是把不是这种类型的对象转成了这种类型。
总结:
只要有多态,就会有类型的转换。
把子类对象赋值给父类的引用,这时发生了向上的转型(隐式类型转换)。
如果我们需要使用子类的特有行为或属性,这时必须向下转型,需要把父类的引用转成具体所指的那个对象的类型。
在向下转型的时候一定要做类型的判断,防止ClassCastException异常的发生。
判断格式:
if( 父类引用变量名 instanceOf 子类对象所属的类名 )
{
进行转换。
}
3.6、多态中调用成员的细节(掌握)
学习多态中的成员使用规律:需要掌握的是以多态形式使用成员,需要考虑程序的编译和运行2个阶段。
3.6.1多态调用成员变量
在使用多态时,子父类中存在相同的成员变量:
结论:
在多态中,使用父类的引用(f)访问成员变量,子父类中存在一模一样的成员变量时:
1)代码在编译的时期(javac 源文件):需要查看父类中有没有这个成员变量,如果有,编译通过,没有编译失败。
2)编译通过的前提下,运行(java 类文件)时期:这时操作的依然是父类中的成员变量。
记住:多态时,子父类中存在一模一样的成员变量时,引用变量,编译运行都看引用类(父类)中的变量。(编译时以等号左边作为参考,运行时也是以等号左边作为参考)
注意:如果发生多态时,只要是在其他类中使用成员变量,那么这个成员变量必须存在于父类中,无论子类中是否含有对应的成员变量,如果父类中没有成员变量,那么编译和运行都不会通过,和子类中是否含有成员变量没有关系。
3.6.2多态调用非静态成员函数
在多态中,使用父类引用调用成员函数的时候,一般函数都复写存在。
在使用多态时,子父类中存在一模一样的成员方法时:
结论:
在多态中,使用父类的引用(f)调用函数的时候,子父类中存在一模一样的成员方法时:
1)代码在编译的时期(javac 源文件):要看父类中有没有这个函数,有,编译通过,没有编译失败。
2)编译通过的前提下,运行(java 类文件)时期:运行的是子类中复写父类之后的那个函数。如果没有复写,运行的肯定还是父类的函数。
记住:多态时,子父类中存在一模一样的成员方法时,编译时以等号左边作为参考,运行时是以等号右边作为参考。
3.6.3多态调用静态成员函数
在使用多态时,子父类中存在一模一样的静态方法时:
静态的成员是随着类的加载而存在,和创建的对象没有任何关系,只跟类有关系。在java中,使用对象去调用静态成员,底层JVM还是会以对象所属的类型(类)去调用静态成员。因此使用多态调用静态函数的时候,编译运行都要看父类中的函数。
结论:
在使用多态时,子父类中存在一模一样的静态方法时:
编译时期是以等号左边作为参考,运行时期也是以等号左边作为参考。
也就是说,在使用多态时,子父类中存在一模一样的静态方法时,与子类是否存在静态函数没有关系,只和父类中有关系。
总结多态中成员使用规律:
成员变量和静态成员函数,编译运行都看左边(父类中的)。只有非静态成员函数,编译看父类,运行看子类对象。
3.7、多态的练习
练习的目的:需要掌握多态中,自始至终只有子类对象存在,没有父类的对象,并且把子类对象交给父类的引用在使用。
练习黑旋风和黑旋风老爸的故事。
黑旋风:
讲课(){}
看电影(){}
黑旋风老爸:
讲课(){}
钓鱼(){}
//多态练习
class Dad
{
void teach()
{
System.out.println("黑旋风老爸讲论语");
}
void fish()
{
System.out.println("黑旋风老爸钓鱼");
}
}
class Hxf extends Dad
{
void teach()
{
System.out.println("黑旋风讲Java");
}
void lookFilm()
{
System.out.println("黑旋风在看速7");
}
}
class DuoTaiTest
{
public static void main(String[] args)
{
/*
Hxf hxf = new Hxf ();
hxf .teach();
hxf .fish();
*/
Dad dad = new Hxf (); //多态
dad .teach();
dad .fish();
//dad.lookFilm();//编译报错,因为父类中没有lookFilm这个函数
/*
现在我就想调用子类中特有的函数,怎么办?
我们这里需要使用强制类型转换,将父类转换为子类
*/
Hxf hxf = (Hxf)dad; //黑旋风卸妆 向下转型
hxf .lookFilm();
}
}
4、Object类概述
在学习面向对象编程思想,遇到需求时,先去找有没有解决问题的功能存在。这些解决问题的功能通常是封装在类中(功能类),使用这些功能类基本可以解决开发中大部分的问题(例:折半查找、选择排序等)。
问题:这些解决问题的功能类都在哪里?
在java设计时,已经提供了很多解决问题的封装类。这些解决问题的封装类,我们统称为:API
在开发中,只要去相应的包(文件夹)中去找对应的封装类就可以解决问题。
API:application programming interface。应用程序接口。我们这里通常把api简称为帮助文档。
想要使用java提供的解决各种问题的API,就需要先学会如何查阅API文档。
4.1 查阅API文档的技巧
使用"索引"查找相应的信息
如下图操作,点击选项,选择显示标签
点击完显示标签后,会出现如下图所示界面:
然后点击索引,会出现如下图所示的界面:
在查找框里输入要查找的类或者接口即可。
在搜索框里输入要查找的类,选中并双击或者回车。
4.2 如何查阅源代码
在开发中,除了查阅API以外,还经常会查看JDK的源代码,帮助解决开发中的问题。
在安装JDK时,随着JDK版本的安装,在JDK安装目录也存在一个当前版本的JDK源码文件
查看源代码的步骤:(前提:需要知道要查找的功能类属于哪个包)
-
新建一个文件夹,并把src.zip文件解压到创建的文件夹中
-
找到要查阅源代码的功能类或接口
-
使用记事本之类的工具,打开要查看的源代码文件
4.3 Object类说明
在所有类中的构造函数中有个隐式的super语句,找父类。如果一个类没有显示指定它的父类,那么这个类的父类默认就是Object类。Object类的构造函数中是没有隐式的super的。
通过API的查阅,可以得到:
1、Object是java提供的功能类(API中的类)和开发人员自己书写的类的父类;
2、因为所有的类都继承了Object类,所以继承了Object类的子类可以使用Ojbect类中的功能(函数);
疑问:既然自己定义的类也要继承Object类,那为什么在代码中没有显式书写继承Object?
Object类属于java.lang包下。而java.lang包会被JVM在运行时自动加载,继承了Object的子类也不需要显式书写,JVM会自动为书写的类添加继承。
Object类中的常用函数:
equals 方法 toString 方法
4.4、equals方法介绍
需求:判断学生是否为同龄人
-
定义一个学生Student类,在Student类中定义name和age属性,并定义构造函数给name和age初始化值;
-
在Student类中定义一个比较是否是同龄的函数,如果相同就返回true,不相同就返回false;
-
定义一个测试类,在这个类中分别创建两个对象,然后使用一个对象调用类中的比较是否同龄的函数,根据函数返回的true或者false判断是否同龄;
/*
判断两个学生是否是同龄人
*/
//定义一个学生类
class Student
{
//属性
String name;
int age;
//定义构造函数给属性初始化值
Student(String name,int age)
{
this.name=name;
this.age=age;
}
/*
定义一个函数根据外界传递过来的值比较年龄是否相等,
使用return关键字将比较的结果返回给调用者
Student a=new Student("技导",18)
因为compareAge函数是s对象调用的,所以在这个函数中的隐式变量this
记录着s对象的堆内存地址名
*/
public boolean compareAge(Student a)
{
/*
this.age表示黑旋风的年龄17
a.age表示技导的年龄18
*/
return this.age==a.age;
}
}
class ObjectDemo1
{
public static void main(String[] args)
{
/*
创建两个对象
下面的两个对象表示在对空间中开辟两个不同的空间
一个空间叫做s,另一个空间叫做s1
*/
Student s=new Student("黑旋风",17);
Student s1=new Student("技导",17);
//使用黑旋风的对象s调用compareAge函数
//使用flag来接受返回回来的值
boolean flag=s.compareAge(s1);
/*
如果返回回来的值是true,说明是同龄人
如果返回回来的值是false,说明不是同龄人
*/
if(flag==true)
{
System.out.println("是同龄人");
}else
{
System.out.println("不是同龄人");
}
}
}
使用以上方式可以解决问题。
面向对象:遇到需求时,先去找有没有存在已经解决问题的功能(功能是封装在类中)。
有,就直接使用封装了功能的功能类解决问题。
以上需求中,是需要解决判断是否为同龄人的功能。(其实就是一个判断是否相等的功能)
首先,去找java API中是否有比较功能。
问题:Student类中不具备比较功能,但是,Student类继承了Object类,所以可以去Object类中找是否存在解决问题的功能
Object类中的功能:
使用Object类中的eqauls函数解决需求中的问题:
以上程序运行结果不正确。
分析:为什么使用Object类中的equals功能会存在结果不正确呢?
查看Object类中的equals功能的源代码
上述代码中的Object类中的this 表示的是调用这个equals函数的那个对象,obj是调用equals方法时传递进来的那个对象,而this中保存的是对象的内存地址,obj中接受的也是传递进来的那个对象内存地址。所以这里使用== ,其实是在比较2个对象的内存地址是否相等。(就是堆内存中的地址)
结论:Object类中的equals方法中,比较的是堆中的地址是否相等
而我们真正在开发中要比较2个对象是否相等,不应该去比较内存地址,而应该比较的是对象中的数据是否相同。单单使用Object类中的equals功能,并不能直接解决我们需求中的问题。遇到这种情况,在开发中的做法是:重写Object类中的equals函数,因此所有的程序中都应该复写Object类中的equals。
/*
判断两个学生是否是同龄人
*/
//定义一个学生类
class Student
{
//属性
String name;
int age;
//定义构造函数给属性初始化值
Student(String name,int age)
{
this.name=name;
this.age=age;
}
//重写Object类中的equals函数(重写:和父类中的方法一模一样)
public boolean equals(Object obj) {
/*
因为这里发生了多态,所以不能使用父类的对象obj调用父类中不存在的属性age,
所以会报错。所以我们应该使用子类Student对象来调用子类中的属性age
而这里obj是父类对象,我们需要使用向下转型将父类对象obj转换为子类对象
因为发生向下类型转换,为了防止发生转换异常,所以我们要判断子类对象类型
*/
Student s=null;
if(obj instanceof Student)
{
s=(Student)obj;
}
return this.age==s.age;
}
}
class ObjectDemo2
{
public static void main(String[] args)
{
/*
创建两个对象
下面的两个对象表示在对空间中开辟两个不同的空间
一个空间叫做s,另一个空间叫做s1
*/
Student s=new Student("黑旋风",17);
Student s1=new Student("技导",17);
//使用黑旋风的对象s调用compareAge函数
//使用flag来接受返回回来的值
//boolean flag=s.compareAge(s1);
/*
public boolean equals(Object obj) {
这里的this记录着调用这个方法的对象s的堆中内存地址名
obj表示传递进来的参数对象s1,Object obj=new Student("技导",17);这里发生多态
obj里面存放的也是s1中的堆中内存地址
s和s1的堆中内存地址名不同
return (this == obj);
}
public boolean compareAge(Student a)
{
return this.age==a.age;
}
*/
boolean flag=s.equals(s1);
System.out.println(flag);
/*
如果返回回来的值是true,说明是同龄人
如果返回回来的值是false,说明不是同龄人
*/
if(flag==true)
{
System.out.println("是同龄人");
}else
{
System.out.println("不是同龄人");
}
}
}
总结:
关系运算中的==和equals的区别:
-
equals函数是用来比较2个对象是否相等的。要比较对象是否相等,必须调用函数(equals)来比较。但是必须得复写equals函数。
-
而学习的关系运算中的 == 只能比较2个具体的数据是否相等。
4.5、toString方法介绍
需求:输出Student类的具体信息,也就是根据输出Student类的对象来输出Student的具体名字和姓名。
-
定义一个Student类,在这个类中定义两个name和age属性;
-
在Student类中定义一个构造函数,给name和age两个属性赋值;
-
定义一个测试类,在这个测试类中创建Student类的对象并初始化值;
-
在屏幕上打印Student类的对象;
以上程序的运行结果,不符合需求。要求是想要输出学生类中的具体信息,比如通过打印对象的名字stu,我们希望打印出具体的stu对象所拥有的名字和年龄,而打印一串地址名在开发中没有什么太大意义。
问题:为什么输出stu时,显示的结果:Student@7ea06d25?为什么输出的是一个引用地址而不是我们想要的对象的属性的值呢?我们应该怎么做才能打印出属性的值而不是打印一串地址的值呢?
这里我们需要借助Object类中的toString方法来解决,toString是一个方法,它需要对象来调用,toString的意思是将调用它的对象转换成字符串形式。
我们在打印对象的时候也可以按照如下方法去做:
System.out.println(stu.toString());打印的结果和我们写System.out.println(stu);是一样的。
在上述打印语句中println()的方法中打印对象stu的时候,如果不加 .toString()方法,在println()的方法中默认也是使用对象stu调用toString()方法,所以在开发中写与不写toString()方法是一样的。
根据以上分析我们想要建立自定义对象的表现形式,我们需要覆盖超类(所有类的父类)中的toString()方法就可以了。
在Student类中可以重写toString方法,让程序输出学生类的具体信息,代码实现如下:
小结:
在开发中,如果子类继承父类时,父类中已经存在了解决问题的功能,但是父类中的功能并不能满足子类的需求,这时,子类就需要重写(覆盖)父类中的方法。