java 泛型generics

参考
为什么使用泛型
JAVA泛型总结
Java泛型:泛型类、泛型接口和泛型方法
java泛型应用实例 - 自定义泛型类,方法

泛型的本质:数据类型参数化

为什么使用泛型

我们先看下面的代码,代码省略了一些内容,但功能是实现一个栈,这个栈只能处理int数据类型:

public class Stack{
        private int[] m_item;
        public int Pop(){...}
        public void Push(int item){...}
        public Stack(int i)
        {
            this.m_item = new int[i];
        }
}

上面代码运行的很好,但是,当我们需要一个栈来保存string类型时,该怎么办呢?很多人都会想到把上面的代码复制一份,把int改成string不就行了。当然,这样做本身是没有任何问题的,但一个优秀的程序是不会这样做的,因为他想到若以后再需要long、Node类型的栈该怎样做呢?还要再复制吗?优秀的程序员会想到用一个通用的数据类型object来实现这个栈:

public class Stack{
        private object[] m_item;
        public object Pop(){...}
        public void Push(object item){...}
        public Stack(int i)
        {
            this.m_item = new[i];
        }
    }

这个栈写的不错,他非常灵活,可以接收任何数据类型,可以说是一劳永逸。但全面地讲,也不是没有缺陷的,主要表现在:
当Stack处理值类型时,会出现装箱、折箱操作,这将在托管堆上分配和回收大量的变量,若数据量大,则性能损失非常严重。
在处理引用类型时,虽然没有装箱和折箱操作,但将用到数据类型的强制转换操作,增加处理器的负担。
在数据类型的强制转换上还有更严重的问题(假设stack是Stack的一个实例):

Node1 x = new Node1();
stack.Push(x);
Node2 y = (Node2)stack.Pop();

上面的代码在编译时是完全没问题的,但由于Push了一个Node1类型的数据,但在Pop时却要求转换为Node2类型,这将出现程序运行时的类型转换异常,但却逃离了编译器的检查。
泛型用用一个通过的数据类型T来代替object,在类实例化时指定T的类型,运行时(Runtime)自动编译为本地代码,运行效率和代码质量都有很大提高,并且保证数据类型安全。

使用泛型

下面是用泛型来重写上面的栈,用一个通用的数据类型T来作为一个占位符,等待在实例化时用一个实际的类型来代替。让我们来看看泛型的威力:

public class Stack
    {
        private T[] m_item;
        public T Pop(){...}
        public void Push(T item){...}
        public Stack(int i)
        {
            this.m_item = new T[i];
        }
}

类的写法不变,只是引入了通用数据类型T就可以适用于任何数据类型,并且类型安全的。这个类的调用方法:

//实例化只能保存int类型的类
Stack a = new Stack(100);
a.Push(10);
a.Push("8888"); //这一行编译不通过,因为类a只接收int类型的数据
int x = a.Pop();
//实例化只能保存string类型的类
Stack b = new Stack(100);
b.Push(10);    //这一行编译不通过,因为类b只接收string类型的数据
b.Push("8888");
string y = b.Pop();

这个类和object实现的类有截然不同的区别

  • 他是类型安全的。实例化了int类型的栈,就不能处理string类型的数据,其他数据类型也一样。
  • 无需装箱和折箱。这个类在实例化时,按照所传入的数据类型生成本地代码,本地代码数据类型已确定,所以无需装箱和折箱。
  • 无需类型转换。
泛型类型

C#泛型类在编译时,先生成中间代码IL,通用类型T只是一个占位符。在实例化类时,根据用户指定的数据类型代替T并由即时编译器(JIT)生成本地代码,这个本地代码中已经使用了实际的数据类型,等同于用实际类型写的类,所以不同的封闭类的本地代码是不一样的。按照这个原理,我们可以这样认为:
泛型类的不同的封闭类是分别不同的数据类型。
例:Stack和Stack是两个完全没有任何关系的类,你可以把他看成类A和类B,这个解释对泛型类的静态成员的理解有很大帮助。

在JAVA中,对于不同传入的类型实参,生成的相应对象实例的类型是不是一样的呢?

 public class GenericTest {
     public static void main(String[] args) {
         Box name = new Box("corn");
         Box age = new Box(712);
         System.out.println("name class:" + name.getClass()); 
         System.out.println("age class:" + age.getClass());
         System.out.println(name.getClass() == age.getClass());// true
     }
}

由此,我们发现,在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型(本实例中为Box),当然,在逻辑上我们可以理解成多个不同的泛型类型。
究其原因,在于Java中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

类型通配符

我们需要一个在逻辑上可以用来表示同时是Box和Box的父类的一个引用类型,由此,类型通配符应运而生。
类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且Box在逻辑上是Box、Box...等所有Box<具体类型实参>的父类。由此,我们依然可以定义泛型方法,来完成此类需求。

public class GenericTest {
     public static void main(String[] args) { 
         Box name = new Box("corn");
         Box age = new Box(712);
         Box number = new Box(314);
         getData(name);
         getData(age);
         getData(number);
     }
     public static void getData(Box data) {
        System.out.println("data :" + data.getData());
     }
 }

有时候,我们还可能听到类型通配符上限和类型通配符下限。具体有是怎么样的呢?
在上面的例子中,如果需要定义一个功能类似于getData()的方法,但对类型实参又有进一步的限制:只能是Number类及其子类。此时,需要用到类型通配符上限。

 public class GenericTest {
     public static void main(String[] args) {
         Box name = new Box("corn");
         Box age = new Box(712);
         Box number = new Box(314);
         getData(name);
         getData(age);
         getData(number); 
         //getUpperNumberData(name);//1
         getUpperNumberData(age);//2
         getUpperNumberData(number);//3
    }
    public static void getData(Box data) {
         System.out.println("data :" + data.getData());
     }    
     public static void getUpperNumberData(Box data){
        System.out.println("data :" + data.getData());
     }
 }

此时,显然,在代码//1处调用将出现错误提示,而//2 //3处调用正常。
类型通配符上限通过形如Box形式定义,相对应的,类型通配符下限为Box形式,其含义与类型通配符上限正好相反

你可能感兴趣的:(java 泛型generics)