关于java:泛型的一些事

43次阅读

共计 7857 个字符,预计需要花费 20 分钟才能阅读完成。

1. 为什么应用泛型

简略来说,泛型使类型在定义类、接口和办法时成为参数。就像在办法申明中应用形式参数一样,类型参数提供了一种应用不同输出重用雷同代码的办法。不同之处在于形式参数的输出是值,而类型参数的输出是类型。

应用泛型的代码比非泛型代码有许多益处:

  • 在编译时进行更弱小的类型查看。
    Java 编译器将强类型查看利用于通用代码,并在代码违反类型平安时收回谬误。修复编译时谬误比修复运行时谬误更容易,后者很难发现错误源头。
  • 打消转型

    以下没有泛型的代码片段须要强制转换:

    List list = new ArrayList();
    list.add("hello");
    String s = (String)list.get(0); 

    应用泛型时,不须要类型转换:

    List <String> list = new ArrayList<String>();
    list.add("hello");
    String s = list.get(0); // 没有转型 
  • 使程序员可能实现通用算法。
    通过应用泛型,程序员能够实现通用算法,这些算法能够解决不同类型的汇合,能够自定义,并且类型平安且易于浏览。
    • *
  1. 泛型类

=======

泛型类是对类型进行参数化的类或接口。上面一步步展现该概念。

2.1 简略的 Box 类

如果咱们想在一个类中寄存任何类型的对象,怎么做呢?没错,应用 Object 即可。

上面展现一个可对任何类对象进行操作的非泛型 Box 类:

public class Box {
    private Object object;

    public void set(Object object) {this.object = object;}
    public Object get() { return object;}
} 

因为它的办法承受或返回一个Object,所以你能够自在地传入任何你想要的货色。在编译时无奈验证类的应用形式。代码的一部分可能会搁置一个Integer,并冀望从中获取Integer,而代码的另一部分可能会谬误地传入String,从而导致运行时谬误。

2.2 Box 类的泛型版本

下面提到,通过 Object 存储,不存在任何类型信息,这可能导致应用时类型谬误。于是泛型发挥作用了。

泛型类定义格局如下:

class name<T1, T2, ..., Tn> {/* ... */} 

用尖括号将类型参数包起来,并跟在类名前面。

于是 2.1 中的代码批改之后如下:

public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) {this.t = t;}
    public T get() { return t;}
} 

如代码所示,所有 Object 都被 T 替换。类型变量能够是制订的任何 非根本 类型:类、接口、数组或者其余类型变量。且类型变量 T 能够在类的任何地位应用。

同样,也实用于将泛型利用于接口,如下:

interface Box<T> {/*...*/} 

2.3 类型参数命名约定

依照常规,类型参数名称是单个大写字母。

最罕用的类型参数 (标识符) 名称是:

  • E – Element(Java Collections Framework 宽泛应用)
  • K – key
  • N – number
  • T – 类(类型)
  • V – value
  • S,U,V 等 – 第 2,第 3,第 4 类型

2.4 调用和实例化泛型类

将 T 替换为某些具体类即可,例如 Integer:

 Box<Integer> integerBox = new Box<Integer>();

// 在 Java SE 7 及更高版本中,只有编译期能够从上下文中确定或推断类型参数,就能够用一组空的类型参数“<>”替换调用泛型类的构造函数所需的类型参数
// 如下:Box<Integer> integerBox = new Box<>(); 

泛型类的调用通常称为参数化类型

2.5 多种类型参数

泛型类能够有多个类型参数,如下展现一个通用的 OrderPair 类,实现了 Pair 接口:

public interface Pair<K, V> {public K getKey();
    public V getValue();}

public class OrderedPair<K, V> implements Pair<K, V> {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
    this.key = key;
    this.value = value;
    }

    public K getKey()   { return key;}
    public V getValue() { return value;}
} 

以下语句创立两个 OrderPair 类的实例:

Pair<String,Integer> p1 = new OrderedPair<String,Integer>("Even",8);
Pair<String,String> p2 = new OrderedPair <String,String>("hello","world");

// 或如下
Pair<String,Integer> p1 = new OrderedPair<>("Even",8);
Pair<String,String> p2 = new OrderedPair <>("hello","world"); 

能够看到,别离将 K、V 实例化为 String、Integer 和 String、String,因为主动装箱机制,这里传入的根本数据类型会主动包装为其对应值的对象。

根本数据类型不能作为参数类型,之所以能够传入根本类型参数,是因为主动装箱机制会将其转化为对应值的对象。

2.6 参数化类型

参数化类型(如 List<String>)耶尔能够作为类型参数,如:

OrderedPair<String, Box<Integer>>> p = new OrderedPair<>("primes",new Box<Integer>(...)); 

2.7 “ 原生 ” 类型

原生类型(Raw type)是没有任何类型参数的泛型类 / 接口的名称,即原生类型的概念只针对泛型而言。

例如,给定泛型 Box 类:

