浅笑博客
浅笑博客
Java进阶学习——泛型_经验之谈
Java进阶学习——泛型_经验之谈

引言

本文根据《Java语言程序设计 进阶篇 》(Y.Daniel Liang)进行学习总结编写。
本文章为原创性文章,如有转载请注明出处,尊重原创,谢谢。

概述

泛型(generic)是指参数化类型的能力。即将类、接口或方法定义为泛型的,然后编译时用具体的类型替换。

jdk1.5开始,允许定义泛型。

为什么使用泛型

1.使代码能够在编译时就能检测出错误,而不是运行时。

例:

在jdk1.5之前Comparable接口如下:

public interface Comparable{
     public int compareTo(Object o);
 }
Comparable c = new Date();
System.out.println(c.compareTo("red"));

jdk1.5中 Comparable接口被定义为泛型接口:

public interface Comparable<T>{
    public int compareTo(T o);
}
Comparable<Date> c = new Date();
System.out.println(c.compareTo("red"));

如上左(jdk1.5前/不使用泛型),申明一个引用对象 Comparable,指向一个Date类,然后使用它与”red”的String比较,String也属于Object,所以编译器认为没错,但在执行的时候会报错,因为Date对象和String不能进行比较。

而上右(使用了泛型),将引用对象的类型定义为Comparable<Date>,然后Date与String进行比较,代码将产生编译错误,因为接口中定义的compareTo传进去的参数为Date类。

从而可见,泛型使程序更加可靠。

2.使代码更优雅。通过使用泛型,不仅优化了代码,提高了代码的可靠性和可读性,而且使你的代码更加高端,更加大气,更加优雅。

一个简单的泛型类例子(实现泛型堆栈)

注意: 定义一个类为泛型类型,要将泛型类型放在类名之后。

import java.util.ArrayList;

public class GenericStack<E> {
    private ArrayList<E> list;

    public GenericStack() {
        this(new ArrayList<>());
    }

    public GenericStack(ArrayList<E> list) {
        this.list = list;
    }

    public int getSize(){
        return list.size();
    }

    public E peek(){
        return list.get(getSize()-1);
    }

    public void push(E o){
        list.add(o);
    }

    public E pop(){
        E o = list.get(getSize()-1);
        list.remove(getSize()-1);
        return o;
    }

    public boolean isEmpty(){
        return list.isEmpty();
    }

    public void print(){
        for(E o:list){
            System.out.print(o+" ");
        }
        System.out.println();
    }

    public GenericStack<E> copy(){
        ArrayList<E> list2 = new ArrayList<>(list);
        return new GenericStack<>(list2);
    }
}

对上面类的使用1:

GenericStack<String> genericStack = new GenericStack<>();
genericStack.push("1");
genericStack.push("2");
genericStack.push("3");
genericStack.print();

输出:

1 2 3 

对上面类的使用2:

GenericStack<Integer> genericStack2 = new GenericStack<>();
genericStack2.push(4);
genericStack2.push(5);
genericStack2.push(6);
genericStack2.print();

输出:

4 5 6 

通过上面的例子,大家应该大概对泛型有个简单的了解。需要注意的是 实例化泛型类对象时,后面的类型可以省略。如上面的

GenericStack<String> genericStack = new GenericStack<>();

即为GenericStack<String> genericStack = new GenericStack<String>();的简化。

简单泛型方法举例

注意: 定义一个方法为泛型类型,要将泛型类型放在<>中放在方法返回类型之前。

public class A{
    public static <E> void  print(E[] list){
        for (int i = 0; i < list.length; i++) {
            System.out.print(list[i]+" ");
        }
        System.out.println();
    }
}

使用(注意在调用时需要将实际类型放在<>中作为方法名的前缀):

但实际编程中发现,前缀也可以省略,因为编译器可以根据形式参数推断出来。

String[] strings = new String[]{"11","22","33"};
A.<String>print(strings);//A.print(strings);
Integer[] integers = new Integer[]{111,222,333};
A.<Integer>print(integers);//A.print(integers);

