「深入Java」Generics泛型
发布日期:2021-11-09 22:51:00 浏览次数:24 分类:技术文章

本文共 7651 字,大约阅读时间需要 25 分钟。

相关文章:

-
-

吐槽:这是目前最不深入的一篇了,因为关于泛型实在有太多需要注意的地方,本文仅人工过滤出了较常见的重点内容,后期再进行不定时更新吧。

必要性

在程序日益复杂庞大的今天,编写泛用性代码的价值愈发变得巨大。

而要做到这一点,其诀窍仅只两字而已——解耦

最简单的解耦,无疑是使用基类替代子类。然而由于Java仅支持单继承,这种解耦方法所带来的局限性未免过大,有种“只准投胎一次”的感觉。

使用接口替代具体类算是更进了一步,算是多给了一条命吧,但限制仍旧存在。要是我们所写的代码本身就是为了应用于“某种不确定的类型”呢?

这时候就轮到泛型登场了。


简单泛型

虽然理想远大。但Java引入泛型的初衷,也许只是为了创造容器类也说不定。

站在类库设计者的角度,我们不妨走上一遭。

得益于单根继承结构,我们可以这样来设计一个持有单个对象的容器:

public class Holder1 {    private Object a;    public Holder1(Object a) {         this.a = a;     }    Object get() {         return a;     }}

这个容器确实能持有多种类型的对象,但通常而言我们只会用它来存储一种对象。也就是说虽然设计时希望能存储任意类型,但使用时却能够只存储我们想要的确定类型

泛型可以达到这一目的,与此同时,这也能使编译器为我们提供编译期检查。

class Automobile {}public class Holder2
{ private T a; public Holder2(T a) { this.a = a; } public void set(T a) { this.a = a; } public T get() { return a; } public static void main(String[] args) { Holder2
h2 = new Holder2
(new Automobile()); Automobile a = h2.get(); // No cast needed // h2.set("Not an Automobile"); // Error // h2.set(1); // Error }}

如你所见,使用方法即为在类名后添加尖括号,然后填写类型参数“T”。使用时用明确的类型参数替换掉“T”,即为该容器指定了其存储的确定类型


泛型方法

泛型可以应用于方法,只需要将泛型参数列表放在方法返回值之前即可。

下面这个例子中,f()的效果看起来像是重载过一样:

//: generics/GenericMethods.javapublic class GenericMethods {    public 
void f(T x) { System.out.println(x.getClass().getName()); } public static void main(String[] args) { GenericMethods gm = new GenericMethods(); gm.f(""); gm.f(1); gm.f(1.0); gm.f(1.0F); gm.f(‘c’); gm.f(gm); }} /* Output:java.lang.Stringjava.lang.Integerjava.lang.Doublejava.lang.Floatjava.lang.CharacterGenericMethods*///:~

能这样做的原因在于编译器拥有称为类型参数推断的功能,能为我们找出具体的类型。

注意,如果调用f()时传入了基本数据类型,自动打包机制将会被触发,将基本数据类型包装为对应的对象。


擦除

Java泛型是使用擦除来实现的,这意味着在泛型代码内部,无法获得关于类型参数的信息

谨记,泛型类型参数将擦除到它的第一个边界,默认边界为Object;对于,第一个边界为Bound,即像是在类的声明中使用Bound替换掉T一样。

以下例子说明了这一问题:

//: generics/ErasedTypeEquivalence.javaimport java.util.*;public class ErasedTypeEquivalence {    public static void main(String[] args) {        Class c1 = new ArrayList
().getClass(); Class c2 = new ArrayList
().getClass(); System.out.println(c1 == c2); }} /* Output:true*///:~

尽管运行时指定了不同的泛型参数,但”ArrayList”与”ArrayList”事实上却被擦除成了相同的原生类型“ArrayList”来进行处理;用类字面常量来进行说明应该会更为直观:“c1”与”c2”的值为“ArrayList.class”,而不是“ArrayList.class”与“ArrayList.class”

知道了这一点后,你或许能猜测出容器类的一些具体实现细节了。

打开ArrayList的源码,会发现在其内部,用来存储数据的数组是这样定义的:

/**     * The elements in this list, followed by nulls.     */    transient Object[] array;

而其get()方法则是这样:

@SuppressWarnings("unchecked") @Override public E get(int index) {        if (index >= size) {            throwIndexOutOfBoundsException(index, size);        }        return (E) array[index];    }

注意,当E的第一个边界为Object时,那么这个方法实际上就根本没有进行转型(从Object到Object)。

知道了这一点后,你大概会对以下代码为何能符合预期地运行感到疑惑:

//: generics/GenericHolder.javapublic class GenericHolder
{ private T obj; public void set(T obj) { this.obj = obj; } public T get() { return obj; } public static void main(String[] args) { GenericHolder
holder = new GenericHolder
(); holder.set("Item"); String s = holder.get(); // Why it works? }} ///:~

使用 javap -c 反编译,我们可以找到答案:

public void set(java.lang.Object);0: aload_01: aload_12: putfield #2; //Field obj:Object;5: returnpublic java.lang.Object get();0: aload_01: getfield #2; //Field obj:Object;4: areturnpublic static void main(java.lang.String[]);0: new #3; //class GenericHolder3: dup4: invokespecial #4; //Method "
":()V7: astore_18: aload_19: ldc #5; //String Item11: invokevirtual #6; //Method set:(Object;)V14: aload_115: invokevirtual #7; //Method get:()Object;18: checkcast #8; //class java/lang/String --------Watch this line--------21: astore_222: return

奥秘就是,编译器在编译期为我们执行类型检查,然后插入了转型代码。

再看下面这个例子:

//: generics/ArrayMaker.javaimport java.lang.reflect.*;import java.util.*;public class ArrayMaker
{ private Class
kind; public ArrayMaker(Class
kind) { this.kind = kind; } @SuppressWarnings("unchecked") T[] create(int size) { return (T[])Array.newInstance(kind, size); } public static void main(String[] args) { ArrayMaker
stringMaker = new ArrayMaker
(String.class); String[] stringArray = stringMaker.create(9); System.out.println(Arrays.toString(stringArray)); }} /* Output:[null, null, null, null, null, null, null, null, null]*///:~

因为擦除的关系,kind只是被存储为Class,使用“Array.newInstance();”创建数组也就只能得到非具体的结果,实际使用中我们需要对其进行向下转型,但是并没有足够的类型信息用以进行类型检查,所以对于编译器报错,只能采用注解“@SuppressWarnings(“unchecked”)”强行将其消去。


通配符

有些时候你需要限定条件,使用通配符可以满足这一特性。

这是指定上界的情况:

//: generics/GenericsAndCovariance.javaimport java.util.*;public class GenericsAndCovariance {    public static void main(String[] args) {        // Wildcards allow covariance:        List
flist = new ArrayList
(); // Compile Error: can’t add any type of object: // flist.add(new Apple()); // flist.add(new Fruit()); // flist.add(new Object()); flist.add(null); // Legal but uninteresting // We know that it returns at least Fruit: Fruit f = flist.get(0); }} ///:~

flist的类型为List

//: generics/Holder.javapublic class Holder
{ private T value; public Holder() {} public Holder(T val) { value = val; } public void set(T val) { value = val; } public T get() { return value; } public boolean equals(Object obj) { return value.equals(obj); } public static void main(String[] args) { Holder
Apple = new Holder
(new Apple()); Apple d = Apple.get(); Apple.set(d); // Holder
Fruit = Apple; // Cannot upcast Holder
fruit = Apple; // OK Fruit p = fruit.get(); d = (Apple)fruit.get(); // Returns ‘Object’ try { Orange c = (Orange)fruit.get(); // No warning } catch(Exception e) { System.out.println(e); } // fruit.set(new Apple()); // Cannot call set() // fruit.set(new Fruit()); // Cannot call set() System.out.println(fruit.equals(d)); // OK }} /* Output: (Sample)java.lang.ClassCastException: Apple cannot be cast to Orangetrue*///:~

同样的道理,对于上例中的fruit来说,其set()方法的参数变成了“? extends Fruit”,这意味着其接受的参数可以是任意类型,只需满足上界为Fruit即可,而编译器无法验证“任意类型”的类型安全性。

反过来看看指定下界的效果:

//: generics/SuperTypeWildcards.javaimport java.util.*;class Jonathan extends Apple {
}public class SuperTypeWildcards {
static void writeTo(List
apples) { apples.add(new Apple()); apples.add(new Jonathan()); // apples.add(new Fruit()); // Error }} ///:~

可以看到,写入操作变得合法。显然,Apple类型满足下界需求,执行写入操作没有安全性问题,而Jonathan是Apple的子类,经过向上转型,也可以符合需求,而Apple的基类Fruit则仍然由于类型不定而被拒绝。


基本类型不能作为类型参数

不能创建List之类,而需使用List,但因为自动包装机制的存在,所以写入数据时可以使用基本数据类型。


实现参数化接口

一个类不能实现同一个泛型接口的两种变体,因为擦除会让它们变成相同的接口:

//: generics/MultipleInterfaceVariants.java// {CompileTimeError} (Won’t compile)interface Payable
{
}class Employee implements Payable
{
}class Hourly extends Employee implements Payable
{
} ///:~

Hourly不能编译。但是,如果从Payable的两种用法中移除掉泛型参数(就像编译器在擦除阶段做的那样),这段代码将能够编译。


重载

以下代码无法编译,因为擦除会让两个方法产生相同的签名:

//: generics/UseList.java// {CompileTimeError} (Won’t compile)import java.util.*;public class UseList
{ void f(List
v) {} void f(List
v) {}} ///:~

自限定类型

class SelfBounded
> {
// ...

待补充…

参考资料

  • 《Java编程思想》

转载地址:https://blog.csdn.net/dbnight/article/details/50688375 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:「译」Fragment事务与Activity状态丢失
下一篇:「译」Android文本测量

发表评论

最新留言

感谢大佬
[***.8.128.20]2024年02月29日 05时45分29秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

linux 强制结束任务管理器,结束拒绝访问的进程 cmd下结束进程 强行结束进程 2019-04-21
高斯勒让德在c语言中的程序,c语言:用递归方法编写程序,求n阶勒让德多项式的值... 2019-04-21
c语言单片机电子时钟,新人求个51单片机的电子时钟汇编语言(C语言的还没学到)... 2019-04-21
c++语言文件流,C++文件流 2019-04-21
android 动态毛玻璃,Android毛玻璃背景效果简单实现代码 2019-04-21
android 按钮提示,的Android按钮工具提示 2019-04-21
iphone通讯录 android,3个方法,教你如何快速而又有效的将联系人从iPhone转移到安卓... 2019-04-21
android horizontalscrollview 滑动事件,ScrollView的滑动监听(以HorizontalScrollView为例) 2019-04-21
win7自定义html为桌面,Win7系统自定义桌面主题的方法 2019-04-21
单系统 台电x80pro_台电x80 pro (ID:E3E6)安装remix OS系统教程整理 2019-04-21
linux存储pdf伟岸_python的reportlab库介绍、制作pdf和作图 2019-04-21
安徽信息技术初中会考上机考试模拟_2020年中小学寒假、考试时间定下了! 2019-04-21
ubuntu 退出anaconda环境_从零开始深度学习第15讲:ubuntu16.04 下深度学习开发环境搭建与配置... 2019-04-21
稳定币usda是哪个发行的_武夷山币装帧款曝光,共4款设计,你喜欢哪款? 2019-04-21
可变车道怎么走不违章_走ETC竟比人工车道贵50%!交警:这3点不知道,吃亏的是自己... 2019-04-21
苹果笔记本的end键_笔记本用户的大烦恼:触控板,想好好用你不容易 2019-04-21
趣玩机器人什么时候成立的_【直播回顾】当我们谈机器人集成调试的时候在谈什么... 2019-04-21
中考大数据大连79_中考大数据 | 大连部分初中2019年中考指标生录取最低分及人数统计!... 2019-04-21
vue 地理位置定位_HTML5地理位置 2019-04-21
pac代理模式什么意思_托管仓库租赁电商仓储运营模式托管什么意思 2019-04-21