java基础-泛型

泛型的由来

什么是泛型

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

泛型的好处

  • 好处一:编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。
    • 在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”;
    • “任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的;
    • 对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。

自定义泛型结构

泛型结构包括泛型类、泛型接口和泛型方法

泛型类、泛型接口

泛型类与泛型接口的区别其实就是类与接口的区别,这里以泛型类为例:

  1. 类中声明的泛型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型,但是在静态方法中不能使用类的泛型。因为泛型类型是在创建类的时候指定的,而静态方法加载的时候,类的实例并没有创建。
  2. 异常类不能是泛型的。

泛型方法

以JDK中的Collection接口为例,区分普通方法和泛型方法:

public interface Collection<E> extends Iterable<E> {
	//普通方法
	boolean add(E data);
	//泛型方法,泛型方法的类型区别于类或接口中的类型,普通类中也可以含有泛型方法
	<T> T[] toArray(T[] a);
}

注意:区别于泛型类中带有类型符号的普通方法的是,泛型方法可以是静态的。具体说来就是,上面的add方法不能是静态的,因为E是在类创建时类型才确定下来;而toArray方法可以是静态的,因为T是在方法调用时就确定下来了。

泛型与面向对象

泛型与继承

  • 在此定义三个类,Father、Son和Grandson
  • Grandson extends Son
  • Son extends Father

类的继承

//Son不是泛型类,它继承了Father中的Integer
public class Son extends Father<Integer> {

}
//Son<T> 仍然是泛型类
public class Son<T> extends Father<T> {

}

参数类型的继承

java中的向上转型有如下几种形式

//1.对象,编译通过
Object abj = null;
String str = null;
abj = str;
//2.数组,编译通过
Object[] objArr = null;
String[] strArr = null;
objArr = strArr
//3.泛型1,编译通过,其实就是平常写的List<String> list = new ArrayList<>(); 
List<String> list1 = null;
ArrayList<String> list2 = null;
list1 = list2
//4.泛型2,编译不通过,此时的objList和strList的类型不具有子父类关系
List<Object> objList = null;
List<String> strList = null;
objList = strList;

## 第四种情况会导致一些问题,比如如下方法,遍历集合中的元素
public void foreach(List<Object> list) {

}
## List<String>类型的变量就无法用这个方法遍历,因为List<Object>类型和List<String>类型
不具备子父类关系,无法使用多态,这样就需要重载foreach方法

总结:List<Object>ArrayList<Object>的父类;List<Object>不是List<String>的父类。针对上述的第四种情况,泛型通配符可以解决。

泛型通配符

示例

泛型通配符用来表示,在泛型中,List<?>List<String>List<Object>的父类,可以声明一下方法解决上述第四种情况

public void foreach(List<?> list) {
	Iterator<?> iterator = list.iterator();
	while (iterator.hasNext()) {
		Object obj = iterator.next();
		System.out.println(obj);
	}
}

通配符上下限

  • ? extends Son,其中Son是通配符?的上限,可以理解为?的类只能是Son及其子类,即小于等于
  • ? super Son,其中Son是通配符?的下限,可以理解为?的类只能是Son及其父类,即大于等于
  • 对于通配符上下限,如List<? extends Son>List<? super Son>,声明以下对象做测试
List<? extends Son> list1 = new ArrayList<>();
List<? super Son> list2 = new ArrayList<>();
List<Grandson> list3 = new ArrayList<>();
List<Son> list4 = new ArrayList<>();
List<Father> list5 = new ArrayList<>();
//测试一
list1 = list3;//(编译成功)
list1 = list4;//(编译成功)
list1 = list5;//(编译失败)
//结论:G<? extends A>可以作为G<A>和G<B>的父类,其中B是A的子类
//测试二
list2 = list3;//(编译失败)
list2 = list4;///(编译成功)
list2 = list5;///(编译成功)
//结论:G<? super A>可以作为G<A>和G<B>的父类,其中B是A的父类

泛型通配符读写要求

  • 对于普通的通配符List<?>
List<String> listStr = new ArrayList<>();
List<Integer> listInteger = new ArrayList<>();
List<?> list = null;
list = listStr ;
list = listInteger ;
//不能添加(只能添加null),编译失败
list.add("1");
list.add(1);
//只能读取,读取的是object类型的对象
Object o = list.get(0);
  • 对于通配符上限List<? extends Son>