输出:

11 22 33 
111 222 333 

 受限的泛型类型

可以将泛型指定为另一种类型的子类型,这样的泛型类型称为受限的(bounded)泛型类型。可以通过下面的例子进行进一步的理解。

/**
 * 受限的泛型类型
 */
public class BoundedGeneric {
    public static void main(String[] args){
        Rectangle rectangle = new Rectangle(2,3);
        Circle circle = new Circle(1.5f);
        System.out.println("equalArea : " + rectangle.equalArea(circle));
    }
}

/**
 * 几何对象
 */
abstract class BaseGeometricObject {
    /**
     * 获取面积
     * @return 面积
     */
    abstract float getArea();

    /**
     * 比较面积是否相等
     * @param geometricObject 比较对象
     * @param <E> 受限的
     * @return 是否相等
     */
    <E extends BaseGeometricObject> boolean equalArea(E geometricObject){
        return getArea()==geometricObject.getArea();
    }
}

/**
 * 矩形
 */
class Rectangle extends BaseGeometricObject {
    private float width;
    private float height;

    Rectangle(float width, float height) {
        this.width = width;
        this.height = height;
    }

    @Override
    float getArea() {
        return width*height;
    }

    public float getWidth() {
        return width;
    }

    public void setWidth(float width) {
        this.width = width;
    }

    public float getHeight() {
        return height;
    }

    public void setHeight(float height) {
        this.height = height;
    }
}

/**
 * 圆
 */
class Circle extends BaseGeometricObject {
    private float radius;
    private final float PI = 3.1415926f;

    Circle(float radius) {
        this.radius = radius;
    }

    @Override
    float getArea() {
        return PI*radius*radius;
    }
}

输出:

equalArea : false

代码上也进行了简单的注释,程序主要功能应该都能理解,比较矩形和圆的面积是否相等。矩形和圆类继承了抽象父类几何对象类,在抽象父类几何对象类中的equalArea函数为泛型函数。受限的泛型类型<E extends BaseGeometricObject>将E指定为BaseGeometricObject的泛型子类型。因此需要传递E( BaseGeometricObject的泛型子类型 )的实例来调用 equalArea 。

通配泛型类型

什么是泛型通配?为什么要用泛型通配,可以通过下面这个例子进行理解。

public class B{
    public static void main(String[] args){
        GenericStack<Integer> genericStack = new GenericStack<>();
        genericStack.push(4);
        genericStack.push(5);
        genericStack.push(6);
        System.out.println("maximun = " + max(genericStack));
    }

    private static double max(GenericStack<Number> mystack){
        GenericStack<Number> stack = mystack.copy();
        double max = stack.pop().doubleValue();
        while (!stack.isEmpty()){
            double value = stack.pop().doubleValue();
            max = value>max?value:max;
        }
        return max;
    }
}

该例中max方法功能为找出数字栈中的maximum,main方法定义了一个整数对象栈并使用max方法查找 maximum 。看起来这段代码没有问题,实际上却编译报错。报错为System.out.println(“maximun = ” + max(genericStack));这句,实际为max方法的调用出错,因为genericStack不是GenericStack<Number>的实例,而是 GenericStack<Integer>的实例 ,而max方法接受的被指定为GenericStack <Number>的实例 。

虽然Integer是Number的子类,但不能说 GenericStack<Integer> 就是 GenericStack<Number> 的子类。因此为了解决这个问题,可以使用通配泛型类型。

通配泛型类型有3种形式: ? 、 ? extends T或者 ? super T,其中T是某个泛型类型。

第一种形式 ? 称为非受限通配,等价于? extends Object。

第二种形式? extends T称为受限通配,表示T或者T的一个未知子类型。

第三中形式? super T称为下限通配,表示T或者T的一个未知父类型。

因此将上述程序改为如下:

public class B{
    public static void main(String[] args){
        GenericStack<Integer> genericStack = new GenericStack<>();
        genericStack.push(4);
        genericStack.push(5);
        genericStack.push(6);
        System.out.println("maximun = " + max(genericStack));
    }

