Java(泛型篇)

一、泛型入门

Java集合有一个缺点,就是把一个对象“丢进”集合之后,集合会“忘记”这个对象的数据类型。当再次取出该对象时,该对象的类型就变成了Object类型。Java之所以这样设计,是因为集合的设计者也不知道程序应用者会用集合来保存什么类型的对象。所以为了方便才做这样的通用性。这样做会带来如下两个问题:

  • 集合对元素类型没有任何限制,当我们想创建一个只能保存String对象的集合时,也可以轻易把Integer对象“丢进”集合。
  • 把对象丢进集合后,会丢失了对象的状态信息,集合只知道它盛装了Object,因此取出时需要进行强制转换。这种转换增加了编程的复杂度,也可能引发ClassCastException异常。

二、使用泛型

Java5之后,引入了“参数化类型”的概念,允许在创建集合时指定集合元素的类型,常见的List<String>,表明该List只能保存字符串类型的对象。Java的参数化类型被称为泛型

package Genericity;

import java.util.ArrayList;
import java.util.List;

public class DemoTest {
	public static void main(String[] args) {
		try {
			noGenericityTest();
		} catch (Exception e) {
			e.printStackTrace();
		}
		isGenericityTest();
	}
	//不加泛型
	private static void noGenericityTest() {
		List list = new ArrayList();
		list.add("string");
		list.add(1);
		list.forEach(str -> System.out.println((String) str));
	}
	
	//加泛型
	private static void isGenericityTest() {
		List<String> list = new ArrayList<>();
		list.add("string");
//		list.add(1);	//编译报错
		list.forEach(str -> System.out.println((String) str));
	}
	
}

输出:
string
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    at Genericity.DemoTest.lambda$0(DemoTest.java:21)
    at java.util.ArrayList.forEach(Unknown Source)
    at Genericity.DemoTest.noGenericityTest(DemoTest.java:21)
    at Genericity.DemoTest.main(DemoTest.java:9)
string

 

 

 

三、深入泛型

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

1、定义泛型接口,类

Java5之后改写了List接口、Iterator接口、Map接口。

public interface List<E> extends Collection<E> {

//部分代码片段

 Iterator<E> iterator();

 <T> T[] toArray(T[] a);

 boolean add(E e);

 boolean remove(Object o);


boolean addAll(Collection<? extends E> c);

}


public interface Map<K,V> {

//部分代码片段

 V get(Object key);

 V put(K key, V value);

 V remove(Object key);

 void putAll(Map<? extends K, ? extends V> m);

}



public interface Iterable<T> {

//部分代码片段

Iterator<T> iterator();


  default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }


}




可以看见一些简单的接口声明,泛型的实质:允许在定义接口、类时声明类型形参,类型形参在整个接口、类体内可当成类型使用,几乎所有使用类型的地方都可以使用这种类型形参。

这里如果使用List类型时,如果为E形参传入String类型实参,则产生一个新类型:List<String>类型。可以把List<String>想象成E被全部替代成String的特殊List子接口。必须指出List<String>在系统中没有进行源码的复制,二进制中没有,磁盘中也没有,内存中也没有。只是逻辑上的子类,物理上这样的子类并不存在。

2、从泛型类派生子类

当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类。需要指出的是,当使用这些接口、父类时不能在包含父类形参。例如,下面的代码是错误的。

//定义一个Apple类,Apple类不能跟类型形参
public class A extends Apple<T> { }

如果想从Apple类派生一个子类,则可以改写为如下代码:

//定义一个Apple类,T形参传入String
public class A extends Apple<String> { }

//或者也可以不传入实际的类型形参
public class A extends Apple { }

如果Apple<String>类派生子类,则在Apple类中所有使用T类型形参的地方都将被替换成String类型。如果使用Apple没有传入实际的类型参数,Java编译器可能会发出警告:使用了未检查或者不安全的操作(就是泛型检查警告)。此时系统会把Apple<T>类的T形参当成Object处理。

3、并不存在泛型类

前面提到的可以把ArrayList<String> 类当成ArrayList的子类,但是系统并没有为ArrayList<String>生成新的class文件,也不会把ArrayList<String>当成新类处理。

package Genericity;

import java.util.ArrayList;
import java.util.List;

public class TestMain {
	
	public static void main(String[] args) throws Exception {
		
		List<String> list1 = new ArrayList<>();
		List<Integer> list2 = new ArrayList<>();
		
		System.out.println(list1.equals(list2));
		System.out.println(list1.getClass() == list2.getClass());
	}
	
}

输出:

true

true

因此不管泛型的实际类型参数是什么,它们在运行时总有同样的类(class)。对于Java来说,不管泛型的类型形参传入什么样的类型实参,它们依然被当成同一类来处理,在内存中也只占用一块内存空间,因此静态方法,静态初始化块或者静态变量的声明和初始化不允许使用类型形参。

由于系统并不会真正有泛型类,所以instanceof运算符后不能使用泛型类。例如下面的代码是错误的。

package Genericity;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class TestMain {
	
	public static void main(String[] args) throws Exception {
		
		List<String> list1 = new ArrayList<>();
		Collection<String> list = new ArrayList<>();
		
		if(list instanceof ArrayList<String>) {
			
		}
	}
	
}

如果是在eclipse编译器你会发现这样的提示:

Cannot perform instanceof check against parameterized type ArrayList<String>. Use the form ArrayList<?> instead since further generic type information will be erased at runtime(无法对参数化类型ArrayList<String>执行instanceof检查。使用ArrayList<?>,因为更多的泛型类型信息将在运行时被删除)

 

四、类型通配符

