J2SE:泛型的基本使用

泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。

什么是泛型?为什么要使用泛型?说的通俗一些就类似于形参,可以在开始时不去指定具体的类型,而是由泛型来替代,通常用T/E/V/K等大写字母来表示。泛型可以用在类、接口和方法中,分别称为泛型类、泛型接口、泛型方法。

// 以List集合为例,如果不使用泛型,那么可以存储所有类型的数据,这样虽然很爽,
// 但是会很乱,操作起来一旦面临转型的问题,就会带来很大的麻烦
List arrayList = new ArrayList()
arrayList.add("aaaa");
arrayList.add(100);
arrayList.add(new Date());


// 如果使用泛型,就可以限定存入数据的类型,这样就可以很好地避免掉ClassCastException等问题
List<String> list = new ArrayList<>();
// 此时list只能存入String类型的数据,并且这种检测是在编译器进行的。

要注意的是泛型只在编译阶段有效。看下面的代码:

List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();

if(classStringArrayList.equals(classIntegerArrayList)){
    sout("类型相同");
}

输出结果:类型相同。

通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译时正确检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。


泛型类

当泛型用于类的定义中,该类被称为泛型类。最典型的就是各种容器类,如:List、Set、Map。

泛型类的最基本写法:

// 泛型标识通常为T/K/V等大写字母
class 类名称 <泛型标识>{
}

泛型类定义之后就可以在该类中使用该标识来作为某种类型,比如

public class Generic<T>{ 
    // key这个成员变量的类型为T,T的类型由外部指定  
    private T key;

    // 泛型构造方法形参key的类型也为T,T的类型由外部指定
    public Generic(T key) { 
        this.key = key;
    }

    // 泛型方法getKey的返回值类型为T,T的类型由外部指定
    public T getKey(){ 
        return key;
    }
}

定义之后怎么使用呢?一般在创建类对象的时候指定具体的类型

// 可以指定该对象的存储类型为Integer
Generic<Integer> genericInteger = new Generic<Integer>(123456);

// 也可以指定该对象的存储类型为String
Generic<String> genericString = new Generic<String>("key_vlaue");

// ……

另外要注意的是泛型的类型实参只能是引用类型,不能是基本数据类型。

那么定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型,就跟没使用泛型一样。

不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。

// 这样不可以
if(ex_num instanceof Generic<Integer>){   
} 

// 但是可以这样
if(ex_num instanceof Generic){   
}

泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:

// 定义一个泛型接口
public interface Generator<T> {
    public T next();
}

泛型接口的实现类如果仍然不确定具体类型,也应当声明泛型,然后在实例化实现类的时候再指定确切类型

class FruitGenerator<T> implements Generator<T> {}

// 如果不声明泛型,如:
class FruitGenerator implements Generator<T> {}
// 编译器会报错:"Unknown class"

还可以在实现接口的时候就指定具体的泛型,此时实现类中所有使用泛型的地方都要相应替换成传入的实参类型。

public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
    }
}

泛型方法

泛型类,是在实例化类的时候指明泛型的具体类型;泛型接口,是在实现接口或者实例化接口实现类的时候指明泛型的具体类型;而泛型方法,是在调用方法的时候指明泛型的具体类型 。

基本写法举例如下

public <T> T genericMethod(T goal) {}

说明:
1)public与返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T,并且优先级高于类上同名泛型。
4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。

另外静态方法是无法访问类上定义的泛型的;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

总之,泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:

无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果能使用泛型方法将整个类泛型化,那么就应该使用泛型方法。


泛型和Object

要注意的一点是,在使用泛型时,只能使用Object中的方法,因为泛型在不确定具体类型时,可以表示任意类型,任意类型都是Object类型。那么为什么不适用Object呢?比如

public class Generic<Object>{ 

    // 泛型通配符的通常用法
    public void show(Object goal){}
}

其实不管是使用T还是Object,你都只能调用Object类型的方法。但是使用泛型的话,就是你指定的类型,如果是使用Object,就相当于是使用了多态,比如List<Object>,我可以传递一个Integer数据进去,但是我获取的结果是Object类型的,此时如果我想使用Integer类的方法,还要做类型转换;有时还可能出现ClassCastException,这点来说泛型在编译时也更加安全。


泛型通配符

泛型通配符不是这样用的:

// 错误用法,会报错
public class Generic<?>{ 
}

 就我的理解是,泛型通配符一般是在参数中的类型还可以使用泛型的时候,比如

public class Generic{ 

    // 泛型通配符的通常用法,我并不知道List里到底是什么类型的,只知道我要个List
    public void show(List<?> list){}
}

// 此时我可以传递List<String>,也可以传递List<Integer>,……
// 如果不适用通配符,而是List<T>,那么我就只能传入一种List,List里的类型由具体指定的类型决定

为什么要用通配符呢?

在java中,数组是可以协变的,比如dog extends Animal,那么Animal[] 与dog[]是兼容的。而集合是不能协变的,也就是说List<Animal>不是List<dog>的父类,能接受List<Animal>的并不能接受List<dog>,这时候就可以用到通配符了。

在学习Java泛型的过程中, 通配符是较难理解的一部分. 主要有以下三类:

1、无边界的通配符(Unbounded Wildcards)

就是<?>, 比如List<?>,无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。

我们以在集合List中使用<?>为例:

public static void printList(List<?> list) {
     for (Object o : list) {
         System.out.println(o);
     }
 }
 
 public static void main(String[] args) {
     List<String> l1 = new ArrayList<>();
     l1.add("aa");
     l1.add("bb");
     l1.add("cc");
     printList(l1);
     List<Integer> l2 = new ArrayList<>();
     l2.add(11);
     l2.add(22);
     l2.add(33);
     printList(l2);
 }

这种使用List<?>的方式就是父类引用指向子类对象。注意,这里的printList方法不能写成public static void printList(List<Object> list)的形式,虽然Object类是所有类的父类,但是List<Object>跟其他泛型的List如List<String>, List<Integer>不存在继承关系,因此会报错。
有一点我们必须明确,我们不能对List<?>使用add方法, 仅有一个例外就,是add(null)。为什么呢? 因为我们不确定该List的类型,不知道add什么类型的数据才对,只有null是所有引用数据类型都具有的元素。请看下面代码:

public static void addTest(List<?> list) {
     Object o = new Object();
     // list.add(o); // 编译报错
     // list.add(1); // 编译报错
     // list.add("ABC"); // 编译报错
     list.add(null);
}

由于我们根本不知道list会接受到具有什么样的泛型List, 所以除了null之外什么也不能add。
还有,List<?>也不能使用get方法, 只有Object类型是个例外,原因也很简单,因为我们不知道传入的List是什么泛型的,所以无法接受得到的get,但是Object是所有数据类型的父类,所以只有接受他可以,请看下面代码:

public static void getTest(List<?> list) {
     // String s = list.get(0); // 编译报错
     // Integer i = list.get(1); // 编译报错
     Object o = list.get(2);
}

那有人说了,不是有强制类型转换么? 是有,但是我们不知道会传入什么类型,比如我们将其强转为String,编译是通过了,但是如果传入个Integer泛型的List,一运行还会出错。那位又说了,那么保证传入的String类型的数据不就好了么?那样是没问题了,但是那还用<?>干嘛呀? 直接List<String>不就行了。

2、固定上边界的通配符(Upper Bounded Wildcards)

使用固定上边界的通配符的泛型,就能够接受指定类及其子类类型的数据。要声明使用该类通配符,采用<? extends E>的形式,这里的E就是该泛型的上边界。注意:这里虽然用的是extends关键字,却不仅限于继承了父类E的子类,也可以代指显现了接口E的类。

public static double sumOfList(List<? extends Number> list) {
     double s = 0.0;
     for (Number n : list) {
         // 注意这里得到的n是其上边界类型的, 也就是Number, 需要将其转换为double.
         s += n.doubleValue();
     }
     return s;
 }
 
 public static void main(String[] args) {
     List<Integer> list1 = Arrays.asList(1, 2, 3, 4);
     System.out.println(sumOfList(list1));
     List<Double> list2 = Arrays.asList(1.1, 2.2, 3.3, 4.4);
     System.out.println(sumOfList(list2));
 }

有一点我们需要记住的是,List<? extends E>不能使用add方法, 请看如下代码:

public static void addTest2(List<? extends Number> l) {
     // l.add(1); // 编译报错
     // l.add(1.1); //编译报错
     l.add(null);
}

 原因很简单,泛型<? extends E>指的是E及其子类,这里传入的可能是Integer,也可能是Double,我们在写这个方法时不能确定传入的什么类型的数据,如果我们调用:

List<Integer> list = new ArrayList<>();
addTest(list);

那么我们之前写的add(1.1)就会出错,反之亦然,所以除了null之外什么也不能add。但是get的时候是可以得到一个Number,也就是上边界类型的数据的,因为不管存入什么数据类型都是Number的子类型,得到这些就是一个父类引用指向子类对象。

3、固定下边界的通配符(Lower Bounded Wildcards)

使用固定下边界的通配符的泛型,就能够接受指定类及其父类类型的数据。要声明使用该类通配符,采用<? super E>的形式,这里的E就是该泛型的下边界。注意:你可以为一个泛型指定上边界或下边界,但是不能同时指定上下边界。

public static void addNumbers(List<? super Integer> list) {
     for (int i = 1; i <= 10; i++) {
         list.add(i);
     }
 }
 
 public static void main(String[] args) {
     List<Object> list1 = new ArrayList<>();
     addNumbers(list1);
     System.out.println(list1);
     List<Number> list2 = new ArrayList<>();
     addNumbers(list2);
     System.out.println(list2);
     List<Double> list3 = new ArrayList<>();
     // addNumbers(list3); // 编译报错
}

我们看到,List<? super E>是能够调用add方法的,因为我们在addNumbers所add的元素就是Integer类型的,而传入的list不管是什么,都一定是Integer或其父类泛型的List,这时add一个Integer元素是没有任何疑问的,即多态原理。但是我们不能使用get方法,请看如下代码:

public static void getTest2(List<? super Integer> list) {
     // Integer i = list.get(0); //编译报错
     Object o = list.get(1);
}

这个原因也是很简单的,因为我们所传入的类都是Integer的类或其父类,所传入的数据类型可能是Integer到Object之间的任何类型,这是无法预料的,也就无法接收。唯一能确定的就是Object,因为所有类型都是其子类型。

通配符总结:

当要读取出数据以供随后使用(想象一下List的get),这时使用extends关键字,固定上边界的通配符。你可以将该对象当做一个只读对象;

当要将已有的数据写入对象(想象一下List的add),这时使用super关键字,固定下边界的通配符。你可以将该对象当做一个只能写入的对象;

当数据只希望使用Object类中的方法访问时,使用无边界通配符;

当你需要一个既能读又能写的对象时,就不要使用通配符了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值