Java泛型到底是什么

Java泛型到底是什么

前言

有一位朋友给我说在她阅读Java内置容器的时候,泛型给她造成了些困扰,想让我当面给她讲讲。我觉得写篇文章效果更好,她想看几遍看几遍。

为什么要学习泛型

在Java中编写通用容器的时候需要使用到大量的泛型类,泛型接口和泛型方法。而这些也正是Java内库容器List, Set,Map能够处理各种类型元素的基础,缺乏这方面的知识对阅读Javan中的容器代码会有些障碍。

Java为什么需要泛型

在泛型引入前,程序员在使用一个通用容器的时候需要来处理下面两个事情。

  • 需要想清楚要向容器中放入什么元素
  • 需要对从容器中取出的元素进行手动类型转换

实例代码

public class Fruit {}
public class Apple extends Fruit {
    public void functionInApple() {}
}
public class Orange extends Fruit {
    public void functionInOrange() {}
}
public class FujiApple extends Apple {}
public class ObjectHolder {

    private Object object;

    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }

}
public class ObjectHolderTest {

    public static void main(String[] args) {

        ObjectHolder holder = new ObjectHolder();
        
        // 需要想清楚要向容器中放入什么元素
        // 向holder容器中放入Apple
        holder.setObject(new Apple());
        
        // 需要对从容器中取出的元素进行手动类型转换
        // 我们知道容器中放入的是Apple,拿出容器中的对象,将他转型为Apple
        Apple apple = (Apple) holder.getObject();
        apple.functionInApple();
        
        // 需要对从容器中取出的元素进行转型,但是可能会因为疏忽造成转型错误。 
        // 我误以为这个holder容器中放的是Orange,结果导致程序运行出现异常
        Orange orange = (Orange) holder.getObject();
        orange.functionInOrange();
    }

}

运行结果:从结果中我们可以看到由于取出的Apple错误的转型为Orange然后调用Orange中的orange.functionInOrange()方法遇到了运行时异常。

functionInOrange has been called
Exception in thread "main" java.lang.ClassCastException: com.lin.chen.javatestdemo.Apple cannot be cast to com.lin.chen.javatestdemo.Orange
    at com.lin.generic.ObjectHolderTest.main(ObjectHolderTest.java:24)

Java泛型帮我们做什么

为了解决上面的两个问题,Java的设计者让编译器帮我们处理了下面两件事情。

  • 编译器检查我们放入容器的元素是否满足泛型容器定义的期许,我们只需要告诉编译器我这个容器是一个处理何种类型的容器即可。
  • 编译器为从容器中取出的元素进行自动转型。

在泛型介入后,程序员的关注点由2点变为1点:

  • 定义容器处理的类型,这样放入容器的检查和取出容器的转型都交由编译器来完成。

泛型类的使用

定义方式:

public class ClassName

解决问题

  • 程序员只需要在实例化容器的时候,定义容器允许处理的类型,那么放入容器的检查和取出容器的转型都交由编译器来完成。

实例代码

public class GenericHolder {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
    
    public boolean equals (Object obj) {
        return t.equals(obj);
    }

}
public class GenericHolderTest {

    public static void main(String[] args) {

        //通过GenericHolder定义容器处理的类型为Apple,之后放入容器和从容器取出数据的检查通通交给编译器
        GenericHolder holder = new GenericHolder();
        holder.setT(new Apple());
        // 编译错误,编译器检查我们放入容器的元素是否满足泛型容器定义的期许
        // 编译器报错原文:The method setT(Apple) in the type GenericHolder is not applicable for the arguments (Orange)
        // holder.setT(new Orange());
        
        //编译器为从容器中取出的元素进行自动转型
        Apple apple = holder.getT();
        apple.functionInApple();
        
        //编译错误:编译器检查出取出的类型不能安全的转型为Orange
        //Type mismatch: cannot convert from Apple to Orange
        //Orange orange = holder.getT();
    }

}

运行结果:

functionInOrange has been called

泛型接口的使用

定义方式:

public interface InterfaceName

解决问题

  • 在没有泛型接口的时候,接口约束的参数类型都只能是具体类型。那么这将是接口设计上的灾难。想象我们有一个叫IntegerIterator的接口约束一个next()方法返回一个具体的Integer类型.
public interface IntegerIterator {
  
    boolean hasNext();

    Integer next();
}

现在情况有变,我们需要用next()方法返回一个String.难道我们还需要再写一个StringIterator吗?显然不是。Java的Iterator接口已经告诉了我们解决方案。这个答案就是使用泛型接口。

public interface Iterator {
  
    boolean hasNext();

    T next();
}

泛型方法

定义方式:

public static  void methodName(T t)

解决问题

  • 一个类中的静态方法要使用泛型参数
  • 一个类中的某一个或者某些方法有使用泛型的需求,但是这并不是一个泛型类

