初识java泛型

时间:2023-12-27 10:59:13

1 协变数组类型(covariant array type)

  数组的协变性:

  if A IS-A B then A[] IS-A B[]

也就是说,java中的数组兼容,一个类型的数组兼容他的子类类型数组。

  协变数组好处:使得代码的灵活性更强。

  协变数组的坏处:过于灵活导致类型混乱,比如:

  Peron[] arr = new Employee[5]; //Employee IS-A Person 可以执行

  arr[0] = new Student();// Student IS-A Person 可以执行

  上面这一段程序在编译器编译完全通过,都是在实际执行中会报ClassCastException异常,因为arr[0] 实际上时Employee类型的引用,但现在将Student类型的对象赋给它,类型不同因此会报错。由此可以看出,只限制类型继承关系的数组协变性会带来数据类型的混乱。

2 Java5之前的泛型结构特性构件

  面向对象的一个重要目标是实现代码的复用,而泛型机制可以实现在不同类型而具有实现方法时只用一种泛型方法实现这些方法。比如:现在我们需要实现一个简单的类,该类只需要对输入的元素进行命令行输出,此时如果函数的形参限定为String 那我们只能输入输出String类型的元素,同理当为Integer时只能输入Integer类型不能输入String类型,否则会报错,这是可以使用他们共同的父类Object,由于java的多态性,可以将子类赋给父类,由此可以只用一种Object作为形参的函数,而分别处理这这两种类型。

  在java5推出泛型机制之前,程序当要实现泛型一般时使用超类(大部分待处理类型的父类)Object。

 下一段代码就是:

public class MemoryCell
{
public Object read(){return storeValue; }
public void write(Object x){storeValue=x;
System.out.println("storevalue="+storedValue);}
private Object storeValue;
}

  在上面这个程序使用Object类作为storeValue的类型,以及wirte形参的类型,由java的多态机制,可以将Object的子类对象作为实参传入。

