首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
80 阅读
2
nvim番外之将配置的插件管理器更新为lazy
58 阅读
3
2018总结与2019规划
54 阅读
4
PDF标准详解(五)——图形状态
33 阅读
5
为 MariaDB 配置远程访问权限
30 阅读
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
Java
emacs
linux
文本编辑器
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
308
篇文章
累计收到
27
条评论
首页
栏目
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
页面
归档
友情链接
关于
搜索到
2
篇与
的结果
2019-06-01
Java 接口与多态
上一篇说了Java面向对象中的继承关系,在继承中说到:调用对象中的成员变量时,根据引用类型来决定调用谁,而调用成员方法时由于多态的存在,具体调用谁的方法需要根据new出来的对象决定,这篇主要描述的是Java中的多态以及利用多态形成的接口多态当时在学习C++时,要使用多态需要定义函数为virtual,也就是虚函数。类中存在虚函数时,对象会有一个虚函数表的头指针,虚函数表会存储虚函数的地址,在使用父类的指针或者引用来调用方法时会根据虚函数表中的函数地址来调用函数,会形成多态。当时学习C++时对多态有一个非常精炼的定义:基类的指针指向不同的派生类,其行为不同。这里行为不同指的是调用同一个虚函数时,会调用不同的派生类函数。这里我们说形成多态的几个基本条件:1)指针或者引用类型是基类;2)需要指向派生类;3)调用的函数必须是基类重写的函数。public class Parent{ public void sayHelllo(){ System.out.println("Hello Parent"); } public void sayHello(String name){ System.out.println("Hello" + name); } } public class Child extends Parent{ public void sayHello(){ System.out.println("Hello Child"); } }根据上述的继承关系,我们来看下面几个实例代码,分析一下哪些是多态Parent obj = new Child(); obj.sayHello();该实例构成了多态,它满足了多态的三个条件:Parent 类型的 obj 引用指向了 new 出来的Child子类、并且调用了二者共有的方法。Parent obj = new Child(); obj.sayHello("Tom");这个例子没有构成多态,虽然它满足基类的引用指向派生类,但是它调用了父类特有的方法。Parent obj = new Parent(); obj.sayHello();这个例子也不满足多态,它使用父类的引用指向了父类,这里就是一个正常的类方法调用,它会调用父类的方法Child obj = new Child(); obj.sayHello();这个例子也不满足多态,它使用子类的引用指向了子类,这里就是一个正常的类方法调用,它会调用子类的方法那么多态有什么好处呢?引入多态实质上也是为了避免重复的代码,而且程序更具有扩展性,我们通过println函数来说明这个问题。public void println(Object x) { String s = String.valueOf(x); synchronized (this) { print(s); newLine(); } } //Class String public static String valueOf(Object obj) { return (obj == null) ? "null" : obj.toString(); }函数println实现了一个传入Object的重载,该函数调用了String类的静态方法 valueOf, 进一步跟到String类中发现,该方法只是调用了类的 toString 方法,传入的obj可以是任意继承Object的类(在Java中只要是对象就一定是继承自Object),只要类重写了 toString 方法就可以直接打印。这样一个函数就实现了重用,相比于需要后来的人额外重载println函数来说,要方便很多。类类型转化上面的println 函数,它需要传入的是Object类的引用,但是在调用该方法时,从来都没有进行过类型转化,都是直接传的,这里是需要进行类型转化的,在由子类转到父类的时候,Java进行了隐式类型转化。大转小一定是安全的(这里的大转小是对象的内存包含关系),子类一定可以包含父类的成员,所以即使转化为父类也不存在问题。而父类引用指向的内存不一定就是包含了子类成员,所以小转大不安全。为什么要进行小转大呢?虽然多态给了我们很大的方便,但是多态最大的问题就是父类引用无法看到子类的成员,也就是无法使用子类中的成员。这个时候如果要使用子类的成员就必须进行小转大的操作。之前说过小转大不安全,由于父类可能有多个实现类,我们无法确定传进来的参数就是我们需要的子类的对象,所以java引入了一个关键字 instanceof 来判断是否可以进行安全的转化,只要传进来的对象引用是目标类的对象或者父类对象它就会返回true,比如下面的例子Object obj = "hello" System.out.println(obj instanceof String); //true System.out.println(obj instanceof Object); //true System.out.println(obj instanceof StringBuffer); //false System.out.println(obj instanceof CharSequence); //true抽象方法和抽象类我们说有了多态可以使代码重用性更高。但是某些时候我们针对几个有共性的类,抽象出了更高层面的基类,但是发现基类虽然有一些共性的内容,但是有些共有的方法不知道如何实现,比如说教科书上经常举例的动物类,由于不知道具体的动物是什么,所以也无法判断该动物是食草还是食肉。所以一般将动物的 eat 定义为抽象方法,拥有抽象方法的类一定必须是抽象基类。抽象方法是不需要写实现的方法,它只需提供一个函数的原型。而抽象类不能创建实例,必须有派生类重写抽象方法。为什么抽象类不能创建对象呢?对象调用方法本质上是根据函数表找到函数对应代码所在的内存地址,而抽象方法是未实现的方法,自然就无法给出方法的地址了,如果创建了对象,而我的对象又想调用这个抽象方法那不就冲突了吗。所以规定无法实例化抽象类。抽象方法的定义使用关键字 abstract,例如public abstract class Life{ public abstract void happy(); } public class Cat{ public void happy(){ System.out.println("猫吃鱼"); } } public class Cat{ public void happy(){ System.out.println("狗吃肉"); } } public class Altman{ public void happy(){ System.out.println("奥特曼打小怪兽"); } }上面定义了一个抽象类Life 代表世间的生物,你要问生物的幸福是什么,可能没有人给你答案,不同的生物有不同的回答,但是具体到同一种生物,可能就有答案了,这里简单的给出了答案:幸福就是猫吃鱼狗吃肉奥特曼爱打小怪兽。使用抽象类需要注意下面几点:不能直接创建抽象类的对象,必须使用实现类来创建对象实现类必须实现抽象类的所有抽象方法,否则该实现类也必须是抽象类抽象类可以有自己的构造方法,该方法仅供子类构造时使用抽象类可以没有抽象方法,但是有抽象方法的一定要是抽象类接口接口就是一套公共的规范标准,只要符合标准就能通用,比如说USB接口,只要一个设备使用了USB接口,那么我的电脑不管你的设备是什么,插上就应该能用。在代码中接口就是多个类的公共规范。Java中接口也是一个引用类型。接口与抽象类非常相似,同样不能创建对象,必须创建实现类的方法。但是接口与抽象类还是有一些不同的。 抽象类也是一个类,它是从底层类中抽象出来的更高层级的类,但是接口一般用来联系多个类,是多个类需要实现的一个共同的标准。是从顶层一层层扩展出来的。接口的一个常见的使用场景就是回调,比如说常见的窗口消息处理函数。这个场景C++中一般使用函数指针,而Java中主要使用接口。接口使用关键字 interface 来定义, 比如public interface USB{ public final String deviceType = "USB"; public abstract void open(); public abstract void close(); }接口中常见的一个成员是抽象方法,抽象方法也是由实现类来实现,注意事项也与之前的抽象类相同。除了有抽象方法,接口中也可以有常量。接口中的抽象方法是没有方法体的,它需要实现类来实现,所以实现类与接口中发生重写现象时会调用实现类,那么常量呢?public class Mouse implements USB{ public final String deviceType = "鼠标"; public void open(){ } public void close(){ } } public class Demo{ public static void main(String[] args){ USB usb = new Mouse(); System.out.println(usb.deviceType); } }常量的调用遵循之前说的重载中的属性成员调用的方式。使用的是什么类型的引用,调用哪个类型中的成员。与抽象类中另一个重要的不同是,接口运行多继承,那么在接口的多继承中是否会出现冲突的问题呢public interface Storage{ public final String deviceType = "存储设备"; public abstract void write(); public abstract void read(); } public class MobileHardDisk implements USB, Storage{ public void open(){ } public void close(){ } public void write(){ } public void read(){ } } public class Demo{ public static void main(String[] args){ MobileHardDisk mhd = new MobileHardDisk(); System.out.println(mhd.deviceType); } }编译上述代码时会发现报错了,提示 USB 中的变量 deviceType 和 Storage 中的变量 deviceType 都匹配 ,也就是说Java中仍然没有完全避免冲突问题。接口中的默认方法有的时候可能会出现这样的情景,当项目完成后,可能客户需求有变,导致接口中可能会添加一个方法,如果使用抽象方法,那么接口所有的实现类都得重复实现某个方法,比如说上述的代码中,USB接口需要添加一个方法通知PC设备我这是什么类型的USB设备,以便操作系统匹配对应的驱动。那么可能USB的实现类都需要添加一个,这样可能会引入大量重复代码,针对这个问题,从Java 8开始引入了默认方法。默认方法为了解决接口升级的问题,接口中新增默认方法时,不用修改之前的实现类。默认方法的使用如下:public interface USB{ public final String deviceType = "USB"; public abstract void open(); public abstract void close(); public default String getType(){ return this.deviceType; } }默认方法同样可以被所有的实现类覆盖重写。接口中的静态方法从Java 8中开始,允许在接口中定义静态方法,静态方法可以使用实现类的对象进行调用,也可以使用接口名直接调用接口中的私有方法从Java 9开始运行在接口中定义私有方法,私有方法可以解决在默认方法中存在大量重复代码的情况。虽然Java为接口中新增了这么多属性和扩展,但是我认为不到万不得已,不要随便乱用这些东西,毕竟接口中应该定义一系列需要实现的标准,而不是自己去实现这些标准。最后总结一下使用接口的一些注意事项:接口没有静态代码块或者构造方法一个类的父类只能是一个,但是类可以实现多个接口如果类实现的多个接口中有重名的默认方法,那么实现类必须重写这个实现方法,不然会出现冲突。如果接口的实现类中没有实现所有的抽象方法,那么这个类必须是抽象类父类与接口中有重名的方法时,优先使用父类的方法,在Java中继承关系优于接口实现关系接口与接口之间是多继承的,如果多个父接口中存在同名的默认方法,子接口中需要重写默认方法,不然会出现冲突final关键字之前提到过final关键字,用来表示常量,也就是无法在程序中改变的量。除了这种用法外,它还有其他的用法修饰类,表示类不能有子类。可以将继承关系理解为改变了这个类,既然final表示常量,不能修改,那么类自然也不能修改修饰方法:被final修饰的方法不能被重写修饰成员变量:表示成员变量是常量,不能被修改修饰局部变量:表示局部变量是常量,在对应作用域内不可被修改
2019年06月01日
4 阅读
0 评论
0 点赞
2016-07-05
C++多态
面向对象的程序设计的三大要素之一就是多态,多态是指基类的指针指向不同的派生类,其行为不同。多态的实现主要是通过虚函数和虚表来完成,虚表保存在对象的头四个字节,要调用虚函数必须存在对象,也就是说虚函数必须作为类的成员函数来使用。编译器为每个拥有虚函数的对象准备了一个虚函数表,表中存储了虚函数的地址,类对象在头四个字节中存储了虚函数表的指针。下面是一个具体的例子class CVirtual { public: virtual void showNumber(){ printf("%d\n", nNum); } virtual void setNumber(int n){ nNum = n; } private: int nNum; }; int main() { CVirtual cv; cv.setNumber(2); cv.showNumber(); return 0; }上述这段代码定义了两个虚函数setNumber和showNumber,并在主函数中调用了他们,下面通过反汇编的方式来展示编译器是如何调用虚函数的26: CVirtual cv; 00401288 lea ecx,[ebp-8];对象中有一个整形数字占4个字节,同时又有虚函数,头四个字节用来存储虚函数表指针,总共占8个字节 0040128B call @ILT+25(CVirtual::CVirtual) (0040101e);调用构造函数 27: cv.setNumber(2); 00401290 push 2 00401292 lea ecx,[ebp-8] 00401295 call @ILT+0(CVirtual::setNumber) (00401005);调用虚函数 28: cv.showNumber(); 0040129A lea ecx,[ebp-8] 0040129D call @ILT+5(CVirtual::showNumber) (0040100a);调用虚函数 29: return 0; 004012A2 xor eax,eax ;构造函数 0401389 pop ecx;还原ecx使得ecx保存对象的首地址 0040138A mov dword ptr [ebp-4],ecx 0040138D mov eax,dword ptr [ebp-4] 00401390 mov dword ptr [eax],offset CVirtual::`vftable' (0042f020);将虚函数表的首地址赋值到对象的头4个字节 00401396 mov eax,dword ptr [ebp-4] 00401399 pop edi 0040139A pop esi 0040139B pop ebx 0040139C mov esp,ebp 0040139E pop ebp 0040139F ret ;setNumber(int n) 0040134A mov dword ptr [ebp-4],ecx 18: nNum = n; 0040134D mov eax,dword ptr [ebp-4];eax = ecx 00401350 mov ecx,dword ptr [ebp+8] 00401353 mov dword ptr [eax+4],ecx 从上面的汇编代码可以看到,当类中有虚函数的时候,编译器会提供一个默认的构造函数,用于初始化对象的头4个字节,这四个字节存储的是一个指针值,我们定位到这个指针所在的内存如下图:这段内存中存储了两个值,分别为0x0040100AH和0x00401005H,我们执行后面的代码发现,这两个地址给定的是虚函数所在的地址。在调用时编译器直接调用对应的虚函数,并没有通过虚表来寻址到对应的函数地址。下面看另外一个例子:class CParent { public: virtual void showClass() { printf("CParent\n"); } }; class CChild:public CParent { public: virtual void showClass() { printf("CChild\n"); } }; int main() { CParent *pClass = NULL; CParent cp; CChild cc; pClass = &cp; pClass->showClass(); pClass = &cc; pClass->showClass(); pClass = NULL; return 0; }上述代码定义了一个基类和派生类,并且在派生类中重写了函数showClass,在调用时用分别利用基类的指针指向基类和派生类的对象来调用这个虚函数,得到的结果自然是不同的,这样构成了多态。下面是它的反汇编代码,这段代码基本展示了多态的实现原理29: CParent *pClass = NULL; 00401288 mov dword ptr [ebp-4],0 30: CParent cp; 0040128F lea ecx,[ebp-8] 00401292 call @ILT+30(CParent::CParent) (00401023);调用构造函数 31: CChild cc; 00401297 lea ecx,[ebp-0Ch] 0040129A call @ILT+0(CChild::CChild) (00401005) 32: pClass = &cp; 0040129F lea eax,[ebp-8];取对象的首地址 004012A2 mov dword ptr [ebp-4],eax; 33: pClass->showClass(); 004012A5 mov ecx,dword ptr [ebp-4] 004012A8 mov edx,dword ptr [ecx];将指针值放入到edx中 004012AA mov esi,esp 004012AC mov ecx,dword ptr [ebp-4];获取虚函数表指针 004012AF call dword ptr [edx];调转到虚函数指针所对应的位置执行代码 004012B1 cmp esi,esp 004012B3 call __chkesp (00401560) 34: pClass = &cc; 004012B8 lea eax,[ebp-0Ch] 004012BB mov dword ptr [ebp-4],eax 35: pClass->showClass(); 004012BE mov ecx,dword ptr [ebp-4] 004012C1 mov edx,dword ptr [ecx] 004012C3 mov esi,esp 004012C5 mov ecx,dword ptr [ebp-4] 004012C8 call dword ptr [edx] 004012CA cmp esi,esp 004012CC call __chkesp (00401560) 36: pClass = NULL; 004012D1 mov dword ptr [ebp-4],0 37: return 0; 004012D8 xor eax,eax从上述代码来看,在调用虚函数时首先根据头四个字节的值找到对应的虚函数表,然后根据虚函数表中存储的内容来找到对应函数的地址,最后根据函数地址跳转到对应的位置,执行函数代码。由于虚函数表中的虚函数是在编译时就根据对象的不同将对应的函数装入到各自对象的虚函数表中,因此,不同的对象所拥有的虚函数表不同,最终根据虚函数表寻址到的虚函数也就不同,这样就构成了多态。对于虚函数的调用,先后经历了几次间接寻址,比直接调用函数效率低了一些,通过虚函数间接寻址访问的情况只有利用类对象的指针或者引用来访问虚函数时才会出现,利用对象本身调用虚函数时,没有必要进行查表,因为已经明确调用的是自身的成员函数,没有构成多态,查表只会降低程序的运行效率。
2016年07月05日
6 阅读
0 评论
0 点赞