    private static double max(GenericStack<? extends Number> mystack){
        GenericStack<? extends Number> stack = mystack.copy();
        double max = stack.pop().doubleValue();
        while (!stack.isEmpty()){
            double value = stack.pop().doubleValue();
            max = value>max?value:max;
        }
        return max;
    }
}

<? extends Number>是一个表示Number或Number的子类型的通配类型,因此调用max(GenericStack<Integer>)是合法的。我们再看下面这个例子:

public class C{
    public static void main(String[] args){
        GenericStack<String> genericStack = new GenericStack<>();
        genericStack.push("1");
        genericStack.push("2");
        genericStack.push("3");
        GenericStack<Integer> genericStack2 = new GenericStack<>();
        genericStack2.push(4);
        genericStack2.push(5);
        genericStack2.push(6);
        popprint(genericStack);
        popprint(genericStack2);
    }

    private static void popprint(GenericStack<?> stack){
        while(!stack.isEmpty()){
            System.out.print(stack.pop()+" ");
        }
        System.out.println();
    }
}

输出:

3 2 1 
6 5 4 

上面刚刚说了,?是泛型通配的第一种形式。<?>是一个通配符,表示任何一种对象类型,等价于<? extends Object>。因此,上面的代码在调用popprint(GenericStack<String>)和 popprint(GenericStack<Integer>) 时都能正确执行,因为 String 和 Integer 都是Object的子类。

说完了?和? extends T,那什么时候需要? super T呢?我们看下面的例子:

public class D{
    public static void main(String[] args){
        GenericStack<String> genericStack = new GenericStack<>();
        genericStack.push("1");
        genericStack.push("2");
        genericStack.push("3");
        GenericStack<Object> genericStack3 = new GenericStack<>();
        genericStack3.push("qianxiao");
        genericStack3.push(123);
        add(genericStack,genericStack3);
        genericStack3.print();
    }
    
    private static <T> void add(GenericStack<T> stack1,GenericStack<? super T> stack2){
        while (!stack1.isEmpty()){
            stack2.push(stack1.pop());
        }
    }
}

输出:

qianxiao 123 3 2 1 

该例定义了一个泛型方法add,main函数调用其实现将genericStack字符串栈中的字符串依次出栈并添加进genericStack3的对象栈中。add方法中<? super T>表示T或者T的父类型的通配类型,调用是T被替换为String,则第一个参数正常匹配,第二个参数则为GenericStack<? super String>, 因为Object是String的父类,所以GenericStack<Object> 也可以匹配成功,程序能够正确执行。

 泛型类的原始类型

原始类型即使用泛型类的时候可以无需指定具体类型,如:

GenericStack genericStack1 = new GenericStack();

书上说的上句可以大体理解为:

GenericStack<Object> genericStack3 = new GenericStack<>();

可以从上面class D的例子中证明,将

GenericStack<Object> genericStack3 = new GenericStack<>();

更换为

GenericStack genericStack3 = new GenericStack();

public class D{
    public static void main(String[] args){
        GenericStack<String> genericStack = new GenericStack<>();
        genericStack.push("1");
        genericStack.push("2");
        genericStack.push("3");
        GenericStack genericStack3 = new GenericStack();//更改了此句
        genericStack3.push("qianxiao");
        genericStack3.push(123);
        add(genericStack,genericStack3);
        genericStack3.print();
    }
    
    private static <T> void add(GenericStack<T> stack1,GenericStack<? super T> stack2){
        while (!stack1.isEmpty()){
            stack2.push(stack1.pop());
        }
    }
}

程序仍能正常编译运行,输出结果:qianxiao 123 3 2 1 。

但容易发现编辑器会提示(建议)我们对GenericStack添加泛型参数。 关于泛型的原始类型,在实际开发过程中其实是不推荐使用的,建议是泛型具体参数化传入。

