Android架构师之路--Java进阶基础--泛型

Java进阶基础--泛型

一、什么是泛型

泛型(Generic)是Java编程语言的强大功能。它们提高了代码的类型安全性,使你在编译时可以检测到更多错误。

二、为什么使用泛型

与非泛型代码相比,泛型的优点

1.在编译时进行更强的类型检查。Java编译器将强类型检查应用于通用代码,如果代码违反类型安全,则会发出错误。修复编译时错误比修复运行时错误容易,后者可能很难找到。

2.消除类型转换。

3.是程序员能够实现通用算法。通过使用泛型,程序员可以实现对不同类型的集合进行工作,可以自定义并且类型安全且易于阅读的泛型算法。

三、通用类型

通用类型是通过类型进行参数化的通用类或接口。

通用类的定义格式:

class name { /* ... */ }

1.类型参数命名约定

按照约定,类型参数名称是单个大写字母。最常用的类型参数名称是:

① E-- Element (Java Collections Framework广泛使用)

② K - Key

③ N - Number

④ T - Type

⑤ V - Value

⑥ S,U,V etc. - 2nd, 3rd, 4th types

2.调用和实例化泛型类型

泛型类型的调用通常称为参数化类型。示例:

Box  Integer为类型参数(实参)

Box T为类型参数(形参)

要实例化此类,Box integerBox = new Box();

2.1The Diamond(菱形)

在Java SE7和更版本中,只要编译器可以从上下文确定或推断出类型参数,就可以用一组空的类型参数(<>)替换调用通用类的构造函数所需的类型参数。这对尖括号<>被非正式地称为菱形。

那么实例化类就变成:Box integerBox = new Box<>();

3.多种类型的参数

泛型类可以具有多个类型的参数。示例:

public interface Pair {

    public K getKey();

    public V getValue();

}

public class OrderedPair implements Pair {

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

}

实例化:

OrderedPair p1 = new OrderedPair<>("Even", 8);

OrderedPair p2 = new OrderedPair<>("hello", "world");

3.1 参数化类型

也可以用参数化类型(即List)替换类型参数(即K或V),示例:

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

四、原始类型

原始类型是没有任何类型参数的泛型类或接口的名称。示例:通用Box类 

public class Box {

public void set(T t) { /* ... */ }

}

创建Box的参数化类型,需要为形式类型参数T(形参)提供一个实际的类型参数(实参):

Box intBox = new Box<>();

如果省略实际的类型参数(实参),则创建Box的原始类型:

Box rawBox = new Box();

因此,Box是通用类型Box的原始类型。但是,非泛型类或接口类型不是原始类型(即原始类型也是一种泛型类型)。

原始类型显示在旧版代码中,因为在JDK5.0之前,许多API类(例如Collections类)不是通用的。使用原始类型是,你实际会获得泛型行为(Box为你提供对象)。为了向后兼容,允许将参数化类型分配给其原始类型:

Box stringBox = new Box<>();

Box rawBox = stringBox;    // OK

但是,如果将原始类型分配给参数化类型,则会有警告:

Box rawBox = new Box();  // rawBox is a raw type of Box

Box intBox = rawBox; // warning: unchecked conversion

如果你使用原始类型类调用在相应的泛型类型中定义的泛型方法,也会有警告:

Box stringBox = new Box<>();

Box rawBox = stringBox;

rawBox.set(8); // warning: unchecked invocation to set(T)

该警告表明原始类型会绕过通用类型检查,从而将不安全代码的捕获推迟到运行时。因此,因避免使用原始类型。

五、通用方法

通用方法是指引入自己的类型参数的方法。这类似声明一个泛型方法,但类型参数的范围仅限于声明它的方法。允许使用静态和非静态的泛型方法,也允许使用泛型类构造函数。

通用方法的语法包括类型参数列表,在尖括号内,该列表出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前(即类型参数列表必须在返回类型boolean 之前)。示例:

public class Util {

    public static boolean compare(Pair p1, Pair p2) {

        return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue());

    }

}

public class Pair {

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

}

调用此方法:

Pair p1 = new Pair<>(1, "apple");

Pair p2 = new Pair<>(2, "pear");

