泛型不只是 Java 语言所特有的特性,泛型是很多编程语言的一种特性。它允许开发人员在编写代码时定义一些可变部分(数据的数据类型),这部分可变的内容在使用时才做出明确指定(指定具体的数据类型)。我们可以这样理解泛型,它的本质是对数据的操作进行封装,而忽略数据因类型造成的差异化。例如,我们定义一个方法,要对两个数据进行加法操作,由于数据类型的不同,我们需要定义N个不同数据类型的形参方法,这样显然不太合理。如果我们能够使用一个“通配符”来代替这些数据,然后直接定义对这些通配符数据进行操作的单一方法,而真正调用这些方法的时候,再确定这些通配符数据的数据类型。这样的设计才是最合理的,也就是泛型的目的。
Java泛型有三种常用的使用方式:泛型类,泛型接口和泛型方法。
泛型类: 在类名后定义通配符,只能用在成员变量上。
泛型接口:在接口名后定义通配符,只能用在抽象方法上。
泛型方法:在方法名之前定义通配符,可以用于方法的返回类型或者方法的参数上。
三者的区别就是位置不同,作用也就不同。我们先介绍泛型类的使用,代码示例如下:
// 泛型类,T表示“任意类型”
class Test {
// T数据类型的成员变量 val
private T val;
// 有参构造方法
public Test(T val) {
this.val = val;
}
// 输出变量val
public void show() {
System.out.println(val.toString());
}
}
上述代码中,我们定义Test类时引入了一个通配符T,用尖括号(<>)括起来,并放在类名的后面。这个T就是泛型字母,可以随意指定,尽量使用单个的大写字母。泛型类可以声明多个通配符。例如,class Test
// 泛型类使用
Test a = new Test("abc");
a.show(); // 输出 abc
Test b = new Test(123);
b.show(); // 输出 123
看到这里,我们基本上对泛型有了初步的认识啦。接下来介绍泛型接口的使用,
// 泛型接口,T表示“任意类型”
interface ITest {
public T val();
}
// CTest类继承泛型接口
class CTest implements ITest{
}
泛型接口的使用和泛型类很相似,只不过接口的泛型字母只能用于方法。然后我们使用一个类去继承这个接口,然后实现接口里面的方法,Eclipse可以帮助我们快速完成,如下图:
点击40行出的红叉叉,会弹出几个选项,点击第一个选项,添加未继承实现的方法,Eclipse就会自动帮我们向CTest类中添加一个val方法,我们稍作修改,如下所示:
@Override
public String val() {
return "abc";
}
接下来就不用说了,我们直接使用CTest类就可以了。
// 使用CTest类
CTest c = new CTest();
System.out.println(c.val());
接下来,我们介绍泛型方法的使用,它可以作为方法返回类型,也可以作为参数,如下:
// 泛型方法
public static void test1(T t) {
System.out.println(t.toString());
}
// 泛型方法
public static T test2(T... t) {
return t[0];
}
请注意,第一个泛型方法中,泛型字母只用于方法参数,第二个泛型方法中,泛型字母不仅仅作为参数,还作为方法的返回类型。当作为方法参数的是时候,“T…”代表数组类型。请注意,以上两个泛型方法是在普通类中定义的,不是在泛型类中定义的哦。接下来,我们看看这两个泛型方法的使用,代码如下:
// 泛型方法的使用
test1(new Integer(100));
test2(new String("abc"));
// 泛型方法的使用
Integer[] arr = {1,2,3,4,5};
Integer r1 = test2(arr);
System.out.println(r1);
String[] str = {"a","b","c","d","e"};
String r2 = test2(str);
System.out.println(r2);
有时候,我们希望对泛型表示的数据类型做范围的约定,这种约定可以分为上限和下限两种,分别使用
// 泛型上限,约束泛型的范围
public static void test3(T t) {
System.out.println("方法参数必须是List或其子类");
}
我们定义了方法的参数,使用泛型字母T表示任意一个数据类型,但是由于我们使用了泛型上限,因此这个T代表的数据类型只能是List或List的子类。我们之前讲解过,List的子类包括ArrayList和LinkedList等等。该方法test3的使用如下所示:
// 泛型上限,方法参数必须是List或其子类
test3(new ArrayList());
test3(new HashSet());
test3(new LinkedList());
请大家注意,我们故意写了一个HashSet的参数,从而导致代码无法编译,说明我们定义test3方法的时候使用泛型上限起作用了。我们注释掉HashSet所在行的代码,就可以编译运行啦。关于泛型下限我们就不再举例说明了。
接下来,我们在说一个特殊的泛型字母:?通配符。我们这样理解它和T通配符的区别。T 是一个不确定的类型,通常用于泛型类和泛型方法的定义,而?也是一个不确定的类型,通常在使用泛型的时候用来代表一个不确定的类型。前者是定义,后者是使用,代码示例如下:
// 通配符 ? 的使用
Test> t1 = null;
t1 = new Test(123);
t1 = new Test("abc");
// 不使用通配符 ?
Test t2 = new Test(123);
Test t3 = new Test("abc");
我们可以看到,当我们使用?通配符去声明一个Test类的对象t1的时候,我们可以将其示例化为各种类型的Test实例对象,非常的方便。而如果我们不使用?通配符的话,就只能固定类型的声明并实例化同一种数据类型的对象啦。
其实泛型的应用中,最广泛的还是和集合的搭配使用。我们知道集合可以存放任意的数据类型,这样虽然看似很好,但是就造成了访问任意元素的时候,需要进行类型转换。因此,我们日常使用集合的时候,都会像数组一样,存储同一种数据类型的元素。这个时候,我们使用泛型对集合元素的数据类型做约束的话,我们在访问元素的时候,就自动类型转换了。
// 集合不使用泛型
List list1 = new ArrayList();
list1.add("123");
// 需要类型转换
String ele1 = (String)list1.get(0);
System.out.println(ele1);
// 集合中使用泛型
List list2 = new ArrayList();
list2.add("456");
// 不需要类型转换
String ele2 = list2.get(0);
System.out.println(ele2);
// 迭代器中使用泛型
Iterator it = list2.iterator();
while(it.hasNext()) {
// 不需要类型转换
String temp = it.next();
System.out.println(temp);
}
另外,?通配符也可以搭配上限和下限来使用,我看如下代码:
// 集合中使用?通配符
List extends Number> l1 = new ArrayList();
List extends Number> l2 = new ArrayList();
List extends Number> l3 = new ArrayList();
请注意,我们将集合List中的元素限制为数字类型,因此最后一行的String是错误的,因此整个代码无法编译运行,我们必须注释掉String所在行的代码,才能成功编译运行。
本课程涉及的代码可以免费下载:
https://download.csdn.net/download/richieandndsc/85645935
今天的内容就讲的这里,我们来总结一下。今天我们主要讲了Java的泛型。本章节重点在于了解泛型的意义,就是参数化数据类型,定义的时候使用通配符表示,使用的时候才确定真正的数据类型。泛型类,泛型接口和泛型方法的三种方式大家一定要了解,即便不会使用,也要能够看懂他人的泛型代码。最后就是泛型和集合的搭配使用,这是重点!好的,谢谢大家的收看,欢迎大家在下方留言,我也会及时回复大家的留言的。