Java进阶知识1:泛型

前言:自JDK 1.5 之后,Java 引入泛型解决了集合容器类型安全这一问题。泛型在Java中有很重要的地位,在集合容器及各种设计模式中有非常广泛的应用。一般的类和方法,只能使用具体的类型,要么是基本类型,要么是自定义的类。如果要编写可以应用多种类型的代码,这种刻板的限制对代码得束缚会就会很大。——《Thinking in Java》

一、泛型的概念

泛型就是参数化类型,把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型。

1、一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用调用时传入具体的类型(类型实参)。

  • 泛型的本质是为了参数化类型把类型当作是参数一样传递。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,然后泛型在实际调用时指定具体类型。

  • 泛型归根到底就是“模版”或"广泛的类型",适用于多种数据类型执行相同的代码(代码更通用化)。

  • 这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

2、在 Java 中以 "" 的形式呈现,"<>" 中写引用数据类型

  • 泛型中的泛指代表T可用任意字母替代

      通常我们使用  T : Type   E : Element   K : Key   V : Value   R : Return

  • 可以代表任意类型的数据类型,可以限制集合中存放元素的类型

    相关术语:

  • ArrayList中的E称为类型参数变量

  • ArrayList中的Integer称为实际类型参数

  • 整个称为ArrayList泛型类型

  • 整个ArrayList称为参数化的类型ParameterizedType

3、泛型擦除

泛型是提供给javac编译器使用的,它用于限定集合的输入类型,让编译器在源代码级别上,即挡住向集合中插入非法数据。但编译器编译完带有泛形的Java程序后,生成的class文件中将不再带有泛形信息,以此使程序运行效率不受到影响,这个过程称之为“擦除”

二、为什么使用泛型

1、我们在操作数据时,有时不知道要操作的数据是什么类型。

2、若集合中不使用泛型,意味着集合中可以存储任意类型的对象,若需要具体到某一个数据类型时,需要强制类型转换,可能引发ClassCastException

Java 5.0以前是使用Object来代表任意类型的,但是向下转型有强制转换的问题,这样程序就不太安全。

首先,我们来试想一下:没有泛型,集合会怎么样

  • Collection、Map集合对元素的类型是没有任何限制的。本来我的Collection集合装载的是全部的Dog对象,但是外边把Cat对象存储到集合中,是没有任何语法错误的,编译能通过。

  • 把对象扔进集合中,集合是不知道元素的类型是什么的,仅仅知道是Object。因此在get()的时候,返回的是Object。外边获取该对象,还需要强制转换,容易忽略导致出现ClassCastException异常。

有了泛型以后:

  • 代码更加简洁【不用强制转换】

  • 程序更加健壮【泛型提供编译检查,只要编译时期没有警告,那么运行时期就不会出现ClassCastException异常】

  • 可读性和稳定性【在编写集合的时候,就限定了类型】

//集合中泛型的相关使用:在创建集合的时候,我们明确了List集合的类型
List list=new ArrayList<>();
list.add("hello");//add()只能添加String
list.add(10);//泛型提供编译检查,compile不通过

三、泛型的作用

1、使用泛型能写出更加灵活通用的代码

泛型的设计主要参照了C++的模板,旨在能让人写出更加通用化,更加灵活的代码。模板/泛型代码,就好像做雕塑时的模板,有了模板,需要生产的时候就只管向里面注入具体的材料就行,不同的材料可以产生不同的效果,这便是泛型最初的设计宗旨。

2、 泛型将代码安全性检查提前到编译期

泛型被加入Java语法中,还有一个最大的原因:解决容器的类型安全,使用泛型后,能让编译器在编译的时候借助传入的类型参数检查对容器的插入,获取操作是否合法,从而将运行时ClassCastException转移到编译时。

举个栗子:

List dogs =new ArrayList();
dogs.add(new Dog());
dogs.add(new Cat());

在没有泛型之前,这种代码除非运行,否则你永远找不到它的错误ClassCastException。但是加入泛型后:

