编译软件:IntelliJ IDEA 2019.2.4 x64
运行环境:win10 家庭中文版
jdk版本:1.8.0_361
提示:以下是本篇文章正文内容,下面案例可供参考
一、泛型是什么?
首先让我们看看百度百科中关于“泛型”的权威解释,如下所示。
Java泛型是J2 SE1.5中引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
-------------------------百度百科
怎么说呢?解释的非常简洁高效准确。但对于初学者而言,可能过于专业化不好理解。不妨看看本文,也许能让你眼见一亮,对泛型的理解瞬间念头通达。
在我给各位阐释之前,先没看以下问题场景:
- 假设有一批空的塑料瓶,它被设计出来专门送给矿泉水公司去装矿泉水,但这些塑料瓶的瓶身没有贴标签【该标签是标注收货人的信息】。塑料瓶的生产厂家不知道这批塑料瓶要发给矿泉水公司,还是别的饮料公司。即厂家有可能会发错产品。
- 还是上面的问题场景中,假设塑料瓶的瓶身贴了标签,那么塑料瓶的生产厂家就不会犯发错产品的可能,矿泉水公司也能如期收到产品。
显而易见,贴标签的作用巨大,能够保证发货人与收货人可以如期准确的发出/收到产品。进而提高生产效率。
泛型,就有点像上述场景中的“标签”,可以在使用之前,表名某个xx的类型是啥,用的时候既方便又安全。
对于Java程序中则起到如下的作用。
(1)可以把类型检查提到“编译时”
(2)使用时,类型是安全的,不需要再强制类型转换。
泛型是如何传递类型的?
我们可以用之前的知识来辅助理解它。之前学习方法的时候,我们就有了形参和实参的概念。
为什么方法有形参?
因为方法体的功能实现,需要外界(使用者)给它提供数据,这个数据在编写方法体时,数据值是不确定的, 需要在调用方法时,才能确定。
public static int add(int a, int b){
int a;
int b;
return a + b;
}
现在在设计某个类时,出现了某个成员的类型未知,需要在使用这个类的时候才能确定,就可以把这个类型作为参数传递。
写在类名后面,如下图代码和样例所示:
public class ArrayList<E> {//E就是代表未知的类型,又称为类型形参
public void add(E e) //添加一个元素到当前集合中
//...
}
}
ArrayList<String> list = new ArrayList<String>();
//<E>就是对应<String>
总结:泛型就是参数化的类型,即泛型是传递类型的一种语法
格式:<泛型>
二、泛型相关名词解释
举例如下:
ArrayList<E>
E是类型变量,因为E是一种未知的类型,可能代表string,可能代表Integer等,可以变,所以称为类型变量。
<E> 是类型参数。
ArrayList<E> 是参数化的类型。
附注:类型变量也称泛型变量,类型参数也称 泛型实参
三、泛型可以在哪里声明?
3.1 在类或接口名后面,这样的类和接口称为泛型类或泛型接口,或者是参数化的类型
代码如下(示例):
java.lang.Comparable接口
public interface Comparable<T> {
public int compareTo(T o); //这个不是泛型方法
}
3.2 在方法的返回值类型前面,这样的方法称为泛型方法。
代码如下(示例):
java.util.Arrays类:
publif static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
注意:<E>、<T>等泛型类型,只能指定为引用数据类型,不能指定为基本数据类型和void。
3.3 演示案例
案例演示代码:
@Test
public void test01(){
Collection<Integer> coll=new ArrayList<Integer>();
for (int i = 0; i <5 ; i++) {
coll.add(new Random().nextInt(100));
}
for (Integer integer : coll) {
System.out.print(integer+"\t");
}
System.out.println();
coll.removeIf(new Predicate<Integer>() {
@Override
public boolean test(Integer integer) {
return integer%2==0;
}
});
Iterator<Integer> itr= coll.iterator();
while (itr.hasNext()){
System.out.print(itr.next()+"\t");
}
}
运行结果:
四、如何自定义泛型类和泛型接口?
4.1 应用概述
当我们在类或接口中定义某个成员时,该成员的相关类型是不确定的,而这个类型需要在使用这接的口难以确定,那么我们可以使用泛型。
- 当某个类/接口的非静态实例变量的类型不确定,需要在创建对象或子类继承时才能确定
- 当某个(些)类/接口的非静态方法的形参类型不确定,需要在创建对象或子类继承时才能确定
语法格式:
注意
- <类型变量列表>:可以是一个或多个类型变量,一般都是使用单个的大写字母表示。例如:<T>、<K,V>等。
- <类型变量列表>中的类型变量不能用于静态成员上。
举例:
例如:我们要声明一个学生类,该学生包含姓名、成绩,而此时学生的成绩类型不确定,为什么呢,因为,语文老师希望成绩是“优秀”、“良好”、“及格”、“不及格”,数学老师希望成绩是89.5, 65.0,英语老师希望成绩是'A','B','C','D','E'。那么我们在设计这个学生类时,就可以使用泛型。
样例演示代码:
- 方式一:直接在类名旁+泛型的类型参数列表
public class Student<T> {
private String name;
private T score;
public Student(String name, T score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", score=" + score +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public T getScore() {
return score;
}
public void setScore(T score) {
this.score = score;
}
}
- 方式二:子类继承,注意,此时子类就不再是泛型类,它的父类才是。
注意上段中的泛型类或泛型接口声明的类型参数,即<E>,不可以用在静态成员上
why?
因为类或接口上面声明的泛型是未知的类型,需要再new对象时确定,而静态是早于对象创建的使用,或者说不依赖与对象的。
4.2 用法小结
IDK1.7支持简写形式: Student<string> stu1 = new Student<>("张三","良好”);
指定泛型实参时,必须左右两边一致,不存在多态现象
附注:
当String类型的元素进行自然排序时,就是按照字符的Unicode编码值进行排序
五、泛型方法
5.1 泛型方法的调用
例如:
在java.util.Arrays数组工具类中,有很多泛型方法
- public static <T> List<T> asList(T... a):将实参对象依次添加到一个固定大小的List列表集合中。
- public static <T> T[] copyOf(T[] original, int newLength):复制任意对象数组,新数组长度为newLength。
注意:
5.2 自定义泛型方法
附注:如何在IDEA中找出一个类的所有子类?
六、类型变量的上限
我们在声明<T>等类型变量时,可以给它限定“上限”。
语法格式:
<T extends 上限> : 表示T的类型必须是<=上限,即要么是上限本身,要是继承上限类或实现了上限接口的类型。
举例如下:
学生类,包含姓名和成绩。
现在成绩类型不确定,但是要求成绩必须是Number或Number的子类对象。
java.Lang.Number表示数值类型,例如它的子类有。
代码如下:
在上面的案例需求中再加一个要求,成绩不仅要求是Number类或它的子类,还要求实现Comparable接口
一个类型变量的上限可以是1个,也可以是多个。但是要求如果有多个的话,类只能有一个,其他的都是接口类型。而且类在左边,其他的类型在右边。
如果自定义的方法实现了类型变量的下限,那么传参给方法的实参中的类也必须实现该接口。此举意在编译前提醒检测类型变量的类型转换问题
6.1 定义泛型类的类型变量时指定上限
代码举例如下:
6.2 定义泛型方法的类型变量时指定上限
编写一个数组工具类,包含可以给任意对象数组进行从小到大排序,调用元素对象的compareTo方法比较元素的大小关系。要求数组的元素类型必须是java.lang.Comparable<T>接口类型。
代码举例如下:
public class MyArrays {
public static <T extends Comparable<T>> void sort(T[] arr){
for (int i = 1; i < arr.length ; i++) {
for (int j = 0; j < arr.length-i ; j++) {
if (((Comparable)(arr[j])).compareTo(arr[j+1])>0){
T temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
}
}
import org.junit.Test;
public class TestArrays {
@Test
public void test01(){
Integer[] nums={4,7,1,2,89,34,67};
System.out.println("排序前:");
for (Integer num : nums) {
System.out.print(num+"\t");
}
System.out.println();
MyArrays.sort(nums);
System.out.println("排序后:");
for (Integer num : nums) {
System.out.print(num+"\t");
}
}
}
运行效果:
七、泛型擦除
如果用户在使用泛型类或泛型接口时,没有主动指定泛型的类型,就会发生泛型的擦除。
泛型擦除后,类型变量按照哪个类型处理? 是统一按照object处理吗?
不是
答案:泛型擦除后,自动按照类型变量声明时的第一个上限处理,如果类型变量上面时没有指定上限,那么按照Object处理
代码举例如下:
八、泛型通配符
8.1 为什么要使用泛型通配符?
当我们声明一个变量/形参时,这个变量/形参的类型是一个泛型类或泛型接口,例如:Comparator<T>类型,但是我们仍然无法确定这个泛型类或泛型接口的类型变量<T>的具体类型,此时我们考虑使用类型通配符 ?
代码举例如下:
import java.util.ArrayList;
import java.util.Collection;
public class TestWildcard {
public static void m4(Collection<?> coll){
for (Object o : coll) {
System.out.println(o);
}
}
public static void main(String[] args) {
//右边泛型指定为任意类型或不指定都可以
m4(new ArrayList<Object>());//Collection<?> coll = new ArrayList<Object>();
m4(new ArrayList<>());//Collection<?> coll = new ArrayList<>();
m4(new ArrayList());//Collection<?> coll = new ArrayList();
m4(new ArrayList<String>());//Collection<?> coll = new ArrayList<String>();
}
}
在Java中,泛型的指定是必须左右两边一致的
8.2 类型通配符的三种使用形式
- <?>:完整形式为:类名<?> 或接口名<?>,此时?代表任意类型。
- <? extends 上限>:完整形式为:类名<? extends 上限类型> 或接口名<? extends 上限类型>,此时?代表上限类型本身或者上限的子类,即?代表 <= 上限的类型。
- <? super 下限>:完整形式为:类名<? super 下限类型> 或接口名<? super 下限类型>,此时?代表下限类型本身或者下限的父类,即?代表>= 下限的类型。
案例:
声明一个集合工具类MyCollections,要求包含:
-
public static boolean different(Collection<?> c1, Collection<?> c2):比较两个Collection集合,此时两个Collection集合的泛型可以是任意类型,如果两个集合中没有相同的元素,则返回true,否则返回false。
-
public static <T> void addAll(Collection<? super T> c1, T... args):可以将任意类型的多个对象添加到一个Collection集合中,此时要求Collection集合的泛型指定必须>=元素类型。
-
public static <T> void copy(Collection<? super T> dest,Collection<? extends T> src):可以将一个Collection集合的元素复制到另一个Collection集合中,此时要求原Collection泛型的类型<=目标Collection的泛型类型。
代码演示如下:
import java.util.Collection;
public class MyCollections {
public static boolean different(Collection<?> c1, Collection<?> c2){
return c1.containsAll(c2) && c2.containsAll(c1);
}
public static <T> void addAll(Collection<? super T> c1, T... args){
for (int i = 0; i < args.length; i++) {
c1.add(args[i]);
}
}
public static <T> void copy(Collection<? super T> dest,Collection<? extends T> src){
for (T t : src) {
dest.add(t);
}
}
}
public class MyCollectionsTest {
public static void main(String[] args) {
Collection<Integer> c1 = new ArrayList<Integer>();
MyCollections.addAll(c1,1,2,3,4,5);
System.out.println("c1 = " + c1);
Collection<String> c2 = new ArrayList<String>();
MyCollections.addAll(c2,"hello","java","world");
System.out.println("c2 = " + c2);
System.out.println("c1 != c2 " + MyCollections.different(c1, c2));
Collection<Object> c3 = new ArrayList<>();
MyCollections.copy(c3,c1);
MyCollections.copy(c3,c2);
System.out.println("c3 = " + c3);
}
}
运行效果如下:
8.3 使用类型通配符来指定类型参数的问题
(1)如果把"泛型类<T>"指定为"泛型类<?>";那么该泛型类中所有参数是T类型的方法或成员都无法正常使用。参数类型不是T类型的方法照常使用。
代码举例如下:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
public class TestProblem {
@Test
public void test01(){
Collection<?> coll = new ArrayList<>();
// coll.add("hello");
// coll.add(1);
// coll.add(1.0);
/*
上面所有添加操作都报错。
为什么?
因为<?>表示未知的类型,集合的元素是不确定的,那么添加任意类型对象都有风险。
void add(E t)方法无法正常使用
因为此时E由?表示,即表示直到add方法被调用时,E的类型仍然不确定,所以该方法无法正常使用
*/
Collection<?> coll2 = Arrays.asList("hello","java","world");
for (Object o : coll2) {
System.out.println(o);
}
}
}
(2)如果把"泛型类<T>"指定为"泛型类<? extends 上限>":那么该泛型类中所有参数是T类型的方法或成员都无法正常使用。参数类型不是T类型的方法照常使用。
代码举例如下:
个人理解:如果把"泛型类<T>"指定为"泛型类<? extends 上限>",之所以无法正常使用,是因为
Java编译器在编译时无法确定添加到集合coll中的元素的类型是否<= Number,泛型类<? extends 上限> 中只说了要<=Number[上限],Number类以下的部分或者说它的子类,编译器是不知道的。
(3)如果把"泛型类<Tdd>"指定为"泛型类<? super 下限>":那么该泛型类中所有参数是T类型的方法或成员都可以使用,但是有要求。参数类型不是T类型的方法照常使用。