public class Box<T> {public void set(T t) {/* ... */}
    // ...
} 

在创立参数化类型的 Box<T>,须要传入理论类型参数,如:

Box <Integer> intBox = new Box<>(); 

然而,如果不指定类型参数,那么则会创立一个原生类型 Box:

Box rawBox = new Box(); 

Box 是泛型 Box<T> 的原生类型。

换个更相熟的例子,List<String> 的 原生类型是 List,即原生类型能够了解为去掉了泛型类型信息。

原生类型次要存在于历史遗留代码中(JDK 5.0 以前),因为许多类 在 JDK 5.0 以前是不反对泛型的,所以为了向后兼容,令原生类型默认提供 Object,而后容许将参数化类型赋值给原始类型:

Box <String> stringBox = new Box<>();
Box rawBox = stringBox;  // 这是没问题的 

然而当将原生类型赋值给参数化类型,或者原生类型调用泛型类型中定义的方形办法,都会收到正告:

 Box rawBox = new Box();  // rawBox 是 Box<T> 的原始类型
 Box <Integer> intBox = rawBox;  //warning: unchecked conversion
 
 Box <String> stringBox = new Box<>();
 Box rawBox = stringBox;
 rawBox.set(8);  //warning: unchecked invocation to set(T) 

上述原生类型会绕过泛型类型查看,这会导致捕捉不平安的代码推延到运行时,因而 应该防止应用原生类型


  1. 泛型办法

========

泛型办法是引入其本人的类型参数的办法。这相似于申明泛型类型,但类型参数的范畴仅限于申明它的办法。容许应用动态和非动态泛型办法,以及泛型类构造函数。

泛型办法的语法包含位于尖括号外部的类型参数列表,它置于办法返回类型之前。

[权限修饰词] <T1,T2,...,Tn> methods(/*...*/) {/*...*/} 

上面举个例子:

public class Util {public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

public class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public void setKey(K key) {this.key = key;}
    public void setValue(V value) {this.value = value;}
    public K getKey()   { return key;}
    public V getValue() { return value;}
} 

Util 类蕴含一个泛型办法 compare,用以比拟两个 Pair 对象。

调用此办法的残缺语法如下:

Pair <Integer,String> p1 = new Pair<>(1,"apple");
Pair <Integer,String> p2 = new Pair<>(2,"pear");
boolean same = Util.<Integer,String> compare(p1,p2); 

因为已明确提供该类型,通常,能够省略参数类型,编译期将推断所需的类型:

Pair <Integer,String> p1 = new Pair<>(1,"apple");
Pair <Integer,String> p2 = new Pair<>(2,"pear");
boolean same = Util.compare(p1,p2); 

此性能称为 类型推断,容许将泛型办法作为一般办法来调用,而无需在尖括号之间指定类型。


  1. 有界类型参数

==========

有时,咱们心愿能够限度类型参数的类型。例如,对数字操作的办法可能只想接管 Number 或其子类的对象。这种状况下,有界类型参数就发挥作用了。

申明有界类型参数,须要指定类型参数的名称,而后是 extends 关键字,后接其下限。

<T extends SomeType> 

留神此情景下的 extends 蕴含了通常意义的 extends(在类中) 和 implements(在接口中)

例子如下:

public class Box<T> {

    private T t;          

    public void set(T t) {this.t = t;}

    public T get() {return t;}

    public <U extends Number> void inspect(U u){System.out.println("T:" + t.getClass().getName());
        System.out.println("U:" + u.getClass().getName());
    }

    public static void main(String[] args) {Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(new Integer(10));
        integerBox.inspect("some text"); // error: this is still String!
    }
} 

这里咱们指定接管 Number 及其子类型对象,于是当咱们想 inspect 办法传递一个 String 对象时,会产生谬误。

上述限度类型只是有界类型参数的作用之一,其实潜在的更重要的性能时,有界类型参数容许咱们调用边界中定义的办法:

public class NaturalNumber<T extends Integer> {

    private T n;

    public NaturalNumber(T n)  {this.n = n;}

    public boolean isEven() {return n.intValue() % 2 == 0;
    }

    // ...
} 

isEven() 办法通过 n 调用 Integer 类中定义的 intValue 办法。

4.1 多个边界

实际上类型参数能够有多个边界:

<T extends B1 & B2 & B3> 

具备多个边界时,类型变量是指定的所有类型的子类型。

留神:边界中必须将 类 Class 放在 接口 interface 之前,否则会出错:

Class A {/* ... */}
interface B {/* ... */}
interface C {/* ... */}

class D <T extends A & B & C> {/* ... */} 

4.2 泛型办法与有界类型参数

有界类型参数往往是通用算法实现的要害。思考以下办法,该办法计算数组 T[] 中大于指定元素 elem 的元素数量。

public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
} 

看起来办法很简略,然而编译会失败,这是因为”>“仅实用于根本类型,不能用与对象比拟。

