欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

泛型程序设计

程序员文章站 2024-03-16 12:20:40
...

1、为什么使用泛型程序设计

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

例如,我们并不希望 为聚集String和File对象分别设计不同的类;实际上,也不需要这样做,因为一个ArrayList类可以聚集任何类型的对象。这是一个泛型城西设计的实例

1.1、类型参数的好处

  • 代码具有易读性
  • 代码具有较高的安全性

下面看看演进的过程

在Java中增加泛型类之前,泛型程序设计使用继承实现的,ArrayList只维护了一个Object引用的数组:

public class ArrayList{
    
    
    private Object[] elementData;
    
    ....
	
    private Object get(int i){...}
    private void add(Object o){....}
}

这种方法有两个问题:

  • 当获取一个值时必须进行强制类型转换

    ArrayList files = new ArrayList();
    .....
    String fileName = (	String)files.get(0);//强制类型转换
    
  • 没有错误检查,可以像数组列表中添加任何类的对象

泛型提供了一个更好的解决方案类型参数(type parameters)。ArrayList类有一个类型参数用来指示元素的类型:

ArrayList<String> files = new ArrayList<>();//使得程序具有更好的刻度性和安全性

2、定义简单泛型类

泛型类:具有一个或多个类型变量的类。

  • 泛型类引入类型变量,用尖括号(<>)括起来,放在类名的后面。
  • 泛型类型可以有多个类型变量,之间用逗号隔开
  • 类定义中的类型变量指定方法的返回类型以及域和局部变量的类型;private T first
  • 类型变量使用大写形式, 在JAVa库中使用E表示集合的元素类型,kV分别表示表的关键字与值的类型;T(U或S)表示任意类型
public class Pair<T> {
    /**
     * 类型变量使用大写形式,且比较短,这是很常见的。
     * 在Java库中,使用变量E表示集合的元素类型,
     *              K和V分别表示关键字与之的类型。
     *              T(需要时还可以用邻近的字母U和S)表示"任意类型"。
     */


    private T first; //use the type variable ;使用类型变量
    private T second;

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

	//使用类型变量返回值
    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public T getSecond() {
        return second;
    }

    public void setSecond(T second) {
        this.second = second;
    }
}

具体的类型替换类型变量就可以实例化泛型类型

Pair<String>

//可以将结果想象成
Pair<String>()
Pair<String>(String,String)

//和方法
String getFirst()
String getSecond()
void setFirst(String)
void setSecond(String)

换句话说,泛型类可看做是普通类的工厂

3、简单泛型方法

语法:修饰符 <T> 返回类型(参数)

  • 类型变量放在修饰符的会面,返回类型的前面
  • 泛型方法可以定义在普通类中,也可以定义在泛型类中
 /*
     * @param a
     * @param <T>
     * @return
     * 这个方法是在普通类中定义的,而不是嘴啊反类型中定义的,
     * 然而这是一个泛型方法,可以从尖括号和类型变量看出这一点。
     * 注意,类型变量放在修饰符(这里是public static)的后面,返回类型的前面
     */
    public static <T> T getMiddle(T...a){
        return a[a.length/2];
    }

3、类型变量的限定

有时需要对泛型变量进行限定,才能进行某些操作

语法: <T extends BoundingType>

  • 表示T应该是绑定类型的子类型T和绑定类型可以是类,也可以是接口。
  • 选择关键字extends的原因是更接近子类的概念,并且Java的设计者也不打算在语言中再添加一个新的关键字(如sub)
  • 一个类型变量通配符可以有多个限定;例如T extends Comparable & Serializable
  • 可以拥有多个接口超类型,但限定中至多有一个类;如果用一个类作为限定,它必须是限定列表中的第一个
  /* 类型变量的限定 通过extends实现对类型变量进行限定
     * @param a
     * @param <T>
     * @return
     *现在,泛型的min方法只能被实现了Comparable接口的类(如String,LocalDate等)的数组调用。
     * 由于Rectangle类没有实现Comparable接口,所以调用min将产生一个差一位
     */
    public static <T extends Comparable> T min(T[] a){
        if(a==null||a.length==0){
            T smallest = a[0];
            for(int i =1;i<a.length;i++){
                if(smallest.compareTo(a[i])>0) smallest=a[i];
            }
            return smallest;
        }
        return  null;
    }

4、泛型代码与虚拟机(虚拟机如何处理泛型)

虚拟机没有泛型类型对象----所有对象都属于普通类

4.1、类型擦除

无论何时定义了一个泛型类型,都自动的提供了一个相应的原始类型(raw type).

原始类型化的名字就是删除类型参数后的泛型类型名

擦除类型变量,并替换为限定类型(无限定类型的变量用Object)

例如,Pair<T>的原始类型如下所示:

因为T是一个无限定的变量,所以直接用Object替换

结果就是一个普通的类,就好像泛型引入Java语言之前已经实现的那样

在程序中可以包含不同类型的pair,例如,Pair<String>Pair<LocalDate>,而擦除类型后就变成原始的Pair类型

public class Pair {


    /**
     * 类型变量使用大写形式,且比较短,这是很常见的。
     * 在Java库中,使用变量E表示集合的元素类型,
     *              K和V分别表示关键字与之的类型。
     *              T(需要时还可以用邻近的字母U和S)表示"任意类型"。
     */


