10_Java泛型

本章章节

> 10.1为什么需要泛型
> 10.2泛型类
> 10.3 泛型接口
> 10.4泛型方法
> 10.5用泛型定义数组
> 10.6建立类型为泛型类的数组
> 10.7泛型类充当泛型的实例化类
> 10.8通配符

 

  Java泛型(generics)是JDK 5中引入的一个新特性,它的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在JDK 5中的新集合类框架中。

10.1 为什么需要泛型

  现在我要设计一个可以表示坐标的点类,坐标由xy组成。可是不同的人,可能想要知道的坐标不一样。有人想要坐标用整型表示;有人想要坐标用小数表示;有有人想要坐标用字符串表示,如下:

A这个人想要看到整型的坐标,例如:(10, 20)。所以我们要定义一个IntPoint类如下:

//IntPoint.java

public class IntPoint {
  private int x;
  private int y;

  public IntPoint(){
  }

  public IntPoint(int x, int y) {
    this.x = x;
    this.y = y;
  }

  public int getX() {
    return x;
  }

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

  public int getY() {
    return y;
  }

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

 

B这个人想要看到小数的坐标,例如(10.5, 20.6)。所以我们要定义一个DoublePoint类如下:

//DoublePoint.java

public class DoublePoint {
  private double x;
  private double y;

  public DoublePoint(){
  }

  public DoublePoint(double x, double y) {  
    this.x = x;
    this.y = y;
  }

  public double getX() {
    return x;
  }

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

  public double getY() {
    return y;
  }

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

 

C这个人想要看到字符串的坐标。例如:("东经180", y="北纬120"),所以我们要定义一个StringPoint类如下:

//StringPoint.java

public class StringPoint {
  private String x;
  private String y;

  public StringPoint(){
  }

  public StringPoint(String x, String y) {
    this.x = x;
    this.y = y;
  }

  public String getX() {
    return x;
  }

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

  public String getY() {
    return y;
  }

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

 

使用过程如下:

//TestPoint.java

public class TestPoint {
  public static void main(String[] args) {
    IntPoint ptInt = new IntPoint(10, 20);
    System.out.println("(" + ptInt.getX() + "," + ptInt.getY() + ")");

    DoublePoint ptDouble = new DoublePoint(10.5, 20.6);
    System.out.println("(" + ptDouble.getX() + "," + ptDouble.getY() + ")");

    StringPoint ptString = new StringPoint("东经180度", "北纬120度");
    System.out.println("(" + ptString.getX() + "," + ptString.getY() + ")");
  }
}

 

  我们发现其实上面的三个类的基本形式都是一模一样的,除了表示坐标的xy数据类型不一样而已。这样写出的代码冗余度太高,而且维护和扩展比较麻烦。比如我现在想要增加一个打印坐标的功能,我不得不在每一个坐标类都增加一个disp函数。那么有没有什么方法能帮我们做到只定义一个点类,就可以表示所有的可能呢?

第一种解决方法是利用Object类,因为Object是所有类的父类,所以将点类声明如下即可:

//ObjectPoint.java

public class ObjectPoint
{
    private Object x;
    private Object y;

    public ObjectPoint()
    {
    }