解决此办法,能够思考应用由 Comparable<T> 接口限定的类型参数:

public interface Comparable<T> {public int compareTo(T o);
} 

批改后的代码如下:

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
} 

说实话,这里有些蒙蔽。。

经搜寻后,<T extends Comparable<T>> 这种写法就是相当于 <T>,然而 T 要 implements Comparable<T>,所以如果传入根本类型都是能够的,因为根本类型都是实现了 Comparable<T>接口的


  1. 泛型与继承

=========

通常,只有类型兼容(继承),就能够将一个类型的对象转换为另一个类型的对象。如下:

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger;  // Object 是 Integer 的父类 

同时 Integer 也是一种 Number(面向对象中继承示意”is-a“关系),所以上面代码也是无效的:

public void someMethod(Number n) {/* ... */}

someMethod(new Integer(10));   // OK
someMethod(new Double(10.1));   // OK 

泛型也是如此,能够执行泛型类型的调用,将 Number 作为其类型参数传递,如果参数与 Number 兼容,则容许任何调用:

Box<Number> box = new Box<Number>();
box.add(new Integer(10));   // OK
box.add(new Double(10.1));  // OK 

然而,世事无相对。思考以下办法:

public void boxTest(Box<Number> n) {/* ... */} 

** 你可能会因为它能够承受一个类型为 Box<Number> 的参数,依照下面的论断,就认为向其传递 Box<Integer> 或者 Box<Double>?这里须要强调,后者是不能传递的。因为 Box<Integer> 和 Box<Double> 并不是 Box<Number> 的子类型。

Box <Integer>不是 Box <Number> 的子类型,即便 IntegerNumber的子类型。

留神:给定具体类型 A、B,MyClass 和 MyClass 无关,二者惟一的交加是公共父类为 Object。

5.1 泛型类和子类型

一个类或接口的类型参数与另一个类或接口的类型参数之间的关系由 extends 和 implements 子句确定。

举个例子:Collection 类,ArrayList <E>实现 List <E>List <E> 扩大 Collection <E>。因而ArrayList <String>List <String>的子类型,它是 Collection <String> 的子类型。只有不扭转类型参数,就会在类型之间保留子类型关系。

image

此时,假如咱们要定义本人的 List 接口 PayloadList,它将 泛型 P 与每个元素绑在一起,它的申明如下:

interface PayloadList<E,P> extends List<E> {void setPayload(int index, P val);
  ...
} 

此时 PayloadList 的以下参数化是 List <String> 的 子类型:

  • PayloadList < 字符串,字符串 >
  • PayloadList < 字符串,整数 >
  • PayloadList < 字符串,异样 >

image


  1. 类型推断

========

类型推断 是 Java 编译器查看每个办法调用和相应申明,依据类型参数(或参数)进行适合的办法调用;类型推断会尝试查找实用于所有参数的_最具体_类型。

6.1 类型推断与泛型办法

在 3. 泛型办法 中介绍了类型推断,它使得你可能向调用一般办法一样调用泛型办法,而无需在尖括号之间指定类型。

通常,Java 编译期能够推断泛型办法调用的类型参数,因而少数状况下,不用指定。

仍旧是后面的例子,两种调用形式:

官网教程中将残缺写法称作 类型见证(type witness)

boolean same = Util.<Integer,String> compare(p1,p2);// 指定类型 
boolean same = Util.compare(p1,p2);// 不指定类型,Java 编译期会主动推断类型参数是 Integer 和 String 

6.2 类型推断和泛型类的实例化

在 2.4 调用和实例化泛型类中也提到过,只有编译器可能从杀昂下文中推断出类型参数,就能够用一组空的类型参数(<>) 替换调用泛型类的构造函数所需的类型参数。

例如,对以下变量申明:

Map<String, List<String>> myMap = new HashMap<String, List<String>>(); 

能够写成:

Map<String, List<String>> myMap = new HashMap<>(); 

6.3 类型推断 与 泛型 / 非泛型类的泛型构造方法

首先明确一点,泛型类和泛型办法没什么关系,一个类是不是泛型类与其中是否蕴含泛型办法无关。

构造函数在泛型和非泛型类中都能够是泛型的,换句话说,它们能够具备本人的类型参数:

class MyClass<X> {<T> MyClass(T t) {// ...}
} 

思考 MyClass 类的实例化:

new MyClass<Integer>(""); 

该语句将泛型类的类型参数 X 指定为 Integer,泛型构造方法的类型参数 T 指定为 String,因而实际上该构造函数的理论参数是 String 对象。

Java SE7 之前,编译期可能推断泛型结构参数的类型参数。Java SE7 之后,应用 <> 使编译期推断正在实例化的泛型类的类型参数:

MyClass<Integer> myObject = new MyClass<>(""); 

此例中,编译期将 泛型类 MyClass<X> 的类型参数 X 推断为 Integer,同时推断出泛型类的构造函数的类型参数 T 的类型为 String。

正文完
 0