java泛型(一)

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

为什么需要泛型

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为Integer
testMethod(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   是否正确?a

List<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可赋给unknowList2
for (Object obj : unknowList2) {// unknowList2的元素类型虽然未知,但总是一个Object
    System.out.println(obj);
}// 输出a

List<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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值