Java基础(六)深入解读泛型(1)

一名合格的Java程序员,当然要经常翻翻JDK的源代码。经常看JDK的API或者源代码,我们才能更加了解JDK,才能更加熟悉底层。


一、引出泛型

然而,在看源代码的过程中,我们经常会看到类似于如下这样的代码:

private void putAllForCreate(Map<? extends K, ? extends V> m) {
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            putForCreate(e.getKey(), e.getValue());
    }

上面代码出自HashMap中的一段。代码中的泛型,虽然不影响我们看懂这段代码,但是属于半个处女座的我,还是有着强烈的强迫症,就有那么一股冲动,怂恿着我把它搞清楚。

我们可以观察一下上面的代码,“? extends K”,很明显,这应该是某种继承关系的体现,但是什么时候会用到这种泛型里的继承关系呢?除了这种泛型关系,你应该还见过一些其他的表现形式,那么好,如果你想搞清楚这些,就好好看这篇文章吧,我相信:搞清楚这些,一定会对你做研发时,封装代码,有很大的帮助。


二、泛型的定义

泛型,大家在学习,工作中,一般都会多多少少的接触过一些。所以,关于定义,我们长话短说,帮大家复习一下即可。

泛型是在JDK1.5增加的,主要用来标记Java集合中元素的数据类型。怎么讲呢?

在JDK1.5之前,一旦把一个对象丢进Java集合中,集合就会忘记对象的类型,把所有的对象当成Object类型处理。当程序冲集合中取出对象时,就需要进行强制类型转换,这种做法,不仅代码看起来很臃肿,而且容易引发类型转换异常ClassCastException。

而通常情况下使用集合,一个集合,只存放同一类型的东西。即一个定义了的String类型的List,不能盛放Integer类型的数据。

而泛型,可以标记或规定集合中元素的类型,并且在编译时,检查其类型。


三、泛型的应用


1、定义泛型接口、类

所谓泛型:就是允许在定义类、接口时,指定类型形参,这个类型形参将在声明变量、创建对象时确定(即传入实际的类型参数,也可以成为类型实参)。

搞个例子看看,List、ListIterator、Map接口,都是在定义时指定类型,而类型的确定是在创建变量的时候,如List<String> list= new ArrayList<String>(),创建list变量时,确定类型实参为String类型:

//定义接口时指定了一个类型形参,该形参名为E
public interface ListA<E> extends Collection<E> {
	//在该接口里,E可作为类型使用
	boolean add(E e); //参数类型
	ListIterator<E> listIterator();
}
//定义接口时,指定你了一个类型形参,该形参名为E
public interface ListIterator<E> extends Iterator<E>{ 
	//在该接口里E完全可以作为类型使用
	E next();
	E previous();
	void set(E e);
}
//定义该接口时,指定了两个类型形参,其形参名为K、V
public interface Map<K,V>{
	//在该接口里K,V完全可以作为类型使用
	Set<K> keySet();
	V put(K key, V value);
	void putAll(Map<? extends K, ? extends V> m);
}


这就是泛型的实质:允许在定义接口、类时,指定类型形参,类型形参在整个接口、类体内可当成类型使用,几乎所有可使用其他普通类型的地方,都可以使用这个类型形参。

上面方法声明返回值类型:ListIterator<E>、Set<K>,这表明Set<K>形式 是一种特殊的数据类型,是一种与Set不同的数据类型——我们可以认为Set<K>是Set类型的子类

例如,我们使用List类型时,为E形参传入String类型实参,则产生了一个新的类型:List<String>类型,我们可以把List<String>想象成E被全部替换成String的特殊List子接口。

我们可以把List<String>想象成E被全部替换成String的特殊List子接口:

//List<String>等同于如下接口
public interface ListString extends List {
	//原来的E形参全部变成String类型实参
	boolean add(String e); //参数类型
	ListIterator<String> listIterator();
}

通过上面这种方式,解决了一个问题:虽然程序只定义了一个List<E>接口,但实际使用时,可以产生无数多个List接口,只要为E传入不同的类型实参,系统就会多出一个新的List子接口。如List<String>,List<Integer>,List<Long>,List<Boolean>等等。

当然,List<String>绝不会被替换成ListString,系统没有进行源代码复制,二进制代码中没有,磁盘没有,内存中也没有。


PS:包含泛型声明的类型可以在定义变量、创建对象时,传入一个类型实参,从而可以动态生成无数多个逻辑上的子类,但这种子类在物理上并不存在


搞个实例:

//定义Apple类时使用了泛型声明
public class Apple<E> {
	//使用E类型形参定义属性
	private E info;
	public Apple() {}
	//下面方法中使用E类型形参来定义方法
	public Apple(E info) {
		this.info = info;
	}
	public E getInfo() {
		return info;
	}
	public void setInfo(E info) {
		this.info = info;
	}
	public static void main(String[] args) {
		//因为传给T形参的是String实际类型,所以构造器的参数只能是String
		Apple<String> a1 = new Apple<String>("苹果");
		System.out.println(a1.getInfo());
		
		//因为传给T形参的是Double实际类型,所以构造器的参数只能是Double或double
		Apple<Double> a2 = new Apple<Double>(5.67);
		System.out.println(a2.getInfo());
	}
}


上面程序定义了一个带泛型声明的Apple<T>类,而在Main函数中,实际使用Apple<T>类时会为T形参传入实际类型,这样就可以生成如Apple<String>、Apple<Double>……形式的多个逻辑子类(物理上并不存在),这时创建对应的逻辑形参

当创建带泛型声明的自定义类,为该类定义构造器时,构造器还是原来的类名,不要增加泛型声明。例如为Apple<T>类定义构造器,其构造器名依然是Apple,而不是Apple<T>,但调用该构造器时,却可以使用Apple<T>的形式,当然应该为T形参传入实际的类型参数。

2、从泛型类派生子类

当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从父类派生子类,但是:当使用这些接口、父类时不能再包含类型形参。

例如下面代码是错误的:

//定义类A继承Apple类,  Apple类后面不能跟类型形参
public class A extends Apple<E> {}

注意:方法中的形参,只有当定义方法时,才可以使用数据形参,当调用方法时,必须为这些数据形参传入实际的数据;与此类似的是:类、接口中的类型形参,只有在定义类、接口时,才可以使用类型形参,当使用类、接口时,应为类型形参传入实际的类型

所以,总结起来就是:方法、类、接口 中的类型形参,只有在定义时,才可以使用类型形参;在使用时,应为类型形参传入实际的类型

所以,上面的代码可以改为如下(当然删除后面的String也是可以的):

//使用Apple类时,为T形参传入String类型
public class A extends Apple<String> {}

public class A extends Apple {}


如果从Apple<String>类派生子类,则在Apple类中所有使用E类型形参的地方都将被替换成String类型,即它的子类将会继承到String  getInfo()  和 void setInfo(String info)两个方法,如果子类需要重写父类的方法,必须注意这一点。

搞个例子,解释一下:

public class subApple extends Apple<String> {
	
	//正确重写了父类的方法,返回值与父类Apple<String>的返回值完全相同
	public String getInfo(){
		return "子类:"+super.getInfo();
	}
	
//	下面方法是错误的,重写父类方法时,返回值类型不一致
//	public Object getInfo(){
//		return "子类";
//	}
}


如果使用Apple类时没有传入实际的参数类型,系统会将Apple<E>类里的E形参当成Object类型处理

上面例子是带泛型声明的父类派生子类,创建带泛型声明的接口的实现类于此几乎完全一样。


3、不存在泛型类

ArrayList<String>类,是一种特殊的ArrayList类,这个ArrayList<String>对象只能添加String对象作为集合元素。

但实际上,系统并没有为ArrayList<String>生成新的class文件,而且也不会把ArrayList<String>当成新类来处理。


下面看个例子:

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

public class CommonTest {
	public static void main(String[] args) {
		List<String> lst1 = new ArrayList<String>();
		List<Integer> lst2 = new ArrayList<Integer>();
		
		Boolean flag = false;
		flag = lst1.getClass() ==lst2.getClass();
		
		//true
		System.out.println(flag);
	}
}

上面程序的输出结果是true。因为不管泛型类型的实际类型参数是什么,它们在运行时总有同样的类(class)

实际上,泛型对其所有可能的类型参数,都具有同样的行为,从而可以把相同的类当成许多不同的类来处理。

与此完全一致的是:类的静态变量和方法也在所有实例间共享,所以在静态方法、静态初始化   或者   变量的声明 和初始化  中,不允许使用类型形参。


下面程序演示了这种错误:

public class Test<E> {
	
	E age;
	//下面代码错误,不能再静态属性声明中使用类型形参
//	static E info;
	
	public void foo(E msg){}
	//下面代码错误,不能在静态方法声明中使用类型形参
//	public static void bar(E msg){}
}


由于系统不并不会正真生成泛型类,所以instanceof运算后,不能使用泛型类,如下代码是错误的:

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

public class Test2 {
	public static void main(String[] args) {
		Collection<String> cs = new ArrayList<String>();
		//下面代码编译时引发错误:instanceof运算符后不能使用泛型类
//		if (cs instanceof List<String>) {}
	}
}

本文初步讲解了泛型基础内容,下一篇继续深入探讨。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值