热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【java核心技术卷一】泛型程序设计

泛型程序设计为什么要使用泛型程序设计?​泛型程序设计(Genericprogramming)意味着编写的代码可以被很多不同类型的对象所重用。​例如ArrayList<

泛型程序设计

为什么要使用泛型程序设计?

​ 泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。

​ 例如ArrayList,其中T可以为任意对象,这也就是说该ArrayList的操作都是以T为基本单位的,T称为类型参数(type parameters)。ArrayList类有一个类型参数用来指示元素的类型

ArrayList<T> list = new ArrayList<T>();

在使用时一看就知道list包含的是T类型对象

在JAVA SE7及以后的版本,构造函数中可以省略泛型类型

ArrayList<T> list = new ArrayList<>();

省略的类型可以从变量的类型推断得出

泛型程序设计使在对list中的对象进行使用时,可以避免使用错误的方法或参数,让程序具有更好额可读性和安全性。

定义简单泛型类

​ 一个泛型类(generic class)就是具有一个或者多个类型变量的类。对于这个类来说,我们只关注泛型,而不会为数据储存的细节烦恼。

public class Pair{
private T first;
private T second;

public Pair(){
first = null; secOnd= null;
}
public Pair(T first , T second){
this.first = first;
this.secOnd= second;
}

public T getFirst(){return first;}
public T getSecond(){return second;}

public void setFirst(T newValue){first =newValue;}
public void setSecond(T newValue){secOnd= newValue;}
}

pair类引入一个类型变量T,用尖括号<>括起来,并放在类名的后面。泛型类可以有多个类型变量。例如,可以定义Pair类,其中第一个域和第二个域使用不同的类型

public class Pair <T,U> {...}

在java中,类型变量使用大写形式且较短为常见。在java库中,使用变量E表示集合的元素类型,KV分别表示表的关键字(key)和值(Value)的类型。T以及附近US表示任意类型

泛型方法

定义一个带有类型参数的简单方法

class ArrayAlg{
public static T getMiddle(T...a){
return a[a.length/2];
}
}

这个方法使在普通类中定义的,而不是在泛型类中定义的,但这是一个泛型方法。注意,类型变量放在修饰符的后面,返回类型的前面。

​ 泛型方法可以定义在普通类中,也可以定义在泛型类中。

​ 当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:

String middle = ArrayAlg.<String>getMiddle("aa","bb","cc");

在这种情况下(也是大多数情况下),方法调用中可以省略类型参数。编译器有足够的信息能够推断出所调用的方法。它用String[]与泛型类型T[]进行匹配并推断出T一定是String。也就是说,可以调用

String middle = ArrayAlg.getMiddle("aa","bb","cc");

几乎在大多数情况下,对于泛型方法的类型引用没有问题。偶尔,编译器也会给出错误提示:

double middle = ArrayAlg.getMiddle(3.14 , 123 , 0);

错误消息会以晦涩的方法指出:解释这句代码有两种方法,而且这两种方法都是合法的。简单地说,编译器会自动打包参数为一个Double和两个Integer对象,而后寻找这些类的共同超类型。事实上,找到2个这样的超类:NumberComparable接口,其本身也是一个泛型类型。这种情况下的措施就是将所有参数改为Double类型

类型变量的限定

有时候,类或方法需要对类型变量加以束缚。例如求数组中最小元素

class Test{
public static T min(T[] a){
if(a ==null||a.length==0){
return null;
}else{
T smallest = a[0];
for(int i = 1 ; i if(smallest.compareTo(a[i]>0))smallest = a[i];
return smallest;
}
}
}
}

但是变量smallest的类型为T,这意味着它可以是任何一类的对象,如何确信T所属的类有compareTo方法?

解决问题的方法是将T限制为实现了Comparable接口的类。可以通过对类型变量T设置限定实现这一点

public static <T extends Comparable> T min(T[] a){...}

现在,泛型的min方法只能被实现了Comparable接口的类的数组调用。

但为什么实现接口的关键字是extends而不是implements?毕竟Comparable是一个接口

<T extends BoundingType>

表示T应该是绑定类型的子类型(subType)。T和绑定类型可以是累,也可以是接口。选择extends是因为更接近子类的概念。

一个类型变量或通配符可以有多个限定,例如

