Java中的泛型

泛型可以是我们在编译期而不是运行期检测出错误,同时能够提高代码的可靠性和可读性。
泛型可以参数化类型,这个功能使我们可以定义带泛型类型的类或者方法,随后编译器会使用具体的类型来替换它。
泛型类或者方法允许用户指定可以和这些类或者方法一起工作的对象类型。如果试图使用一个不相容的对象,编译器就会检测出这个错误。

可以使用<T> 来表示形式泛型类型,随后可以使用一个实际的具体类型来替换它。替换泛型类型的过程称为泛型实例化
(按照惯例,一般使用 E 和 T 这样的大写字母表示泛型类型)
泛型类型必须是引用类型。不能使用 int|double|char 这样的基本类型来替换泛型类型。(如果要使用 int 这种基本类型,应该用它们的包装类型替换)

定义泛型类和接口

可以为类或者接口定义泛型。当使用类来创建对象或者使用类或者接口来声明引用变量时,必须指定具体的类型。

public class GenericStack<E> {
	private ArrayList<E> list = new ArrayList<E>();

	public GenericStack() {
		// do nothing
	}
	
	public int getSize() {
		return list.size();
	}
	
	// 读取数据
	public E peek() {
		return list.get(getSize() - 1);
	}
	
	// 入栈
	public void push(E o) {
		list.add(o);
	}
	
	// 出栈
	public E pop() {
		E o = peek();
		list.remove(getSize() - 1);
		return o;
	}
	
	// 判断栈是否为空
	public boolean isEmpty() {
		return list.isEmpty();
	}
}

为了创建一个字符串栈类的对象,可以使用new GenericStack<String>()。但是这并不意味着 GenericStack 类的构造方法形式是 public GenericStack<E>(){},泛型类的构造方法和普通类的构造方法在形式上没有任何区别。

有时候,泛型类可能需要多个参数。在这种情况下,应该将所有参数一起放在尖括号中,并用逗号分隔开,比如<E, T>

可以定义一个类或者接口作为泛型类或者泛型接口的子类型。例如。在 Java API中,java.lang.String 类被定义为实现 Comparable 接口:public class String implements Comparable<String>

定义泛型方法

可以定义泛型类和泛型接口,也可以使用泛型类型来定义泛型方法。

public class GenericMethodDemo {
	public static void main(String[] args) {
		Integer[] arr1 = {1, 2, 3, 4, 5, 6, 7};
		String[] arr2 = {"12", "23", "34", "45" , "56"};
		int[] arr = new int[] {1, 3, 4};
		GenericMethodDemo.<Integer>print(arr1);
		print(arr2);
//		print(arr);
	}
	public static <E> void print(E[] list) {
		for (int i = 0; i < list.length; i++) {
			System.out.print(list[i] + " ");
		}
		System.out.println();
	}
}	

可以为静态方法定义泛型类型。
为了声明泛型方法,将泛型类型<E>置于方法头关键字 static 之后,方法返回值之前。如果要定义的泛型方法是非静态方法,那么泛型标识应该是在权限标识符之后,返回值类型之前。
为了调用泛型方法,需要将实际类型放在尖括号内作为方法名的前缀。也可以省略由系统自动判断。

受限泛型

可以将泛型指定为另一种类型的子类型。这样的泛型称为受限泛型
受限泛型的语法是在尖括号之内使用extends关键字对泛型指定父类型:<E extends ParentObject>
非受限泛型类型<E>等同于<E extends Object>

原始类型和向后兼容

为什么会有原始类型?

在严格的泛型程序中,使用带泛型声明的类应该总是为之指定类型实参,但是为了和早期的Java版本向后兼容,Java也允许声明带泛型的类时不指定类型参数。
如果使用带泛型声明的类时没有传入类型参数,那么这个类型默认是声明该参数时指定的第一个上限类型,这个类型参数被称为原始类型(raw type)
按照泛型的使用,应该使用如下方法创建 ArrayList:

ArrayList<String> list = new ArrayList<String>();

但是对于原始类型,可以是这样的:

ArrayList list = new ArrayList();

此时,会有警告:ArrayList is a raw type. References to generic type ArrayList<E> should be parameterized.(ArrayList是一个原始类型。应该参数化对泛型类型ArrayList<E>的引用)
当尝试把原始类型的变量赋给带类型的变量时,会发生一些有趣的事情。

  • 当程序把一个原始类型的变量赋给一个带泛型类型的变量的时候,只要它们的类型保持兼容,系统都不会出现问题,总是可以通过编译,只是会提示一些警告信息。
    public class RawTypeTest {
        public static void main(String[] args) {
    	    List list = new ArrayList();
    	    list.add("aaa");
    	    list.add("bbb");
    	    list.add("ccc");
    	    List<Integer> intList = list;
        	for (int i = 0; i < intList.size(); i++) {
    	    	System.out.println(intList.get(i));
    	    }
    	}
    }
    
    list 是原始类型的变量,由于里面存储的是字符串,所以实际类型是字符串类型的。intList是带有泛型信息的变量,泛型指定类型是 Integer ,上文描述中说的类型兼容是指 list 和 intList 都是 java.util.List 类型,而不是说 String 类型和 Integer 类型兼容。赋值之后,JVM 会把原始集合中的元素都当作 Integer 类型处理。但是由于上面的示例中仅仅是做了输出操作,而输出操作无所谓类型,就没有报错。
  • 当程序试图访问带泛型声明的集合元素时,编译器总会把集合元素当成泛型类型处理,而并不关心集合里的元素的实际类型。JVM会遍历每个集合元素并自动执行强转,如果集合元素的实际类型与泛型类型信息不匹配,运行时就会发生 ClassCastException.
    public class RawTypeTest {
        public static void main(String[] args) {
    	    List list = new ArrayList();
    	    list.add("aaa");
    	    list.add("bbb");
    	    list.add("ccc");
    	    List<Integer> intList = list;
        	for (int i = 0; i < intList.size(); i++) {
    	        Integer in = intList.get(i);
    		    System.out.println(in);
    	    }
    	}
    }
    
    执行上面的程序,发生 java.lang.ClassCastException

