为什么要使用泛型?
Java设计之初并不知道会往容器中存放什么类型的元素,因此元素类型都设定为Object,这样就什么都能放了。但是这么设计有明显的缺点:①取出元素的时候必须进行强制类型转换异常;②如果不小心往集合里加了不相同类型的元素可能会导致类型异常,进行equals、compare比较的时候尤为明显;③在很多地方都需要强制类型转换,增加了变成的复杂度,影响了代码的美观和维护成本。
因此泛型应运而生。
泛型概述:
泛型(generics)是Java 1.5中引入的新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。只要编译时不出现问题,运行时就不会出现ClassCastException(类型转换异常)。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
Java集合都实现了泛型,允许程序在创建集合时就可以指定集合元素的类型,例如List<String>就表明这是一个只能存放String类型的List。List<String>就是参数化类型,也就是泛型,而String就是该List<String>泛型的类型参数。
泛型只在编译阶段有效,在编译之后会进行泛型擦除的操作,不会传递到运行阶段。也就是说编译后的class文件中是不包含任何泛型信息的。
泛型的规则:
1.泛型的类型参数只能是类类型(包括自定义类)和通配符,不能是简单类型;
2.同一种泛型可以对应多个版本(类型参数不同),不同版本的泛型类实例是不兼容的;
3.泛型的类型参数可以有多个,例如<K , V>;
4.泛型的类型参数可以使用extends语句,形如<T extends Number>称为有界的类型参数。
5.不能对确切的泛型类型使用 instanceof 操作;
注:A instanceof B:判断A是否是B的实例对象或者B子类的实例对象。
6.不能创建一个确切的泛型类型的数组;
泛型的好处:
1.在编译的时候检查类型安全,减少了运行时异常,降低crash率;保证了如果在编译时没有发出警告,则在运行时就一定不会产生ClassCastException(类型转换异常);
2.集合创建时就指定了集合元素的类型,取出元素的时候不需要强制类型装换了;
3.提高了代码的复用性、减少维护成本;
泛型类:
1.定义一个类,在该类名后面添加类型参数声明部分(由尖括号分隔)。
2.每一个类型参数声明部分可以包括一个或多个类型参数,参数间用逗号隔开。
3.使用<T>来声明一个类型持有者名称,然后就可以把T当作一个类型来声明成员、参数、返回值类型。T仅仅是个名字,可以自行定义。
/**
* 泛型类
*/
public class GenericsBox<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
}
//泛型类调用
GenericsBox<Integer> integerGenericsBox = new GenericsBox<>();
GenericsBox<String> stringGenericsBox = new GenericsBox<>();
integerGenericsBox.add(new Integer(25));
stringGenericsBox.add(new String("年龄"));
LogUtil.e("泛型类调用", stringGenericsBox.get() + ":" + integerGenericsBox.get());
泛型接口:
1.定义一个接口,在该接口名后面添加类型参数声明部分(由尖括号分隔)。
2.每一个类型参数声明部分可以包括一个或多个类型参数,参数间用逗号隔开。
/**
* 泛型接口
*/
public interface GenericsInterface<T> {
public abstract void genericsInterface1(T element);
public abstract <T> void genericsInterface2();
}
泛型方法:
定义一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当的处理每一个方法调用。
定义泛型方法的规则:
1.所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回值类型之前;
2.每一个类型参数声明部分包括一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符;
3.类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符;
/**
* 泛型方法
* 打印各种类型的数组中的元素
*
* @param inputArray
* @param <T>
*/
public static <T> void printLog(T[] inputArray) {
for (T element : inputArray) {
LogUtil.e("打印数组中的元素", element + "");
}
}
//创建各种类型的数组(Integer、Double、Character)
Integer[] integerArray = {1, 2, 3, 4, 5};
Double[] doubleArray = {1.1, 2.2, 3.3, 4.4, 5.5};
Character[] characterArray = {'a', 'b', 'c', 'd', 'e'};
printLog(integerArray);
printLog(doubleArray);
printLog(characterArray);
静态方法与泛型:
如果在类中定义使用泛型的静态方法,需要添加额外的泛型类型参数声明,即将该静态方法定义为泛型方法。即使静态方法要使用泛型类中已经声明过的类型参数也不可以。
/**
* 静态方法与泛型
*/
public class GenericsStatic<T> {
public static <T> void show(T t) {
}
//以下是错误的
// public static void show(T t) {
// }
}
有界的类型参数:
限定被允许传递到一个类型参数的类型种类范围。
1.要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,继承上界。例如:上界通过形如<T extends Comparable<T>>来定义,表示类型只能接受Comparable及其下层子类类型。这就是有界的类型参数的目的。
注意:<T extends Comparable<T>>这里的限定使用关键字extends,后面可以是类也可以是接口。extends统一的表示了原有的extends和implements的概念,应该理解为T类型是实现了Comparable接口的类型,或者是继承了XX类的类型。
2.泛型仍要遵循Java单继承、多实现的体系,所以当某个类型参数需要使用extends限定,且有多种类型的时候,只能存在一个类,并且类写在第一位,接口列在后面。形如<T extends Number & Comparable & Callback>
/**
* 泛型方法
* 比较三个值并返回最大值
*
* @param x
* @param y
* @param z
* @param <T>
* @return
*/
public static <T extends Comparable<T>> T maxValue(T x, T y, T z) {
//假设x是最大值
T max = x;
//如果y比max大,则将y赋值给max
if (y.compareTo(max) > 0) {
max = y;
}
//如果z比max大,则将z赋值给max
if (z.compareTo(max) > 0) {
max = z;
}
//返回最大值
return max;
}
//输出最大值
LogUtil.e("最大值为:", maxValue(3, 7, 5) + "");
LogUtil.e("最大值为:", maxValue(3.3, 3.7, 5.3) + "");
LogUtil.e("最大值为:", maxValue("apple", "pear", "orange"));
类型通配符(?)
为了解决类型被限制死了不能动态根据实例来确定的缺点,引入了类型通配符。类型通配符一般使用 ? 代替具体的类型参数。例如:List<?>在逻辑上是List<String>、List<Integer>等所有List<具体类型实参>的父类。
1.如果只指定了<?>,而没有extends,则默认是允许Object及其下的任何Java类,也就是任意类。
2.通配符也可以向上限制,通过形如List<? extends Comparable>来定义,表示类型只能接受Comparable及其下层子类类型;
3.通配符还可以向下限制,通过形如List<? super Comparable>来定义,表示类型只能接受Comparable及其上层父类类型;
因为getData()的参数是List<?>类型,所以name、age、number都可以作为这个方法的实参,这就是通配符的作用。
因为getNumberData()的参数是有上限的List<? extends Number>类型的,如此定义就是类型通配符泛型值接受Number及其下层子类类型。
/**
* 类型通配符的使用
*/
public static void getData(List<?> data) {
LogUtil.e("类型通配符的使用", data.get(0) + "");
}
public static void getNumberData(List<? extends Number> data) {
LogUtil.e("类型通配符的使用(Number)", data.get(0) + "");
}
//类型通配符
List<String> name = new ArrayList<>();
List<Integer> age = new ArrayList<>();
List<Character> blood = new ArrayList<>();
name.add("张三");
age.add(25);
blood.add('A');
getData(name);
getData(age);
getData(blood);
// getNumberData(name);
getNumberData(age);
// getNumberData(blood);
通配符的PECS原则:
1.Producer Extends:使用<? extends T>的集合类,只能作为Producer(生产者)向外提供(get)元素;而不能作为Consumer(消费者)对外获取(add)元素;
2.Consumer Super:使用<? super T>的集合类,只能作为Consumer(消费者)对外获取(add)元素;而不能作为Producer(生产者)向外提供(get)元素;
3.如果同时需要读取以及写入,那就不能使用通配符。
//通配符的PECS原则
List<? extends String> extendsList = new ArrayList<>();
List<? super String> superList = new ArrayList<>();
// extendsList.add("李四");
String s = extendsList.get(0);
superList.add("张三");
// Object s1 = superList.get(0);
早计划,早准备,早完成。 欢迎关注!交流!Star!
GitHub:https://github.com/wangyang0313
微信公众号:一个灵活的胖子MrWang
CSDN:https://blog.csdn.net/qq941263013