欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java-Java5.0泛型解读

程序员文章站 2022-05-23 15:18:05
...

概述

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。


泛型类

我们先看一个简单的类的定义

package com.xgj.master.java.generics;

public class GenericClass {

    private String str;

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
    }

}

这是我们最常见的做法,但是这样做有一个坏处 GenericClass类中只能装入String类型的元素,如果我们以后要扩展该类,比如转入Integer类型的元素,就不想要从重写,代码得不到复用,我们使用泛型可以很好地解决这个问题。

如下代码所示:

package com.xgj.master.java.generics;
/**
 * 

 * @ClassName: GenericClass

 * @Description: 泛型类

 * @author: Mr.Yang

 * @date: 2017年8月31日 下午3:35:38

 * @param <T>
 */
public class GenericClass<T> {

    // T stands for "Type"
    private T  t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

}

这样GenericClass类便可以得到复用,我们可以将T替换成任何我们想要的类型:

假设我们有一个User类

GenericClass<String> string = new GenericClass<String>();
GenericClass<User> user = new GenericClass<User>();

另外一个示例:

package com.xgj.master.java.generics;

import org.junit.Test;

public class NormalClass {

    @Test
    public void test(){

        Point point = new Point();
        point.setX(100); // int -> Integer -> Object
        point.setY(20);

        int x = (Integer) point.getX(); // 必须向下转型
        int y = (Integer) point.getY();

        System.out.println("This point is:" + x + ", " + y);

        point.setX(25.4); // double -> Integer -> Object
        point.setY("字符串");

         // 必须向下转型
        double m = (Double) point.getX();

        // 运行期间抛出异常  java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Double
        double n = (Double) point.getY(); 

        System.out.println("This point is:" + m + ", " + n);
    }

    /**
     * 

     * @ClassName: Point

     * @Description: 一般内部类

     * @author: Mr.Yang

     * @date: 2017年8月31日 下午8:23:31
     */
     class Point {
        Object x = 0;
        Object y = 0;

        public Object getX() {
            return x;
        }

        public void setX(Object x) {
            this.x = x;
        }

        public Object getY() {
            return y;
        }

        public void setY(Object y) {
            this.y = y;
        }

    }
}

上面的代码中,设置值的时候不会有任何问题,但是取值时,要向下转型,因向下转型存在着风险,而且编译期间不容易发现,只有在运行期间才会抛出异常,所以要尽量避免使用向下转型。

那么,有没有更好的办法,既可以不使用重载(有重复代码),又能把风险降到最低呢?

可以使用泛型类(Java Class),它可以接受任意类型的数据。所谓“泛型”,就是“宽泛的数据类型”,任意的数据类型。

package com.xgj.master.java.generics;

import org.junit.Test;

public class GenericClass2 {

    @Test
    public void test() {
        Point<Integer, Integer> point = new Point<Integer, Integer>();
        point.setX(200);
        point.setY(400);

        Integer x = point.getX();
        Integer y = point.getY();

        System.out.println("This point is:" + x + ", " + y);

        Point<Double, String> point2 = new Point<Double, String>();
        point2.setX(25.4);
        point2.setY("字符串");
        double m = point2.getX();
        String n = point2.getY();
        System.out.println("This point is:" + m + ", " + n);
    }

    /**
     * 
     * 
     * @ClassName: Point
     * 
     * @Description: 泛型类, T1 T2仅表示类型
     * 
     * @author: Mr.Yang
     * 
     * @date: 2017年8月31日 下午8:20:26
     * 
     * @param <T1>
     * @param <T2>
     */
    class Point<T1, T2> {
        T1 x;
        T2 y;

        public T1 getX() {
            return x;
        }

        public void setX(T1 x) {
            this.x = x;
        }

        public T2 getY() {
            return y;
        }

        public void setY(T2 y) {
            this.y = y;
        }

    }
}

与普通类的定义相比,上面的代码在类名后面多出了 <T1, T2>T1, T2 是自定义的标识符,也是参数,用来传递数据的类型,而不是数据的值,我们称之为类型参数。在泛型中,不但数据的值可以通过参数传递,数据的类型也可以通过参数传递。T1, T2 只是数据类型的占位符,运行时会被替换为真正的数据类型。

传值参数(我们通常所说的参数)由小括号包围,如 (int x, double y),类型参数(泛型参数)由尖括号包围,多个参数由逗号分隔,如 <T><T, E>