boolean same = Util.compare(p1, p2); // 也可以是Util.compare(p1, p2);

5.1、泛型,继承和子类型之间的关系

给定两种具体的类型A和B(例如Number和Integer),无论A和B是否相关,MyClass和MyClass没有关系。MyClass和MyClass公共父类是Object。

5.2、通用类和子类型之间的关系

可以通过扩展或实现来泛型通用类或接口。一个类或接口的类型参数与另一类或接口的类型参数之间的关系由 extends 和 implements 确定。interface PayloadList extends List { void setPayload(int index, P val); }

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

六、限定类型参数

1、限定类型参数的用途

对泛型变量的范围作出限制

2、声明一个限定的类型参数

单一限制:

多种限制:

extends通常用于表示“扩展”(如在类中)或“实现”(如在接口中)。

多种限制下的格式语法要求:如果范围之一是类,必须首先指定它(如果A是类,必须A放在最前面),否则编译错误。

3、通用方法和限定类型参数

限定类型参数是实现通用算法的关键。

七、类型推断

类型推断是Java编译器查看每个方法调用和相应声明以确定使调用适用的类型参数的能力。推断算法确定参数的类型,以及确定结果是否被分配或返回的类型(如果有)。最后,推断算法尝试找到与所有参数一起使用的最具体的类型。

重点理解编译器如何利用目标类型类推算泛型变量的值。

1、类型推断和通用方法

通用方法为你引入了类型推断,使你可以像调用普通方法一样调用通用方法,而无需在尖括号之间指定类型。

2、泛型类的类型推断和实例化

可以用一组空的类型参数(<>)替换调用通用类的构造函数所需的类型参数,只要编译器可以从上下文中推断类型参数即可。这对尖括号被非正式地称为菱形。

3、泛型和非泛型类的类型推断和泛型构造函数

构造函数在通用和非通用类中都可以是通用的(换句话说,声明自己的形式类型参数(形参))。

4、目标类型

Java编译器利用目标类型类推断通用方法调用的类型参数。表达式的目标类型是Java编译器期望的数据类型,具体取决于表达式出现的位置。

static List emptyList();

赋值:List listOne = Collections.emptyList();  //在Java SE 7和8中都可以使用

但是,如果是这样的方法:

void processStringList(List stringList) {

// process stringList

}

Java SE 7  中使用processStringList(Collections.emptyList()); 会出错 List cannot be converted to List 则必须使用processStringList(Collections.emptyList());

Java SE 8就不需要带上具体类型。

八、通配符

在通用代码中,称为通配符的问号(?)表示未知类型。可以在多种情况下使用:作为参数,字段或者局部变量的类型;有时作为返回类型(尽管更具体的做法是更好的变成习惯)。通配符从不用作泛型方法调用,泛型类实例创建或超类型的类型参数。

1、上限通配符

声明上限通配符,请使用通配符( ? ),后跟 extends 关键字,然后是其上限。请注意,在这种情况下, extends 通常用于表示“扩展”(如在类中)或“实现”(如在接口中)。上限通配符将未知类型限制为特定类型或该类型的子类型。

语法格式:

优点:扩大兼容的范围

对比:List要比List更加严格,因为前者仅能匹配XXX类型的列表,然而后者却可同时匹配XXX及其子类的列表

2、无限通配符

无界通配符类型使用通配符( ? )来指定。

使用场合:①如果正在编写一个可以使用Object类中提供的功能实现的方法。

②当代码使用通用类中不依赖类型参数方法时。如,List.size()方法,它并不关心List中元素的具体类型。

知识点:List是List的一个子类型

List和 List不同:前者不支持NULL,后者支持传入null。

3、下限通配符

声明下限通配符,请使用通配符( ? ),后跟 super 关键字,然后是其下限。下限通配符将未知类型限制为特定类型或该类型的超类型。

语法格式:

优点:扩大兼容的范围

对比:List要比List更加严格,因为前者仅能匹配XXX类型的列表,然而后者却可同时匹配XXX及其超类的列表

4、通配符之间的关系

泛型类(接口)间并无关联。仅仅因为它们和它们的类型相关。然而,可通过使用通配符在泛型类(接口)间建立联系