T extends Comparable & Serializable

限定类型用&分隔,而类型变量用,分隔。

Java的继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。

public class A <T extends List&Comparable&Serializable> implements Serializable{}

约束与局限性

  1. 不能使用基本类型实例化类型参数

    没有Pair,只有Pair。原因是类型擦除,擦除之后Object类型不能储存double的值。而当包装器类型不能接受替换时,可以使用独立的类和方法处理

  2. 运行时类型查询只适用于原始类型

    所有类型查询只能产生原始类型。

    例如

    if(a instanceof Pair<String>) //ERROR
    if(a instanceof Pair<T> )//ERROR
    Pair<String> p = (Pair<String>) a;//WARNING

    无论何时使用instance或涉及泛型类型的强制类型转换表达式都会看到一个警告。

    同样,getClass总是返回原始类型

    Pair a = ...; Pair b = ...;
    if(a.getClass()==b.getClass()) //always true

    因为getClass的返回结果都是Pair.class

  3. 不能创建参数化的数组

    例如

    Pair<String>[] table = new Pair<String>[10];//ERROR

    擦除之后,table的类型是Pair[],可以转换为Object[]:

    Object[] objarray = table;

    数组会记住它的元素类型,如果试图储存其他类型的元素,就会跑出一个ArrayStoreException异常

    objarray[0] = "hello" //ERROR component type is Pair

  4. Varargs警告

    向参数个数可变的方法传递一个泛型类型的实例。例如下面这个方法

    public static <T> void addAll(Collection<T> coll,T ... ts){
    for(t:ts)
    coll.add(t);
    }

    实际上参数ts是一个数组,包含提供的所有实参

    Collection > table = ...;
    Pair pair1 = ...;
    Pair pair2 = ...;
    addAll(table,pair1,pair2);

    为了调用这个方法,JVM必须建立一个Pair数组,但这违反了前面的规则。但你只会得到一个警告。

    可以使用@SuppressWarnings("unchecked")或者在Java SE7中使用@SafeVarargs来标注addAll方法

  5. 不能实例化类型变量

    不能使用new T(...)new T[...]或者T.class这样的表达式中的类型变量。例如

    public Pair(){first = new T();}      //ERROR

    因为在类型擦除后会使得firstObject,而本意肯定并非如此。不过可以使用烦着调用Class.newInstance方法来构造泛型对象。但非下面这样

    first = T.class.newInstance();       //ERROR

    表达式是不合法的,必须像下面这样

    public static  Pair makePair(Class<T> cl){
    try{
    return new Pair<>(cl.newInstance(),cl.newInstance());
    }catch(Exception e){
    return null;
    }
    }

    这个方法可以按照下列方式调用

    Pair<String> p = Pair.makePair(String.class);

    注意,Class类本身是泛型。例如,String.class是一个也是唯一Class的实例。因此,makePair可以判断pair的类型。

    不能构造一个泛型数组,类型参数会让这个方法永远构造Object[]数组。

    如果数组仅仅作为一个类的私有实例,就可以将这个数组声明为Object[],并在获取元素时进行类型转换。

  6. 泛型类的静态上下文中类型变量无效

    不能在静态实例域或方法中引用类型变量

    public class Pair{
    private T pair1; //ERROR

    public T getPair1(){ //ERROR
    return pair1;
    }
    }

  7. 不能抛出或捕获泛型类的实例

    不能抛出或捕获泛型类对象,甚至泛型类扩展Throwable都是不合法的

    catch子句中不能使用类型变量

    public static <T extends Throwable> void do(Class<T > t){
    try{
    ...
    }catch(T e){ //ERROR
    ...
    }
    }

    不过,在异常规范中使用类型变量是允许的

    public static <T extends Throwable> void do(Class<T > t) throws T{       //OK
    try{
    ...
    }catch(Throwable cause){
    t.initCause(cause);
    throw t;
    }
    }

  8. 擦除后的冲突

    要想支持擦除的转换,就需要强行限制一个类或者类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化

泛型类型的继承规则

永远可以将一个参数化类型转换为一个原始类型。

例如StudentPerson的一个子类,但是Pair并不是Pair的子类,他们抽象化后都是Pair

通配符类型

Pair extends Employee>