但在另一个例子中,原始类型又可以理解为通配类型,即<?>,也即<? extends Object>,这就是上面的class C例中,如果我们将popprint方法的形式参数类型由GenericStack<?>改为GenericStack后发现,这里程序仍然能够正常执行。

public class C{
    public static void main(String[] args){
        GenericStack<String> genericStack = new GenericStack<>();
        genericStack.push("1");
        genericStack.push("2");
        genericStack.push("3");
        GenericStack<Integer> genericStack2 = new GenericStack<>();
        genericStack2.push(4);
        genericStack2.push(5);
        genericStack2.push(6);
        popprint(genericStack);
        popprint(genericStack2);
    }

    private static void popprint(GenericStack stack){
        while(!stack.isEmpty()){
            System.out.print(stack.pop()+" ");
        }
        System.out.println();
    }
}

但若改为 GenericStack <Object>则无法执行,因为GenericStack<Integer>不是GenericStack< Object >的子类。

因此我的个人理解,原始类型在作形式参数时,可以理解为通配类型,而在做实例对象类型时可理解为Object的泛型类型。不过,我也还是建议大家尽量不要使用泛型原始类型,使用时把 泛型具体参数化传入。因为直接使用原始类型是不安全的。

我们可以看下面的这个例子来理解为什么使用原始类型使不安全的。

public class Max{
    public static void main(String[] args){
        System.out.println(max("qianxiao",123));
    }

    public static Comparable max(Comparable c1,Comparable c2){
        return c1.compareTo(c2)>0?c1:c2;
    }
}

上述的代码会引起一个运行时错误,但在代码编辑器中不会被检测出错误。因为 String和Integer都是Object 的子类,还有刚才说的形式参数的原始类型可以理解为通配类型。但为什么在运行时错误呢?因为字符串和整数对象无法进行比较。

http://blog.qianxiao.fun/wp-content/uploads/2020/03/图片.png

一个更好的max方法的实现应该使用泛型类型,如下例:

public class Max{
    public static void main(String[] args){
        System.out.println(max("qianxiao",123));//参数报红
    }

    public static <E extends Comparable<E>> E max(E c1,E c2){
        return c1.compareTo(c2)>0?c1:c2;
    }
}

因为max函数需要传入的2个对象必须是同一类型的,而且必须时Comparable<E>的子类型。更明确的说,是需要2个实现了 Comparable<E> 接口的的相同E类,因为String是实现了 Comparable<String> ,Integer是实现了 Comparable<Integer>,所以String和Integer不属于实现了同意接口( Comparable<E> )的类。

通配类型和原始类型使用注意事项

不知道大家有没有发现,<?>和<? extends T>的泛型类型只能从中取值,不能进行设值。具体大家可以看下面的这个例子,使用的是最开始举的GenericStack泛型类:

public class ContrastDemo {
    public static void main(String[] args) {
        GenericStack genericStack = new GenericStack();
        genericStack.push(1);
        genericStack.push("2");
        genericStack.push(new Object());
        //String s = genericStack.peek();//报红
        //int i = genericStack.peek();//报红
        //原因:原始类型在实例化对象时可以理解为Objectd的泛型类,genericStack为Object对象栈,而非String对象栈或Integer对象栈
        Object o = genericStack.peek();
        genericStack.print();

        GenericStack<Object> genericStack1 = new GenericStack<>();
        genericStack1.push(1);
        genericStack1.push("2");
        genericStack1.push(new Object());
        //String s1 = genericStack.peek();//报红
        //int i1 = genericStack.peek();//报红
        //原因:同上
        Object o1 = genericStack.peek();
        genericStack.print();

        GenericStack<?> genericStack2 = new GenericStack<>();
        //或GenericStack<? extends Object> genericStack2 = new GenericStack<>();
        //genericStack2.push(1);//报红
        //genericStack2.push("2");//报红
        //genericStack2.push(new Object());//报红
        //原因:genericStack2栈不能往进去放任何类型,原因下文中有介绍
        //String s2 = genericStack2.peek();//报红
        //int i2 = genericStack2.peek();//报红
        //原因:取值的这里就比较好理解了,因为取出来的可以是Integer,也可以是String,也可以是Object,所以不能直接赋值给子类型,只能以Object形式取出。
        Object o2 = genericStack2.peek();
        genericStack2.print();

        GenericStack<? extends B> genericStack3 = new GenericStack<>();
        //genericStack3.push(new B());//报红
        //genericStack3.push(new C1());//报红
        //genericStack3.push(new C2());//报红
        //genericStack3.push(new A());//报红
        //原因:和上面类似
        //String s3 = genericStack3.peek();//报红
        //int i3 = genericStack3.peek();//报红
        Object o3 = genericStack3.peek();
        genericStack3.print();

        GenericStack<? super B> genericStack4 = new GenericStack<>();
        //genericStack4 的泛型可以是B,也可以是A,所以就可以放入B,也可以放B的子类
        genericStack4.push(new B());
        genericStack4.push(new C1());
        genericStack4.push(new C2());
        //但是不能放B的父类A,具体原因我也不太清楚
        //genericStack4.push(new A());//报红
        //String s4 = genericStack4.peek();//报红
        //int i4 = genericStack4.peek();//报红
        Object o41 = genericStack4.peek();
        System.out.println("o41 = " + o41);
        B o42 = (B) genericStack4.peek();
        System.out.println("o42 = " + o42);
        genericStack4.print();
        A o43 = (B) genericStack4.peek();
        System.out.println("o43 = " + o43);
        GenericStack<B> genericStack5 = (GenericStack<B>) genericStack4.copy();
        genericStack5.print();
    }
}
class A{
    @Override
    public String toString() {
        return "A";
    }
}
class B extends A{
    @Override
    public String toString() {
        return "B";
    }
}
class C1 extends B{
    @Override
    public String toString() {
        return "C1";
    }
}
class C2 extends B{
    @Override
    public String toString() {
        return "C2";
    }
}

首先看实例化对象genericStack,它是一个GenericStack的原始类型,原始类型在实例化对象时可以理解为Objectd的泛型类,所以genericStack为Object对象栈,而非String对象栈或Integer对象栈,所以取出的值不能直接转化为String和Integer,但一定能转成Object。

第二个对象 genericStack1 和第一个一样。

再看第三个实例化对象 genericStack2 ,它是由GenericStack<?>实例化,可以看到不能给它中放任何类型,正是因为 genericStack2 的泛型<?>,即genericStack2栈为Object或者其子类的栈,这里特别注意不是Object的栈,也不是Object子类的栈,而是Object或Object其子类的栈,大家可以多多体会一下。 Integer、String单个都不能代表为任意类型。或者可以说,编译器认为它需要的参数为String,你传个Integer就不行,同理编译器认为它需要的参数为Integer,你传个String或Object就不行。这里确实不太容易理解,大家可以就这样记住就行。再说一般这种泛型通常只会在方法的形参类型中用到。

第四个对象genericStack3 和第3个类似, genericStack3栈为B类或者B子类的栈。

第五个对象 genericStack4 由GenericStack<? super B>实例化,这表示这是一个B泛型或者一个B父类泛型, genericStack4 的泛型可能是B,也可能是A。可以向 genericStack4 中放B类以及B类的子类,但不能放A类,这一点我也不是很理解,有机会请教别人后会再来补充。在取得时候就,取出来的时候默认为Object,可以将其强制转化为B以及A,也可以使用 GenericStack 类中的copy函数进行复制,得到的 GenericStack<? super B> 可以以强制转换为 GenericStack<B> 或者 GenericStack<A> 。

对泛型的限制

1.不管实际的具体类型是什么,泛型类是被它的所有实例所共享的,可以参看写下面的代码进行理解:

ArrayList<String> stringList = new ArrayList<>();
ArrayList<Integer> integerList = new ArrayList<>();
System.out.println(stringList instanceof ArrayList);
System.out.println(integerList instanceof ArrayList);

输出:

true
true