5、通配符匹配和辅助方法

利用辅助方法解决通配符类型推断问题。原理:泛型的类型推断。

6、PECS原则

如果只需要从集合中获得类型T,使用通配符,即只读。

如果只需要将类型T放到集合中,使用通配符,即只写。

如果既要获取又要放置元素,则不适用通配符。

PECS即Producer extends Consumer super,优点是提升了API的灵活性。

九、类型擦除

功能:在编译时提供更严格的类型检查并支持泛型编程,也是保证泛型不在运行时出现。

类型擦除可确保不会为参数化类型创建新的类,因此,泛型不会产生运行时开销。

1、什么是泛型擦除机制

Java的泛型是JDK5新引入的特性,为了向下兼容,虚拟机其实不支持泛型,所以Java实现的是一种伪泛型机制,也就是Java在编译期擦除了泛型信息,这样Java就不需要产生新的类型字节码,所有的泛型类型最终都是一种原始类型,在Java运行时根本不存在泛型信息。

2、Java编译器具体是如何擦除泛型的

1. 检查泛型类型,获取目标类型

2. 擦除类型变量,并替换为限定类型

    ①如果泛型类型的类型变量没有限定(),则用Object作为原始类型

    ②如果有限定(),则用XClass作为原始类型

    ③如果有多个限定(T extends XClass1&XClass2),则使用第一个边界XClass1作为原始类

3. 在必要时插入类型转换以保持类型安全

4. 生成桥方法以在扩展时保持多态性。Bridge Methods 桥接方法:当编译一个扩展参数化类的类或一个实现了参数化接口的接口时,编译器有可能因此要创建一个合成方法,名为桥接方法。它是类型擦除过程中的一部分。

3、使用泛型以及泛型擦除带来的影响

3.1、无法实例化具有基本类型的泛型类型

比如没有ArrayList,只有ArrayList。当类型擦除后,ArrayList的原始类中的类型变量T替换成Object,但Object类型不能存放int值。

3.2、无法将Casts或instanceof与参数化类型一起使用

当类型擦除后,ArrayList中类型变量String丢失不存在,所有没法使用instanceof。

使用ArrayList可以

3.3、无法声明类型为类型参数的静态字段

泛型类中类型参数的实例化是在定义泛型类型对象(ArrayList)的时候指定的,而静态成员不需要使用对象来调用,所有对象也就没有创建,无法确定这个泛型参数是什么。

3.4、无法重载每个重载的形式参数类型都擦除为相同原始(raw)类型的方法

一个类不能有两个重载的方法,这些方法在类型擦除后将具有相同的签名

3.5、无法创建类型参数的实例

类型不确定,T t = new T(); //创建不了

可以通过反射创建一个参数化类型的实例

3.6、无法创建参数化类型的数组

因为数组是协变,擦除后无法满足数组协变的原则。

A 父类 是 B,A[]  父类是 B[], 这就是数组的协变

3.7、无法创建,捕获或抛出参数化类型的对象

泛型类不能直接或间接扩展 Throwable 类

4、不可具体化类型

一个无法在整个运行时可知其类型信息的类型,其类型信息已在编译时被擦除。

例如:List和List,JVM无法在运行时分辨这两者

5、可具体化类型

一个可在整个运行时可知其类型信息的类型。

包括:基本类型、非泛型类型、原始类型和调用的非受限通配符。

6、堆污染

当参数化类型的变量引用的对象不是该参数化类型的对象时,就会发生堆污染。

在正常情况下,当同时编译所有代码时,编译器会发出未经检查的警告,以引起你对潜在堆污染的注意。如果分别编译代码部分,则很难检测到堆污染的潜在风险。如果确保代码在没有警告的情况下进行编译,则不会发生堆污染。

6.1、带泛型的可变参数的问题

T...将会被翻译为T[],根据类型擦除,进一步会被处理为Object[],这样就可能造成潜在的堆污染避免堆污染警告

@SafeVarargs:当你确定操作不会带来堆污染时,使用此注释关闭警告

@SuppressWarnings({"unchecked", "varargs"}):强制关闭警告弹出(不建议这么做)

你可能感兴趣的:(Android架构师之路--Java进阶基础--泛型)