    public ObjectPoint(Object x, Object y)
    {
        this.x = x;
        this.y = y;
    }

    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;
    }
}

 

使用过程如下:

//TestPoint.java

public class TestPoint
{
    public static void main(String[] args)
    {
        ObjectPoint ptObj1 = new ObjectPoint(new Integer(10), new Integer(20));
     //或ObjectPoint ptObj1 = new ObjectPoint(10, 20);

        int iV1 = Integer.parseInt(ptObj1.getX().toString());
     //或采用int iV1 = (Integer)(ptObj1.getX());强制类型转换为Integer后再自动拆箱

        int iV2 = Integer.parseInt(ptObj1.getY().toString());
        System.out.println("(" + iV1 + "," + iV2 + ")");

        ObjectPoint ptObj2 = new ObjectPoint(new Double(10.5), new Double(20.6));
     //或ObjectPoint ptObj2 = new ObjectPoint(10.5, 20.6);

        double dV1 = Double.parseDouble(ptObj2.getX().toString());
        double dV2 = Double.parseDouble(ptObj2.getY().toString());

        System.out.println("(" + dV1 + "," + dV2 + ")");

        ObjectPoint ptObj3 = new ObjectPoint(new String("东经180度"), new String("北纬120度"));
     //或ObjectPoint ptObj3 = new ObjectPoint("东经180度", "北纬120度");

        String sV1 = ptObj3.getX().toString();
        String sV2 = ptObj3.getY().toString();
        System.out.println("(" + sV1 + "," + sV2 + ")");
    }
}

 

  在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。比如写ObjectPoint ptObj1 = new ObjectPoint(10, 20);的时候,容易造成一些隐藏的问题,一不小心写成:ObjectPoint ptObj1 = new ObjectPoint(10.3, 20);编译的时候不报错,但是运行的时候就会有类型不匹配问题。所以Object是一种弱类型检测,执行的时候才知道具体的数据类型,所以不安全。

  第二种方法是利用泛型(Generics),泛型的目的在于对参数类型本身进行参数化。它属于强类型检测(在使用的时候数据类型是确定的,编译器和执行器可以进行类型检测)。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

 

10.2 泛型类

  声明包含泛型的类的格式如下:

     [访问修饰符]  class 类名<泛型1, 泛型2, >{

    [访问权限] 泛型类型标识  变量名称;

    [访问权限] 构造方法([<泛型类型>] 参数名称){}

    [访问权限] 泛型类型标识  getXxx(){}

    [访问权限] 返回值类型声明 setXxx(泛型类型标识 变量名称){}

  }

比如:

/*
 * 此处声明了一个包含泛型T的泛型类,T代表所有可能的类型
 * 而T的实际类型在MyGenerics类实例化时指定。
 */
class MyGenerics<T>
{
    private T x; // x为泛型成员
    public MyGenerics(T x)
    { // 构造方法的参数类型为泛型T
        this.x = x;
    }

    public void setX(T x)
    { // setX方法的参数类型为泛型T
        this.x = x;
    }

    public T getX()
    { // setX方法的返回类型为泛型T
        return this.x;
    }
}

 

  创建泛型类的实例时,可以使用一对尖括号指定泛型的真正类型,但是需要注意的是,尖括号中的类型必须是类类型,比如对于10,不能为int,而是Integer

  泛型对象实例化格式如下:

    类名称<具体类> 对象名称 = new 类名称<具体类>();

比如:

//将T替换为Boolean,并调用一个参数 构造函数,传递的值为boolean类型
MyGenerics<Boolean> gen1 = new MyGenerics<Boolean>(true);
gen1.setX(false); // 调用setX方法,传递boolean值
System.out.println(gen1.getX()); // 调用getX方法,返回boolean值


//将T替换为Integer,并调用一个参数 构造函数,传递的值为int类型
MyGenerics<Integer> gen2 = new MyGenerics<Integer>(10);
gen2.setX(20); // 调用setX方法,传递int值
System.out.println(gen2.getX()); // 调用getX方法,返回int值

 

泛型使用的注意点:

  1、泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。

  2、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。如:gen2 = gen1,则会报错。

  3、泛型的类型参数可以有多个。

  4、泛型的参数类型可以使用extends语句,例如<T extends superclass>。习惯上称为“有界类型”。

  5、泛型的参数类型还可以是通配符类型。例如Class<?> classType = Class.forName("java.lang.String");

  下面我们利用泛型来解决我们之前坐标的问题:

//GenericsPoint.java

public class GenericsPoint<T>
{
    private T x;
    private T y;

    public GenericsPoint()
    {
    }