    private Object first; //use the type variable ;使用类型变量
    private Object second;

    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }


    public Object getFirst() {
        return first;
    }

    public void setFirst(Object first) {
        this.first = first;
    }

    public Object getSecond() {
        return second;
    }

    public void setSecond(Object second) {
        this.second = second;
    }
}

原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换

下面是类型变量有限定的情形:

泛型程序设计

为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界列表的末尾

4.2、翻译泛型表达式

当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。

Pair<Employee> buddies =.....;
Employee buddy = buddies.getFirst();


//擦除之后的原始
 public Object getFirst() {
        return first;
    }

擦除getFirst的返回类型将返回Object类型,编译器自动插入Employee的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:

  1. 对原始方法 Pair.getFirst的调用
  2. 对返回Object类型强制转换为Employee类型

4.3、翻译泛型方法

类型擦除也会出现在泛型方法中

例如:

public static <T extends Comparable> T min(T[] a) //是一个完整的方法族

类型擦除后

public static Comparable min(Comparable[] a) //类型参数T已经被擦除了,只留下了限定类型Comparable

Java泛型转换的事实:

  • 虚拟机没有泛型,只有普通的类和方法
  • 所有类型参数都用他们的限定类型替换
  • 桥方法被合成来保持多态
  • 为保持类型安全性,必要时插入强制类型转换‘

5、约束和局限性

下面介绍Java泛型时需要考虑的一些限制,

大多数限制都是由类型擦除引起的

5.1、不能用基本类型实例化类型参数

不能用类型参数代替基本类型。===> 基本数据类型不能作为类型参数

没有Pair<double>,只有Pair<Double>

原因是类型参数;擦除之后,Pair类含有Object类型的域,而Object类型不能存储double

5.2、运行时类型查询只适用于原始类型

虚拟机中的对象总有一个特定的非泛型类型。

因此,所有的类型查询只产生原始类型

  • 试图查询一个对象是否属于某个泛型类型时,倘若使用instanceof会得到一个编译期错误i
  • 若使用强制类型转换会得到一个警告
  • getClass方法总是返回原始类型

例如:

if(a instanceOf Pair<String>) —> 实际上仅仅测试a是否是任意类型的一个Pair

if(a instanceOf Pair<T>) ----> 实际上仅仅测试a是否是任意类型的一个Pair

Pair<String> p = (Pair<String>)a;----->Warning----->can only test that is a pair

Pair<String> stringPair = '';
Pair<Employee> employer='...';
if(StringPair.getClass==empliyee.getClass)//equal;这是因为两次调用getClass都将返回Pair.class

5.3、不能创建泛型类型的数组

不能实例化参数化类型的数组,例如

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

这有什么问题?擦除之后,table类型是Pair[];可以把它转换为Object[]:

Object[] objarray = table;

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

objarray[0]="Hello"//errot-Component is POair

如果需要收集参数化类型对象,只有一种安全而有效的方法:使用ArrayList:ArrayList<Pair<String>>

5.4、Varags警告

考虑下面代码

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

现在考虑调用下面:

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

为了调用这个方法,Java虚拟机必须建立一个Pair<String>数组,这就违反了 5.3的规则。不过,对于这种情况,规则有所放松,会得到一个警告,而不是错误

可以采用两种方法来抑制警告。

  1. 为包含addAll调用的方法增加注解SuppressWarnings("unchecked")
  2. @SafeVarargs直接标注addAll方法

5.5、不能实例化类型变量

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

例如,下面的Pair<T>构造器就是非法的:

public Pair(){
    first = new T();
    second = new T();//error
    //类型擦除将T变成Object;而且本例不希望调用new Object()
}

在JavaSE 8之后,最好的解决办法是让调用者提供一个构造器表达式:

public<String> p = Pair.makePair(String::new);

makePair方法接收一个Supplier<T>,这是一个函数式接口,表示一个无参数而且返回类型为T的函数:

public static <T> Pair<T> makePair(Supplier<T> constr){
    return new Pair<>(constr.get(),constr.get());
}

5.6、不能构造泛型数组

就像不能实例化一个泛型实例一样,也不能实例化数组。

不过原因有所不同,毕竟数组会填充null值,构造时看上去是安全的。

不过数组本身也有类型,用来存储在虚拟机中的数组。这个类型会被擦除

public static<T extends Comparable> T[] minmax(T[] a){  T[] mm = new T[2];}//ERROT

类型擦除会让这个方法永远构造Comparable[2]数组

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

例如,ArrayList就可以这样实现:

public class ArrayList<E>{
    
   private Object[] elements;
    
    @SuppressWarnings("unchecked")
    public  E get(int n){
        return (E)elements[n];
    }
    
    public void set(int n,E e){
        elements[n]=e;
    }
}

6、通配符类型

固定的泛型类型系统使用起来并没有那么令人愉快,类型系统的研究人员知道这一点已经有一段时间了

Java的设计者发明了一种巧妙的(仍然是安全的)“解决方案”:通配符类型

6.1、通配符概念

通配符类型中,允许类型参数变化

例如:

Pair <? extends Employee> 表示任何泛型Pair类型,它的类型参数是Employee的子类,如Pair<Manager>,但不是Pair<String>

类型Pair<Manager>Pair<? extends Employee>的子类型

泛型程序设计

6.2、通配符的超类型限定

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

? super Manager

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

相关标签: Java基础整理