java泛型(一)

来源:互联网 发布:arrival to earth知乎 编辑:程序博客网 时间:2021/05/15 19:31

本文主要讲解泛型产生背景、泛型的定义、泛型的调用基本知识

为什么需要泛型

1、在没有泛型之前,java集合的设计者不知道我们会用集合保存什么类型的对象,为了保证通用性,他们把集合设计成能保存任何类型的对象(object)。

这样做带来两个问题:

  • 增加了编程复杂度
    • 一旦把一个对象“丢进”java集合,集合只知道它装的是Object,而忘记对象原本类型。因此取出集合元素后通常还需要进行强制类型转换,增加了编程的复杂度
  • 运行时容易引发ClassCastException异常
    • 集合对元素类型没有任何限制,因此创建一个保存String对象的集合,程序很容易将Integer对象“丢”进去(此时编译不会发出警告或错误)。这样在取集合元素进行强制类型转换时会引发ClassCastException异常。

2、在没有泛型之前,类中的方法要支持多个数据类型,需要对方法进行重载;引入范型后,一个方法变可解决此问题(多态)。

// 未使用泛型public void example(Integer i, Integer[] iAry);public void example(Double  d, Double[] dAry);
// 使用泛型 一个函数搞定public <T> void write(T t, T[] tAry);

因此,泛型既可以保证程序通用性,又可以在编译时对类型进行检查。让程序更简洁、健壮(泛型可以保证程序在编译时没有发出警告,运行时就不会产生ClassCastException异常)。

泛型的定义

上面例子可以看出,泛型其实就是形参类型参数化,即:参数化类型被称为泛型;

泛型形参的命名风格

推荐使用简练的名字表示类型参数(如单个字符),最好避免小写字母,使它和其他普通的形参易于区分。

常用的表示方法:

  • T
  • E
  • K
  • V

【注】

  • 如果有多个类型参数,可用字母表中T临近的字母表示,比如S
  • 泛型类中出现泛型函数,避免在方法的类型参数和类的类型参数中使用同样的名字,以免混淆。

类、接口、方法均可使用泛型,下面分别介绍使用方法

自定义泛型类/接口

  • 紧跟类名/接口名之后的<>内,指定一个或多个类型参数名
  • 多个类型参数之间用英文逗号隔开
  • 可以对类型参数的取值范围进行限定
  • 定义完类型参数后,可以在定义位置之后的任何地方使用类型参数,跟使用普通类型一样

【注】父类定义的类型参数不能被子类继承

// 泛型类public class ClassTest<T,S extends T>{// 对类型参数的取值范围进行限定,此处为上限,即S只能是T或T的子类,上限:s super T,表示S只能是T或T的父类}//泛型接口public interface InterfaceTest<E>{}

自定义泛型方法

  • 紧跟可见范围修饰(如public)之后的<>内,指定一个或多个类型参数名
  • 多个类型参数之间用英文逗号隔开
  • 可以对类型参数的取值范围进行限定
  • 定义完类型参数后,可以在定义位置之后的方法的任何地方使用类型参数,跟使用普通类型一样
public <T, S extends T> T testGenericMethodDefine(T t, S s){     ... }

【注】

  • 此处注意泛型方法与类型通配符的区别!(详细参见java泛型二)

泛型类的构造器

  • 为自定义泛型类定义构造器时,构造器名还是原来的类名, 不要增加泛型声明
    • 例如public ArrayList(){}
  • 与泛型方法类似,java也允许在构造器签名中声明类型形参,即泛型构造器
    • 泛型构造器不仅可以让java根据数据实参的类型推断类型形参的类型,程序员也可以显示地为构造器中的类型形参指定实际的类型
// 普通构造器public Foo(T t){}// 泛型构造器public <T> Foo(T t){}

注意事项

1、虽然构造器名不能增加泛型声明,但是调用构造器时却可是使用Foo<>的形式

new ArrayList<String>();new ArrayList<>();// java7菱形语法

2、泛型构造器的使用