假如我们想声明一个接口,该接口可以迭代出集合中的元素,此时可以这样设计。

public void test(List<Object> list) {
		for (int i = 0; i < list.size(); i++) {
			System.out.println(list.get(i));
		}
	}

但是假如我们想迭代出一个List<String> list 集合中的元素时,并不能呢个调用上面的接口(List<String> 并不是List<Object>的子类),这意味着我们需要为不同的类型编写接口。为了表示各种泛型List的父类,可以使用泛型通配符,一个问号“?”写作List<?>。它可以匹配任何类型。因此可以将上面改写为:

	public void test(List<?> list) {
		for (int i = 0; i < list.size(); i++) {
			System.out.println(list.get(i));
		}
	}

这种带通配符的List仅表示各种泛型List 的父类,但是在一些特殊的情形下,程序不希望这个List<?>是任何泛型的父类,它只是代表某一类型泛型的父类。于是可以使用通配符上限。List<? extends Number>这是一个受限制的通配符,此处?表示一个未知的类型,但是这个未知的类型一定是Number的子类型也可以是本身,因此Number称为这个通配符的上限。

 

五、泛型方法

假设需要实现这样的一个方法,该方法负责将一个Object数组的所有的元素添加到Collection集合中。考虑到采用如下的代码实现该方法。

public void fromArraytoConllection(Object[] objs, Collection<Object> c) {
		for (Object object : objs) {
			c.add(object);
		}
	}

上面没有任何问题,关键是形参c,它的数据是形参Collection<Object>,正如前面介绍Collection<String> 不是Collection<Object>的子类型,所以这个功能非常有限。但是有人说把Collection<Object>声明成Collection<?>是否可行呢?显然是不行的,Java不允许把对象放入一个未知类型集合中。为了解决这一个问题,可以在声明方法时,定义一个或者多个形参。上面可以改写成

	public <T> void fromArraytoConllection(T[] objs, Collection<T> c) {
		for (T object : objs) {
			c.add(object);
		}
	}

该方法中的定义了一个T类型的形参,这个T类型形参就可以在普通方法中当成普通类型使用。与接口、类声明中定义的类型形参不同的是,方法声明定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。

 

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

到底什么时候使用泛型方法,什么时候使用类型通配符呢?大多数时候可以使用泛型方法来代替通配符的。

如果某一个方法中一个形参a的类型或返回值的类型依赖于另一个形参b的类型,则形参b的类型声明不应该使用通配符,因为形参a类型依赖于该形参b类型。如果形参b类型无法确定,程序就无法定义形参a的类型。在这种情况下,只能考虑使用在方法签名中声明类型形参——也就是方法泛型。

 

设定通配符下限:

在假设自己实现一个工具;实现将src集合里的元素复制到dest集合里的功能。因为dest集合可以保存src集合里的所有的元素,所以dest集合元素应该是src集合元素的父类。为了表示参数之间的依赖,考虑同时使用通配符和泛型方法来实现。

public <T> void copy( Collection<T> dest, Collection<? extends T> src) {
		for (T e : src) {
			dest.add(e);
		}
	}

假如该方法需要一个返回值,返回最后一个复制的元素

	public <T> T copy( Collection<T> dest, Collection<? extends T> src) {
		T last = null;
		for (T e : src) {
			last = e;
			dest.add(e);
		}
		return last;
	}

表面上看起来,上面实现了这样的一个功能,实际上有一个问题,当遍历src集合元素时,src元素类型并不确定的(只可以肯定它是T的子类),程序只能用T来笼统地表示各种src集合元素类型。例如下面的代码:

List<Number> ln = new ArrayList<>(); List<Integer> li = new ArrayList<>(); 下面会引起编译错误 Integer last = copy(ln, li);

上面ln 类型是List<Number> ,与copy方法签名中的形参类型进行对比即得到T的实际类型时Number,而不是Integer类型。即copy()方法的返回值也是Number类型,而不是Integer类型。也就是说程序在复制时,丢失了src集合中元素的类型。

对于上面copy方法,可以理解两个集合之间的依赖关系:不管src集合元素是什么,只要dest集合元素类型与前者相同或者是前者的父类即可。为了表达这种关系,Java允许使用通配符下限:<? super Type> 这个通配符必须是Type本身或者父类。

因此上面的代码可以改为


	public static <T> T copy( Collection<? super T> dest, Collection<T> src) {
		T last = null;
		for (T e : src) {
			last = e;
			dest.add(e);
		}
		return last;
	}

 Integer last = copy(ln, li);就不会引起编译错误了。

 

六、泛型与数组

Java有一个重要额设计原则是——如果一段代码在编译时,没有提出“[unchecked]未经检查的转换”警告,则程序在运行时是不会引发ClassCastException异常。正是基于这个原因,所以数组元素类型不能包含类型变量或者类型形参,除非是无上限的类型通配符。但是可以声明元素类型包含类型变量或类型形参的数组。也就是说,只能声明List<String>[]形式的数组,但不能创建ArrayList<String>[10]这样的对象。

Java允许创建无上限的通配符泛型数组,例如:new ArrayList<?>[10]。但是这种情况下,程序不得不进行强制类型转换。为了避免ClassCastException异常的发生,程序用instanceof来保证数据类型。

public static void main(String[] args) throws Exception {
		List<?>[] arr = new List<?>[10];
		Object[] oa = arr;
		
		List<Integer> li = new ArrayList<Integer>();
		li.add(new Integer(3));
		oa[0] = li;
		
		Object o = arr[0].get(0);
		if(o instanceof Integer) {
			System.out.println((Integer)o);
		}
	}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值