class TestMemoryCell
{
public static void main(String[] args)
{
MemoryCell mc = new MemoryCell();
mc.write("32");//将一个Object的子类字符串作为实参
mc.read();//输出val=32
  
     MemoryCell mc2  = new MemoryCell();
mc.write(new Integer(343));//将一个Object的子类字符串作为实参
mc.read();//输出val=343
} {

 由此,我们可以讲任意形如String等Object的子类,作为实参输入给函数,完成对该元素的打印功能。

3 java5提出的泛型机制

  在java5之前人们想实现java机制都需要通过上述机制,都是上述机制存在一些问题,当用Object作为形参不可避免的带来了和数组协变性相同的问题--数据类型的混乱。可是实际开发又要求java实现泛型特性以完成对代码的复用,怎么办?只能推出新的规范呗,正好大家都喜欢那样做。

  泛型包括泛型类与泛型static方法。

3.1 泛型类

  形式:class ClassName <AnyType1|,AnyType2...>{...} //AnyType

  泛型类的实例化: ClassName<String> cn = new ClassName<String>(); or  ClassName<String> cn = new ClassName<>();//String 只是一个例子,可以为其他类型,如Integer

  注意:AnyType在定义是可以为任意字符串,包括String等,而此时的String只作为一个变量名,而不是说将泛型类型定为字符串;AnyType:可以作为类型在类中使用。

  由此可以看出,泛型类就是在类名后加了对尖括号,在尖括号里可以为类型参数。同样接口也可以定义为泛型。

  java5的泛型机制和java5之前的泛型结构有着一个显著的区别:泛型可以记住数据的类型,而Object类型的元素输出都需要强制转换。记住数据类型还可以保证在数据输入时数据类型得以确定。

  使用泛型的优点:①由(2)可以知道泛型可以实现代码的复用;

  ②可以将原本运行时才能发现的错误提前到编译时发现。例如:数组协变性,同样在java5之前的泛型也存在这种问题,但是使用泛型后可以在编译的时候报错。

  

3.2 类型通配符

  一个简单的泛型类MemoryCell:

class MemoryCell<X>
{
public X read()
{
return storedValue;
}
public void write(X x)
{
storedValue = x;
System.out.println("storedValue="+storedValue);
}
private X storedValue;
}

  

  MemoryCell<String>与MemoryCell<Object>是不是两个不同的类?

class TestMemoryCell
{
public static void main(String[] args)
{
MemoryCell<String> mc1 = new MemoryCell<String>();
MemoryCell<Object> mc2 = new MemoryCell<Object>();
System.out.println("new MemoryCell<String>().ClassName=="+mc1.getClass()); //输出:new MemoryCell<String>().ClassName==class MemoryCell
System.out.println("new MemoryCell<Object>().ClassName=="+mc2.getClass()); //输出:new MemoryCell<Object>().ClassName==class MemoryCell
System.out.println(mc1.getClass()==mc2.getClass());//输出:true
}
}

  由上程序可以看出MemoryCell<String>与MemoryCell<Object>是同一个类。也就是说在内存中只保留了一个MemoryCell类。具体是如何实现数据类型限制的看这篇博客吧http://blog.csdn.net/yi_afly/article/details/52002594,吼吼,是用隐形的重载方法(java中不能有返回值不同的重载,但这里的class就是有两个签名),在运行时让JVM来区别。

  MemoryCell<String>是不是MemoryCell<Object>的子类?

class TestMemoryCell
{
public static void main(String[] args)
{
MemoryCell<String> mc1 = new MemoryCell<String>();
MemoryCell<Object> mc2 = mc1; //错误: 不兼容的类型: MemoryCell<String>无法转换为MemoryCell<Object> }
}

  假设是子类,那mc1可以直接赋给mc1,所以不是子类。但是明显Object时String的父类,为什么他们对应的泛型却不存在这种关系呢?这是设计者有意为之,就是不想出现如图一开始讲的数组协变性带来的编译通过而运行时因为数据类型混乱而报错。

  那么假设有程序如下:

class TestMemoryCell
{
public static void main(String[] args)
{
MemoryCell<String> mc1 = new MemoryCell<String>();
MemoryCell<Object> mc2 = new MemoryCell<Object>();
//System.out.println("new MemoryCell<String>().ClassName=="+mc1.getClass());
//System.out.println("new MemoryCell<Object>().ClassName=="+mc2.getClass());
//System.out.println(mc1.getClass()==mc2.getClass());
//MemoryCell<Object> mc3 = mc1;
mc1.write("haha");
get(mc1); }
public static void get(MemoryCell<Object> mc)
{
System.out.println("storedValue="+mc.storedValue);
}
}

  则该程序报错,错误: 不兼容的类型: MemoryCell<String>无法转换为MemoryCell<Object>。但是有事我们只需要Object的toString方法,这么严格的限制会影响程序的灵活性,于是提出了类型通配符。

  类型通配符:<?> //? 作为实参传递给泛型类

  类型通配符<?>的意思是,可以为任意通配符。所以,

  对上述程序做如下修改即可顺利输出

	public static void get(MemoryCell<?> mc)
{
System.out.println("storedValue="+mc.storedValue);
}

3.2 类型通配符的上限,类型通配符下限,设定类型形参的上限

  类型通配符的上限:有时,我们希望?代表的是某一类有相同父类的子类,如此便可以保证父类方法的顺利执行,于是使用<? extends AnyType>这是便要求?必须为Anytype的子类。

class TestMemoryCell
{
public static void main(String[] args)
{
MemoryCell<String> mc1 = new MemoryCell<String>();
MemoryCell<Object> mc2 = new MemoryCell<Object>();
mc1.write("haha");
get(mc1);//String extens String 可以执行
mc2.write("aha");
get(mc2);//Object IS-NOT-A String 报错,不兼容的类型: MemoryCell<Object>无法转换为MemoryCell<? extends String> }
public static void get(MemoryCell<? extends String> mc)
{
System.out.println("storedValue="+mc.storedValue);
}
}

  同样可以定义下限,<? super AnyType> ?必须为AnyType的父类。

  在类的定义中,同样可以设定类型形参的上限,如此便限制了创建实例时传入的形参。

4 Java5 泛型方法

  何时要用到泛型方法?

  在定义类、接口时没有使用类型形参但是在定义方法的时候却想自己定义类型形参,这是就需要java5提供的泛型方法。

4.1 定义泛型static方法

 

class GenericMothod
{
static void fromArrayToCollection(Object[ ] a ,Collection<Object> c)
{
for( Object o : a )
c.add( o );
} public static void main(String [ ] args)
{
String[ ] strArr = {"a " , "b"};
List<String> strList = new ArrayList<>();
fromArrayToCollection(strArr , strList );//编译报错,无法将Collection<String>转换为Collection<Object>
}
}

  上一段代码编译无法通过,因为Collection<String> IS-NOT-A Collection<Object>,但是如果不用集合而是使用数组则可以实现,并且执行也不会报错,因为数组具有协变性。那这样岂不是说用泛型会带来很大的不便,或者说降低了代码的复用性,因为若要使用Collection<String>则必须重新定义一个函数,利用java的方法重载来实现。所以java5 提出了新的泛型方法。

  泛型方法格式: 修饰符 <T,S> 返回类型 方法名 (形参列表){...}  //在定义方法的时候定义一个或者多个类型形参;

  如此上述程序的fromArrayToCollection可以变为如下形式:

  

  
import java.util.*;
class GenericMethod
{
static<T> void fromArrayToCollection(T[ ] a ,Collection<T> c)
{
for( T o : a )
c.add( o );
} public static void main(String [ ] args)
{
String[ ] strArr = {"a " , "b"};
Object[ ] strArr2 = {"a " , "b"};
List<String> strList = new ArrayList<String>();
List<Object> strList2 = new ArrayList<Object>();
fromArrayToCollection(strArr , strList );//编译通过
fromArrayToCollection(strArr2 , strList2 );//编译通过
}
}

  由该程序可以看出泛型方法与泛型类不同,泛型方法的形参无需显示的传入实际参数,而是通过fromToCollection(strArr , strList)编译器根据实参类型推断出类型参数。那么如果前后两个实参推断出来的类型参数不一致呢?那么编译自然错误。例如:

import java.util.*;
class GenericMethod
{
static<T> void fromArrayToCollection(T[ ] a ,Collection<T> c)
{
for( T o : a )
c.add( o );
} public static void main(String [ ] args)
{
String[ ] strArr = {"a " , "b"};
Object[ ] strArr2 = {"a " , "b"};
List<String> strList = new ArrayList<String>();
List<Object> strList2 = new ArrayList<Object>();
fromArrayToCollection(strArr , strList );//编译报错,无法判别T类型
fromArrayToCollection(strArr , strList2 );//编译不报错,T为Object
}
}

  泛型方法同样也可以使用类型参数通配符,例如:

static<T> void test(Collection <? extend T>from ,Collection<T> to)
{
for(T ele : from)
to.add(ele);
} pubic static void main(String[ ] args)
{
List<Object> ao =new ArrayList<>();
List<String> as = new ArrayList<>();
test(as, ao); //T判别为Object ,编译通过。
}

5 泛型方法与类型通配符的区别

 // Collection接口的类型通配符方式定义
public interface Collection<E>
{
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extend E> c);
...
}
// Collection接口的泛型函数方式定义
public interface Collection<E>
{
<T> boolean containsAll(Collection<T> c);
<T extends E>boolean addAll(Collection<T> c);
...
}

  由上面的例子可以看出来泛型方法和类型通配符可以实现相同的功能。那么我们什么时候使用泛型方法?

  使用泛型方法:运行类型形参用来表示方法的一个或者多个参数之间的依赖关系,或者返回值与参数之间的关系。如果不存在这种关系就应该使用类型通配符。

  泛型方法与类型通配符的显著区别:类型通配符可以在方法签名中定义形参类型,也可以用于定义变量的类型;但是泛型方法的类型形参必须在对应方法中显示说明。

6 泛型的限制

  由于泛型类在编译器会通过类型擦除变为非类型类(泛型类中提到了泛型类在内存中只存一个非泛型类),所以对泛型定义的时候也存在限制。

  ①基本类型不能作为类型参数

  ②instanceof检测不能对泛型类使用

  ③在泛型类中static修饰的类变量类方法均不可以引用类的类型变量,因为类型擦除后类型变量不存在并且由于只存在一个原始类所以static变量在泛型实例中时共享的。

  ④泛型的类型变量不能实例化

  ⑤不存在泛型的数组

  ⑥参数化类型的数组的实例化时非法的。

  

Reference:

1.《数据结构与算法分析 java描述》

2.Java总结篇系列:java泛型 http://www.cnblogs.com/lwbqqyumidi/p/3837629.html

3.《疯狂java讲义》第九章 泛型