表示任何泛型Pair类型,它的类型参数是Employee的子类,如Pair,但不是Pair

假如要编写一个打印雇员的方法,例如

public static void printBuddies(Pair p){
System.out.print(p.getFirst.getName());
}

正如泛型类型的继承规则,不能将Pair传递给这个方法,这一点很受限,但是可以用通配符解决

public static void printBuddies(Pair p )

类型PairPair的子类型

通配符的超类型限定

通配符限定与类型变量限定十分类似,但是,还有一个附加的能力,即可以指定一个超类型限定(supertype bound):

? super Manager

这个通配符限制为Manager的所有超类型。

带有超类型限定的通配符,可以为方法提供参数,但不能使用返回值。例如,Pair有方法

void setFirst(? super Manager)
? super Manager getFirst()

编译器不知道setFirst方法的确切类型,但是可以用任意Manager对象调用它,而不能用Employee对象调用。然而,如果调用getFirst,返回的对象类型就不会得到保证,只能把它赋给一个Object

直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取

无限定通配符

无限制的通配符Pair看起来好像与原始的Pair类型一样,实际上有很大不同。类型Pair有方法如下所示:

? getFirst()
void setFirst(?)

getFirst()的返回值只能赋给一个ObjectsetFirst方法不能被调用,甚至不能用Object调用PairPair本质的不同在于:可以用任意Object对象调用原始的Pair类的setObject方法

可以调用setFirst(null)

为什么要使用这样的类型?例如,下面这个方法用来测试一个pair是否包含一个null引用,它不需要实际的类型。

public static boolean hasNulls(Pair p){
return p.getFirst()==null||p.getSecond()==null;
}

通过将hasNulls转换成泛型方法,可以避免使用通配符类型

public static  boolean hasNulls(Pair p)

但是,带有通配符的版本可读性更强

通配符捕获

一个交换pair元素的方法

public static void swap(Pair P)

通配符不是类型变量,因此,不能再编写代码中使用“?”作为一种类型。也就是说下列代码是非法的

? t = p.getFirst()  //ERROR
p.setFirst(p.getSecond())
p.setSecond(t)

在交换的时候必须临时保存第一个元素。不过我们可以写一个辅助方法swapHelper

public static  void swapHelper(Pair p){
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}

swapHelper是一个泛型方法,而swap不是,它具有固定的Pair类型参数

现在可以用swap调用swapHepler

public static void swap{swapHepler(p);}

这种情况下,swapHepler方法的参数T捕获通配符。他不知道是哪种类型的通配符,但是,这是明确的类型,而且swapHepler的定义只有在T指出类型时才有明确的含义

反射和泛型

现在,Class类是泛型的。例如,String.class实际上是一个也是唯一一个Class`类的对象。

类型参数十分有用,这是因为它允许Class方法的返回类型更加具有针对性。下面Class中的方法就是用了类型参数

T newInstance()
T cast(Object obj)
T[] getEnumConstants()
ClassT> getSuperClass()
Constructor<T> getConstructor(Class ... param)
Constructor<T> getDeclaredConstructor(Class ... param)

newInstance方法返回一个实例,这个实例所属的类由默认的构造器获得。他的返回类型目前声明为T,其类型与Class描述的类相同,这样就免除了类型转换。

如果给定的类型确实是T的一个子类型,cast方法就会返回一个现在声明为类型T的对象,否则,抛出一个BadCastException异常

如果这个类不是enum类或者类型T的枚举值的数组,getEnumConstans方法将返回null

最后,getConstructorgetdeclaredConstructor方法返回一个Constructor对象。Constructor类也已经变成泛型,以便newInstance方法有一个正确的返回类型


推荐阅读
  • Android JSON基础,音视频开发进阶指南目录
    Array里面的对象数据是有序的,json字符串最外层是方括号的,方括号:[]解析jsonArray代码try{json字符串最外层是 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文详细介绍了Spring的JdbcTemplate的使用方法,包括执行存储过程、存储函数的call()方法,执行任何SQL语句的execute()方法,单个更新和批量更新的update()和batchUpdate()方法,以及单查和列表查询的query()和queryForXXX()方法。提供了经过测试的API供使用。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
author-avatar
淑月冠廷婷婷
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有