    public GenericsPoint(T x, T y)
    {
        this.x = x;
        this.y = y;
    }

    public T getX()
    {
        return x;
    }

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

    public T getY()
    {
        return y;
    }

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

 

使用过程如下:

public class TestGenerics
{
    public static void main(String[] args)
    {
        GenericsPoint<Integer> gen1 = new GenericsPoint<Integer>(10, 20);
        System.out.println("(" + gen1.getX() + "," + gen1.getY() + ")");

        GenericsPoint<Double> gen2 = new GenericsPoint<Double>(10.5, 20.6);
        System.out.println("(" + gen2.getX() + "," + gen2.getY() + ")");

        GenericsPoint<String> gen3 = new GenericsPoint<String>("东经180度", "北纬120度");
        System.out.println("(" + gen3.getX() + "," + gen3.getY() + ")");
    }
}

 

  需要注意的是,泛型与子类继承的限制:在泛型操作中,子类的泛型类型是无法使用父类的泛型类型接收的。

  由前面的例子,我们知道,Object是所有类的父类,因此,所有的类型的实例都可赋值给声明为Object类型的变量。如:

Boolean f1 = new Boolean(true);

Integer f2 = new Integer(1);

Object f = f1;   //OK

f=f2;   //OK

 

  但在实例化泛型类时,将泛型指定为Object类型却不存在着和其他类型之间的兼容性:

GenericsPoint<Integer> gen1 = new GenericsPoint<Integer>(10, 20);
GenericsPoint<Double> gen2 = new GenericsPoint<Double>(10.5, 20.6);
GenericsPoint<Object> gen = gen1; // gen1和gen类型并不兼容,发生编译错误

 

  除了可以定义包含一个泛型的类的定义之外,还可以包含多个泛型的类定义。其定义格式如下:

[修饰符] class类名<T1, T2, …> {
    [修饰符] T1 f1;
    [修饰符] T2 f2;
    //....
}

 

示例如下:

class MyGenerics<T1, T2>
{
    private T1 x;
    private T2 y;

    public MyGenerics()
    {
    }

    public MyGenerics(T1 x, T2 y)
    {
        this.x = x;
        this.y = y;
    }

    public void setXY(T1 x, T2 y)
    {
        this.x = x;
        this.y = y;
    }

    public T1 getX()
    {
        return this.x;
    }

    public T2 getY()
    {
        return this.y;
    }
}

 

  此时MyGenerics泛型类的实例化格式:

    给出泛型T1, T2的实际类型:

      MyGenerics<Integer, String> gen1 = new MyGenerics<Integer, String>(1001, "张三");

    也可以不给出T1, T2的实际类型。此时T1,T2将被默认为是Object类型:

      MyGenerics gen2 = new MyGenerics();

  但通常不会这么干。

 

  在泛型类中的泛型成员不能直接实例化,其实例必须要通过方法的参数传递给泛型成员,看下面的代码:

public class MyGenerics<T>
{
    private T[] array; //此处不能用new T[]实例化array

    public void setArray(T[] array)
    {
        this.array = array;
    }

    public T[] getArray()
    {
        return array;
    }
}

 

可以通过方法的泛型参数,将数组的实例传递给类中的泛型数组。看下面代码:

String[] color = {"red", "green", "blue"};

MyGenerics<String> gen = new MyGenerics<String>();

gen.setArray(color);  //向泛型成员array传递实际的字符串数组

String[] strs = gen.getArray();//读取泛型成员array的值,将其赋给字符串数组strs

 

10.3 泛型接口

  在JDK1.5之后,不仅仅可以声明泛型类,也可以声明泛型接口,声明泛型接口和声明泛型类的语法类似,也是在接口名称后面加上<T>

  泛型接口定义格式:

    [访问权限]  interface  接口名称<泛型标识>{}

比如:

interface I<T1, T2>{
  void fun1(T1 t);
  T2 fun2();
}

 

