理解Java泛型与应用

95 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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之前没有泛型,当把带有泛型特性的集合赋值给老版本的集合时候,就会把泛型擦除掉。