JAVA泛型

来源:互联网 发布:mac微信接收的文件 编辑:程序博客网 时间:2024/04/30 16:46

基础

当一个对象被放进集合时,集合就会忘记该对象本身的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型。当程序取出对象后,若要进行强制类型转换,就可能出现ClassCastException。而且如果对放入对象没有限制,就可以什么对象都放进去,下面举一个例子。
import java.util.ArrayList;import java.util.Collection;import java.util.Iterator;import java.util.List;public class ListTest {public static void main(String []args){List l=new ArrayList();l.add("hello,world!");l.add("welcome!");l.add(3);Iterator i= l.iterator();while(i.hasNext()){System.out.println((String)i.next());}}}

运行结果如下:
hello,world!welcome!Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Stringat 泛型.ListTest.main(ListTest.java:19)

可以看到,因为意外导致一个int型数据放入了队列,导致了ClassCaseException。而泛型可以解决这个问题。

public static void main(String []args){List <String> l=new ArrayList<> ();l.add("hello,world!");l.add("welcome!");//如果不注释下一行在编译时便会报错//l.add(3);Iterator i= l.iterator();while(i.hasNext()){System.out.println((String)i.next());}}
运行结果
hello,world!welcome!

在JAVA7之前,如果使用带泛型的接口、类定义变量,在构造器之后也需要带泛型,举个例子:
List <String> ls=new ArrayList<String>();Map<String,Integer>scores=new HashMap<String,Integer>();
其实后面的泛型有些多余,JAVA7之后,允许构造器之后只写一对<>即可。

深入泛型

泛型就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即实际传入的类型参数,也可称为类型实参)

当在一个接口(或类)中使用类型形参时,在该接口(类)中,该类型形参可以当做类使用。当实际使用接口时,只要为类型形参传入实际的类型实参。这样,尽管程序只定义了一个接口,但实际使用时可以产生无数个子接口。但是这种子接口是逻辑上的,物理上并不存在——系统中没有进行源码复制。

通过下面的例子就可以很清楚地看出,这些子类或子接口只是逻辑上的:
public class Apple <T>{private T info;public Apple(T in){this.info=in;}public void setInfo(T info){this.info=info;}public T getInfo(){return this.info;}public static void main(String [] args){Apple<String> a1=new Apple<>("I'm a apple");System.out.println(a1.getInfo());System.out.println(a1.getClass());Apple<Integer> a2=new Apple<>(33);System.out.println(a2.getInfo());System.out.println(a2.getClass());}}
运行结果如下:
I'm a appleclass 泛型.Apple33class 泛型.Apple
两个对象的类型是一致的,并没有区别。

当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是其类名,不需要加泛型声明。就像上面的Apple<T>类,其构造器名为Apple,而不是Apple<T>,当实际调用构造器时,可以使用Apple<T>的形式。

在创建带泛型声明的接口、父类之后,可以为该接口创建实现类,或者从该父类派生子类,当使用这些父类、接口时不能再包括形参。
以下代码是错误的:
public class A extends Apple<T>{}
如果需要派生子类或实现接口,可以这样:
public class A extends Apple<String>{}
当需要重写父类方法时,要注意函数的返回值类型

调用方法必须为所有的数据形成传入参数值,不过调用类、接口时也可以部位类型形参传入参数值,比如以下代码是正确的
public class A extends Apple
如果使用Apple类时没有传入实际的类型参数,JAVA编译器可能发出这样的警告:使用了未经检查或不安全的操作——这是泛型检查的警告,此时,系统会把Apple<T>里的T形参当成Object类型处理。

JAVA中不存在泛型类,系统不会把ArrayList<String>视为ArrayList的子类,也不会生成新的class文件,不管为泛型的类型形参传入哪一种类型实参,对于JAVA来说,他们还是被视为同一个类,在内存中也只占一块内存空间。因此在类成员、类方法、静态初始化块中变量的声明或初始化不能使用类型形参。

由于系统不会生成真正的泛型类,所有instanceof运算符后也不能使用泛型类。


类型通配符

类型通配符的基本使用

List<String>对象不能被当成List<Object>对象来使用,也就是说,List<String>不是List<Object>的子类。

假设Foo是Bar的一个子类型(子类或子接口),那么Foo[ ]依然是Bar[ ]的子类型;但G<Foo>不是G<Bar>的子类。

为了表示各种泛型List的父类,可以使用类型通配符?,将一个?作为类型实参传入,写作List<?>,表示元素类型未知的List,可以匹配任何类型。
import java.util.List;public class ListTest {public void Test(List <?> c){  for(int i=0;i<c.size();i++)    {System.out.println(c.get(i));  }   System.out.println(c.get(0).getClass());}public static void main(String []args){List <String> l=new ArrayList<> ();l.add("hello,world!");l.add("welcome!");ListTest lt=new ListTest();lt.Test(l);}}
输出结果为:
hello,world!welcome!class java.lang.String

