以前刚在《数据结构与算法分析》中看到泛型这个章节总觉得莫名其妙,因为它更像是一种约束,而不是能称作为具体的类型,毕竟泛型在编译后不会有任何信息,最终还是回归到具体的数据结构。
那么既然书本上写了这么章节,应该有它的用意,再说老朽本身就是java出身,特地绕过它总给人一种“自己学得不扎实”的样纸,那就索性装个B。
泛型为什么会存在?
学过java的朋友都知道-万物皆对象(基本类型也可以包装成对象,如int ->Integer),那既然什么东西都是对象,我们把操作入口的接收类型改成对象不就完事了吗?
比如说人类吃东西,大体上是先嚼再吞,所以我们可以这样定义:
class Human {
public void eat(Object sth) {
}
}
这样我们可以定义些吃的东西,比如米饭、包子、苹果、辣条等,eat方法都能接收。
这看起来不是很好吗?
你还别说,java 1.5以前确实是这样玩的。
但是再细想一下,总感觉有些不对劲儿,比如说吃米饭要用筷子,包子直接咬就行,苹果可能要清洗或者削皮,辣条还要拆开包装,总之每个东西的吃法还不一样。
有人说,我在eat()方法中加个判断不行吗?
如果是米饭……如果是……如果……如……女……很明显不是程序员能干出来的事儿。
那么是不是可以这样呢,java不是有继承吗?我写个基类先:
class Food {
/**
* Teach human how to eat it.
*/
public void eatMethod() {}
}
然后再定义每种食物的食用方法,比如苹果。
class Apple {
public void eatMethod() {
// Wash first.
}
}
最后我们规定人类只能吃食物。
public void eat(Food food) {
food.eatMethod();
// Continue ...
}
看似合理些了,但是也并非食物才能吃,土、树皮、草根虽然大部分不是食物,但也是可以吃的啊,只不过吃下去可能会出事罢了。
那我们再改下,先约定什么是可以吃的:可以嚼并能吞下去的东西。
interface Eatable {
void chew()
void swallow()
}
这样我们平常吃的食物理所当然可被定义为“可以吃的”。
class Food implements Eatable {
void chew() {}
void swallow() {}
}
然后在所有的食物中实现”嚼“和”吞“的方法。
Human的eat方法也接收Eatable。
public void eat(Eatable sth) {
sth.chew();
sth.swallow();
// Continue ...
}
即使土、树皮、草根、烂叶子也来实现这个接口。
这样人都能去吃它们了。。。
完美但不可行,按这个搞法,每个物体都要实现成千上万个接口了。
*还是得找一个更加科学的方式~~~~*
我们知道吃东西是为了获取能量的,吃肉能获取吃肉的力量,吃大米有吃大米的力量,吃辣条有吃辣条的力量……
所以eat应该返回力量才合理。
我们不妨先定义一个力量。
class Strength {
}
那所谓“吃肉的力量”、“吃大米的力量”、“吃辣条的力量”等到底怎么去定义呢?诶,我们按照1.5之前java的做法定义一个Object。
class Strength {
/**
* Eaten object as well as the source of strength.
*/
Object source;
}
再改下eat方法。
public Strength eat(Eatable sth) {
sth.chew();
sth.swallow();
// Continue to eat.
// Create a strength after human eaten.
Strength strength = new Strength();
strength.source = sth;
return strength;
}
虽然编译是通过了,但是Strength仍然不知道人类吃进去的到底是什么,吃了把土却给了你士力架的力量,这是不科学的。
所以说,即使你把食物从Object缩小成了Eatable这个很小的范围,仍然不能确切知道它具体是什么。
泛型的出现,在具体和抽象之间架起了一座桥梁。
奥斯特洛夫的司机
于是乎,泛型便应运而生了。
有了泛型,我们再来修改Strength类的定义。
class Strength<T> {
T source;
}
再修改eat方法。
public <T extends Eatable> Strength<T> eat(T sth) {
// 嚼
// 吞
TODO("Pending to implement.")
}
这样的话,它就可以约束我们吃啥返回啥的能量,不需要任何的类型转换,编译器会告诉你一切理所当然。
Strength<Apple> appleStrength = new Human().eat(new Apple());
反过来说,你想获得苹果的能量你只能吃苹果,非常合理!
使用泛型
其实上面也简单地介绍了泛型的基本用法,下面我们还结合书本谈谈泛型容易让人误解的地方。
继续上面的例子,人吃东西获取能量,接下来是消耗,我们在Human里接着写一个方法。
public void use(Strength<Eatable> strength) {
}
由于参数是一个吃了东西的能量,这样我们就可以吃任何可以吃的东西并使用它们能量了。
// Eat an apple.
Strength<Apple> appleStrength = human.eat(new Apple());
human.use(appleStrength); // COMPILE ERROR!
// Eat an orange.
Strength<Orange> orangeStrength = human.eat(new Orange());
human.use(orangeStrength);// COMPILE ERROR!
可以当我们看似合情合理地写出上面这段代码后,发现IDE竟然报红了。
就是说Strength<Apple> IS NOT A Strength<Eatable>。
明明都是一个Strength啊,为什么加了个类型就不是一个东西了呢?因为泛型不支持协变,那么为什么不支持呢?因为假如我们支持的话,就可以写出如下代码:
Strength<Eatable> sten1 = new Strength<Apple>();
sten1 = new Strength<Orange>();
这个问题就可以回归到:Apple apple = new Orange(),很明显是不合逻辑的。
值得一提的是,数组是支持协变的,而集合却不支持。
// Covariance in Arrays and Collections.
Eatable[] eatArray = new Apple[10];
ArrayList<Eatable> eatList = new ArrayList<Apple>();// COMPILE ERROR!
所以如果想让一开始编译通过,可以加个通配符。
public void use(Strength<? extends Eatable> strength) {
}
既然Strength<Apple> IS NOT A Strength<Eatable>,那二者有什么关系呢?
@Test
public void instanceofTest() {
// Create an apple strength.
Strength<Apple> appleStrength = new Strength<>(new Apple());
// Set to a more generic type.
Strength<? extends Eatable> eatStrength = appleStrength;
// Generic converting from super to child.
Strength<Orange> orangeStrength = (Strength<Orange>)eatStrength;
// 这一句会不会报错???
System.out.println("instanceofTest " + orangeStrength.source);
}
大家不凡先仔细想想,会报错吗?
结果并没有报错:
但只要稍微调用Orange的方法,则报类型转换错误。
// 这一句会不会报错???
System.out.println("instanceofTest " + orangeStrength.source.getPetalCount());
其实报错很好理解,就是把Apple转换成Orange是不可能的,但上面不报错却难以理解,为什么instanceof会认为Strength<Apple>的实例是一个Strength<Orange>呢?
泛型的擦除
之所以说Strength<Apple>被误认为是Strength<Orange>,归根结底还是java中泛型的实现方式,那就是擦除。
什么叫擦除呢?就是说泛型在编译期有关泛型的相关信息都会被擦除,比如Strength<Apple>在编译后就只剩下。
class Strength {
Apple source;
Strength(Apple source) {
this.source = source;
}
}
同理,Strength<Orange>只是把source类型换成Orange了而已,所以二者就是同一个东西,换句话说,无论泛型参数是什么,所有Strength<T>的实例都是从Strength实例化来的。
泛型与static
有时候我们很容易写出这样代码。
class Strength<T> {
/***
* Add strength with another.
* @param another strength with same source.
* @return sum of two strength.
*/
public static Strength<T> add(Strength<T> another) {
}
}
把两个力量相加,吃一个苹果,再吃一个,最终的能量肯定是两个之和,很合理,但是代码这样写却是有问题的。
根本原因我认为只有一点:那就是静态方法是类方法,是供所有实例使用的统一方法,那么这个方法就不应该随着类的泛型参数的变化而变化,如果说上面的add方法中的参数根据实例化时指定的类型绑定,再谈静态方法的全局特性在逻辑上是说不通的,并非技术上无法实现。
既然说人都能说话,你就不能强制中国人只能说中国话,美国人只能说美国话,一个道理。
至于什么泛型参数在编译后类型被擦除云云,我认为是靠不住的,因为编译器完全知道被擦除前的类型,所以完全可以把静态方法中的T替换为具体的类型或者Object。
好了,今天的废话就聊这么多了,其实很多都是java里的知识,就当和大家一起复习了。
git: https://github.com/codersth/dsa-study.git
文件:GenericTypeTest