Java基础知识点汇总 四 泛型

一、泛型概述

1、什么是泛型?

        泛型意味着参数化类型。允许类型(整数、字符串等,以及用户定义的类型)作为方法、类和接口的参数。使用泛型,可以创建使用不同数据类型的类。对参数化类型进行操作的实体(例如类、接口或方法)是泛型实体。

        Object是所有其他类的超类,Object 引用可以引用任何对象。这些功能缺乏类型安全性。泛型添加了这种类型的安全功能。

        Java 中的泛型类似于 C++ 中的模板。比如HashSet、ArrayList、HashMap等类,就很好的使用了泛型。

public class ArrayList extends AbstractList
        implements List, RandomAccess, Cloneable, java.io.Serializable

2、类型参数命名约定

        按照惯例,类型参数名称是单个大写字母。这与变量命名约定形成鲜明对比 ,并且有充分的理由:没有这种约定,就很难区分类型变量和普通类或接口名称之间的区别。

        最常用的类型参数名称是:

  • E - 元素(被 Java 集合框架广泛使用,比如上面的ArrayList
  • K - 键
  • N - 数字
  • T - 类型
  • V - 价值
  • S、U、V 等 - 第 2、第 3、第 4 类型

3、使用泛型的优点 

        1、代码重用:我们可以编写一次方法/类/接口,然后将其用于我们想要的任何类型。

        2、类型安全:泛型使错误在编译时出现而不是在运行时出现(在编译时知道代码中的问题总是比让代码在运行时失败更好)。

二、泛型的类型

 1、泛型的限制 

(1)泛型不能使用基本数据类型

        当我们声明泛型类型的实例时,传递给类型参数的类型参数必须是引用类型。我们不能使用基本数据类型,如intchar。

        比如下面的代码会导致编译时错误。

Test obj = new Test(20); 

        但是基本类型数组可以传递给类型参数,因为数组是引用类型。 

ArrayList a = new ArrayList<>();

(2)避免使用原始类型

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

        参考使用

Box intBox = new Box<>();

        请避免下面的用法

Box rawBox = new Box();

(3)无法创建类型参数的实例

        编译错误

public static  void append(List list) { 
    E elem = new E(); // 编译时错误
    list.add(elem); 
}

        可以通过反射创建类型参数的对象

public static  void append(List list, Class cls) throws Exception { 
    E elem = cls.newInstance(); // OK 
    list.add(elem); 
}

List ls = new ArrayList<>();
append(ls, String.class);

(4)不能声明类型为类型参数的静态字段

        编译错误示例

public class MobileDevice {
    private static T os;

    // ...
}

(5)不能将 Casts 或instanceof与参数化类型一起使用

         编译错误示例

public static  void rtti(List list) { 
    if (list instanceof ArrayList) { // 编译时错误
        // ... 
    } 
}

(6)不能创建参数化类型的数组

List[] arrayOfLists = new List[2]; // 编译时错误

(7)无法创建、捕获或抛出参数化类型的对象

        编译时错误示例

// 例1:间接扩展 Throwable 
class MathException extends Exception { /* ... */ } // 编译时错误

// 例2:直接扩展 Throwable 
class QueueFullException extends Throwable { /* ... */ // 编译时错误

// 例3:方法不能捕获类型参数的实例:
public static  void execute(List jobs) { 
    try { 
        for (J job : jobs) 
            // ... 
    } catch (T e) { // 编译时错误
        // .. . 
    } 
}

        可以在throws子句中使用类型参数

class Parser { 
    public void parse(File file) throws T { // OK 
        // ... 
    } 
}

(8)无法重载形式参数类型擦除为相同原始类型的方法

public class Example {
    public void print(Set strSet) { }
    public void print(Set intSet) { }
}

2、单参数示例 

class Test {
    // 对象
    T obj;
    // 构造函数
    Test(T obj) 
    { 
        this.obj = obj; 
    }

    public T getObject() { return this.obj; }
}
  

class Main {
    public static void main(String[] args)
    {
        // 实例化Integer类型
        Test iObj = new Test(15);
        System.out.println(iObj.getObject());
  
        // 实例化String类型
        Test sObj
            = new Test("泛型的简单示例");
        System.out.println(sObj.getObject());
    }
}

        输出

15
泛型的简单示例

3、多参数示例

class Test
{
	T obj1; // T 类型 对象
	U obj2; // U 类型 对象

	// 构造函数
	Test(T obj1, U obj2)
	{
		this.obj1 = obj1;
		this.obj2 = obj2;
	}

	// 打印
	public void print()
	{
		System.out.println(obj1);
		System.out.println(obj2);
	}
}


class Main
{
	public static void main (String[] args)
	{
		Test  obj =
			new Test("多参数泛型", 15);

		obj.print();
	}
}

        输出

多参数泛型
15

三、通用方法

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

class Test {
	// 泛型方法
	static  void genericDisplay(T element)
	{
		System.out.println(element.getClass().getName()
						+ " = " + element);
	}

	// Driver method
	public static void main(String[] args)
	{
		// 调用方法,传递Integer类型参数
		genericDisplay(11);

		// 调用方法,传递String类型参数
		genericDisplay("泛型方法");

		// 调用方法,传递Double类型参数
		genericDisplay(1.0);
	}
}

        输出

java.lang.Integer = 11
java.lang.String = 泛型方法
java.lang.Double = 1.0

四、有界类型参数

        有时您可能想要限制可用作参数化类型中的类型参数的类型。例如,对数字进行操作的方法可能只想接受 Numbers 或其子类的实例。这就是有界类型参数的用途。 

1、有界类型

class Bound
{
	
	private T objRef;
	
	public Bound(T obj){
		this.objRef = obj;
	}
	
	public void doRunTest(){
		this.objRef.displayClass();
	}
}

class A
{
	public void displayClass()
	{
		System.out.println("Inside super class A");
	}
}

class B extends A
{
	public void displayClass()
	{
		System.out.println("Inside sub class B");
	}
}

class C extends A
{
	public void displayClass()
	{
		System.out.println("Inside sub class C");
	}
}

public class BoundedClass
{
	public static void main(String a[])
	{
		Bound bec = new Bound(new C());
		bec.doRunTest();

		Bound beb = new Bound(new B());
		beb.doRunTest();

		Bound bea = new Bound(new A());
		bea.doRunTest();
		
		Bound bes = new Bound(new String());
		bea.doRunTest();
	}
}

        说明,最后的Bound bes = new Bound(new String());是编译不过去的。因为类型只限定类型 A 及其子类。

2、多重界限

        有界类型参数可以与方法以及类和接口一起使用。

        前面的示例说明了使用具有单个边界的类型参数,但类型参数可以具有多个边界。

class Bound
{
	
	private T objRef;
	
	public Bound(T obj){
		this.objRef = obj;
	}
	
	public void doRunTest(){
		this.objRef.displayClass();
	}
}

interface B
{
	public void displayClass();
}

class A implements B
{
	public void displayClass()
	{
		System.out.println("Inside super class A");
	}
}

public class BoundedClass
{
	public static void main(String a[])
	{
		Bound bea = new Bound(new A());
		bea.doRunTest();
		
	}
}

        具有多个边界的类型变量是边界中列出的所有类型的子类型。如果其中一个边界是一个类,则必须首先指定它。

        如果未首先指定 bound A,则会出现编译时错误:

class D  { /* ... */ } // 编译时错误

3、泛型方法和有界类型参数

        编译错误的示例,因为大于运算符 ( > ) 仅适用于原始类型,例如short、int、double、long、float、byte和char。

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

        但是Long、Integer等均实现了Comparable接口,所以可以使用Comparable接口限定的类型参数。

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

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

五、泛型、继承和子类型

1、子类型的误区

        看一段示例代码,下面的代码编译不过去,是因为Box和Box不是Box的子类型。

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

public class Box{ }

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

public void test()
{
    boxTest(new Box());
}

2、泛型类和子类型

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

//下面示例都是List有效的的子类型
PayloadList
PayloadList
PayloadList

六、类型推断

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

1、类型推断

        泛型方法类型推断,它使您能够像调用普通方法一样调用泛型方法,而无需在尖括号之间指定类型。

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

        上下文推断,只要编译器可以从上下文推断类型参数, 您就可以用一组空的类型参数 () 替换调用泛型类的构造函数所需的类型参数。

Map> myMap = new HashMap<>();

        推断构造函数的参数类型,它推断该泛型类的构造函数的形式类型参数 T 的类型为 String。

class MyClass {
         MyClass(T t) {
            // ...
        }
    }

    public void test()
    {
        MyClass myObject = new MyClass<>("");
    }

2、目标类型

// 下面两句都可以,但是第二句是没有必要的,Java SE 7 和 8都有效
List listOne1 = Collections.emptyList();
List listOne2 = Collections.emptyList();

void processStringList(List stringList) {
    // 处理 stringList
}

void test()
{
    //Java SE 7 和 8都有效
    processStringList(Collections.emptyList());
    //Java SE 7中编译不过去, Java SE 8有效
    processStringList(Collections.emptyList());
}

七、通配符

        在通用代码中,称为通配符 的问号 ( ? )表示未知类型。通配符可用于多种情况:作为参数、字段或局部变量的类型;有时作为返回类型(尽管更具体的是更好的编程实践)。通配符永远不会用作泛型方法调用、泛型类实例创建或超类型的类型参数。

1、上界通配符

        表示可以匹配匹配Foo和 Foo 的任何子类型。

public static void process(List  list) { /* ... */ }

2、无界通配符

        可以使用printList打印任何类型的列表

public static void printList(List list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

3、下限通配符

        表示可以匹配匹配Integer和其超类。

public static void addNumbers(List list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

八、类型擦除

        为了实现泛型,Java 编译器将类型擦除应用于:

        1、如果类型参数是无界的,则将泛型类型中的所有类型参数替换为其边界或Object 。因此,生成的字节码只包含普通的类、接口和方法。

        2、必要时插入类型转换以保持类型安全。

        3、生成桥方法以保留扩展泛型类型中的多态性。

1、擦除泛型类型

        在类型擦除过程中,Java 编译器擦除所有类型参数,如果类型参数是有界的,则将每个类型参数替换为其第一个边界,如果类型参数是无界的,则将其替换为Object。

2、擦除通用方法

        Java 编译器还会删除泛型方法参数中的类型参数。如果类型参数是无界的,则将其替换为Object。

        如果参数是有界的

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

// 泛型方法
public static  void draw(T shape) { /* ... */ }

// 被替换为
public static void draw(Shape shape) { /* ... */ }

3、桥接方法

        当编译扩展参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法,称为桥方法,作为类型擦除过程的一部分。您通常不需要担心桥接方法,但如果出现在堆栈跟踪中,您可能会感到困惑,所以需要了解是如何产生的。 

        泛型类

public class Node {

    public T data;

    public Node(T data) { this.data = data; }

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node {
    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

        下面代码运行时会报异常

MyNode mn = new MyNode(5);
Node n = mn;            // 原始类型
n.setData("Hello");     // 这句会报类型转换异常
Integer x = mn.data; 

         原因是:类型擦除后,Node和MyNode类变为

public class Node {

    public Object data;

    public Node(Object data) { this.data = data; }

    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node {

    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

        类型擦除后,方法签名不匹配;Node.setData (T)方法变为Node.setData(Object)。因此,MyNode.setData(Integer)方法不会覆盖 Node.setData(Object)方法。

        为了解决这个问题并在类型擦除后保留泛型类型的 多态性,Java 编译器生成了一个桥接方法以确保子类型按预期工作。

class MyNode extends Node {

    // 编译器生成的桥接方法
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    // ...
}

        桥接方法MyNode.setData(object)委托给原始MyNode.setData(Integer)方法。结果, n.setData("Hello");语句调用了方法 MyNode.setData(Object),并且 aClassCastException被抛出,因为"Hello"不能强制转换为Integer。

九、例题

1、问:下面的类

class Node implements Comparable {
    public int compareTo(T obj) { /* ... */ }
    // ...
}

        这样调用,是否可以编译

Node node = new Node<>();
Comparable comp = node;

答:可以

2、问:给定下面的类

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

class Node { /* ... */ }

        问是否可以编译。

Node nc = new Node<>();
Node  ns = nc;

答:不可以,因为Node不是Node的子类型。

3、问:下面的代码是否可以编译?

public class Singleton {

    public static T getInstance() {
        if (instance == null)
            instance = new Singleton();

        return instance;
    }

    private static T instance = null;
}

答:不可以,您不能创建类型参数T的静态字段。

4、问:类型擦除后下面的方法转换成什么?

public static > 
    int findFirstGreaterThan(T[] at, T elem) { 
    // ... 
}

答:

public static int findFirstGreaterThan(Comparable[] at, Comparable elem) { 
    // ... 
    }

5、问:类型擦除后下面的类转换成什么?

public class Pair {

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

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

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

    private K key;
    private V value;
}

答:

public class Pair {

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

    public Object getKey()   { return key; }
    public Object getValue() { return value; }

    public void setKey(Object key)     { this.key = key; }
    public void setValue(Object value) { this.value = value; }

    private Object key;
    private Object value;
}

6、问:如果编译器在编译时擦除了所有类型参数,为什么还要使用泛型?

答:因为
        Java 编译器在编译时对泛型代码执行更严格的类型检查。
        泛型支持编程类型作为参数。
        泛型使您能够实现泛型算法。

你可能感兴趣的:(java,JAVA,泛型,通用方法,代码复用,类型安全)