概述
泛型即为参数化类型。就是将数据类型参数化,作为一种类型安全机制而产生的。
使用泛型集合时,可以将一个集合中的所有元素限定为一个特定类型,这样集合中就只能存储特定的类型的对象, 这样比较安全;并且获取集合中存储的数据时,编译器也直到这个元素的类型,不需要进行窄化处理,这样使用也比较方便。
为什么使用泛型?
没有泛型的时候
public class node {
public int value;//节点的结果
node next;//下一个连接的节点
public node(){}
public node(int value)
{
this.value=value;
}
}
//这个node 节点存的是int类型,如果是存一个字符串的链表或者是一个double类型数据链表呢?只能重复写
public class node {
public String value;//节点的结果
node next;//下一个连接的节点
public node(){}
public node(String value)
{
this.value=value;
}
}
如果使用Object类表示泛型,在向下转型的过程中,人为操作失败编译器无法识别,只能等到代码执行的时候才会出错,这样的机制让java代码很不安全。
public class test {
public static void main(String[] args) {
Integer a=10;
node node1=new node(a);
Integer va1=(Integer) node1.getValue();
System.out.println(va1);
node node2=new node("hello");
String va2=(String) node2.getValue();
System.out.println(va2);
}
}
class node
{
private Object value;
public node(Object value)
{
this.value=value;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
集 合 对 象 没 有 记 录 元 素 的 具 体 类 型 , 获 取 数 据 时 需 要 进 行 类 型 转 换 , 很 容 易 出 现 运 行 时 异 常 ClassCastException 。需要一种方法实现集合能够记住集合中元素的类型,编译期就能够进行合法类型判断,获取时不需要进行类型转换。于是引出泛型,即可以将运行时的类型检查提前到编译期实现,直接获取指定类型数据避免强制类型转换操作。
特性
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();
Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();
if(classStringArrayList.equals(classIntegerArrayList)){
Log.d("泛型测试","类型相同");
}
输出结果:D/泛型测试: 类型相同
。
泛型只存在于编译期,编译过后会自动去泛型化,在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。
使用泛型的好处
- 可读性,从字面上就可以判断集合中的内容类型
- 类型检查,避免插入非法类型的数据
- 获取数据时不再需要强制类型转换
泛型的使用
泛型类
泛型类的语法,具有一个或者多个类型参数的类,一个泛型类可以有多个泛型声明,所有的泛型声明都在<>内部。
类名<T> 其中T代表一个类型名称
类名<T1,T2> 可能有多个类型。如果引用多个类型,可以使用逗号作为分隔符
//类型参数名称可以使用任意字符串,但一般建议使用有代表含义的单个字符,以便于和普通类型名称进行区分。
//例如 T 代表 type,源数据 S,目标数据 D,子元素类型 E
T就是一个类型的说明,可以用在说明任何实例方法中的局部变量、方法的形参以及方法的返回值,类的成员变量;但是类型T不能直接使用在静态方法中。
- 泛型的类型参数只能是引用类型(包括自定义类),不能是基本类型,List×
- 泛型类中的使用了泛型的成员方法并不是泛型方法
- 注意传递给泛型参数的类型必须时类类型,不能使用 int 或者 char 之类的简单类型
- 如果定义了泛型类,但是引用时不声明泛型对应的类型值,则系统识别为 Object 类型
Generic en=new Generic(); en.setName("yanjun"); //有警告信息,但是语法正确,因为 String 也是 Object 类型
en.setName(123); //有警告信息,但是编译可以通过。因为 123 经过自动装箱操作后,可以识别为 Object 类型
- Collection和 Collection是两个没有任何关系的参数化类型接口,不存在什么谁可以
赋值给谁的可能
集合类中的泛型
List<String> list=new ArrayList<>();
Map<String,Object> map=new ArrayList<>(); List<Map<String,Object>> list=new ArrayList<>();
小结泛型类
- 定义泛型类
public class MyClass<T,ID>{
private ID id;
private T name;
//也可以在方法上直接使用T和ID类型
}
- 给定义在泛型类上的泛型家约束
public class MyClass<T extends IA & AClass>{
}
- 在泛型类或者泛型接口中不允许直接使用 super,一般 super 只用于方法的参数类型声明中
List<? super Integer> arr1=new ArrayList<Integer>();
List<? super Integer> arr2=new ArrayList<Number>();
List<? super Integer> arr3=new ArrayList<Object>();
//但是以下语法错误
List<Number> arr1=new ArrayList<Integer>();
List<Object> arr2=new ArrayList<String>();
- <? extends T>可以用于方法的返回值或者用于频繁向外读取内容时,<? super T>一般用于限制方法的参数或者插入数据时
泛型接口
同泛型类
泛型通配符
定义
当使用泛型类或者接口时,传递的数据中。泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。(此时只能接收数据,不能往集合中存储数据)
使用方式
- 不能创建对象使用
- 只能作为方法的参数使用
public class Test1 {
public void pp(List<?> list) {
// list.add("123");//× 此时使用List时不能涉及List中的元素的类型,否则会报错
for (Object tmp : list)
System.out.println(tmp);
System.out.println(list.size());
list = new ArrayList<Date>();
// list.add(new Date());//× 因为编译器识别的类型是不确定的类型
System.out.println(list.size());
//语法细节
List<?> aList = new ArrayList<String>();
// aList.add("bbb");//×
}
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
//List<?>中的?表示可以传入任意类型,到底?是什么类型,只有运行时才能确定,所以 List<?> list=new ArrayList<String>()和 List<?> list=new ArrayList<Integer>()都可以,但是试图添加元素 则会出现问题
List<?> aList = new ArrayList<String>();
// aList2.add("123");//×
list.add("123");
}
}
泛型方法
- 泛型定义在泛型类上,在方法中直接使用类上声明的泛型
public class Test<T,ID>{
public void pp(T t){}
public T load(ID,id){
return null;
}
}
- 方法定义在普通类中,需要自定义参数类型
- 静态方法不能访问类的泛型,如果需要泛型则只能在方法上声明使用泛型
//语法报错
public static void aa(T t){}
//语法正确
public static <T> void show(T t){}
- 一个方法如果声明为泛型方法,那么它拥有一个或者多个类型参数,不过不同于泛型类的地址在于,类型参数只能在所修饰的方法之中使用
public class MyClass{ //下列为泛型方法
public <T> T show(T data){}
public <D> void show(D data){}
}
泛型上下边界
- 类型的上界:格式为<? extends T>,即类型必须为T类型或者T类型的子类
- 类型的下界:格式为<? super T>,即类型必须为T类型或者T类型的父类
//在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<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;
}
泛型的上下边界添加,必须与泛型的声明在一起 。
实例
需求:要求对数值类型的不确定个数的数据进行累加
如果使用泛型定义,但是不使用上界约束
使用上界约束
注意:这里的extends不是继承,只是用于说明传入的类型必须是Nmber类型或者是Number类型的后代类型
接口和类都可以作为泛型的上界,当使用接口作为上界时,关键字还是extends,而不是impl。同时允许一个参数数类型有多个限界,限界类型可以使用&符号分割。如果使用只使用类型作为上界,则只能定义一个类 型上界;由于 java 的单根继承体系的要求,所以不能使用同时使用多个类作为类型上界,即使是父子类型也不能 定义。允许使用一个类和多个接口或者多个接口
模板模式的改写
泛型类的写法
测试类
泛型方法的写法
泛型类的继承
泛型类是可以继承的,任何一个泛型类都可以作为父类或者子类,不过和普通的继承略有区别
父类
public class A<T>{}
- 泛型类的子类必有将泛型父类所需要的类型参数沿着继承链向上传递
- 子类不是泛型类,则需要给泛型父类传递类型常量
public class B extends A<String>{}
- 子类是泛型类可以给父类传递类型常量,也可以传递类型变量
public class B<T> extends A<T>{}
- 如果父类的泛型中有约束
public class A<T extends Serializable>{}
public class B<D extends Serializable> extends A<D>{}
//特殊写法
public class B<D> extends A{}
//等价于
public class B<D> extends A<Object>{}
泛型的局限性
- 不能使用基本数据类型,应该使用对应类型的包装类
- 不能使用泛型类异常
- 不能使用泛型数组。声明泛型数组可以,但是不能实例化
- 不能实例化参数类型对象 T obj = new T();。如果需要实例化则需要通过反射机制实现
Comparable和Comparator
接口Comparable
如果一个类实现了 Comparable 接口,则表示当前类型的对象是可比较大小的,也就是说可以进行排序。实现了
Comparable 接口的类支持排序,也就是可以使用工具类 Collections 对集合对象进行排序
public class Pig implements Comparable<Pig>{
private long id;
private Double weight;
public Pig(long id, Double weight) {
super();
this.id = id;
this.weight = weight;
}
@Override
public String toString() {
return "Pig [id=" + id + ", weight=" + weight + "]";
}
@Override
public int compareTo(Pig o) {
double res = this.weight-o.weight;
if (Math.abs(res)<1e-6) {
return 0;
}else if (res>0) {
return 1;
}else {
return -1;
}
}
public static void main(String[] args) {
List<Pig> list = new ArrayList<Pig>();
Random r = new Random();
for (int i = 0; i < 10; i++) {
Pig tmp = new Pig(1L+i, r.nextDouble()*480+20);
list.add(tmp);
}
Collections.sort(list);
for (Pig pig : list) {
System.out.println(pig);
}
}
}
接口Comparator
Comparator 是比较器接口。如果类本身不支持排序比较,即实现 Comparable 接口,则可以建一个类型的比较 器专门用于排序比较。
例如 Pig 类没有实现 Comparable 接口,则使用 Collections.sort(list)则会报错。报错的原因是 Collection 工具类中的方法定义
public class Pig {
private long id;
private Double weight;
public Pig(long id, Double weight) {
super();
this.id = id;
this.weight = weight;
}
@Override
public String toString() {
return "Pig [id=" + id + ", weight=" + weight + "]";
}
public static void main(String[] args) {
List<Pig> list = new ArrayList<Pig>();
Random r = new Random();
for (int i = 0; i < 10; i++) {
Pig tmp = new Pig(1L+i, r.nextDouble()*480+20);
list.add(tmp);
}
Comparator<Pig> c = (obj1,obj2)->{
double res = obj1.weight-obj2.weight;
if (Math.abs(res)<1e-6)
return 0;
else if(res>0)
return 1;
else
return -1;
};
for (Pig pig : list) {
System.out.println(pig);
}
}
}
Comparable 和 Comparator 接口比较
- Comparable 接口是排序接口,如果一个类实现了 Comparable 接口就意味着该类型的对象是可比较的
- Comparator 接口是比较器,如果需要控制某个类的次序,可以临时建议一个该类的比较器进行排序
- 可以将 Comparable 当作内部比较器,而 Comparator 相当于外部比较器
面试题
泛型的擦除
实际上从VM的角度上来说是不存在泛型概念的。泛型是运用在编译期的技术:在编译时编译器会按照泛型声明对容器中的元素进行检查,检查不匹配则编译失败;如果全部检查成功则编译通过。但是编译通过后生成的.class字节码文件中没有泛型声明的标识也就是文件中并不会出现泛型,这就是泛型的擦除。
- 在.java文件中运行泛型技术时,编译器在文件编译通过后自动擦除泛型标识
- 由于泛型的擦除,类文件中实际上是没有泛型机制,同时也没有使用向下类型转换,为什么运行时没有出现异常?这是由于泛型的补偿
- 编译器在擦除泛型后,会自动将类型转换为原定义的泛型,这样就不必再做窄化操作
- 泛型的擦除和补偿都是编译器内部完成的
典型题目
模板模式的冒泡算法两种实现
Comparable 接口和 Comparator 接口