简单理解就是,一个对象可以表现出多种状态。可以看做是对抽象对象的逆过程,具体化抽象对象的行为。而它是如何实现这种表现出多种状态功能的呢。
从Java语法上来讲有如下两种方式:
使用继承:
将父对象(更抽象的对象/或者说基类)设置成为一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
class Aninal{ void sound(发出声音);}//sound是动物的一个叫声方法
class cat extend Animal{sound(喵);}
class dog extend Animal{sound(汪);}
//Animal a 可以根据其具体的子对象(cat dog)来发出声音
//这就是多态,animal根据其子类表现出了不同的特性
Animal a = new dog();
a.sound();
Animal a = new cat();
a.sound();
从上面可能并没有表现出多态的真正作用,后面我会详细解释。
实现接口(推荐):
在某种程度上来说,接口是一种更为广泛的概念,并不仅仅是Java语法中的Interface,在编程思想中,凡是一种抽象对象,作为一个中间接入的都是接口。但这里我们只说Java中通过Interface来实现多态。
其实在我看来,Java的Interface可以看做是一个特殊的对象,一个普通对象可以实现多个接口(类似于继承多个Interface)。因为Java中只允许单继承,但是很多情况下,为了提高程序的扩展性,需要支持这种实现多个接口的功能,于是才有了Interface。
interface Animal
{
void sound();//接口中不写具体实现
}
class cat implements Animal
{
//具体实现类必须要实现接口中的方法
@Override
public void sound(喵喵喵);
}
但是得提醒一点,实现接口并不等同于是多重继承,这种实现接口的方式,还弥补了多重继承的不足。因为实现接口,要求子类必须实现接口方法,而多重继承则不会有这种强制要求,这就有可能造成多重继承的混乱,比如砖石问题:
有两个类B和C继承自A。假设B和C都继承了A的方法并且进行了覆盖,编写了自己的实现。假设D通过多重继承继承了B和C,那么D应该继承B和C的重载方法,那么它应该继承哪个的呢?是B的还是C的呢?
另外补充说一句,像python、C++语言就没有Interface,因为他们支持多继承,所以在这些语言中
普通类就可以直接实现多继承,从而实现Java的接口功能,所以我上面才说接口不仅仅是Interface,接口更是一种程序设计的概念。
总结一下,在实现多态时,推荐通过实现Interface来实现,因为在软件开发的过程中,无法保证当前对象只需要来自于一个基类的功能,可能在后期的开发中,还要有其他功能需要实现,而由于java的单继承特性无法满足后面的扩展要求。
所以推荐将一些通用对象做成接口,这样方便以后扩展。其实Java语言中有很多这样的体现,比如说在实践开发中对于很多对象,需要同时实现序列化接口、可比较接口等,这就需要在类中重写这些接口方法。
基本功能上来说,就是解决项目中紧偶合的问题,提高程序的可扩展性.。耦合度讲的是模块模块之间,代码代码之间的关联度,通过对系统的分析把他分解成一个一个子模块,子模块提供稳定的接口,达到降低系统耦合度的的目的,模块模块之间尽量使用模块接口访问,而不是随意引用其他模块的成员变量。
下面举一个例子来说明,如何通过多态来降低耦合,提高程序的可扩展性。
class master
{
//主人对象,buy_pet,购买宠物
List<> pets = new ArrayList();
public void buy_pet(Animal a){pets..add(a);}
}
当我们需要买dog时,我们只需要传一个dog对象即可,当我们买cat时只需要传一个cat对象,而不需要在master类中写多个关于不同参数动物对象的buy_pet方法。
而且当宠物店进了新品种,master也不需要改动,只需要新品种实现Animal接口即可。这就实现了降低master同具体宠物之间的耦合,提高了程序的可扩展性,master对象和具体宠物对象只需要通过Animal接口来进行访问。
说到原理,就不得不提到,Java前期(静态)绑定和后期(动态)绑定。
下面来源参考:https://www.cnblogs.com/jstarseven/articles/4631586.html
绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来。对java来说,绑定分为静态绑定和动态绑定;或者叫做前期绑定和后期绑定.
在程序执行前方法已经被绑定(也就是说在编译过程中就已经知道这个方法到底是哪个类中的方法),此时由编译器或其它连接程序实现。例如:C。
针对java简单的可以理解为程序编译期的绑定;这里特别说明一点,java当中的方法只有final,static,private和构造方法是前期绑定
在运行时根据具体对象的类型进行绑定。
若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。
动态绑定的过程:
比如:Parent p = new Children();
其具体过程细节如下:
1:编译器检查对象的声明类型和方法名。
假设我们调用x.f(args)方法,并且x已经被声明为C类的对象,那么编译器会列举出C 类中所有的名称为f 的方法和从C 类的超类继承过来的f 方法。
2:接下来编译器检查方法调用中提供的参数类型。
如果在所有名称为f 的方法中有一个参数类型和调用提供的参数类型最为匹配,那么就调用这个方法,这个过程叫做“重载解析”。
3:当程序运行并且使用动态绑定调用方法时,虚拟机必须调用同x所指向的对象的实际类型相匹配的方法版本。
假设实际类型为D(C的子类),如果D类定义了f(String)那么该方法被调用,否则就在D的超类中搜寻方法f(String),依次类推。
JAVA 虚拟机调用一个类方法时(静态方法),它会基于对象引用的类型(通常在编译时可知)来选择所调用的方法。相反,当虚拟机调用一个实例方法时,它会基于对象实际的类型(只能在运行时得知)来选择所调用的方法,这就是动态绑定,是多态的一种。动态绑定为解决实际的业务问题提供了很大的灵活性,是一种非常优美的机制。
与方法不同,在处理java类中的成员变量(实例变量和类变量)时,并不是采用运行时绑定,而是一般意义上的静态绑定。所以在向上转型的情况下,对象的方法可以找到子类,而对象的属性(成员变量)还是父类的属性(子类对父类成员变量的隐藏)。
public class Father {
protected String name = "父亲属性";
}
public class Son extends Father {
protected String name = "儿子属性";
public static void main(String[] args) {
Father sample = new Son();
System.out.println("调用的属性:" + sample.name);
}
}
输出结果还是“父亲属性“,因为成员变量是静态绑定,在编译器就确定了sample对象的成员变量。
现在试图调用子类的成员变量name,该怎么做?最简单的办法是将该成员变量封装成方法getter形式。
public class Father {
protected String name = "父亲属性";
public String getName() {
return name;
}
}
public class Son extends Father {
protected String name = "儿子属性";
public String getName() {
return name;
}
public static void main(String[] args) {
Father sample = new Son();
System.out.println("调用的属性:" + sample.getName());
}
}
这样sample.getName()是动态绑定,可以找到其子类son,并通过getName()返回子类son的name。
从名称上看,指的就是更广泛的类型,它是一种更高级的抽象对象,可以在不同层次的抽象中使用。
从上面看多态的父对象是一级抽象,而泛型可以在其父对象或者说某一类对象的基础上,再进行一层抽象,当然也可以直接对具体对象进行一级抽象,所以这就是说可以实现不同层次的抽象。
多态也可以实现多级抽象,只需要链式继承多次,就可以得到更高的抽象对象。比如object对象就是原始最高级的抽象对象,只需要在需要使用泛型的方法中将参数类型设为object基类,那么,该方法就可以接受从这个基类中导出的任何类作为参数。这也算是实现了泛型想要的功能。
在类的内部,凡是需要说明类型的地方,如果都使用object基类,确实能够具备很好的灵活性。但是如果考虑除了final类(不能扩展),其他任何类都可以被扩展,虽然灵活性高,而这种动态绑定行为无疑会带来性能损耗。
正是由于这种通过object对象来使用多态实现泛化对象的弊端(运行时检查带来的无法在编程中发现类型错误,性能损失)所以才出现了泛型。
举个例子:
在Java5之前,泛型程序设计是用继承实现的。例如ArrayList类想要实现泛型,则维护一个Object引用的数组:
//before generic classes
public class ArrayList {
private Object[] elementData;
…
public Object get(int i) {…}
public void add(Object object) {…}
}
这种方法有两个问题,当获取一个值时必须进行强制类型转换
ArrayList files = new ArrayList();
…
String fileName = (String)files.get(0);
此外,这里没有错误检查。可以向数组列表中添加任何类的对象。
files.add(new File("…"));
对于这个调用,编译和运行都不会出错。然而在其他地方,如果将get的结果强制类型转换为String类型,就会产生一个错误。
泛型提供了一个更好的解决方案:
Java5以后,ArrayList有一个类型参数来指定元素的类型:
ArrayListfiles = new ArrayList ();
在Java7及以后的版本中,构造函数中可以省略泛型类型:ArrayListfiles = new ArrayList<>();
这显然使得代码具有更好的可读性。人们一看这个数组列表中包含的是String对象。
从上面可以看出,泛型在集合中实现了强类型,实现了参数化类型的概念,在编译时就可以检查类型。所以总结一下,Java的泛型机制就是为了弥补以前的版本中,在实现泛型化功能中编译阶段无法检查到类型错误的不足,而提出的。
所以说Java的泛型就是参数化类型,为了实现类型参数化,在编译阶段即可检查到错误,提升编码安全。
泛型的实现主要就是考虑如何实现参数类型化,即可以在编译阶段就发现错误。
下面参考:https://blog.csdn.net/romantic112/article/details/80513372
Java泛型是编译时技术,在运行时不包含类型信息,仅其实例中包含类型参数的定义信息。
Java利用编译器擦除(erasure,前端处理)实现泛型,基本上就是泛型版本源码到非泛型版本源码的转化。
擦除去掉了所有的泛型类内所有的泛型类型信息,所有在尖括号之间的类型信息都被扔掉.
举例来说:List类型被转换为List,所有对类型变量String的引用被替换成类型变量的上限(通常是Object)。
而且,无论何时结果代码类型不正确,会插入一个到合适类型的转换。
public T badCast(T t, Object o) {
return (T) o; // unchecked warning
}
这说明String类型参数在List运行时并不存在。它们也就不会添加任何的时间或者空间上的负担。但同时,这也意味着你不能依靠他们进行类型转换。
一个泛型类被其所有调用共享
对于上文中的GenericClass,在编译后其内部是不存入泛型信息的,也就是说:
GenericClass gclassA = new GenericClass();
GenericClass gclassB = new GenericClass();
gClassA.getClass() == gClassB.getClass()
这个判断返回的值是true,而非false,因为一个泛型类所有实例运行时具有相同的运行时类,其实际类型参数被擦除了。
那么是不是GenericClass里完全不存AClass的信息呢?这个也不是,它内部存储的是泛型向上父类的引用,比如:
GenericClass, 其编译后内部存储的泛型替代是Charsequence,而不是Object。
那么我们编码时的泛型的类型判断是怎么实现的呢?
其实这个过程是编译时检查的,也就是说限制gClassA.add(new BClass()) 这样的使用的方式的主体,不是运行时代码,而是编译时监测。
泛型的意义就在于,对所有其支持的类型参数,有相同的行为,从而可以被当作不同类型使用;类的静态变量和方法在所有实例间共享使用,所以不能使用泛型。
泛型与instanceof
泛型擦除了类型信息,所以使用instanceof检查某个实例是否是特定类型的泛型类是不可行的:
GenericClass genericClass = new GenericClass();
if (genericClass instanceof GenericClass) {} // 编译错误
同时:
GenericClass class1 = (GenericClass) genericClass; //会报警告