  泛型接口实现的两种方式:

    定义子类:在子类的定义上也声明泛型类型。

    定义子类:如果实现接口的子类不想使用泛型声明,则在实现接口的时候直接指定好其具体的操作类型即可。如果不指定,则此时泛型会自动变为Object

  实现泛型接口时,类在定义时可以不声明泛型接口中的泛型,此时接口中的泛型也会自动变为Object类型。如下:

public class IC implements I
{

    public void fun1(Object t)
    {…} //对应接口方法fun1的实现

    public Object fun2()
    {…} //对应接口方法fun2的实现

}

 

如果需要在实现泛型接口时,保留接口中的泛型,类在定义时就必须要保留泛型接口中的泛型声明。如下:

public class IC<T1, T2> implements I<T1, T2>{

public void fun1(T1 t){…} //对接口方法getT1的实现

public T2 fun2(){…} //对接口方法getT2的实现

}

 

此时可以声明泛型接口的变量,并利用泛型实现类进行实例化。如下:

I<String, Integer> i = new IC<String, Integer>();

 

在实现泛型接口时,也可直接指定接口中的泛型的实际类型。如下:

//实现接口I时,直接指定泛型T1、T2的类型
class IC implements I<String, Integer>
{

  //由于指定接口I中T1类型为String,fun1参数t的类型必须为String
    public void fun1(String t)
    {…}

  //由于指定接口I中T2类型为Integer,fun2返回类型必须为Integer
    public Integer fun2()
    {…}

}

 

同理,当包含泛型的类有继承关系的时候,如需保留父类泛型,需要在声明时加入父类泛型。如下:

public class SubGeneric<T1, T2, T3> extends Generic<T1, T2>
{
    private T3 f;

    public void setF(T3 f)
    {
        this.f = f;
    }

    public T3 getF()
    {
        return f;
    }
}

 

如果不保留父类中的泛型声明,则继承下来的T1T2自动变为Object类型。建议父类中的泛型声明在子类中都要保留。

如果在继承时,不想保留父类中的泛型,但也不想使用默认的Object类型,此时可以直接指定父类中的泛型。如下:

public class SubGeneric<T3> extends Generic<String, Object>
{
    private T3 f;

    public void setF(T3 f)
    {
        this.f = f;
    }

    public T3 getF()
    {
        return f;
    }
}

 

10.4 泛型方法

  之前的所有的泛型除了可以为类中的属性制定类型之外,也可以定义方法,泛型方法所在的类中是否是泛型类本身是没有任何关系的。泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型,使用如下的格式定义泛型方法。

  泛型方法的简单定义:

    [访问权限] <泛型标识>  泛型标识  方法名称([泛型标识  参数名称])

  调用泛型方法和调用普通方法没有任何不同,只需传递含有具体类型的实参即可。比如:

class Demo
{
    public <T> T fun(T t)
    {    // 可以接收任意类型的数据
        return t;           // 直接把参数返回
    }
}

public class GenericsDemo
{
    public static void main(String args[])
    {
        Demo d = new Demo();       // 实例化 Demo 对象
        String str = d.fun("张三");     // 传递字符串
        int i = d.fun(30);             // 传递数字,自动装箱
        System.out.println(str);        // 输出内容
        System.out.println(i);         // 输出内容
    }
}

 

可以将泛型方法和泛型类结合起来使用,如下范例:

class Info<T>
{
    private T value;

    public Info(T value)
    {
        this.value = value;
    }

    public T getValue()
    {
        return this.value;
    }

    public void setValue(T value)
    {
        this.value = value;
    }

    public String toString()
    {
        return this.value.toString();
    }

}

public class TestGenerics
{
    public static <K> void conn(Info<K> a, Info<K> b)
    {
        System.out.println(a + " " + b);
    }

