Skip to content

1. 为什么使用泛型程序设计?

使用泛型机制编写的程序代码要比那些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。 泛型对于集合类尤其有用。

泛型程序设计(Generic programming) 意味着编写的代码可以被很多不同类型的对象所 重用

2. 泛型定义

声明类的泛型版本与声明类的一般版本的语法格式很类似,只有稍微的不同。

声明一般类的语法格式为:

java

[修饰符] class类名 [extends父类名] [implements接口1名,接口2名,…]{
        //类体
}

而声明泛型类型时,只需要在类名后面跟上一个类型参数即可,其语法格式为:

  • 单类型参数泛型类型声明
java

[修饰符] class类名<类型参数> [extends父类名] [implements接口1名,接口2名,。。。]{
       //类体
}
  • 多类型参数泛型类型声明
angelscript
[修饰符] class类名<类型参数1,类型参数2,类型参数3> [extends父类名] [implements接口1名,接口2名,。。。]{
       //类体
}

每个类型参数必须唯一。

代码示例:

  • box类
java

public class Box {
    private Object object;

    public void add(Object object){
        this.object = object;
    }
    public Object get(){
        return object;
    }
}
  • box 泛型类
java

// Box类的泛型版本.
public class Box<T> {
    private T t;        // T代表"类型"
    public void add(T t) {
      this.t = t;
    }
    public T get() {
      return t;
  }
}
  • T:”代表类型变量” Type

3. 泛型使用

一个泛型类型调用通常被看做是“参数化类型”。要实例化这个类,照常使用new关键字,但是将<Integer>放在类名和圆括号之间:

java

Box<Integer> integerBox = new Box<Integer>();
integerBox.add(new Integer(10));
// 不需要类型转换
Integer someInteger = integerBox.get();
System.out.println(someInteger)

4.泛型方法

4.1 定义泛型方法

类型参数还可以在方法和构造器签名中声明,用来创建“泛型方法”和“泛型构造器”。这与声明一个泛型类型相似,但是类型参数的作用域被限制在它被声明的方法或构造器中。

声明格式:

java

[修饰符] <类型变量> [返回类型] 方法名(参数列表){
    // 方法体
}

代码示例:

java

class ArrayAlg{ 
   public static <T> T getMiddle(T... a){ 
     return a[a.length / 2];
  }
}

这个方法是在普通类中定义的, 而不是在泛型类中定义的。 然而, 这是一个泛型方法, 可以从尖括号和类型变量看出这一点。注意:类型变量放在修饰符(这里是 public static) 的后面,返回类型的前面。

4.2 调用泛型方法

当调用一个泛型方法时’ 在方法名前的尖括号中放入具体的类型

调用ArrayAlg类中的泛型方法:

java

String middle = ArrayAlg.<String>getMiddle("]ohnM, "Q.n, "Public");

在大多数情况下,方法调用中可以省略类型参数。编译器有足够的信息能够推断出所调用的方法。它用 names 的类型(即 String[]) 与泛型类型 T[ ] 进行匹配并推断出 T 一定是 String。也就是说上述代码也可以修改为:

java

String middle = ArrayAlg.getHiddle("]ohn", "Q.", "Public");

5.类型变量

5.1 类型参数命名惯例

按惯例,类型参数命名为单个的大写字母。这与已经知道的变量命名规则不太相同。这样命名的原因是:如果不这样命名,将很难区分在一个类型变量和一个变通类或接口名称之间的不同。最普遍使用的类型参数名称如下:

  • E——元素(被Java集合框架所广泛地使用 )。
  • K——键。
  • N——数字。
  • T——类型。
  • V——值。
  • S,U,V等——第二、第三、第四个类型。

5.2 类型变量的限定

5.2.1 类型参数边界

默认可以使用任何类型来实例化一个泛型类对象,但Java中也对泛型类实例的类型作了限制。

语法如下:

java

<T exteds anyClass>
  1. anyClass指某个接口或类
  2. anyClass指的是上限
  3. 限定上限的类型中,最多只能有1个类(或0个类),其余的必须均为接口。
  4. 限定上限的类型中,接口可以有多个,中间用&字符分割。

使用泛型限制后,泛型类的类型必须实现或者继承了anyClass 这个接口或类。无论anyClass是接口还是类,在进行泛型限制时都必须使用extends关键字。

  • 泛型类限制使用
java

import java.util.List;
public class LimitClass<T extends List>{
    // ....
}
  • 泛型方法限制使用

import java.util.List;
public class LimitClass{
    public static <T extends List> void testMedthod(T t){
        // ....
    }
}

对类型参数的限定,也可以包括指定额外的必须被实现的接口,使用&字符,例如指定类型变量U可接收的值为实现了MyInterface接口的Number类或其之类。 语法如下:

java
javascript
// 上限类型中有一个接口
<U extends Number & MyInterface>

// 上限类型中有n个接口
<U extends Number & MyInterface1 & MyInterface2 & ... &MyInterface...>

5.2.2 使用通配符: ?

固定的泛型类型使用起来不灵活,java提供一种巧妙的方案:通配符类型。

定义格式

swift
//不设置上限:表示任何类型
AnyGeneric<?>
AnyGeneric<?,?>

// 设置上限
AnyGeneric<? extends MyClass>

表示任何泛型AnyGeneric类型,它的类型参数是MyClass的子类

代码1:

java

public class Demo {
    public static void main(String[] args){
        Point<Integer, Integer> p1 = new Point<Integer, Integer>();
        p1.setX(10);
        p1.setY(20);
        printPoint(p1);

        Point<String, String> p2 = new Point<String, String>();
        p2.setX("东经180度");
        p2.setY("北纬210度");
        printPoint(p2);
    }
    // 使用通配符
    public static void printPoint(Point<?, ?> p){  
        System.out.println("This point is: " + p.getX() + ", " + p.getY());
    }
}
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;
    }
}
/*
运行结果:
This point is: 10, 20
This point is: 东经180度, 北纬210度
*/

代码2:

java
java
public class Demo {
    public static void main(String[] args){
        Point<Integer, Integer> p1 = new Point<Integer, Integer>();
        p1.setX(10);
        p1.setY(20);
        printNumPoint(p1);

        Point<String, String> p2 = new Point<String, String>();
        p2.setX("东经180度");
        p2.setY("北纬210度");
        printStrPoint(p2);
    }

    // 借助通配符限制泛型的范围
    public static void printNumPoint(Point<? extends Number, ? extends Number> p){
        System.out.println("x: " + p.getX() + ", y: " + p.getY());
    }

    public static void printStrPoint(Point<? extends String, ? extends String> p){
        System.out.println("GPS: " + p.getX() + "," + p.getY());
    }
}
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;
    }
}
/*
运行结果:
x: 10, y: 20
GPS: 东经180度,北纬210度
*/

6.类型擦除

当一个泛型被实例化时,编译器通过称为“类型擦除”的技术来编译这些类型。所谓类型擦除,指的是编译器移除一个类或方法中所有与类型参数相关的信息。类型擦除能够使使用泛型的Java应用程序与Java类库和在泛型出现之前创建的应用程序保持二进制上的兼容性。

例如:将泛型类Pair 进行类型擦除 擦除前:

java
public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

擦除后:


public class Pair {
    private Object first;
    private Object last;
    public Pair(Object first, Object last) {
        this.first = first;
        this.last = last;
    }
    public Object getFirst() {
        return first;
    }
    public Object getLast() {
        return last;
    }
}

因此,Java使用擦拭法实现泛型,导致了:

  • 编译器把类型视为Object;
  • 编译器根据实现安全的强制转型。

7.泛型的局限

Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。了解了Java泛型的实现方式——擦拭法,我们就知道了Java泛型的局限:

7.1 不能用类型参数代替基本类型

不能用类型参数代替基本类型,因此没有 Pair、Pair…

arduino
Pair<int> p = new Pair<int>(1, 2); // compile error!

7.2 无法判断带泛型的Class

lasso
Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>.class) {
}

原因和前面一样,并不存在Pair.class,而是只有唯一的Pair.class。

7.3 无法实例化T类型

java
public class Pair<T> {
    private T first;
    private T last;
    public Pair() {
        // Compile error:
        first = new T();
        last = new T();
    }
}

上述代码无法通过编译,因为构造方法的两行语句:

java
first = new T();
last = new T();
//擦拭后实际上变成了:
first = new Object();
last = new Object();

7.4 无法创建参数化类型的数组

haxe
Pair<String>[] table = new Pair<String>[10]; //error

需要说明的是,只是不允许创建这些数组,而声明类型为Pair[]的变量仍是合法的,不过不能用new Pair[10]初始化这个变量。

7.5 总结

Java的泛型是采用擦拭法实现的;擦拭法决定了泛型<T>

  • 不能是基本类型,例如:int;
  • 不能获取带泛型类型的Class,例如:Pair.class;
  • 不能判断带泛型类型的类型,例如:x instanceof Pair;
  • 不能实例化T类型,例如:new T()。

https://liuqh.icu/tags/SpringBoot/

java



static int add(Pair<? extends Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        return first.intValue() + last.intValue();
 }

static int add(Pair<Number> p) {
    //读取操作
    Number first = p.getFirst();
    Number last = p.getLast();
    return first.intValue() + last.intValue();
}

问题1:为什么需要 extends?

