《Java编程思想》是一本Java学习的经典书籍,其中Java泛型部分大约占了10%的篇幅,可见其重要性,但是这也使其成为了很多Java初学者翻不过去的一道坎。本着不抛弃、不放弃的精神,我决定来帮帮这些同学。
因为内容太多,我决定把它写成系列文章,本系列的营销策略是:“允许让人看得烦,不许让人看不懂。”,不愧是失眠多梦,焦虑烦躁的必备良药。
1 泛型的意义
大师说:“泛型的意义在于代码的复用”。
代码复用是软件工程的一个核心问题,下面我们就来讲一讲泛型是如何解决软件复用问题的。
1.1状态的复用
珠宝箱
首先来个常规的问题,我们手头有一件珠宝,需要装到箱子里:
public class Jewelry { @Override public String toString() { return "珠宝"; } public void light() { System.out.println("亮晶晶"); } } public class JewelryBox { private Jewelry jewelry; public Jewelry getJewelry() { return jewelry; } public void setJewelry(Jewelry jewelry) { this.jewelry = jewelry; } } public class Tester { @Test public void test_JewelryBox() { JewelryBox box = new JewelryBox(); box.setJewelry(new Jewelry()); System.out.println(box.getJewelry()); } }
输出结果:
珠宝
代码点评:
太简单,略。
Object箱子
JewelryBox只能放Jewelry,这使得它的可复用性很低,如果我们还需要一个装钥匙的箱子的怎么办?只能重新实现一个钥匙箱子吗?有聪明的人想到了“万类之母”Object,于是有了下面的代码:
public class Key { @Override public String toString() { return "钥匙"; } public void openLock() { System.out.println("开锁"); } } public class ObjectBox { private Object object; public Object getObject() { return object; } public void setObject(Object object) { this.object = object; } } public class Tester { @Test public void test_ObjectBox() { ObjectBox box = new ObjectBox(); box.setObject(new Jewelry()); System.out.println(box.getObject()); ((Jewelry)box.getObject()).light(); box.setObject(new Key()); System.out.println(box.getObject()); ((Key)box.getObject()).openLock(); } }
输出结果:
珠宝
亮晶晶
钥匙
开锁
代码点评:
ObjectBox还是挺强悍的,即能装珠宝,也能装钥匙。
关键是,ObjectBox这个类是可以无限复用的。
但是这两个强制类型转换是什么鬼?这是一种代码的坏味道,意味着ObjectBox很容易出错。
泛型箱子
针对ObjectBox的缺点,是该泛型出场了。
泛型,从字面理解可以理解为“泛指的类型”,即“可定制的类型”。简单的说,你可以把泛型理解为类型的占位符,本身并不能工作,一旦指定了具体的类型就可以工作得很好。
于是有了下面的代码:
public class GenericBox<T> { private T item; public T getItem() { return item; } public void setItem(T item) { this.item = item; } } @Test public void test_GenericBox() { { GenericBox<Jewelry> box = new GenericBox<Jewelry>(); box.setItem(new Jewelry()); System.out.println(box.getItem()); box.getItem().light(); } { GenericBox<Key> box = new GenericBox<Key>(); box.setItem(new Key()); System.out.println(box.getItem()); box.getItem().openLock(); } }
输出结果:
珠宝
亮晶晶
钥匙
开锁
代码点评:
珠宝箱装珠宝,钥匙箱装钥匙,一切都很好。
讨厌的强制类型转换也没有了。
关键是,GenericBox这个类可以安全地无限复用了。
真正的箱子
一个箱子只装一件东西,这可是巨大的浪费,是该做一个真正的箱子了,还是用泛型:
public class Box<T> { private Object[] elements = new Object[0]; public T get(int i) { return (T)elements[i]; } public void add(T o) { int length = elements.length; Object[] newElments = new Object[length + 1]; for (int i = 0; i < length; i++) { newElments[i] = elements[i]; } newElments[length] = o; elements = newElments; } public int size() { return elements.length; } } @Test public void test_Box() { Box<Jewelry> box = new Box<Jewelry>(); for (int i = 0; i < 10; i++) { box.add(new Jewelry()); } for (int i = 0; i < box.size(); i++) { System.out.println(box.get(i)); box.get(i).light(); } }
输出结果:
珠宝
亮晶晶
珠宝
亮晶晶
。。。
代码点评:
Box内部是用Object数组来实现的,这或许看起来有些不爽。但这是由于Java泛型的先天残疾而造成的,以后我们会讲到原因,现在先无视它。
看起来,Box似乎并不比Java内置的数组强多少,但是请注意这里的box已经突破了内置数组长度无法扩充的限制(尽管实现上效率很低)。而且我们还可以随意地在Box增加更多的功能。
Box已经有了泛型容器ArrayList的一些影子了。
我们可以用真正的珠宝箱,也可以用真正的钥匙箱。再强调一遍,关键是,Box这个类可以安全地无限复用了。
1.2行为的复用
尽管我们刚刚看到了泛型在容器类中大显身手,但是如果我们以为泛型只能复用变量状态那就大错特错了,实际上泛型更重要的作用的是复用行为。
面向对象的行为复用
首先,让我们回顾一下面向对象如何对行为进行复用:
public class ObjectProgramming { interface Workable { void work(); } void do_work(Workable workable) { System.out.println("开始工作"); workable.work(); System.out.println("结束工作"); System.out.println(); } abstract class Person implements Workable { String name; String age; } class Employee extends Person{ @Override public void work() { System.out.println("努力工作"); } } class Manager extends Employee { @Override public void work() { System.out.println("拼命工作"); } } @Test public void test_Workable () { do_work(new Employee()); do_work(new Manager()); } }
输出结果:
开始工作
努力工作
结束工作
开始工作
拼命工作
结束工作
代码点评:
面向对象通过“父类的引用可以被子类的引用赋值”这一约定实现了上层代码的复用。其核心思想是通过接口来约束行为。
泛型的行为复用
然后,让我们看看泛型是如何实现对行为的复用:
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class GenericsProgramming { <T> void do_run(T runner) { System.out.println("开始跑"); Class<?> meta = runner.getClass(); try { Method method = meta.getMethod("run"); method.invoke(runner); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } System.out.println("结束跑"); System.out.println(); } class Dog { public void run() { System.out.println("狗,跑"); } } class Human { public void run() { System.out.println("人,跑"); } } class Car { public void run() { System.out.println("车,跑"); } } @Test public void test_run () { do_run(new Dog()); do_run(new Human()); do_run(new Car()); } }
输出结果:
开始跑
狗,跑
结束跑
开始跑
人,跑
结束跑
开始跑
车,跑
结束跑
代码点评:
先不用管泛型方法内部是怎么实现的,因为Java泛型的先天残疾,这里的实现是很丑,请先无视它。
泛型对行为的复用本质上是应用了鸭子类型(duck typing)如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。通过鸭子类型对行为进行约束。
鸭子类型在Python、Javascript等动态语言中是基本标配,就思想而言并不是多么新鲜的玩意。
1.3组合起来
面向对象思想和泛型编程思想是可以配合使用的,两者代表的两个正交的维度。
如果说面向对象思想是通过从上至下的继承关系实现了纵向维度的代码复用;那么也可以说泛型编程思想是通过将平行关系的类联系起来实现了横向维度的代码复用;两者的组合就实现了更具有表现力的二维结构。
善用面向对象思想和泛型编程思想,对编码能力是一个极大的提升。正所谓:“武林至尊,宝刀屠龙,号令天下,莫敢不从!倚天不出,谁与争锋?”。