//读取数据
list1 = list4;//用list1 = list3同理
//由上面的测试可知,list1中的类型最大为Son,这里用了最大的类型,保证了多态的特性(父类引用指向子类对象)
Son s = list1.get(0);

//写入数据
//无法确定list1的下限,传入的Son类型不能确定是?的子类还是父类,不满足多态特性
list1.add(new Son());//(编译失败)

无法写入数据,那具体怎么使用呢?通配符在声明局部变量时没什么意义,但是作为形参,限制传入的参数类型时,它非常重要

  • 对于通配符下限List<? super Son>
//读取数据
list2 = list4;
//只能返回Object,因为list2中的对象最小类型是Son,但是上限没有定,只能用*父类Object才能保证多态特性
Object o = list2.get(0);

//写入数据
//list2的下限是Son,可以添加Son及其子类,以满足多态特性
list2.add(new Son());
list2.add(new Grandson());//(编译成功)

使用案例:TreeSet的构造器中传入的比较器使用了Comparator<? super E>

类型擦除

定义

  • 泛型是java1.5版本才引入的概念,在这之前没有泛型那个。但是,泛型代码可以很好的兼容以前的版本,那是因为泛型信息只存在于代码编译阶段。在进入JVM之前,与泛型相关的信息会被擦除,我们称之为类型擦除

演示

ArrayList<String> strList = new ArrayList<>();
ArrayList<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass().getSimpleName());//ArrayList
System.out.println(intList.getClass().getSimpleName());//ArrayList
System.out.println(strList.getClass() == intList.getClass());//true

种类

  • 无限制的类型擦除
    java基础-泛型
  • 有限制的类型擦除
    • 类的类型擦除
      java基础-泛型
    • 方法的类型擦除
      java基础-泛型
  • 桥接方法
    java基础-泛型
    在jvm中,对接口进行类型擦除后,生成的info方法,在实现类中为了保持接口和类的实现关系,也需要有这个方法

类型擦除中的一些坑

//todo

泛型与反射

一些区别

?和T的区别

  • ?是一个不确定的类型,通常用于接收返回值或者作为形参
  • T是一个确定的类型,通常用于泛型类和泛型方法的定义

class<?>和class<T>的区别

  • class<?>作为通配泛型,当我们不知道确切类型的时候,可以做以下声明:
public class<?> clazz;
  • class<T>List<T>类似,一般用在反射中,具体用法如下:
// 通过反射的方式生成  Son
// 对象,这里比较明显的是,我们需要使用强制类型转换,强制类型转换可能在运行期报ClassCastException
Son son= (Son) Class.forName("com.test.model.Son").newInstance();

//使用class<T>优化
//当传入的类型不是T时,无法创建T类型的对象,且在编译期就会报错
public static <T> T createInstance(class<T> clazz) throw Exception {
	return clazz.newInstance();
}

泛型数组

创建泛型数组

  • 可以声明带泛型数组的应用,但是不能直接创建带泛型的数组对象
//编译失败
new ArrayList<String>[5];

//编译成功
ArrayList[] list = new ArrayList[5];
ArrayList<String>[] listArr = list;
  • 不能使用new E[],但是可以E[] elements = (E[]) new Object[capacity],参考ArrayList源码中声明Object[] elementData,而非泛型数组。
public class Fruit<T> {
	//编译成功, 可以声明T[]
	private T[] array;
	
	//public Fruit(int length) {
		//编译失败
		//array = new T[length];
	//}

	//可以使用反射加类型强转
	public Fruit(Class<T> clazz, int length) {
		array = (T[]) Array.newInstance(clazz, length);
	}

	public void put(int index, T item) {
		array[index] = item;
	}
}

泛型的使用场景

DAO

数据库中的一张表对应java中的一个类型,对数据库数据的增删改查,都可以归结为操作这个类,针对多张不同的表,映射到多个类,所要进行的操作都相同(CRUD),因此可以使用泛型。具体用法体现在:

public class BaseDAO<T> {
	//带有类型参数T的增删改查
	public T get(Query query) {
		...
	}
	...
}

public class XXXDAO extends BaseDAO<XXX> {
	//直接继承增删改查方法
}
上一篇:关于JAVA底层编码和输出中文乱码问题


下一篇:Java学习记录——高级:网络编程socket、多线程编程