持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情
泛型是什么?
- 泛型: 在实例化的时候再去明确类型,即参数化类型。简单来说,就是把类型当作一种参数来传递。很多语言都支持泛型,在C++中称为模板,Java是强类型语言,在JDK1.5中引进泛型这个概念,主要目的是加强类型安全,减少类型转换的次数。
- 引用类型: 指向一个对象,而不是原始值,指向对象的变量是引用变量。在java里面除去基本数据类型的其它类型都是引用数据类型,自己定义的类也是引用类型。
- 泛型的设计原则: 只要在编译时期没有出现警告,那么运行时期就不会出现
ClassCastException类型转换异常。
为什么要使用泛型?
- 早期用Object来代替任意类型,向上转Object容易,但是Object向下强转就不太安全。
- 如果没有泛型,由于集合中是不限制元素类型的,那么可以往里面丟任何元素,并且不会报语法错误,集合中的元素默认都是
Object类型,取出来的时候,就返回Object类型的,可谓乱丢一时爽,get火葬场。 - 有了泛型,就不用强制转换了,事先就规定好这个集合中装什么类型的元素,不符合规定的类型,压根就塞不进集合中,在编译时就会报错。这样的规范也使得代码更简洁。无论是可读性还是稳定性都有增强,而且配合增强for循环使用也非常方便
泛型对比测试
- 首先看不加泛型的情况下,在list集合中添加不同类型的数据,并且在遍历的时候强转,编译的时候会报
ClassCastException异常的。这就是由于集合中元素类型不统一造成的。
List arrayList = new ArrayList();
//添加一个String类型的元素
arrayList.add("aaaa");
//添加一个Integer类型的元素
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
//都强转成String类型
String item = (String)arrayList.get(i);
Log.d("泛型测试","item = " + item);
}
- 加上泛型之后的,就规定了这个集合中只能添加这个类型的元素,要是装别的,写代码的时候IDEA就会报错,
List<String> list = new ArrayList<String>();
//可以添加
list.add("daiaoqi");
//IDEA提示编译不通过
list.add(100)
如何使用泛型?
在定义类,接口,方法的时候,都可以使用泛型,尤其是看List的源码的时候,会看到定义了大量的泛型类和泛型接口。常用的泛型标识有: (T, E, K, V,?) 通常,K和V在Map中结合使用的比较多,这几个代表的含义如下:
- ?表示不确定的 java 类型
- T (type) 表示具体的一个java类型
- K V (key value) 分别代表java键值中的Key Value
- E (element) 代表Element
泛型接口
- 泛型接口的定义如下:
public interface 接口名称<泛型标识1,泛型标识2,....> {
泛型标识1 方法名();
泛型标识2 方法名();
}
①子类明确泛型类的类型参数变量(泛型接口)
//把泛型定义在接口上
public interface Inter<T> {
public abstract void show(T t);
}
//子类明确泛型类的类型参数变量,则直接把接口中定义的泛型标识符拿过来用
public class InterImpl implements Inter<String> {
@Override
public void show(String s) {
System.out.println(s);
}
}
②子类不明确泛型类的类型参数变量
- 此时,外界使用子类的时候,也需要传递类型参数变量进来,在实现类上需要定义出类型参数变量。
//实现类要定义出<T>类型
public class InterImpl<T> implements Inter<T> {
@Override
public void show(T t) {
System.out.println(t);
}
}
泛型类
- 注意: 类上声明的泛型,只对非静态成员有效 ,泛型标识符可以写多个,传几个进来,在类中就可以使用几个,定义语法如下:
class 类名称 <泛型标识1, 泛型标识2, ...> {
private 泛型标识1 变量1;
private 泛型标识2 变量2;
...
}
- 定义泛型类
public class GenericClass<T, E> {
private T key1;
private E key2
public GenericClass(T key1, E key2) {
this.key1 = key1;
this.key2 = key2
}
public T getKey1() {
return key1;
}
public void setKey1(T key) {
this.key1 = key;
}
}
- 使用泛型类,在实例化的时候,在<>中将类型传入,就将E的类型确立下来了,不传任何类型则当做Object类型处理,如下所示,泛型类在逻辑上可以看作是多个不同的数据类型,但是实际上都是相同类型,都是上面定义的
GenericClass类
public static void main(String[] args) {
// 引用类型
Student student = Student.builder().age(10).build();
GenericClass<Student> g1 = new GenericClass<>(student);
// 包装类型也算引用类型
GenericClass<String> g2 = new GenericClass<>("daiaoqi");
GenericClass<Integer> g3 = new GenericClass<>(123);
// 没有指定类型,则按照Object处理
GenericClass g4 = new GenericClass(student);
Object key3 = g4.getKey();
// 泛型类不支持基本数据类型
}
- 应用示例:定义一个奖品类,子类对应具体的实现,通过ProductPool泛型类中做业务处理,这些子类如果都有相同的属性或者方法,直接在ProductPool类中做业务处理
public class Prize {
public static class Car extends Prize{
private String name = "本田汽车";
@Override
public String toString() {
return "Car{" +
"name='" + name + '\'' +
'}';
}
}
public static class Iphone extends Prize{
private String name = "Iphone13";
@Override
public String toString() {
return "Iphone{" +
"name='" + name + '\'' +
'}';
}
}
}
public class ProductPool<T> {
private T product;
Random random = new Random();
ArrayList<T> productList = new ArrayList<>();
public T getProduct() {
product = productList.get(random.nextInt(productList.size()));
return product;
}
public void setProduct(T product) {
productList.add(product);
}
}
public static void main(String[] args) {
// ---------- 泛型类实际应用 ----------
ProductPool<Prize> pl = new ProductPool<>();
pl.setProduct(new Prize.Car());
pl.setProduct(new Prize.Iphone());
System.out.println(pl.getProduct());
}
泛型方法
- 如果外界仅仅对一个方法感兴趣,而不关心类中的其他属性,那么将泛型定义在类上就有些小题大做,这里直接定义在方法上,精准打击!!
public <T> void show(T t){
System.Out.println(t);
}
- 泛型方法的使用
public static void main (String[] args){
//创建对象
ObjectTool obj = new ObjectTool();
//调用方法,传进来什么类型,返回值就是什么类型
obj.show("hello");
obj.show(4);
obj.show(false);
}
泛型擦除
- 在C++ 中,有以下代码使用了模板。
template<typename T>
struct Foo
{
T bar;
void doSth(T param) {
}
};
Foo<int> f1;
Foo<float>f2;
- 编译器发现要用到
Foo<int>和Foo<float>,这时就会为每个泛型生成一份执行代码,相当于新创建如下两个类,这样做是方便了代码编写,编译器自动创建出了两个类,但是当多次使用不同类型的模板的时候,就会创建很多新的类,导致代码膨胀。
struct FooInt
{
int bar;
void doSth(int param) {
}
};
struct FooFloat
{
float bar;
void doSth(float param){
}
};
- 然而Java为了使程序运行效率不受到影响,在编译源java文件后生成的class文件中不带有泛型信息,这个过程称之为“泛型擦除”。比如有如下代码:
public class Foo<T> {
T bar;
void doSth(T param) {
}
};
Foo<String> f1;
Foo<Integer> f2;
- 在编译的时候并不会创建多份执行代码,在编译后的字节码文件中,会把泛型的信息抹除,如下:
public class Foo {
Object bar;
void doSth(Object param){
}
};
-
也就是说,代码中的
Foo<String>和Foo<integer>,使用的类经过编译后,都是同一个类,其实泛型技术实际上就是Java语言的一颗语法糖,泛型经过编译处理之后就被擦除了,编译器根本不认识泛型,所以这也就是为什么java的泛型被人说是假泛型。 -
泛型擦除这一点应用在兼容老版本上,因为JDK1.5之前没有泛型,当把带有泛型特性的集合赋值给老版本的集合时候,就会把泛型擦除掉。