Java-Java5.0泛型解读

概述

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>之间并没有任何的关系。

如下:

这里写图片描述

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>, ... }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小工匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值