    public static void main(String[] args)
    {
        Info<Integer> infoInt1 = new Info<Integer>(10);
        Info<Integer> infoInt2 = new Info<Integer>(20);
        conn(infoInt1, infoInt2);

        Info<String> infoStr1 = new Info<String>("张三");
        Info<String> infoStr2 = new Info<String>("李四");
        conn(infoStr1, infoStr2);
    }
}

 

10.5 用泛型定义数组

  在使用泛型方法的时候,也可以传递或返回一个泛型定义的数组。例如:

public class TestGenerics
{
    public static <T> T[] fun1(T... arr)
    {
        return arr;
    }

    public static <T> void fun2(T arr[])
    {
        for (T val : arr) {
            System.out.print(val + "  ");
        }
        System.out.println();
    }

    public static void main(String[] args)
    {
        Integer i[] = fun1(1, 2, 3, 4, 5, 6);
        fun2(i);

        String str[] = fun1("aaa", "bbb", "ccc", "ddd");
        fun2(str);
    }
}

 

  泛型定义的数组在接收数据的时候,必须保证数据的一致性。即传入的参数类型必须统一。假如修改传入的参数类型,使其与其他参数类型不一致,编译时就会报错。例如:

    Integer i[] = fun1(1, 2, 3, 4, 5.0f, 6); // 编译报错

 

10.6 建立类型为泛型类的数组

  除了可以用泛型去定义数组外,还可以定义泛型类的数组。如果要建立泛型类的数组,需要注意new关键字后面不要加入泛型的实际类型名。如下:

 Generic<String>[]  gs;   //声明泛型类的数组

//先对泛型数组进行初始化
gs = new Generic[5]; //不要写成new Generic<String>[5]

//再分别为每一个数组元素进行初始化
gs[0]=new Generic<String>(); //为第一个数组元素赋值

//

 

实例如下:

class Info<T>
{
    private T value;

    public Info(T value)
    {
        this.value = value;
    }

    public T getValue()
    {
        return this.value;
    }

    public void setValue(T value)
    {
        this.value = value;
    }

    public String toString()
    {
        return this.value.toString();
    }
}


@SuppressWarnings("unchecked")
public class TestGenerics
{
    public static void main(String[] args)
    {
        Info<String>[] info = new Info[5];

        for (int i = 0; i < info.length; i++) {
            info[0] = new Info<String>("hello" + i);
            System.out.println(info[0]);
        }
    }
}

 

10.7 泛型类充当泛型的实例化类

  之前所讲解的全部泛型操作,都是直接用一个实际的类来实例化的。Java中也允许用一个泛型类来实例化另一个泛型类。这种结构称为泛型嵌套。

  首先定义了两个泛型类InfoDemo,如下所示:

class Info<T, V>
{    //  接收两个泛型类型

    private T var;
    private V value;

    public Info(T var, V value)
    {
        this.setVar(var);
        this.setValue(value);
    }

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

    public void setValue(V value)
    {
        this.value = value;
    }

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

    public V getValue()
    {
        return this.value;
    }
}

class Demo<S>
{
    private S info;

    public Demo(S info)
    {
        this.setInfo(info);
    }

    public void setInfo(S info)
    {
        this.info = info;
    }

    public S getInfo()
    {
        return this.info;
    }
}

 

现在希望Demo类中info属性是Info类的这种类型。但是Info类本身使用的时候需要设置两个泛型。如下所示:

public class GenericsDemo
{
    public static void main(String args[])
    {
        Info<String, Integer> i = new Info<String, Integer>("张三", 30);
        Demo<Info<String, Integer>> d = new Demo<Info<String, Integer>>(i);

        System.out.println("内容一:" + d.getInfo().getVar());
        System.out.println("内容二:" + d.getInfo().getValue());
    }
}

 

10.8 通配符

  在开发中对象的引用传递是最常见的,但是在泛型类的操作中,进行引用传递的时候泛型类型必须匹配才可以传递,否则是无法传递的。例如:

class Info<T>
{
    private T var;

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

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