List dogs=new ArrayList<>();//限制数据类型为一个,只作编译时限制,当执行时不作读取。通过查看.class文件,<>是不存在的
dogs.add(new Cat());//Error Compile

会在编译的时候就检查出来。

3、泛型能够省去类型强制转换

在JDK1.5之前,Java容器都是通过将类型向上转型为Object类型来实现的,因此在从容器中取出来的时候需要手动的强制转换。

Dog dog=(Dog)dogs.get(1);

加入泛型后,由于编译器知道了具体的类型,因此编译期就会限定集合的输入类型,这样得到的都是合法类型,不用强制转换了,使得代码更加优雅。

四、泛型有三种使用方式:泛型类、泛型接口、泛型方法

a、何时定义:当我们在写时不确定以后要用的是什么类型,但知道肯定要有这个元素
b、何时确定:使用时确定

1、泛型类

泛型类就是把泛型定义在类上,用户使用该类的时候,才把类型明确下来。这样的话,用户明确了什么类型,该类就代表着什么类型。用户在使用的时候就不用担心忽略强制转换导致运行时的ClassCastException。

/*
    1:把泛型定义在类上
    2:类型变量定义在类上,方法中也可以使用
 */
public class ObjectTool {
    private T obj;

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }
}

用户想要使用哪种类型,就在创建的时候指定类型。使用的时候,该类就会自动转换成用户想要使用的类型:

   public static void main(String[] args)
   {
        //创建对象并指定元素类型
        ObjectTool tool = new ObjectTool<>();

        tool.setObj(new String("建国70周年快乐"));
        String s = tool.getObj();
        System.out.println(s);

        //创建对象并指定元素类型
        ObjectTool objectTool = new ObjectTool<>();
        /**
         * 如果我在这个对象里传入的是String类型的,它在编译时期就通过不了了.
         */
        objectTool.setObj(10);
        int i = objectTool.getObj();
        System.out.println(i);
    }

2、泛型方法

前面已经介绍了泛型类,在类上定义的泛型,在方法中也可以使用.....现在,我们可能就仅仅在某一个方法上需要使用泛型.,外界仅仅是关心该方法,不关心类其他的属性。这样的话,我们在整个类上定义泛型,未免就有些大题小作了。

当写方法时返回值不确定,就可以定义泛型方法。泛型是先定义后使用的:

    //定义泛型方法
    public  void show(T t) 
    {
        System.out.println(t);
        return t;
    }

用户传递进来的是什么类型,返回值就是什么类型

    public static void main(String[] args) {
        //创建对象
        ObjectTool tool = new ObjectTool();

        //调用方法,传入的参数是什么类型,返回值就是什么类型
        String str = tool.show("hello");
        Interger int = tool.show(12);
        tool.show(12.5);
    }

3、泛型接口

前面我们已经定义了泛型类,泛型类是拥有泛型这个特性的类,它本质上还是一个Java类,那么它就可以被继承

那它是怎么被继承的呢?这里分两种情况:

  • 子类明确泛型类的类型参数变量

  • 子类不明确泛型类的类型参数变量


/*
 * 把泛型定义在接口上
 */
public interface Inter 
{
    public abstract void show(T t);
}


/**
 * 1、子类明确泛型接口的类型参数变量
 */

public class InterImpl implements Inter 
{
    @Override
    public void show(String s) 
    {
        System.out.println(s);

    }
}


/**
 * 2、子类不明确泛型类的类型参数变量:
 *    实现类也要定义出类型的
 *
 */
public class InterImpl implements Inter
 {

    @Override
    public void show(T t) {
        System.out.println(t);

    }
}

五、泛型通配符

Java中,容器里装的东西之间有继承关系,但容器之间是没有继承关系的。所以我们不可以把Plate的引用传递给Plate。Java 泛型通配符的出现是为了使 Java 泛型也支持向上转型,从而保持 Java 语言向上转型概念的统一。但与此同时,也导致 Java 通配符出现了一些缺陷,使得其有特定的使用场景。

Java泛型中的通配符机制的目的:让一个持有特定类型(如A类型)的集合能够强制转换为持有A的子类或父类型的集合!