什么是泛型
思考以下的场景:你希望开发一个容器,它可以用来在你的应用里传递一个对象。然而这个对象的类型可能经常会变化,因此你需要开发一个能够存储各种类型的对象的容器。
基于这个场景,最容易想到的解决方案是开发一个存储 Object
类型的对象的容器,然后当使用各种类型的时候,只需要把 Object
强转为其他类型。接下来开发一下这样的容器。
public class ObjectContainer {
private Object obj;
/**
* @return the obj
*/
public Object getObj() {
return obj;
}
/**
* @param obj the obj to set
*/
public void setObj(Object obj) {
this.obj = obj;
}
}
尽管这个容器可以达到预期的效果,但它不是最合适的解决方案,由于它不是类型安全的而且需要使用强制类型转换,因此它可能造成潜在的异常
ObjectContainer container = new ObjectContainer();
// store a string
container.setObj("Test");
System.out.println("Value of container:" + container.getObj());
// store an int (which is autoboxed to an Integer object)
container.setObj(3);
System.out.println("Value of container:" + container.getObj());
List objectList = new ArrayList();
objectList.add(myObj);
// We have to cast and must cast the correct type to avoid ClassCastException!
String myStr = (String) ((ObjectContainer)objectList.get(0)).getObj();
System.out.println("myStr: " + myStr);
// we will see there is a ClassCastException!
泛型可以帮助我们更好的使用容器,该容器可以在实例化时分配类型。泛型类型是在类型上参数化的类或接口,这意味着可以通过执行泛型类型调用来分配类型,这将用分配的具体类型替换泛型类型。
然后,赋值的类型将用于限制容器内使用的值,可以在编译时提供更强的类型检查,这样就消除了强制转换的要求,也就避免了烦人的ClassCastException
接下来我们就使用泛型类型参数,来替代Object
类型吧!
public class GenericContainer<T> {
private T obj;
public GenericContainer(){
}
// Pass type in as parameter to constructor
public GenericContainer(T t){
obj = t;
}
/**
* @return the obj
*/
public T getObj() {
return obj;
}
/**
* @param obj the obj to set
*/
public void setObj(T t) {
obj = t;
}
}
最显著的区别是,类定义包含,而类成员obj不再是Object
类型,而是泛型类型T
. 类定义中的尖括号包含了类型参数部分,表明了将在类中使用的类型参数(或多个参数)。T
是一个参数,它与在这个类中定义的泛型类型相关联。
为了使用泛型容器,你只需要在实例化容器的时候,使用一对将T替换成你需要的类型即可,就像下面的code这样,实例化了Integer类型的GenericContainer并将其分配给myIntegerContainer。
GenericContainer<Integer> myIntegerContainer = new GenericContainer<Integer>();
就相当于告诉了编译器,“我里面就放Integer,其他类型的我不要!你帮我看着点儿,如果不是Integer,就别让放进来!”
myInt.setObj(3); // OK
myInt.setObj("Int"); // Won't Compile
使用泛型的好处
通过上面的例子,相信大家能多少体会到一些泛型的好处了吧,其实,更强的类型检查是最重要好处之一,因为它通过在编译时的强类型检查,避免了可能在运行时抛出的ClassCastExceptions,这样一来就可以节省时间。
另一个好处是消除了强制转换,因为编译器确切地知道集合中存储了什么类型。
另外多提一嘴,集合的大部分API本身其实就是使用泛型来开发的,试想,如果不使用泛型,那么这些API使用起来该有多麻烦啊!
深入研究泛型
接下来,我们将探索泛型的更多特性,准备好小板凳了嘛!
泛型这么厉害,那该怎么用嘞,有哪些用法捏?
泛型的用法
public class GenericContainer<T> {
...
第一个泛型示例,我们是在类名后面加上,这个玩意儿叫类型参数,也叫类型变量,既然叫变量了那么肯定可以给它分配值嘛,也就是给它分配一个具体的类型。没错,只是一个占位符,当运行程序时就会被分配具体的类型。
按照惯例,类型参数是单个大写字母,使用的字母表示要定义的参数类型。下面就是每种用法的标准类型参数:
E
: ElementK
: KeyN
: NumberT
: TypeV
: ValueS
,U
,V
, and so on: 尖括号里面的第二、三、四位置的占位符,即多类型的泛型
上面的例子中, 表明一个类型将会被分配,因此 GenericContainer
在初始化时将会被分配一个类型。当下面的代码实例化容器的时候,就会将 类中所有的 占位符全部类换成 String
类型
GenericContainer<String> stringContainer = new GenericContainer<String>();
泛型还可以在构造函数中用于传递用于类成员变量初始化的类型参数。GenericContainer有一个构造函数,允许在实例化时传入任何类型:
GenericContainer gc1 = new GenericContainer(3);
GenericContainer gc2 = new GenericContainer("Hello");
需要注意的是,未分配类型的泛型被称为原生类型,例如:
GenericContainer rawContainer = new GenericContainer();
原生类型对于向后兼容很有用,但是就无法利用到泛型的优点:编译时期强类型检查,和避免类型转换异常。
多类型的泛型
多类型的泛型也就是尖括号里面放了多个类型,比如<T, S>
下面我们就创建一个可以容纳两个元素的容器
public class MultiGenericContainer<T, S> {
private T firstPosition;
private S secondPosition;
public MultiGenericContainer(T firstPosition, S secondPosition){
this.firstPosition = firstPosition;
this.secondPosition = secondPosition;
}
public T getFirstPosition(){
return firstPosition;
}
public void setFirstPosition(T firstPosition){
this.firstPosition = firstPosition;
}
public S getSecondPosition(){
return secondPosition;
}
public void setSecondPosition(S secondPosition){
this.secondPosition = secondPosition;
}
}
MultiGenericContainer
可以用来存储两个不同类型的元素,当容器被实例化的时候,每个元素的类型就可以被具体化。
MultiGenericContainer<String, String> mondayWeather =
new MultiGenericContainer<String, String>("Monday", "Sunny");
MultiGenericContainer<Integer, Double> dayOfWeekDegrees =
new MultiGenericContainer<Integer, Double>(1, 78.0);
String mondayForecast = mondayWeather.getFirstPosition();
//这里存在一个由Double =》double的自动拆箱的过程
double sundayDegrees = dayOfWeekDegrees.getSecondPosition();
/*
注:
自动装箱和拆箱允许开发人员编写更干净的代码,使其更易于阅读。java编译器自动帮我们做的。
它允许我们互换使用原始类型和包装类型,并且我们不需要显式类型转换。
另外装箱和拆箱都会影响程序的性能
*/
这里需要提醒一下,泛型中的类型不可以是原始类型,即<T, S>
的类型只能是引用类型。
另外实例化容器时,可以简化书写:
MultiGenericContainer<String, String> mondayWeather =
new MultiGenericContainer<>("Monday", "Sunny");
MultiGenericContainer<Integer, Double> dayOfWeekDegrees =
new MultiGenericContainer<>(1, 78.0);
这里补充一个概念,目标类型,它允许编译器推断泛型调用的类型参数。目标类型是编译器期望的数据类型,具体取决于用于实例化泛型对象的类型、表达式出现的位置等。
下面这行代码,目标类型是Double
,因为getSecondPosition()
返回类型是S
,而上面实例化容器时给 S
分配的类型就是 Double
。加上前面所说的编译器自动拆箱,就会将调用方法的结果指定为原始类型double
double sundayDegrees = dayOfWeekDegrees.getSecondPosition();
有界类型
如果希望将类型限制为特定类型或该特定类型的子类型,可以使用以下语法:
<T extends UpperBoundType>
同样地,如果希望将类型限制为特定类型或该特定类型的超类型,可以使用以下语法:
<T super LowerBoundType>
下面,我们接着上面的GenericContainer
的例子来使用一下有界类型
public class GenericNumberContainer <T extends Number> {
private T obj;
public GenericNumberContainer(){
}
public GenericNumberContainer(T t){
obj = t;
}
/**
* @return
the obj
*/
public T getObj() {
return obj;
}
/**
* @param obj the obj to set
*/
public void setObj(T t) {
obj = t;
}
}
这样通过规定上界,就限制了GenericContainer
的泛型类型为Number
或者Number的子类
泛型方法
思考一下,如果一个方法的参数,你并不确定是什么类型,你会怎么办,使用Object?难道这也可以用泛型?当然了,记不记得上面的例子中,构造方法的参数不就是一个泛型类型嘛!
假如现在希望开发一个接受Number
类型参数的计算器类。Add()方法使用泛型来限制这两个参数的类型的上界,方法的返回值为double
public static <N extends Number> double add(N a, N b){
double sum = 0;
sum = a.doubleValue() + b.doubleValue();
return sum;
}
通过将参数类型限制为Number
,可以将Number子类
的任何对象作为参数传递。
此外,通过将类型限制为Number,我们可以确保传递给该方法的任何参数都将包含doubleValue()
方法。
通配符
在使用泛型的代码中,我们可以使用通配符<?>
来代表未知类型,通配符可以用在参数
、字段
、局部变量
、方法返回值
,来表明这些东东的类型。但是方法返回值不建议使用通配符,因为确切的指定方法返回值的类型是比较安全的。
考虑这样的情况,我们想要编写一个方法来验证指定的对象是否存在于指定的List
中。
我们希望该方法接受两个参数:未知类型的列表和任何类型的对象。
public static <T> void checkList(List<?> myList, T obj){
if(myList.contains(obj)){
System.out.println("The list contains the element: " + obj);
} else {
System.out.println("The list does not contain the element: " + obj);
}
}
下面我们就来使用一下这个方法
// Create List of type Integer
List<Integer> intList = new ArrayList<Integer>();
intList.add(2);
intList.add(4);
intList.add(6);
// Create List of type String
List<String> strList = new ArrayList<String>();
strList.add("two");
strList.add("four");
strList.add("six");
// Create List of type Object
List<Object> objList = new ArrayList<Object>();
objList.add("two");
objList.add("four");
objList.add(strList);
checkList(intList, 3);
// Output: The list [2, 4, 6] does not contain the element: 3
checkList(objList, strList);
/* Output: The list [two, four, [two, four, six]] contains
the element: [two, four, six] */
checkList(strList, objList);
/* Output: The list [two, four, six] does not contain
the element: [two, four, [two, four, six]] */
通配符通常使用上限或下限进行限制。与指定带边界的泛型很相似,通过指定通配符和extends或super关键字,然后指定用于上界或下界的类型,也可以声明带界限的通配符类型。例如,如果我们想修改checkList方法,使其只接受以Number
为上界的类型的List,我们可以这样写
public static <T> void checkNumber(List<? extends Number> myList, T obj){
if(myList.contains(obj)){
System.out.println("The list " + myList + " contains the element: " + obj);
} else {
System.out.println("The list " + myList + " does not contain the
element: " + obj);
}
}
Java 8 的新特性
我们已经了解了如何使用泛型以及泛型的重要性。现在让我们看看与Java SE 8中的新特性lambda表达式相关的泛型用例。Lambda表达式表示一个匿名函数,该函数实现函数式接口的单个抽象方法。让我们看一个例子。
假设我们希望遍历一个书名列表,并比较这些书名,以便返回包含指定搜索词的所有书名。我们可以通过开发一个方法来实现这一点,该方法接受一个书名列表,以及我们希望用于执行比较的断言。Predicate功能接口可用于比较的目的,返回一个布尔值来表明给定的对象是否满足断言。Predicate接口可以用于所有类型的对象,如下
@FunctionalInterface
public interface Predicate<T>{
...
}
如果我们希望遍历每个书名并查找包含文本“Java EE”的书名,则可以将contains(“Java EE”)作为断言参数传递。如下的方法可用于遍历给定的书名列表,并应用这样一个断言,打印出那些匹配的书名。在本例中,接受的参数使用泛型来指示字符串列表和将测试每个字符串的断言。
public static void compareStrings(List<String> list, Predicate<String> predicate) {
list.stream().filter((n) -> (predicate.test(n))).forEach((n) -> {
System.out.println(n + " ");
});
}
我们来创建一个书名列表,并添加一些书名,测试一下这个方法
List<String> bookList = new ArrayList<>();
bookList.add("Java 8 Recipes");
bookList.add("Java EE 7 Recipes");
bookList.add("Introducing Java EE 7");
bookList.add("JavaFX 8: Introduction By Example");
compareStrings(bookList, (n)->n.contains("Java EE"));
我们会发现,所有包含"Java EE"的书名会被打印出来,这就是一个泛型
与Lambda
表达式的简单应用。
总结
泛型允许使用更强的类型检查、取消强制类型转换。如果没有泛型,我们今天在Java中使用的许多特性就不可能实现。
我们还看到了泛型在Collections API和函数接口中扮演着重要角色,,它们用于启用lambda表达式。
本文只触及了泛型的表面,如果哪里有误,希望指正!
最后附上可爱的十三~