【一起学数据结构与算法分析】第四篇:泛型漫谈

以前刚在《数据结构与算法分析》中看到泛型这个章节总觉得莫名其妙,因为它更像是一种约束,而不是能称作为具体的类型,毕竟泛型在编译后不会有任何信息,最终还是回归到具体的数据结构。

那么既然书本上写了这么章节,应该有它的用意,再说老朽本身就是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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Meta章磊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值