原始类型带来的擦除

当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有尖括号之间的类型信息将被全部丢掉。比如,将一个 List<String>类型的对象转型为 List,则该 List 对集合元素的类型检查变成了类型变量的上限 Object。
当把一个带泛型信息的Java对象赋值给不带泛型信息的变量时,Java程序会发生擦除,这种擦除不仅会擦除使用该Java类时传入的类型实参,而且会擦除所有的泛型信息,也就是擦除所有尖括号里的信息。

通配泛型

通配泛型的分类

  • ? (extends Object)非受限通配
  • ? extends T受限通配,表示 T 或 T的子类型
  • ? super T下限通配,表示 T 或 T的父类型

可以使用非受限通配、受限通配或者下限通配来对一个泛型类型指定范围。

为什么使用通配泛型

public class WildCardNeedDemo {
	public static void main(String[] args) {
		GenericStack<Integer> stack = new GenericStack<>();
		stack.push(1);
		stack.push(2);
		stack.push(8);
		System.out.println(max(stack));
	}
	
	public static double max(GenericStack<Number> stack) {
		double max = stack.pop().doubleValue();
		while (!stack.isEmpty()) {
			double value = stack.pop().doubleValue();
			if (value > max) {
				max = value;
			}
		}
		return max;
	}
}

上面的程序中,main 方法的最后调用 max(GenericStack<Number> stack) 方法的时候发生错误。错误原因:The method max(GenericStack<Number>) in the type WildCardNeedDemo is not applicable for the arguments (GenericStack<Integer>). 即实参和形参类型不匹配。
Integer 是 Number 的子类型,但是 GenericStack<Integer> 并不是 GenericStack<Number> 的子类型。为了解决在这个问题,就应该使用通配泛型类型。上面程序 max 方法的声明格式应该是 public static double max(GenericStack<? extends Number> stack).

关于下限通配类型的使用情况说明:

public class SuperWildCardDemo {
	public static void main(String[] args) {
		GenericStack<String> stack1 = new GenericStack<>();
		GenericStack<Object> stack2 = new GenericStack<>();
		stack2.push("Java");
		stack2.push(2);
		stack1.push("Sun");
		stack1.push("CCF");
		add(stack1, stack2);
		AnyWildCardDemo.print(stack2);
	}

	public static <T> void add(GenericStack<T> stack1, GenericStack<? super T> stack2) {
		while (!stack1.isEmpty()) {
			stack2.push(stack1.pop());
		}
	}
}

add 方法的功能是将 stack1 中的元素压入 stack2 栈中。所以,要求 stack1 栈中元素的类型是 stack2 栈中元素的类型或者它的子类型。GenericStack<T> 和 GenericStack<? super T> 很好地满足了这一要求。
上面的情况实际上可以被 GenericStack<? extends T> GenericStack<T> 替代。

消除泛型和对泛型的限制

编译器可以使用泛型信息进行类型安全检测,但是这些信息在运行时是不可用的。这种现象称为类型消除。所以,泛型信息在运行期间是不可用的。换言之,泛型只存在于编译期,一旦编译器确认泛型类型是安全使用的,就会将它转换为原始类型。那么,在程序设计的时候就不能将泛型使用在运行期才能确定的地方,否则就会引发错误。

取消泛型

ArrayList<String> list = new ArrayList<>();
list.add("Java");
String str = list.get(0);

上面的程序在编译时将发生消除泛型,编译之后程序成为下面的样子:

ArrayList list = new ArrayList();
list.add("Java");
String str = (String)(list.get(0));

对于受限泛型,编译器会使用受限类型代替泛型,非受限泛型将使用 Object 代替。

需要注意的是,不管实际的具体类型是什么,泛型类是被它的所有示例所共享的。假定按照下面的方式创建 list1 和 list2:

ArrayList<String> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();

尽管在编译的时候 ArrayList<String> 和 ArrayList<Integer> 是两种类型,但是,在运行的时候只有一个 ArrayList 类会被加载到 JVM 中。list1 和 list2 都是 ArrayList 的实例。

泛型的使用限制

由于泛型类型在运行时被消除,因此,对于如何使用泛型类型是有一些限制的。

  • 不能使用 new E()
    不能使用泛型类型参数创建实例(对象)。
  • 不能使用 new E[]
    不能使用泛型类型创建数组。但是可以通过创建一个 Object 类型的数组然后使用 E[] 类型强转来规避这条限制。
  • 在静态上下文中不允许类的参数是泛型类型
    由于泛型类的所有实例都有相同的运行时类,所以泛型类的静态变量和方法是被它的所有实例所共享的。因此,在静态方法、数据域或者初始化语句中,为类引用泛型类型参数是非法的。
  • 异常类不能是泛型的
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值