类型参数需要在类名后面给出。一旦给出了类型参数,就可以在类中使用了类型参数必须是一个合法的标识符,习惯上使用单个大写字母,通常情况下,K 表示键,V 表示值,E 表示异常或错误,T 表示一般意义上的数据类型。

泛型类在实例化时必须指出具体的类型,也就是向类型参数传值,格式为:

  className variable<dataType1, dataType2> = new className<dataType1, dataType2>();

也可以省略等号右边的数据类型,但是会产生警告,即:

    className variable<dataType1, dataType2> = new className();

因为在使用泛型类时指明了数据类型,赋给其他类型的值会抛出异常,既不需要向下转型,也没有潜在的风险,比上个例子的自动装箱和向上转型要更加实用。


泛型方法

我们可以编写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

定义泛型方法的规则如下:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前, 比如 public static <E> void printArray( E[] inputArray ){}

  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。

  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。

  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是short, int, double, long, float, byte, char等原始类型。但是传递基本类型不会报错,因为它们会自动装箱成对应的包装类。

    那如何声明一个泛型方法呢? 声明一个泛型方法很简单,只要在返回类型前面加上一个类似<K, V>的形式就可以啦。

比如:

package com.xgj.master.java.generics;

/**
 * 

 * @ClassName: Compare

 * @Description: 内部类,泛型类

 * @author: Mr.Yang

 * @date: 2017年8月31日 下午4:01:39

 * @param <K>
 * @param <V>
 */
public class ComPare<K,V> {

        private K  key;
        private V value;
        /**
         * 

         * @Title:ComPare

         * @Description:构造函数

         * @param key
         * @param value
         * @return 
         */
        public  ComPare(K key, V value) {
            this.key = key;
            this.value = value;
        }

        public K getKey() {
            return key;
        }
        public void setKey(K key) {
            this.key = key;
        }
        public V getValue() {
            return value;
        }
        public void setValue(V value) {
            this.value = value;
        }
}
package com.xgj.master.java.generics;


/**
 * 

 * @ClassName: GenericMethod

 * @Description: 泛型方法演示

 * @author: Mr.Yang

 * @date: 2017年8月31日 下午3:54:23
 */
public class GenericMethod {
    /**
     * 

     * @Title: cofer

     * @Description: 泛型方法

     * @param c1
     * @param c2
     * @return

     * @return: boolean
     */
    public static <K,V> boolean cofer(ComPare<K,V> c1, ComPare<K,V> c2){
        return c1.getKey().equals(c2.getKey()) && c1.getValue().equals(c2.getValue());
    }

    // 泛型方法的调用
    public static void main(String[] args) {

        ComPare<Integer,String> c1 = new ComPare<>(1, "dog");

        ComPare<Integer, String> c2 = new ComPare<>(2, "cat");

        boolean different = GenericMethod.<Integer,String>cofer(c1, c2);

        System.out.println("c1 compares c2,and the result is  " + different);


        // 在Java1.7/1.8可以利用type inference,让Java自动推导出相应的类型参数
        boolean different2 = GenericMethod.cofer(c1, c2);

        System.out.println("自动推导 c1 compares c2,and the result is  " + different2);
    }
}

运行结果:

c1 compares c2,and the result is  false
自动推导 c1 compares c2,and the result is  false

第二个示例:

package com.xgj.master.java.generics;

public class GenericMethod2 {

    public static void main(String[] args) {
        // 实例化泛型类
        Point<Integer, Integer> p1 = new Point<Integer, Integer>();
        p1.setX(10);
        p1.setY(20);
        p1.printPoint(p1.getX(), p1.getY());

        Point<Double, String> p2 = new Point<Double, String>();
        p2.setX(25.4);
        p2.setY("字符串");
        p2.printPoint(p2.getX(), p2.getY());
    }
}

/**
 * 
 * 
 * @ClassName: Point
 * 
 * @Description: 定义泛型类
 * 
 * @author: Mr.Yang
 * 
 * @date: 2017年8月31日 下午7:59:47
 * 
 * @param <T1>
 * @param <T2>
 */
class Point<T1, T2> {
    T1 x;
    T2 y;

    public T1 getX() {
        return x;
    }

    public void setX(T1 x) {
        this.x = x;
    }

    public T2 getY() {
        return y;
    }

    public void setY(T2 y) {
        this.y = y;
    }

