Java泛型学习

1.1 使用泛型的原因:

​ 泛型:指参数化类型的参数。可以定义带泛型类型的类或者方法,随后编译器会具体的类型代替它。泛型程序设计( Generic programming ) 使用原因:为了编写的代码可以被不同类型的对象所重用。并且可以在编译时而非运行时检测出错误。使用泛型机制编写的代码,比使用 Object 变量然后进行强制类型转换的代码具有更好的安全性和可读性。

1.1.1 类型参数( type parameters ):

​ Java 加入泛型机制之前,人们使用继承实现泛型的功能,但是使用继承实现的泛型功能会使得获取对象值时必须使用强制类型转换,这会在某些情况下产生一个错误。并且使得代码的可读性变得很差。使用类型参数< 尖括号 >的好处在于:使得程序具有更好的可读性和安全性。

1.2 定义简单泛型类:

​ 一个泛型类( generic class ) 就是具有一个或者多个类型变量的类。使用具体的类型替换类型变量就可以实例化泛型类型。泛型类可以看作普通类的工厂。泛型类型必须是引用类型,不能由int, double,或char基本类型来代替泛型类型。

public class Pair<T>{
  /*	定义一个泛型类 Pair类引用一个类型变量 T用尖括号< >括起来。
  		类定义中的类型变量指定方法的返回类型以及局部变量的类型。*/
  private T first; // 类型变量指定局部变量的类型
  private T second;
  public Pair(){
    first = null;
    second = null;
  }
  public T getFrist(){ // 类型变量指定方法的返回值
    return first;
  }
}

​ 泛型类可以有多个类型变量,尖括号中可以使用不同类型的域,用逗号隔开。

public class Pair<T, U>{...}

1.3 泛型方法:

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

String middle = ArrayAlg.<String>getMiddle("john","Q","public()")
  //这个式子等价于下面这个式子。
  String middle = ArrayAlg.getMiddle("john","Q","public()")
  //多数情况下,编译器可以根据方法参数类型自动推断出泛型的类型,当出现不同类型参数的时候,编译器或报错。
  double middle = ArrayAlg.getMiddle(3.14,1729,0)

​ 借助类型参数,可以在一个普通类中定义一个泛型方法,类型变量放在修饰符后面,返回类型前面。*泛型方法可以定义在普通类中,也可以定义在泛型类中。*当调用泛型方法时在方法名前的尖括号中放入具体的类型。

1.4 类型变量的限定:

​ 有时,类或者方法需要对类型变量加以约束,使用泛型时意味着 smallest 变量可以是任意类型的对象,但是不能确保所有的类中都实现了compareTo( )方法。所以要对T类型进行限定。限定 T 为实现了Comparable 接口的方法。可以对类型变量 T 设置限定( bound )来实现

// 计算数组中最小元素。
class ArrayAlg{
  public static <T> min(T[] a){
    if(a == null || a.length == 0) return null;
    T smallest = a[0];
    for(int i = 1; i< smallest.length;i++){
      if(samllest.compareTo(a[i]) > 0) smallest = a[i];
      return smallest;
    }
  }
}
//String.compareTo()方法按字典序比较两个字符串的大小。当两个字符串一样长时,比较相同索引处的字符charAt(k)处的Union值,排在前面的值小返回一个负值;两个字符串不一样长时,短的字符串排在前面。
public static <T extends Comparable> T min( T[] a)...
  //此处限定 T 为实现Comparable接口的类型。此时泛型方法 min 只能由实现了Comparable 接口的类调用,未实现 Comparable 接口的类调用会产生编译错误。

​ 此处使用关键字 extends 而不使用 implements 是因为 T 应该是绑定类的子类,绑定类即可能是接口又可能是类,所以使用 extends 关键字。一个类型变量可以有多个限定类型,限定类型用 “&” 分隔,逗号用来分隔类型变量。

为了定义一个类为泛型类型,需要将泛型类型放在类名之后;为了定义一个方法为泛型类型,要将泛型类型放在方法的返回类型之前。

1.5 泛型代码与虚拟机:

​ 虚拟机没有泛型类型对象,所有的对象都属于普通类。

1.5.1 类型擦除:

​ 定义一个泛型类型,虚拟机会自动提供一个相应的原始类型,原始类型的名字就是删去类型参数后的泛型类类型名。编译器使用泛型类型信息来编译代码随后在运行时消除它,即擦除类型变量,泛型存在于编译时,一旦编译器确认泛型类型是安全使用的,就会将它转换成原始类,并替换为限定类型,无限定类型的变量用 Object 代替。原始类型用第一个限定的类型变量来替换。