public class Foo{    public <T> Foo(T t){}// 泛型构造器}new Foo("由java推断泛型构造器中T为String");new <String> Foo("显示指定泛型构造器中T为String");new <Integer> Foo("java推断的类型实参与程序员显示指定的类型实参不一致,编译错误");

3、泛型构造器与java7菱形语法

public class Foo<T>{    // 注意构造器名是类名,不能带尖括号    public <E> Foo(E e){}// 泛型构造器,且类型参数与泛型类Foo的类型参数不一样}Foo<Integer> foo1 = new Foo<>("T类型为Integer,E类型为String");// T与E没有限定关系,因此可以相同也可以不同Foo<Integer> foo2 = new <String> Foo<>("如果显示指定泛型构造器的E为String,就不能用菱形语法");// 编译错误Foo<Integer> foo3 = new <String> Foo<Integer>("正确");

泛型的调用

泛型的调用即使用泛型类、接口、方法,与调用方法需要给变量形参赋变量实参一样,调用泛型需要给类型形参赋类型实参,下面针对泛型类、接口、方法分别介绍。

给泛型类/接口进行类型参数赋值

1、声明泛型类对象

List<String> list;//紧接类名的尖括号内给定类型实参

2、实例化泛型类对象
此种情况要结合类定义的构造器的具体形式,具体参见”java泛型(二)”

list = new ArrayList<String>();// 构造函数后的尖括号内给定类型实参list = new ArrayList<>();// java7提供的菱形语法

给泛型方法进行类型参数赋值

与泛型类/接口不同的是,方法中的泛型参数无须显示传入实际类型参数。当调用泛型方法时,编译器根据变量实参推断类型参数的值,当不能成功推断时编译器报错。

public <T,S extends T> T testMethod(T t, S s){    ...}Number n = null;Integer i = null;String str = null;testMethod(n,i);// 此时T为Number,S为IntegertestMethod(n,str);// 此时T为Number,S为String,编译错误

给类型参数赋不确定值(通配符)

上面两节都是给类型参数赋予具体的值,除此之外,还可以给类型参数赋不确定的值。例如:

List<?> unknowlist;List<? extends Number> unknowNumberList;

为什么要使用通配符”?”

public Class GenericTest{    // 需求:test方法需要传入一个集合c,但该集合的元素类型不确定,有可能是String或Integer等    // 为了保证通用性,给类型参数赋值Object    public void test(List<Object> c){        for(Object obj : c){            System.out.println(obj);        }    }    // 测试test方法    // 集合c的元素是否可以为任意类型    public static void main(String[] args) {        List<String> strList = new ArrayList<>();        test(strList);// 编译错误,无法将test(java.util.List<java.lang.Object>)应用于(java.util.list<java.lang.String>)    }}

上述例子出现编译错误,说明List<String>不能当成List<Object>对象使用,即List<String>类并不是List<Object>类的子类
【注】

  • 如果Foo(String)是Bar(Object)的一个子类型(子类或子接口),G(List)是具有泛型声明的类或接口,G<Foo>不是G<Bar>的子类型 List<String>并不是List<Object>的子类型
  • 数组不同于泛型,假设Foo是Bar的一个子类型(子类或子接口),那么Foo[]依然是Bar[]的子类型;

上述方法不可行,但是为了通用性,必须找到一个在逻辑上同时是List<String>和List<Integer> 的父类型的一个引用类型。因此,类型通配符应运而生。上述例子可修改为:

public Class GenericTest{    public void test(List<?> c){// List<?>表示元素类型未知的List        for(Object obj : c){// 虽然集合元素类型未知,但可以肯定的是,它总是一个Object            System.out.println(obj);        }    }    public static void main(String[] args) {        List<String> strList = new ArrayList<>();        test(strList);// OK        List<Integer> intList = new ArrayList<>();        test(intList);// OK     }}

类型通配符注意事项

  • 类型通配符是使用?代替类型实参,而不是类型形参!
  • 带通配符的List无法调用add()方法添加元素
    • 根据List<E> 接口的方法add(E e) 知,传给 add的参数必须是E类的对象或其子类的对象。但本例中不知道E是什么类型,所以无法将任何对象”丢进”该集合
      这里写图片描述
    • 唯一例外是null,它是所有引用类型的实例
  • 带通配符的List可以调用get()方法获取元素
    • get()方法返回一个未知类型,但可以肯定它总是一个Object

举例说明

List<?> list2= new ArrayList<>();list2.add(null);// 编译正确!!!Object e = null;unknowList1.add(e);// 编译错误!!!

泛型转换

List<Integer> intList = new ArrayList<>();intList.add(1);List list = intList;// a   是否正确?aList<String> strList = lsit;// b   是否正确?System.out.print(strList.get(0);// c  是否正确

1、a处正确,只是list对象无法保持元素类型

  • 将intList对象赋给list对象,通过java擦除技术,丢失intList集合的元素类型信息,认为list默认元素类型为声明该类型信息时指定的第一个上限类型。此处根据List接口的声明,元素类型为Object

2、b处正确,但引起“未经检查的转换”警告

  • java允许直接把一个List对象即List list = new ArrayList<>();赋给一个List<Type> 类型的变量(Type可以是任何类型),所以编译通过,但并不确定List对象的元素类型,因此会给出“未经检查的转换”警告

3、c处错误

  • list变量实际上引用的是List<Integer> 集合,所以将集合中的元素当成String取出时引发异常
/*************List<?> List<String> List<Integer>的相互赋值问题**********/List<String> strList = new ArrayList<>();strList.add("a");List<?> unknowList2 = new ArrayList<>();unknowList2 = strList;// List<?>是List<String>的父类,因此strList可赋给unknowList2for (Object obj : unknowList2) {// unknowList2的元素类型虽然未知,但总是一个Object    System.out.println(obj);}// 输出aList<Integer> strList2 = new ArrayList<>();strList2 = (List<Integer>) unknowList2;// 强制类型转换,虽然编译通过,但是很可能出现java.lang.ClassCastException异常for (Integer str : strList2) {    System.out.println("===\r\n" + str);}// 抛出java.lang.ClassCastException异常List<String> strList3 = new ArrayList<>();strList3 = (List<String>) unknowList2;// 强制类型转换for (String str : strList3) {    System.out.println("===\r\n" + str);}// 输出a
0 0
原创粉丝点击