不过带通配符的List仅表示为各种List的父类,并不能实际传入对象——因为程序无法确定List中元素的类型。
另一方面,程序可以调用get()方法来返回List<?>集合指定索引处的元素,其返回值类型未知,不过必然是一个Object,所以可以赋值给一个Object对象

设定类型通配符的上限

类型通配符除了可以表示所以类型的父类,还可以表示特定类型的父类,即被限制的泛型通配符。
List<? extends X>//X为某个父类,其所有子类及该类自己的List都可以传入
这里的类型依旧是未知的,但可以肯定一定是X的子类型,因此X也被称为这个通配符的上限。

设定类型形参的上限

JAVA不仅允许使用类型通配符设定上限,还可以在定义类型形参时设定上限,用于表示传给该类型的类型实参要么是该上限类型,要么是其子类型。
public class Apple <T extends Number>
在更极端的情况下,程序需要为类型形参设定多个上限(至多一个父类上限,可以有多个接口上限),表明该类型实参必须是其父类或其子类,并必须实现多个接口。
//表明T类型必须是Number类或其子类,并必须实现这个接口public class Apple <T extends Number &java.io.Serializable>
与实现接口类似,当为类型形参之多多个上限的时候,所有接口上限都必须位于类上限之后。

泛型方法

如果在定义类、接口时没有使用类型形参,还可以在在定义方法时定义其自己的类型形参。

与类、接口不同,使用泛型方法时无需显式的传入实际类型参数,因为编辑器可以根据实参的类型形参的值推断出最直接的类型参数。

当将一个集合中的元素复制到另一个集合,那么前一个集合的元素必然是后一个集合元素的父类(或是同一个类),这样两个集合之间就有关联。
static<T> void Test(Collection<? extends T>From,Collection<T> to)

泛型方法和类型通配符的区别

当类型形参唯一的作用是在不同调用点传入不同的是积累下,此时应使用通配符——通配符被设计出来就是用来支持灵活的子类化的。
泛型方法允许类型形参表示方法的一个或多个参数直接的类型依赖关系,或返回值与参数直接的依赖关系,如果没有这样的依赖关系,不应使用泛型方法。

泛型构造器

JAVA允许在构造器中定义类型形参,在调用类型形参时,既可以显示地指定类型实参,也可以让编译器来推断。

但如果程序显示制定了泛型构造器中声明的类型形参的实际类型,则不可以使用菱形语法。
class MyClass<E>{public <T>MyClass(T t){System.out.println("t的参数值: "+t);}}public class DiamondTest {public static void main(String [] args){//MyClass类中声明的E形参是String型//泛型构造器中形参T为Integer型MyClass <String> mc1=new MyClass<>(5);//依旧是Integer型MyClass <String> mc2=new <Integer> MyClass<String>(5);//以下显示地声明了泛型构造器的形参类型,却使用了菱形语法,是错误的//MyClass <String >mc3=new <Integer>Myclass<>(5);}}
结果为:
t的参数值: 5t的参数值: 5

设定通配符下限

JAVA允许设定通配符的下限<? super Type>,这个通配符表示必须是Type本身或是其父类。

通过使用这种通配符下限的方式来定义TreeSet构造器的参数,就可以将所有可用的Comparator作为参数传入,从而增加了程序的灵活性。不仅TreeSet有这种用法,TreeMap也有类似的用法。


擦除和转换

在严格的泛型代码中,带泛型声明的类总应该带着泛型参数。但为了与老的JAVA代码保持一致,也允许使用带泛型声明的类时不指定实际的泛型参数。如果没有给这个泛型类指定实际的类型参数,则该类型称为实际原始类型,默认是声明该类型参数时指定的第一个上限类型。
当一个指定实际的类型参数的类赋值给了一个没有指定知己参数的类时,编译器会丢失<>内的信息(泛型信息,及丢失集合里元素的类型信息),这就是所谓的擦除。

从逻辑上讲,List<String>是List的子类,将List对象直接赋值给List<String>对象会引起编译错误,但实际上不会,编译器仅仅提示“未经检查的转换”。但在实际访问元素时会出现异常。

泛型和数组

JAVA泛型有一个很重要的设计原则——如果一段代码在编译时没有提出“[unchecked]未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。

所以除了无上限的类型通配符,数组元素不能包括类型变量或类型形参(可以声明,不能产生对象)

创建无上限的通配符泛型数组应该用instanceof运算符来保证其类型

创建元素类型是类型变量的数组对象会导致编译错误。
2 0
原创粉丝点击