关于泛型你要知道二三事
本内容均为原创,如需转载请注明出处:https://www.jianshu.com/p/d13ed2b58c8a
写这篇文章的初衷,是对于大家遇到的几道泛型(Generic)题目的不理解。那不妨我们在这里
展开,好好讨论一下泛型这个鬼东西。
备注<由于内容很多,所以会通过几个章节依次讨论>
为了更好的说明泛型这个内容我会从以下几个方向依次展开讨论:
1、什么是泛型?
i、什么是类型安全的
ii、泛型的语法格式
2、为什么需要泛型?
3、泛型主要分类:
i、泛型类
ii、泛型方法
iii、泛型接口
iv、泛型构造器
4、泛型的注意事项:
i、优点:确保数据安全性
a、泛型中能使用基本数据类型
b、不同类型的泛型对象能互相转换
ii、反射对于泛型的影响
iii、泛型边界问题
iv、泛型的擦除:
5、jdk8增强的泛型类型推断
6、拓展jdk10增强的局部变量的类型推断
什么是泛型?
什么是泛型呢?就是一个广泛的类型,注意它首先是一个类型,其次因为广泛,所以就是不明确类型。
所以我们说泛型就是一个不明确的类型。那如果类型不明确我们如何使用呢?别着急我们往下继学。
泛型是从jdk1.5之后引入的新特性。它不光增加了java的新的语法,同样也改变了核心API中的许多类以及方法。常见的比如java.util.*
。通过泛型,我们可以创建类型安全的类、接口等。
什么是类型安全的呢?
基于这个问题,我会在第二块中详细阐述这个问题。这里先不深入
泛型的语法格式
我们在后续的泛型分类中会详细介绍泛型不同的创建方式和细节,这里我们要注意泛型的书写方式!
通过一组<> 去定义泛型。然后在<>中编写任意的字母即可。但是不能以数字开头。
我们通常会通过在<>中定义26个英文字母的大写表示。一般常见的字母为T,E,K,V等。
例子:
<T> //定义一个泛型类型 T
<K,V> //定义了一个泛型 包含两个泛型类型分别是K和V
2 为什么需要泛型?
我们试想,如果没有泛型,假设在一下代码中,会出现什么问题?
测试代码1:
List ls = new ArrayList();
ls.add("str");
ls.add(new Integer(12));
for(int i = 0;i<ls.size();i++){
Integer s = (Integer) ls.get(i);
System.out.println(s);
}
以上代码会出现问题,ClassCastException<类型转换异常>。原因大家应该都很清楚,那我们试想,如果没有泛型,所有对于数据的操作都会按照显式
的方式进行转换,所以我们说泛型在一定程度上帮助我们做到了确保数据安全性。当然安全性我们还会通过其他示例来深入阐述。
测试代码2:
public static void main(String[] args) {
List lists = new ArrayList();
lists.add("hello");
lists.add(12);
List<String> ls = new ArrayList<String>();
ls.add("hello");
ls.add(12);//the compile-time error
}
以上代码我们能够很直观的看出来,如果对于一个没有声明泛型的泛型的List集合来讲,可以往集合中添加任意类型(注意只能是引用类型)。而对于声明类泛型类型为String的List集合而言只能添加Stirng对象,当要往List集合添加12时(注意,这里的12会做自动装箱),编译时会报错。很好帮我解决了数据验证的问题。其实我们很难有场景要在一个集合中填充不同对象,对于后期来讲这样是不太方便的。大多数场景下我们还是填充的统一类型数据。(当然有时候我们确实有添加不同对象的需求,我们在后续的例子中会展开讨论)。
测试代码3:此时泛型的行为时期
在这里我们将上述的代码编译之后,通过反编译工具打开再次查看:
public static void main(String[] args)
{
List lists = new ArrayList();
lists.add("hello");
lists.add(Integer.valueOf(12));
List ls = new ArrayList();
ls.add("hello");
}
这里无论是lists集合还是ls集合,在编译完之后的我们发现,泛型都不存在了,而且12的填充确实调用了Integer.valueOf()进行了自动装箱。所以我们说泛型是一个编译器行为
。那其实这也就为我们通过反射在运行期动态往一个泛型集合中添加不同数据类型埋下伏笔。
3、泛型分类
接下来我会通过四个方向依次详细说明泛型在不同情境下定义要遵守的一些规则。
泛型类:
public class Test01 {
public static void main(String[] args) {
//create generic type Gen instance and assign
//generic type of Gen is Integer
//it`s use of autoboxing to encapsulate
//the value 12 within an Integer Object
Gen<Integer> g1 = new Gen<Integer>();
g1.t = 12;
g1.getT();
//jdk1.7 enhancement type inference
Gen<String> g2 = new Gen<>();
g2.t = "hello";
g2.getT();
//create generic type Gen instance and
//don`t assign generic type
Gen g3 = new Gen();
g3.t = new Date();
g3.getT();
}
}
//declared a generic type Gen
class Gen<T>{
T t;
public void getT(){
System.out.println(t.getClass().getName());
}
}
在这里说明了一个Generic类->Gen,泛型类型是T,我们上文提到过,泛型就是一个类型,那么这里的T你不防理解为一个类型。并且为了获取T类型方便一些,我们将T类型也声明为了一个成员变量。我们在代码中通过getT()方法获取成员变量T的Class对象的名称。其次我们在测试类创建了3个Gen的对象。
在第一个用例中:创建对象时指定类型为Integer,且通过给对象中的T类型赋值为12。我们发现获取到的Class对象的名称为Integer。
第二个用例中:创建对象时指定类型为String,且这里我们在对象创建中只通过<>
,而没有在括号中给具体的类型,这是jdk1.7中的增强类型推断,因为已经声明过时String类型,所以会自动推断出Gen对象的T类型是String。同样这里获取到的Class对象名称为String。
第三个用例中:创建对象时没有指定泛型类型,我们这里直接赋值发型可以给T传入任何Object类型。其实这就是典型的擦除。我们后续分享会逐一解开这个面纱。这里获取到的泛型的name成为了Object。
总结:在一个类声明时通过<T>会指定泛型类型。创建对象时可以在<>指定具体的泛型类型。
如果不指定则经过擦除之后,类型变为Object。