```java
public class Main {
    public static void main(String[] args) {
        //Pair<Number> p = new Pair<Number>(1, 2)
        Pair<Integer> p = new Pair<>(123, 456);

        int n = add(p);
        System.out.println(n);
    }
     
     //incompatible types: Pair<Integer> cannot be converted to Pair<Number>
     //原因很明显,因为Pair<Integer>不是Pair<Number>的子类,因此,add(Pair<Number>)不接受参数类型Pair<Integer>。
    //但是从add()方法的代码可知,传入Pair<Integer>是完全符合内部代码的类型规范,因为语句:Number first = p.getFirst();
     //实际类型是Integer,引用类型是Number,没有问题。问题在于方法参数类型定死了只能传入Pair<Number>。
    //有没有办法使得方法参数接受Pair<Integer>?办法是有的,这就是使用Pair<? extends Number>使得方法接收所有泛型类型为Number或Number子类的Pair类型。我们把代码改写如下:
    static int add(Pair<Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();

        return first.intValue() + last.intValue();
    }
   
   //这样一来,给方法传入Pair<Integer>类型时,它符合参数Pair<? extends Number>类型。这种使用<? extends Number>的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。
   //除了可以传入Pair<Integer>类型,我们还可以传入Pair<Double>类型,Pair<BigDecimal>类型等等,因为Double和BigDecimal都是Number的子类。
    static int add(Pair<? extends Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();

        //如果我们考察对Pair<? extends Number>类型调用getFirst()方法,实际的方法签名变成了:
        //<? extends Number> getFirst();
        //即返回值是Number或Number的子类,因此,可以安全赋值给Number类型的变量:
        //编译器只能【确定】类型一定是Number的子类(包括Number类型本身),但具体类型无法确定。

        return first.intValue() + last.intValue();
    }


      static int add(Pair<? extends Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();

        p.setFirst(new Integer(first.intValue() + 100));
        //public void setFirst(Pair<? extends Number> p) 
         //Pair<Double> p2=new Pair<>(123.1, 456.23);
        //此时 P2是Doule类型,如果我们写的方法扩大的画,可以写入Inter类型,这样是不对的 
        //Pair<Double>的setFirst(Pair<? extends Number> p)显然无法接受Integer类型。
        //读的时候我们可以放大界限,但是写的时候,是什么就得是什么。
        //这就是<? extends Number>通配符的一个重要限制:方法参数签名setFirst(? extends Number)无法传递任何Number的子类型给setFirst(? extends Number)。
        //   
            //  总结
            // 使用类似<? extends Number>通配符作为方法参数时表示:

            // 方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();;
            // 方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);。
            // 即一句话总结:使用extends通配符表示可以读,不能写。

            // 使用类似<T extends Number>定义泛型类时表示:

            // 泛型类型限定为Number以及Number的子类。

        p.setLast(new Integer(last.intValue() + 100));
        return p.getFirst().intValue() + p.getLast().intValue();
    }

}

class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }

    public void setFirst(T first) {
        this.first = first;
    }

     public void setFirst(T first) {
        this.first = first;
    }

    public void setLast(T last) {
        this.last = last;
    }
}
```




extends 为什么无法写数据,为什么能读数据
java
public class Main {
    public static void main(String[] args) {

        Pair<Number> p1 = new Pair<>(12.3, 4.56);
        Pair<Integer> p2 = new Pair<>(123, 456);

        setSame(p1, 100);
        setSame(p2, 200);
        System.out.println(p1.getFirst() + ", " + p1.getLast());
        System.out.println(p2.getFirst() + ", " + p2.getLast());
    }
       
    //extends通配符相反,这次,我们希望接受Pair<Integer>类型,以及Pair<Number>、Pair<Object>,
    //因为Number和Object是Integer的父类,setFirst(Number)和setFirst(Object)
    //实际上允许接受Integer类型。
    //Integer 职员(能写), 领导(肯定也能写)(最小权限
    static void setSame(Pair<? super Integer> p, Integer n) {
        p.setFirst(n);
        p.setLast(n);
    }

    //? super Integer getFirst();
    //这里注意到我们无法使用Integer类型来接收getFirst()的返回值,即下面的语句将无法通过编译:
    // 因为我们现在是职员,但是容器里面有领导的权限,我们不能拿出来
    // Integer x = p.getFirst();
    //因为如果传入的实际类型是Pair<Number>,编译器无法将Number类型转型为Integer

       // extends 
       //这里是Number 是领导,肯定能读取到职权的权限
       //但是写入的时候,领导能写入的,只能不一定能写(最大权限)
       static int king(Pair<? extends Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        p.setFirst(new Integer(first.intValue() + 100));
        p.setLast(new Integer(last.intValue() + 100));
        return p.getFirst().intValue() + p.getLast().intValue();
    }

}

class Pair<T> {
    private T first;
    private T last;

    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}

.