Java-Java5.0泛型解读

xiaoxiao2021-02-28  95

概述泛型类泛型方法泛型接口边界符通配符PECS原则类型擦除

概述

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>, ... }
转载请注明原文地址: https://www.6miu.com/read-64014.html

最新回复(0)