    /**
     * 
     * 
     * @Title: printPoint
     * 
     * @Description: 定义泛型方法
     * 
     * @param x
     * @param y
     * 
     * @return: void
     */
    public <T1, T2> void printPoint(T1 x, T2 y) {
        T1 m = x;
        T2 n = y;
        System.out.println("This point is:" + m + ", " + n);
    }
}

上面的代码中定义了一个泛型方法 printPoint(),既有普通参数,也有类型参数,类型参数需要放在修饰符后面、返回值类型前面。一旦定义了类型参数,就可以在参数列表、方法体和返回值类型中使用了。

与使用泛型类不同,使用泛型方法时不必指明参数类型,编译器会根据传递的参数自动查找出具体的类型。泛型方法除了定义不同,调用就像普通方法一样。

注意:泛型方法与泛型类没有必然的联系,泛型方法有自己的类型参数,在普通类中也可以定义泛型方法。

泛型方法 printPoint() 中的类型参数 T1, T2 与泛型类 Point 中的 T1, T2 没有必然的联系,也可以使用其他的标识符代替:

public static <V1, V2> void printPoint(V1 x, V2 y){
    V1 m = x;
    V2 n = y;
    System.out.println("This point is:" + m + ", " + n);
}

泛型接口

在Java中也可以定义泛型接口, 示例如下:

package com.xgj.master.java.generics;

public class GenericInterfaceDemo {
    public static void main(String arsg[]) {
        Info<String> obj = new InfoImp<String>("xiaogongjiang");
        System.out.println("Length Of String: " + obj.getVar().length());
    }
}

/**
 * 
 * 
 * @ClassName: Info
 * 
 * @Description: 定义泛型接口
 * 
 * @author: Mr.Yang
 * 
 * @date: 2017年8月31日 下午8:06:06
 * 
 * @param <T>
 */
interface Info<T> {
    public T getVar();
}

/**
 * 
 * 
 * @ClassName: InfoImp
 * 
 * @Description: 泛型接口实现类
 * 
 * @author: Mr.Yang
 * 
 * @date: 2017年8月31日 下午8:06:13
 * 
 * @param <T>
 */
class InfoImp<T> implements Info<T> {

    private T var;

    /**
     * 
     * 
     * @Title:InfoImp
     * 
     *              定义泛型构造方法
     * 
     * @param var
     */
    public InfoImp(T var) {
        this.setVar(var);
    }

    public void setVar(T var) {
        this.var = var;
    }

    public T getVar() {
        return this.var;
    }
}

边界符

在上面的代码中 , 类型参数可以接受任意的数据类型,只要它是被定义过的。但是,很多时候我们只需要一部分数据类型就够了,用户传递其他数据类型可能会引起错误。例如,编写一个泛型函数用于返回不同类型数组(Integer 数组、Double 数组、Character 数组等)中的最大值

package com.xgj.master.java.generics;


public class CountGreater {

    public static <T>  int  countGreaterThan(T[] array, T element){
        int count = 0 ;
        // 遍历数组
        for (T t : array) {
            if (t > element) {   // 编译报错
                ++count;
            }
        }
        return count;
    }
}

但是这样很明显是错误的,因为除了short, int, double, long, float, byte, char等原始类型,其他的类并不一定能使用操作符>,所以编译器报错,那怎么解决这个问题呢?答案是使用边界符, 通过 extends 关键字可以限制泛型的类型.

public interface Comparable<T> {

    public int comparable(T t);
}

做个类似下面这样的声明,这样就等于告诉编译器类型参数T代表的都是实现了Comparable接口的类,这样等于告诉编译器它们都至少实现了compareTo方法。

public class CountGreater {

    public static <T extends Comparable<T>>  int  countGreaterThan(T[] array, T element){
        int count = 0 ;
        // 遍历数组
        for (T t : array) {
            if (t.comparable(element) > 0) { 
                ++count;
            }
        }
        return count;
    }

}

extends 后面可以是类也可以是接口。但这里的 extends 已经不是继承的含义了,应该理解为 T 是继承自 Comparable类的类型,或者 T 是实现了 XX 接口的类型。


通配符

在了解通配符之前,我们首先必须要澄清一个概念,还是借用我们上面定义的GenericClass类,假设我们添加一个这样的方法:

public void test(GenericClass<Number> n){/***/}

那么现在GenericClass<Number> n允许接受什么类型的参数?
我们是否能够传入GenericClass<Integer>或者GenericClass<Double>呢?
答案是否定的,虽然Integer和Double是Number的子类,但是在泛型中GenericClass<Integer>或者GenericClass<Double>GenericClass<Number>之间并没有任何的关系。