public class Pair<T> {	//没有限定的原始类型Pair
    private T first;
    private T second;
    public Pair(){
        first = null;
        second = null;
    }
    public Pair(T First, T second){
        this.first = first;
        this.second = second;
    }
}
// 虚拟机提供一个原始类型,将限定类型带入类型参数将类型变量擦除。结果是一个普通类。
public class Pair{
    private Object first; //由于Pair未限定类型变量,所以用Object替换。
    private Object second;
    public Pair(){
        first = null;
        second = null;
    }
    public Pair(Object First, Object second){
        this.first = first;
        this.second = second;
    }
}

1.5.2 翻译泛型表达式:

​ 当程序调用泛型方法时,若擦除返回类型,编译器插入强制类型转换。同时,存取一个泛型域时也会发生插入强制类型转换。

Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst(); 
//擦除getFirst()返回类型后返回Object类型,编译器自动插入Employee的墙壁类型转换。

​ 编译器为上述两条指令做了这些虚拟机指令翻译:

1) 对原始方法Pair.getFirst()方法的调用。	
2) 将返回的Object类型强制转换成Employee类型。

1.6 通配泛型:

​ 下面程序定义了一个泛型栈,设定为Integer类型,max方法接收一个Numer类型的泛型栈,但是下面程序会报运行时错误,因为虽然 Integer 是 Numer 的子类型,但是 intStack 不是GenericStack的实例,所以不能调用max方法.使用通配泛型类型可以解决这类问题。

​ 通配泛型类型分为三种形式,分别是:

​ 第一种: ? 称为非受限通配( unbounded wildcard ) 。同第二种 ? extends Object 等价。

​ 第二种: ? extends T 称为受限通配(bounded wildcard), 表示 T 或 T的一个未知子类型。

​ 第三种: ? super T 称为下限通配 (lower-bound wildcard表示T或T的一个未知父类型)。

public class WildCardDemo1 {
    public static void main(String[] args) {
        GenericStack<Integer> intStack = new GenericStack<>();
        intStack.push(1);
        intStack.push(2);
        intStack.push(-2);
        // 这里会出现运行时错误。虽然 Integer 是 Numer 的子类型,但是 intStack 不是GenericStack的实例。不能调用max.
        System.out.println("The max number is:" + max(intStack));
    }
  // 改为下述写法即可修复错误。
  //public static double max(GenericStack stack)
  // GenericStackstack 表示Numer或 Number 的子类型的通配类型。
    public static double max(GenericStack<Number> stack){
        double max = stack.pop().doubleValue();
        while(!stack.isEmpty()){
            double value = stack.pop().doubleValue();
            if(value > max){
                max = value;
            }
        }
        return max;
    }
}

​ 泛型类型和通配类型之间的继承关系如下图所示:不同继承关系要使用不同的通配类型表达。
Java泛型学习_第1张图片

需要注意的是:不管实际的具体类型是什么,泛型类是被它的所有实例所共享的,例如:创建list1和list2两种不同的实例,由于虚拟机中没有泛型类型对象,所以在运行时在虚拟机中加载的只有ArrayList类,list1与list2都是ArrayList的实例。

ArrayList<String> list1 = new ArrayList<String>();
ArrayList<Integer> list2 = new ArrayList<String>();
// 创建两个不同类型的泛型实例,在虚拟机中他们都是ArrayList的实例。
System.out.println(list1 instanceof ArrayList);
System.out.println(list2 instanceof ArrayList);
// 上述表达式都是True;但是list1 instanceof ArrayList是错误的。

1.7 使用泛型的一些限制:

1.不能使用 new E();不能使用泛型类型创建实例。

```java
E object = new E();
// 错误,运行时执行 new E()但是运行时泛型类型 E是不可用的。
```

2.不能使用new E []; 不能使用泛型参数创建数组。

E[] elements = new E[capacity];
// 错误。
// 可以创建一个Object类型的数组,然后强制类型转换为E[]来避开这种写法
E[] elements = (E[]) new Object[capacity];
// 这种写法会得到一个编译警告,编译无法保证强制类型转换的正确性。

3.在静态环境下不允许类的参数是泛型类型。

泛型类中不存在有泛型类型参数的方法与泛型参数类型数据域。

public class Test<E>{
  public static void m(E 01){
    
  }
  public static E o1;
  static{
    
  }
}
// 上述语句都是非法的。因为反省了的所有实例都是相同的运行时类,故反省了的静态变量和方法是被所有实例所共享的。

4.异常不能是泛型的。

泛型类不能扩展 java.lang.Trhrowable 。JVM会坚持try…catch中的类型是否匹配,然而运行时泛型类的类型信息是不出现的。

你可能感兴趣的:(java学习,java,开发语言,后端)