    public String toString()
    {
        return this.var.toString();
    }
}

public class GenericsDemo
{
    public static void main(String args[])
    {
        Info<String> i = new Info<String>();
        i.setVar("xiaoxiao");
        fun(i);
    }

    public static void fun(Info<Object> temp)
    { //接收Object泛型类型的Info对象
        System.out.println("内容:" + temp);
    }
}

 

由前面的知识我们知道,这里企图利用Info<Object>来接收Info<String>类型的变量,编译出错。

  解决方法:

    1. 泛型对象进行引用传递的时候,类型必须一致。将fun函数改成如下形式:

public static void fun(Info<String> temp){
  System.out.println("内容:" + temp);
}

 

    2. fun()方法中Info参数的泛型取消掉(不建议):

public static void fun(Info temp){
  System.out.println("内容:" + temp);
}

 

    3. 利用通配符:

public static void fun(Info<?> temp){
  System.out.println("内容:" + temp);
}

 

  通配符?”,表示可以接收任意的内容。但是使用“?”声明的对象,此时不能修改。如下:

public static void fun(Info<?> temp){
  temp.setVar("star"); //不能修改,报错
  System.out.println("内容:" + temp);
}

  所以使用?”表示只能接收,不能修改

 

10.8.1 受限泛型

  之前设置泛型类型的时候,只要是类就可以任意设置。但是在Java的泛型中可以指定一个泛型的上限和下限。

  在引用传递中,泛型操作中也可以设置一个泛型对象的范围上限和范围下限。范围上限使用extends关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。而范围下限使用super关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object 类。

  设置上限:

    声明对象:类名称<?  extends  类>  对象名称

    定义类:[访问权限]  类名称<泛型标识  extends 类>{}

  设置下限:

    声明对象:类名称<?  super  类>  对象名称

  

·设置上限实例(Info泛型类跟前面一样):

public class GenericsDemo
{
    public static void main(String args[])
    {
        Info<Integer> i1 = new Info<Integer>();
        Info<Float> i2 = new Info<Float>();

        i1.setVar(30);
        i2.setVar(30.1f);

        fun(i1);
        fun(i2);
    }

    public static void fun(Info<? extends Number> temp)
    { // 只能接收Number及其子类
        System.out.println(temp);
    }
}

 

  如果你在main方法中增加下面几行,则编译报错,因为fun方法已经限制了上限,必须是Number类及其子类。如下:

Info<String> i3 = new Info<String>();
i3.setVar("xiaoxiao");
fun(i3); //报错

 

  你还可以在声明Info的时候才用class Info<T extends Number>{}的形式,则此时利用Info<String> i3 = new Info<String>()就会报错。

    定义泛型类时,如果写成下面的代码:

      public class Info<T>{}

    相当于下面的定义方式:

      public class Info<T extends Object>{}

·设置下限实例:

  当使用的泛型只能在本类及其父类类型上应用的时候,就必须使用泛型的范围下限来配置。如下:

public class GenericsDemo
{
    public static void main(String args[])
    {
        Info<String> i1 = new Info<String>();     // 声明String的泛型对象
        Info<Object> i2 = new Info<Object>();     // 声明Object的泛型对象

        i1.setVar("hello");
        i2.setVar(new Object());

        fun(i1);
        fun(i2);
    }

    public static void fun(Info<? super String> temp)
    {//只能接收String或Object类型泛型
        System.out.print(temp + "、");
    }
}

 

  在main方法中增加下面几行,则编译出错。因为fun已经设置了下限,则此时传递的必须是String类及其父类,如下:

Info<Integer> i3 = new Info<Integer>();
i3.setVar(30);
fun(i3);

 

 

感谢阅读。如果感觉此章对您有帮助,却又不想白瞟

                                 

 

 

 

 

posted @ 2020-08-23 18:03  SpringL  阅读(171)  评论(0编辑  收藏  举报