这一点非常重要,接下来我们通过一个完整的例子来加深一下理解。

首先我们先定义几个简单的类,下面我们将用到它:

class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}

我们创建了一个泛型类Reader,然后在f1()中当我们尝试Fruit f = fruitReader.readExact(apples);编译器会报错,因为List<Fruit>List<Apple>之间并没有任何的关系。

如下:

Java-Java5.0泛型解读

package com.xgj.master.java.generics;

import java.util.Arrays;
import java.util.List;

public class GenericReading {

    static List<Apple> apples = Arrays.asList(new Apple());
    static List<Orange> oranges = Arrays.asList(new Orange());

    static class Reader<T> {
        T readExact(List<T> list) {
            return list.get(0);
        }
    }

    static void f1() {
        Reader<Fruit> fruitReader = new Reader<Fruit>();
        // The method readExact(List<Fruit>) in the type
        // GenericReading.Reader<Fruit> is not applicable for the arguments (List<Apple>)
        Fruit f = fruitReader.readExact(apples); // 编译报错
    }

    public static void main(String[] args) {
        f1();
    }
}

但是按照我们通常的思维习惯,Apple和Fruit之间肯定是存在联系,然而编译器却无法识别,那怎么在泛型代码中解决这个问题呢?我们可以通过使用通配符来解决这个问题:

package com.xgj.master.java.generics;

import java.util.Arrays;
import java.util.List;

public class GenericReading {

    static List<Apple> apples = Arrays.asList(new Apple());
    static List<Orange> oranges = Arrays.asList(new Orange());

    static class CovariantReader<T> {
        T readCovariant(List<? extends T> list) {
            return list.get(0);
        }
    }
    static void f2() {
        CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();
        Fruit f = fruitReader.readCovariant(oranges);
        Fruit a = fruitReader.readCovariant(apples);
    }
    public static void main(String[] args) {
        f2();
    }
}

这样就相当与告诉编译器, fruitReader的readCovariant方法接受的参数只要是满足Fruit的子类就行(包括Fruit自身),这样子类和父类之间的关系也就关联上了。


PECS原则

上面我们看到了类似<? extends T>的用法,利用它我们可以从list里面get元素,那么我们可不可以往list里面add元素呢?

比如

package com.xgj.master.java.generics;

import java.util.ArrayList;
import java.util.List;

public class GenericsAndCovariance {

     public static void main(String[] args) {
            // Wildcards allow covariance:
            List<? extends Fruit> flist = new ArrayList<Apple>();

            // The method add(capture#1-of ? extends Fruit) in the type List<capture#1-of ? extends Fruit> 
            // is not applicable for the arguments   (Apple)

            // flist.add(new Apple()); 编译报错
            // flist.add(new Orange());编译报错
            // flist.add(new Fruit());编译报错
            // flist.add(new Object());编译报错

            flist.add(null); // 虽然可以添加null ,但是没有意义
            // We Know that it returns at least Fruit:
            Fruit f = flist.get(0);
        }
}

答案是否定,Java编译器不允许我们这样做,为什么呢?对于这个问题我们不妨从编译器的角度去考虑。因为List

List<? extends Fruit> flist = new ArrayList<Fruit>();
List<? extends Fruit> flist = new ArrayList<Apple>();
List<? extends Fruit> flist = new ArrayList<Orange>();
  • 当我们尝试add一个Apple的时候,flist可能指向new ArrayList<Orange>();
  • 当我们尝试add一个Orange的时候,flist可能指向new ArrayList<Apple>();
  • 当我们尝试add一个Fruit的时候,这个Fruit可以是任何类型的Fruit,而flist可能只想某种特定类型的Fruit,编译器无法识别所以会报错。

所以对于实现了<? extends T>的集合类只能将它视为Producer向外提供(get)元素,而不能作为Consumer来对外获取(add)元素。

如果我们要add元素应该怎么做呢?可以使用<? super T>

package com.xgj.master.java.generics;

import java.util.ArrayList;
import java.util.List;

public class GenericWriting {

    static List<Apple> apples = new ArrayList<Apple>();
    static List<Fruit> fruit = new ArrayList<Fruit>();

    static <T> void writeExact(List<T> list, T item) {
        list.add(item);
    }