尽管在编译时ArrayList<String>和 ArrayList<Integer> 是两种类型,但在运行时只有一个 ArrayList 被加载到JVM。即stringList 和integerList 都是 ArrayList 的实例。

2.不能使用new E(),即不能使用泛型类型参数创建实例。

例:

E object = new E();//错

因为运行时new E()中泛型类型E是不可用的。

3.不能使用new E[],即不能使用泛型类型参数创建数组。

例:

E[] es = new E[MAXN];//错

可以通过创建一个Object类型的数组,然后再转换成E[]来规避。如下,但将会导致一个编译警告,因为编译器无法确保运行时转换成功。

E[] es = (E[])new Object[MAXN];//警告

再例:

ArrayList<String>[] stringlists = new ArrayList<>[10];//错

规避:

ArrayList<String>[] stringlists = (ArrayList<String>[])new ArrayList[10];

4.在静态环境下不允许类的参数是泛型类型

什么时 静态环境 ? 静态环境 是指静态方法、静态变量、静态初始化语句块。具体什么意思呢,如下例就是非法的:

public class Test<E>{
    public static E o;
    
    static{
        E p;
    }
    
    public static void m(E o){
        //TODO
    }
}

为什么会是非法?刚才说了,泛型类的所有实例都有相同的运行时类,所有泛型类的静态变量和方法是被它所有实例所共享的。因此,在静态环境为了类而引用泛型类型时非法的。

5.异常类不能时泛型的

即如下代码时非法的:

public class MyException<T> extends Exception{

}

泛型的应用

泛型的应用其实很广泛,在我们需要实现的类中如果需要有可变类型的变量,把类设计为泛型的即可。以下简单根据个人的经验,谈谈泛型的应用。

1.Android开发中原生的findViewById(int)方法,我在用Android6.0(sdk23)开发时, findViewById 为先获取view,再转换类型, 使用起来比较麻烦, 所以通常我会在基类(抽象父类)中会写f泛型方法方便获取控件:

@SuppressWarnings("unchecked")
protected <E> E f(int id) {
	return (E) findViewById(id);
}

该方法被定义为泛型,返回对象即为泛型类型参数,findViewById方法原本返回为View,在f函数返回时被强制转换成 泛型类型 。因此,获取控件如:

TextView textView = f(R.id.…);

后来的Android版本也将 findViewById 方法定义为了泛型类,也有了一些第三方库进行控件的绑定。

2.Android开发架构MVP模式中的应用。

将所有的Presenter继承一个抽象父类BasePresenter,将 BasePresenter 实现为泛型类,泛型类型参数T为view层的接口,当然也可以选择把view层(Activity)也泛化成E。如下:

public abstract class BasePresenter<T,E> {
    public T mView;
    public E aView;

    public BasePresenter<T,E> attch(T mView) {
        this.mView = mView;
        return this;
    }

    public BasePresenter<T,E> onTakeView(E aView){
        this.aView = aView;
        return this;
    }
}

这样view层 (Activity) 实现view层接口,在初始化Presenter时,使用:

new MainPresenter(context).attch(this).onTakeView(this);

即可将view层和view接口层都传入 Presenter 层,分别 Presenter 层 的一些事件处理。同理 view层的基类BaseActivity也可以写成泛型类,将 Presenter 泛化,在 view层继承其时传入。

关于Android开发MVP架构的设计,我有时间会出一篇文章进行具体的介绍和分享。

结语

以上为本人对泛型的学习经验总结,如果有什么地方有误,敬请指正。欢迎评论指点。

本文章为原创性文章,如有转载请注明出处,尊重原创,谢谢。

发表评论

textsms
account_circle
email

浅笑博客

Java进阶学习——泛型_经验之谈
引言 本文根据《Java语言程序设计 进阶篇 》(Y.Daniel Liang)进行学习总结编写。 本文章为原创性文章,如有转载请注明出处,尊重原创,谢谢。 概述 泛型(generic)是指参数…
扫描二维码继续阅读
2020-03-27