一个类中的静态方法要使用泛型参数的例子

public class GenericMethod {
    
        //  泛型类中的静态方法在使用泛型参数的时候遇到了编译错误因为一个静态方法不能使用非静态引用T,这个T是泛型类定义的参数类型
        //  编译器报错原文:Cannot make a static reference to the non-static type T
        //  public static void printClassName(T t) {
        //      System.out.println(t.getClass().getName());
        //  }
    
    //定义泛型方法使类中的静态方法可以使用泛型参数
    public static  void printClassName(T t) {
        System.out.println(t.getClass().getName());
    }
    
    public static void main(String[] args) {

        GenericMethod.printClassName(1.0f);
        GenericMethod.printClassName(1);
        GenericMethod.printClassName("I'm String");
        GenericMethod.printClassName(new ArrayList());
    }
}

输出结果

java.lang.Float
java.lang.Integer
java.lang.String
java.util.ArrayList

拥有泛型方法的类,并不一定是泛型类,而泛型类中使用了泛型参数的方法也不一定是泛型方法。
泛型方法和泛型类中使用到泛型的方法的区别:
泛型类中使用到泛型参数的方法:public static void printClassName(T t)
泛型方法:public static void printClassName(T t),注意到static与void之间的斜体加粗 ,这是泛型方法参数列表的定义,也是泛型方法定义的标志。
既然泛型方法并不要存在在泛型类中,那么上面的代码可以做如下修改:public class GenericMethod

public class GenericMethod {

    public static  void printClassName(T t) {
        System.out.println(t.getClass().getName());
    }

    public static void main(String[] args) {

        GenericMethod.printClassName(1.0f);
        GenericMethod.printClassName(1);
        GenericMethod.printClassName("I'm String");
        GenericMethod.printClassName(new ArrayList());
    }
}

输出结果

java.lang.Float
java.lang.Integer
java.lang.String
java.util.ArrayList

泛型边界

定义方式:

TeacherHolder 

解决问题

  • 定义一个泛型容器类中持有的是某个类和他的子类

有这样一种场景。我有一个Teacher类和一个装Teacher或者Teacher子类的容器,Teacher有个teach方法,我会在这个容器中,调用这个Teacher类或者他子类的teach方法。那么就会使用到泛型的边界。

public class Teacher {
    //Teacher的teach方法
    public void teach() {}
}
public class TeacherHolder {
    
    private T t;
    
    public TeacherHolder (T t) {
        this.t = t;
    }
    
    //没有边界的泛型类,无法通过编译,因为编译器并不知道这个T应该拥有什么样的方法
    //编译器报错原文:The method teach() is undefined for the type T
    //public  void callTeach() {
    //  t.teach();
    //}
}
public class TeacherHolder {

    private T t;
    
    public TeacherHolder (T t) {
        this.t = t;
    }
    
    //通过定义边界,t拥有了调teach方法的能力
    public  void callTeach() {
        t.teach();
    }
}

泛型通配符

定义

  • 上边界通配符Holder
  • 下边界通配符Holder
  • 无界通配符Holder
    解决问题
  • 解决泛型容器不能支持协变得问题

协变这个概念有可以简单理解为可以使用父类型的地方均可以用子类型替代。Java的数组是支持协变的。举一个例子,Apple是Fruit的子类型,能使用Fruit[] 的地方均可以使用Apple[]代替。而支持Holder的地方并不支持Holder,因为在Java的编译器看来Holder与Holder完全是不同的类型(最好的佐证就是Holder fruits = new Holder是一个让编译器报错的语句),虽然他们在运行时经过泛型的擦除使得他们是同一种类型。
Java泛型容器的协变由泛型的上界通配符来实现

public class CovariantArrays {

    public static void main(String[] args) {
        //定义一个Fruit[]可以将Apple[]赋给Fruit[]的,这就是协变的应用
        Fruit[] fruits = new Apple[10];
        fruits[0] = new Apple();
        fruits[1] = new FujiApple();
        fruits[3] = new Fruit();
        fruits[4] = new Orange();
    }

}

泛型类如何进行协变

public class ConvarianGenericHolder {

    public static void main(String[] args) {

        // 编译错误:编译器不会认为GenericHolder是GenericHolder的子类
        // 编译器报错原文:Type mismatch: cannot convert from GenericHolder to GenericHolder 
                //GenericHolder fruitsHolder = new GenericHolder();
            //使用上边界通配符实现Java泛型容器的协变
        GenericHolder fruitsHolder = new GenericHolder();

    }

}

各种泛型通配符的使用限制

