什么是泛型
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
例如我们平常声明一个集合变量List<String> list = new ArrayList<String>();
这里
泛型的本质
泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型的作用
提高了代码的复用性
// 没有泛型之前
private static int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static float add(float a, float b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static double add(double a, double b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
// 有了泛型之后
private static <T extends Number> double add(T a, T b) {
System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
return a.doubleValue() + b.doubleValue();
}
保证了类的安全性
相当于告诉编译器每个集合接收的对象类型是什么,编译器在编译期就会做类型检查,告知是否插入了错误类型的对象,使得程序更加安全,增强了程序的健壮性。
// 没有泛型的时候
public static void noGeneric() {
ArrayList names = new ArrayList();
names.add("mikechen的互联网架构");
names.add(123); //编译正常
}
// 有泛型的时候
public static void useGeneric() {
ArrayList<String> names = new ArrayList<>();
names.add("mikechen的互联网架构");
//names.add(123); //编译不通过
}
消除强制转换
泛型的一个附带好处是,消除源代码中的许多强制类型转换,这使得代码更加可读,并且减少了出错机会。
// 没有泛型的代码段需要强制转换
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
//当重写为使用泛型时,代码不需要强制转换
List<String> list2 = new ArrayList<String>();
list2.add("hello");
String s2 = list2.get(0); // no cast
避免了不必要的装箱、拆箱操作,提高程序的性能
在非泛型编程中,将筒单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。
泛型变量固定了类型,使用的时候就已经知道是值类型还是引用类型,避免了不必要的装箱、拆箱操作。
public static void main(String[] args) {
// 没有泛型之前
Object a = 1;//由于是object类型,会自动进行装箱操作。
int b = (int)a;//强制转换,拆箱操作。这样一去一来,当次数多了以后会影响程序的运行效率
// 有了泛型之后
int value = getValue(1);
}
public static <T>T getValue(T a){
return a;
}
怎么使用泛型
泛型类
泛型类就是把泛型定义在类上
public class 类名 <泛型类型1,...> {
}
:::info 注意事项:泛型类型必须是引用类型(非基本数据类型) ::: 当然,这个后面的参数类型也是有规范的,不能像上面一样随意,通常类型参数我们都使用大写的单个字母表示:
- T:任意类型的type
- E:集合中元素的类型Element
- K:k-v形式的Key
V:k-v形式的V
public class Test6<T> {
private T value;
public Test6(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
public class Test7<E> {
private List<E> data;
public Test7(List<E> data) {
this.data = data;
}
public E get(int index){
return data.get(index);
}
}
public class Test8<K,V> {
private Map<K,V> data;
public void setData(Map<K, V> data) {
this.data = data;
}
public V get(K k){
return data.get(k);
}
public static void main(String[] args) {
Test8<String,Integer> data = new Test8<>();
Map<String,Integer> map = new HashMap<>();
map.put("a",97);
data.setData(map);
Integer a = data.get("a");
System.out.println(a);
}
}
泛型接口
```java public <泛型类型> 返回类型 方法名(泛型类型 变量名) {
}
:::info
方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。当调用fun()方法时,根据传入的实际对象,编译器就会判断出类型形参T所代表的实际类型。
:::
```java
public interface Test9<T> {
T show(T param);
}
class Test94String implements Test9<String>{
@Override
public String show(String param) {
return param;
}
}
class Test94Integer implements Test9<Integer>{
@Override
public Integer show(Integer param) {
return param;
}
}
class Test9main{
public static void main(String[] args) {
//Test9<String> test9 = new Test94Integer(); // 编译异常
Test9<Integer> test9 = new Test94Integer(); // 编译正常
// 或者干脆不指定类型,那么 new 什么类型都是可以的:
Test9 test1= new Test94String();
Test9 test2= new Test94String();
}
}
泛型方法
修饰符 <代表泛型的变量> 返回值类型 方法名(参数){
}
public class Test10 {
public <T> T get(T t){
System.out.println(t.getClass());
System.out.println(t);
return t;
}
public static void main(String[] args) {
Test10 test10 = new Test10();
Integer integer = test10.get(1);
String a = test10.get("a");
}
}
这里可以看出,泛型方法随着我们的传入参数类型不同,他得到的类型也不同。泛型方法能使方法独立于类而产生变化。
泛型通配符
Java泛型的通配符是用于解决泛型之间引用传递问题的特殊语法, 主要有以下三类:
- <?> :无边界的通配符,表示类型参数可以是任何类型
- <? extends E> :固定上边界的通配符,表示类型参数必须是A或者是A的子类
- <? extends E> :固定上边界的通配符,表示类型参数必须是A或者是A的超类型
<?> :无边界的通配符
无边界的通配符的主要作用就是让泛型能够接受未知类型的数据.<? extends E> :固定上边界的通配符
使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据。
要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界。
注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类<? extends E> :固定上边界的通配符
使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据.。
要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界.。
注意: 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界。泛型的实现原理
泛型本质是将数据类型参数化,它通过类型擦除的方式来实现,即编译器会在编译期间「擦除」泛型语法并相应的做出一些类型转换动作。
例如:
我们定义了一个泛型类,定义了一个属性成员,该成员的类型是一个泛型类型,这个 T 具体是什么类型,我们也不知道,它只是用于限定类型的。public class Caculate<T> {
private T num;
}
反编译一下这个 Caculate 类:
发现编译器擦除 Caculate 类后面的两个尖括号,并且将 num 的类型定义为 Object 类型。public class Caculate{
public Caculate(){}
private Object num;
}
那么是不是所有的泛型类型都以 Object 进行擦除呢?大部分情况下,泛型类型都会以 Object 进行替换,而有一种情况则不是。那就是使用到了extends和super语法的有界类型,如:
public class Caculate<T extends String> {
private T num;
}
这种情况的泛型类型,num 会被替换为 String 而不再是 Object。
这是一个类型限定的语法,它限定 T 是 String 或者 String 的子类,也就是你构建 Caculate 实例的时候只能限定 T 为 String 或者 String 的子类,所以无论你限定 T 为什么类型,String 都是父类,不会出现类型不匹配的问题,于是可以使用 String 进行类型擦除。
实际上编译器会正常的将使用泛型的地方编译并进行类型擦除,然后返回实例。但是除此之外的是,如果构建泛型实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。
实际上编译器不仅关注一个泛型方法的调用,它还会为某些返回值为限定的泛型类型的方法进行强制类型转换,由于类型擦除,返回值为泛型类型的方法都会擦除成 Object 类型,当这些方法被调用后,编译器会额外插入一行 checkcast 指令用于强制类型转换,这一个过程就叫做『泛型翻译』。