    static void f1() {
        writeExact(apples, new Apple());
        writeExact(fruit, new Apple());
    }

    static <T> void writeWithWildcard(List<? super T> list, T item) {
            list.add(item);
        }

    static void f2() {
        writeWithWildcard(apples, new Apple());
        writeWithWildcard(fruit, new Apple());
    }

    public static void main(String[] args) {
        f1();
        f2();
    }
}

这样我们可以往容器里面添加元素了,但是使用super的坏处是以后不能get容器里面的元素了,原因很简单,我们继续从编译器的角度考虑这个问题,对于List<? super Apple> list,它可以有下面几种含义:

List<? super Apple> list = new ArrayList<Apple>();
List<? super Apple> list = new ArrayList<Fruit>();
List<? super Apple> list = new ArrayList<Object>();

根据上面的例子,我们可以总结出一条规律,”Producer Extends, Consumer Super”:

  • “Producer Extends” – 如果你需要一个只读List,用它来produce
    T,那么使用? extends T

  • “Consumer Super” – 如果你需要一个只写List,用它来consume T,那么使用? super T

如果需要同时读取以及写入,那么我们就不能使用通配符了。

如何阅读过一些Java集合类的源码,可以发现通常我们会将两者结合起来一起用,比如像下面这样:

public class Collections {
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++)
            dest.set(i, src.get(i));
    }
}

类型擦除

Java泛型中最令人苦恼的地方或许就是类型擦除了. 如果在使用泛型时没有指明数据类型,那么就会擦除泛型类型.

因为在使用泛型时没有指明数据类型,为了不出现错误,编译器会将所有数据向上转型为 Object,所以在取出坐标使用时要向下转型.

比如

public class Demo {
    public static void main(String[] args){
        Point p = new Point();  // 类型擦除
        p.setX(10);
        p.setY(20.8);
        int x = (Integer)p.getX();  // 向下转型
        double y = (Double)p.getY();
        System.out.println("This point is:" + x + ", " + y);
    }
}
class Point<T1, T2>{
    T1 x;
    T2 y;
    public T1 getX() {
        return x;
    }
    public void setX(T1 x) {
        this.x = x;
    }
    public T2 getY() {
        return y;
    }
    public void setY(T2 y) {
        this.y = y;
    }
}

因为在使用泛型时没有指明数据类型,为了不出现错误,编译器会将所有数据向上转型为 Object,所以在取出坐标使用时要向下转型,和不使用泛型没什么两样。

另外一个例子

public class Node<T> {
    private T data;
    private Node<T> next;
    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }
    public T getData() { return data; }
    // ...
}

编译器做完相应的类型检查之后,实际上到了运行期间上面这段代码实际上将转换成:

public class Node {
    private Object data;
    private Node next;
    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }
    public Object getData() { return data; }
    // ...
}

这意味着不管我们声明Node<String>还是Node<Integer>,到了运行期间,JVM统统视为Node<Object>。有没有什么办法可以解决这个问题呢?这就需要我们自己重新设置bounds了,将上面的代码修改成下面这样:

public class Node<T extends Comparable<T>> {
    private T data;
    private Node<T> next;
    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }
    public T getData() { return data; }
    // ...
}

这样编译器就会将T出现的地方替换成Comparable而不再是默认的Object了:

public class Node {
    private Comparable data;
    private Node next;
    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }
    public Comparable getData() { return data; }
    // ...
}

上面的概念或许还是比较好理解,但其实泛型擦除带来的问题远远不止这些,接下来我们系统地来看一下类型擦除所带来的一些问题。

  • 在Java中不允许创建泛型数组

  • Java泛型很大程度上只能提供静态类型检查,然后类型的信息就会被擦除,所以像下面这样利用类型参数创建实例的做法编译器不会通过

public static <E> void append(List<E> list) {
    E elem = new E();  // compile-time error
    list.add(elem);
}

但是如果某些场景我们想要需要利用类型参数创建实例,我们应该怎么做呢?可以利用反射解决这个问题:

public static <E> void append(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.newInstance();   // OK
    list.add(elem);
}

实际上对于这个问题,还可以采用Factory和Template两种设计模式解决.

  • 我们无法对泛型代码直接使用instanceof关键字,因为Java编译器在生成代码的时候会擦除所有相关泛型的类型信息.
public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // compile-time error
        // ...
    }
}
=> { ArrayList<Integer>, ArrayList<String>, LinkedList<Character>, ... }

相关标签: 泛型