泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
注意:一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为Object类型。
泛型的作用
泛型有四个作用:类型安全、自动转换、性能提升、可复用性。即在编译的时候检查类型安全,将所有的强制转换都自动和隐式进行,同时提高代码的可复用性。
1、保证类型安全
在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,如果不小心插入了错误的类型对象,在运行时的转换处理就会出错。
集合使用泛型前后对比:
//没有泛型时 编译正常通过,但是使用的时候可能转换处理出现问题
ArrayList arr = new ArrayList();
arr.add("字符串");
arr.add(1);
//使用泛型后
ArrayList<String> arr = new ArrayList();
arr.add("字符串");
arr.add(1); //编译时会报错
2、类型自动转换,消除强制转换
泛型可消除源代码中的强制类型转换,这样代码可读性更强,且减少了转换类型出错的可能性。
集合取值时使用泛型前后类型转换对比:
// 未使用泛型时 从集合中取值要强制类型转换
List list = new ArrayList();
list.add("字符串");
String s = (String) list.get(0);
//使用泛型后 可自动转换
List<String> list = new ArrayList();
list.add("字符串");
String s = list.get(0);
3、避免装箱、拆箱,提高性能
在非泛型编程中,将筒单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。
泛型变量固定了类型,使用的时候就已经知道是值类型还是引用类型,避免了不必要的装箱、拆箱操作。
//使用泛型前
object a=1;//由于是object类型,会自动进行装箱操作。
int b=(int)a;//强制转换,拆箱操作。这样一去一来,当次数多了以后会影响程序的运行效率。
//使用泛型后
public static T GetValue<T>(T a) {
return a;
}
public static void Main(){
int b=GetValue<int>(1);//使用这个方法的时候已经指定了类型是int,所以不会有装箱和拆箱的操作。
}
4、提升程序可复用性
适用于多种数据类型执行相同的代码
//使用泛型前,不同数据类型相同逻辑代码重复率高
private static int add(int a, int b) {
return a + b;
}
private static float add(float a, float b) {
return a + b;
}
private static double add(double a, double b) {
return a + b;
}
//使用泛型后,可以复用同一方法
private static <T extends Number> double add(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
泛型的使用
java 中泛型标记符:
- E - Element (在集合中使用,因为集合中存放的是元素)
- T - Type(Java 类)
- K - Key(键)
- V - Value(值)
- N - Number(数值类型)
- ? - 表示不确定的 java 类型
1、泛型方法
在调用方法的时候指明泛型的具体类型 。
定义泛型方法的规则:
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的 <E>)。
- 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
- 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。
定义格式如下:
public <泛型类型> 返回类型 方法名(泛型类型 变量名) {
// todo
}
举例,根据传入的对象,打印它的值和类型:
/**
* 泛型方法
* @param <T> 泛型的类型
* @param c 传入泛型的参数对象
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E等形式的参数常用于表示泛型。
*/
public <T> T genercMethod(T c) {
System.out.println(c.getClass());
System.out.println(c);
return c;
}
public static void main(String[] args) {
//这里的泛型跟下面调用的泛型方法可以不一样。
GenericsClassDemo<String> genericString = new GenericsClassDemo("Hello World");
//传入的是String类型,返回的也是String类型
String str = genericString.genercMethod("brand");
//传入的是Integer类型,返回的也是Integer类型
Integer i = genericString.genercMethod(100);
}
有界的类型参数:
可能有时候,你会想限制那些被允许传递到一个类型参数的类型种类范围。例如,一个操作数字的方法可能只希望接受Number或者Number子类的实例。这就是有界类型参数的目的。
要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界。
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" ) );
}
}
2、泛型类
泛型类型必须是引用类型,非基本数据类型
定义格式如下:
public class 类名 <泛型类型1,...> {
// todo
}
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
Box<String> stringBox = new Box<String>();
integerBox.add(new Integer(10));
stringBox.add(new String("菜鸟教程"));
System.out.printf("整型值为 :%d\n\n", integerBox.get());
System.out.printf("字符串为 :%s\n", stringBox.get());
}
}
3、泛型接口
泛型接口常被用在各种类的生产器中
格式如下
public interface 接口名<T> {
// todo
}
方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。当调用fun()方法时,根据传入的实际对象,编译器就会判断出类型形参T所代表的实际类型。
public interface GenericInterface<T> {
void show(T value);}
}
public class StringShowImpl implements GenericInterface<String> {
@Override
public void show(String value) {
System.out.println(value);
}
}
public class NumberShowImpl implements GenericInterface<Integer> {
@Override
public void show(Integer value) {
System.out.println(value);
}
}
使用泛型的时候,前后定义的泛型类型必须保持一致,否则会出现编译异常:
// 编译的时候会报错,因为前后类型不一致
GenericInterface<String> genericInterface = new NumberShowImpl();
// 编译正常,前面泛型接口不指定类型,由new后面的实例化来推导。
GenericInterface g1 = new NumberShowImpl();
GenericInterface g2 = new StringShowImpl();
泛型通配符
当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。
Java泛型的通配符是用于解决泛型之间引用传递问题的特殊语法, 主要有以下三类:
- 无边界的通配符,使用精确的参数类型
- 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
- 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
1、类型通配符一般是使用 ? 代替具体的类型参数。例如 List<?> 在逻辑上是 List<String>,List<Integer> 等所有 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));
}
}
解析: 因为 getData() 方法的参数是 List<?> 类型的,所以 name,age,number 都可以作为这个方法的实参,这就是通配符的作用。
2、类型通配符上限通过形如List来定义,如此定义就是通配符泛型值接受Number及其下层子类类型。
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);
//getUperNumber(name);//1
getUperNumber(age);//2
getUperNumber(number);//3
}
public static void getData(List<?> data) {
System.out.println("data :" + data.get(0));
}
public static void getUperNumber(List<? extends Number> data) {
System.out.println("data :" + data.get(0));
}
}
解析: 在 //1 处会出现错误,因为 getUperNumber() 方法中的参数已经限定了参数泛型上限为 Number,所以泛型为 String 是不在这个范围之内,所以会报错。
3、类型通配符下限通过形如 List<? super Number> 来定义,表示类型只能接受 Number 及其上层父类类型,如 Object 类型的实例。