📢泛型
什么是泛型?有什么作用?
Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如ArrayList<Person> persons = new ArrayList<Person>()
这行代码就指明了该ArrayList
对象只能传入Person
对象,如果传入其他类型的对象就会报错。
ArrayList<E> extends AbstractList<E>
并且,原生的List返回类型是Object,需要手动转换类型才能使用,使用泛型后编译器自动转换。
对于泛型,只是允许程序员在编译时检测到非法的类型。但是在运行期时,其中的泛型标志会变化为Object类型,例如:
class Test{
public static void main(Stirng[] args){
List<Integer> list = new ArrayLsit<>();
list.add(2);
list.add("a");//这里直接添加会在编译时就报错,因为指定list的类型是Integer
//但是编译后运行期,使用Java反射机制是可以执行添加其他类型元素的
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add",Object.class);
add.invoke(list,"k1");//能成功添加
}
}
java 中泛型标记符:
- E - Element (在集合中使用,因为集合中存放的是元素)
- T - Type(Java 类)
- K - Key(键)
- V - Value(值)
- N - Number(数值类型)
- ? - 表示不确定的 java 类型–类型通配符
泛型的使用方式有几种?
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
泛型类
泛型类型用于类的定义中,被称为泛型类。听过泛型可以完成对一组类的操作对外开放相同的接口。
最典型的就是各种容器类,如:List、Set、Map。
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;
//泛型构造方法-形参key的类型也为T,T的类型由外部指定
public Generic(T key){
this.key = key;
}
//泛型方法getKey的返回值类型为T,T的类型由外部指定
public T getKey(){
return key;
}
}
如何实例化泛型类:
public static void main(String[] args){
//泛型的类型参数只能是引用类型(封装类、自定义类),不能是基本数据类型
//传入的实参类型需与泛型的类型参数类型相同
Generic<Integer> genericInteger = new Generic<Integer>(1234);
Generic<String> genericString = new Generic<String>("字符串");
System.out.println("整型值为 :", genericInteger.get());
System.out.println("字符串为 :", genericString.get());
}
编译以上代码,运行结果如下所示:
整型值为 :1234
字符串为 :字符串
定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);
泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中
//定义一个泛型接口
public interface Generator<T>{
public T method();
}
实现泛型接口,不指定类型(即不传入泛型实参)
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method(){
return null;
}
}
实现泛型接口,指定类型:
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method(){
return "hello";
}
}
泛型方法
假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组、甚至其他任何类型的数组进行排序,该如何实现?
答案是可以使用Java泛型。
使用Java泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
下面是定义泛型方法的规则:
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的 )。
- 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
- 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。
/**
* 泛型方法的基本介绍
* @param tClass-传入的泛型实参
* @return T-返回值为T类型
* 说明:
* 1)public与返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public <T> T genericMethod(Class<T> tClass){
T instance = tClass.newInstance();
return instance;
}
实例
public class GenericMehodTest{
//定义声明泛型方法
public static <E> void printArray(E[] inputArray){
//使用for-each循环遍历
for(E element : inputArray){
System.out.print(element+" ");
}
System.out.println();//换行
}
//使用
public static void main(Stirng[] args){
//创建不同类型的数组
Integer[] intArray = {1,2,3};
String[] stringArray = {"hello","world"};
Character[] charArray = {"H","E","L","L","O"};
Double[] doubleArray = {1.1,1.2,1.3,1.4};
//调用泛型方法打印不同类型的数组
printArray(intArray);
printArray(stringArray);
printArray(charArray);
printArray(doubleArray);
}
}
//输出结果
1 2 3
hello world
H E L L O
1.1 1.2 1.3 1.4
注意:
public static <E> void printArray(E[] inputArray)
一般被称为静态泛型方法;在Java中泛型只是一个占位符,必须在传递类型后才能使用。
类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的,只能使用自己声明的
<E>
。
泛型上下边界
可能有时候,会想限制那些被允许传递到一个参数类型的类型种类范围。如:类型实参只准传入某种类型的父类或者某种类型的子类。
- 上界:例如:一个操作数字的方法可能只希望接受Number或者Number子类的实例。
要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界
<? extends T>
:表示该通配符是T类型的子类
实例
public class MaximumTest
{
// 比较三个值并返回最大值-在泛型方法中声明泛型类型的上界
public static <T extends Comparable<T>> T maximum(T x, T y, T z)
{
T max = x; // 假设x是初始最大值
if ( y.compareTo( max ) > 0 ){
max = y; //y 更大
}
if ( z.compareTo( max ) > 0 ){
max = z; // 现在 z 更大
}
return max; // 返回最大对象
}
public static void main( String args[] )
{
System.out.printf( "%d, %d 和 %d 中最大的数为 %d\n\n",
3, 4, 5, maximum( 3, 4, 5 ) );
System.out.printf( "%.1f, %.1f 和 %.1f 中最大的数为 %.1f\n\n",
6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 ) );
System.out.printf( "%s, %s 和 %s 中最大的数为 %s\n","pear",
"apple", "orange", maximum( "pear", "apple", "orange" ) );
}
}
//编译以上代码,运行结果如下所示:
3, 4 和 5 中最大的数为 5
6.6, 8.8 和 7.7 中最大的数为 8.8
pear, apple 和 orange 中最大的数为 pear
//在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加
//public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound"
public <T extends Number> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
T test = container.getKey();
return test;
}
在泛型类中使用:
public class Generic<T extends Number>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
//实例化
Generic<Integer> generic1 = new Generic<Integer>(11111);
//这一行代码会报错,因为String不是Number的子类
Generic<String> generic1 = new Generic<String>("11111");
- 下界:使用方法与上界一样。
语法格式:<? super T>:表示该通配符所代表的类型是T类型的父类。
类型通配符
- 类型通配符一般是使用
?
代替具体的类型参数。例如List<?>在逻辑上是List,List等所有List<具体类型实参>
的父类。
import java.util.*;
public class GenericTest {
public static void main(String[] args) {
List<String> name = new ArrayList<String>();
List<Integer> age = new ArrayList<Integer>();
List<Number> number = new ArrayList<Number>();
name.add("icon");
age.add(18);
number.add(314);
getData(name);
getData(age);
getData(number);
}
public static void getData(List<?> data) {
System.out.println("data :" + data.get(0));
}
}
//输出
data :icon
data :18
data :314
解析:因为 getData() 方法的参数是 List<?>
类型的,所以 name,age,number 都可以作为这个方法的实参,这就是通配符的作用。
Tips:不能创建一个确切的泛型类型的数组。