泛型通配符的使用限制是让初学Java最头疼的问题之一,下面例子为了便于大家理解我直接使用List容器来做讲解,要理解这些限制需要们牢记下面5个知识点:

  1. List, List, List中的在编译器看来所表达的并不是一个类型范围,而是一个没指定名字的具体类型,注意具体类型4个字。
  2. Object是所有类型的基类。
  3. 编译器认为null是所有类型的子类,由null可以向任何类型赋值可知。相信大家都使用过String str = null, ArrayList list = null诸如此类的赋值。
  4. 子类可以安全的转型为超类。
  5. 超类无法安全的转型为子类,因为子类拥有超类并不拥有的信息,例如子类有个超类没有的方法,那么超类无法安全的转型为子类。
    有了这几条基础那么我们可以开始去理解通配符使用的限制了。

的限制

  • 添加:不能添加除null之外的任何元素进入容器。由知识点1知道List是一个Fruit子类的具体类型。既然List是一个具体类型,那么可以理解为为一个不知道名字的具体的水果。用表示。向一个List的list添加Apple,Orange元素。就如同向List的List添加Apple元素不会被编译器通过。只能添加null时由知识点3推导出来的。
  • 读取:可以从容器中安全的读取Fruit和Fruit的子类并安全的转型为Fruit。由5可知向上转型是安全的,所以这个unknowNameFruit可以安全的从容器中取出并转型为Fruit。
    示例代码:
public class ExtendTypeWildcards {

    public static void main(String[] args) {
        List fruits = new ArrayList();
        // 编译错误:这样的行为你可以理解为向一个List里添加Apple元素会失败一样,就像
        //我们向List的list中添加Apple会失败一样
        // 编译器报错原文: The method add(? extends Fruit) in the type ? extends Fruit> is not applicable for the arguments (Apple)
        //fruits.add(new Fruit());
        //fruits.add(new Apple());
        //fruits.add(new Orange());
        //fruits.add(new FujiApple());
        fruits.add(null);
        //从容器中取出unknowNameFruit并安全转型为Fruit
        Fruit fruit = fruits.get(0);
    }

}

的限制

  • 添加:可以添加Fruit和Fruit的子类进入容器。我们可以理解为是一个某中类型的SomeObject,他们的继承关系是 Fruit extends SomeObject, SomeObject extends Object。
  • 读取:从容器中取出的元素只能安全转型为Object.这也是由2&4&5推导出来的。因为这个SomeObject是Fruit的超类,那么将他转型为任何Fruit和Fruit的子类都无法满足知识点5,故只能转型为Object。

示例代码:

public class SuperTypeWildcards {

    public static void write(List fruits) {
        //compile error: The method add(? super Fruit) in the type List is not applicable for the arguments (Object)
        //fruits.add(new Object());
        //可以成功的添加Fruit和Fruit的子类
        fruits.add(new Fruit());
        fruits.add(new Apple());
        fruits.add(new Orange());
        fruits.add(new FujiApple());
            //无法安全的转型为除Object之外的任何类型
        //T编译器报错原文: cannot convert from capture#5-of ? super Fruit to Fruit
        //Fruit fruit = fruits.get(0);
        //只能安全的转型为Object
        Object object = fruits.get(0);
    }
}

的限制

无界通配符的限制是类统配和父类通配符的并集。

  • 添加:同子类通配符一样。不能添加除null之外的任何元素进入容器。
  • 读取:同父类通配符一样。从容器中取出的元素只能安全转型为Object.

示例代码:

public class Wildcards {

    public void unboundArgsForList(List list) {

        // 编译错误:无法向容器中添加任何除null外的元素
        // 编译器报错原文:The method add(capture#4-of ?) in the type List is not applicable for the arguments (Fruit)
        //list.add(new Fruit());
        //list.add(new Apple());

        list.add(null);

        // 编译错误:从容器中取出的元素只能安全转型为Object
        // 编译器报错原文:Type mismatch: cannot convert from capture#5-of ? to Apple
        //Apple apple = list.get(0);
        
        Object obj = list.get(0);

    }
}

参考书籍

Java编程思想(第四版)15章-泛型

博文https://blog.csdn.net/s10461/article/details/53941091

一课一练

下面是Java8中Stream这个泛型接口中的部分实现.
问题一:filter和map哪一个是泛型方法,哪一个不是?
问题二:为什么filter的方法中filter(Predicate predicate)的Predicate使用这个下边界通配符。
为什么map的方法中map(Function Function中的T的定义用了下边界通配符,而R< ? extends R>使用了上边界通配符,他的目的是什么?

public interface Stream extends BaseStream> {
    Stream filter(Predicate predicate);
     Stream map(Function mapper);
}

请你在留言区写下你的答案,我会选出经过认真思考的留言,然后抽取一位幸运读者,什么礼品都不送。如果你也有朋友为泛型发愁,你可以分享给他,或许对他有所帮助。

你可能感兴趣的:(Java泛型到底是什么)