[CoreJava]Java泛型详解

/**
 * @version 1.0
 * @author 子月初七
 */
复制代码

1.泛型简介

泛型是从Java1.5开始引进的,所谓的泛型可以理解成参数化类型,即类型是以参数的方式传入泛型类或者泛型方法。 泛型这个术语的意思是:“适用于许多许多的类型”。

1.1 泛型的好处

泛型可以使编写的代码被很多不同的类型对象所重用。

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

泛型类可以看成普通类的工厂。

  • 使用泛型之前
package com.corejava.genericprogramming;

public class ArrayList{
    private Object[] elementData;
    ...
    public Object get(int i ){...}
    public void add(Object o){...}

}
复制代码

这种方法有两个问题。

  1. 获取值的时候必须强制转换。容易出现类型转换错误。
 ArrayList files = new ArrayList();
 String filename = (String) files.get(0);
复制代码
  1. 可以向数组列表添加任意类型对象。
  • 使用泛型之后
//ArrayList类有一个类型参数用来指示元素的种类
ArrayList<String> files = new ArrayList<String>();
//JavaSE 7后,构造函数可以省略泛型类型。
ArrayList<String> files = new ArrayList<>();
复制代码

当调用get的时候,不需要进行强制类型转换。 编译器就知道返回值类型为String,而不是Object。

String filename = files.get(0);
复制代码

1.2 泛型类 Generic Class

具有一个或多个类型变量的类称之为泛型类。

下面是一个泛型类。

package com.corejava.genericprogramming;

public class Pair<T> {

    private T first;

    private T second;

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

	public T getFirst() {
		return first;
	}

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

	public T getSecond() {
		return second;
	}

	public void setSecond(T second) {
		this.second = second;
	}
	
}

复制代码

由上段代码可见,类型变量位于类名之后的尖括号<>中。

可以引进多个类型变量。

public class Pair<T, U> {...}
复制代码

在Java库中,E表示集合的元素类型,K和V表示关键字和值,T或者U或者S等表示任意类型。

1.3 泛型方法 Generic Method

泛型方法可以定义在普通类中,也可以定义在泛型类中。

注意,类型变量放在修饰符后面,返回类型前面。

package com.corejava.genericprogramming;

public class ArrayAlg {
    //如果没有<T>声明,不能称之为泛型方法
    public static <T> T getMiddle(T... a) {
        return a[a.length / 2];
    }

    public static void main(String[] args) {
        //System.out.println(ArrayAlg.<String>getMiddle("Matthew","Xu","CoreJava"));
        //一般情况下可以省略<String>类型参数。
        //编译器有足够的信息推断出所调用的方法。
        System.out.println(ArrayAlg.getMiddle("Matthew","Xu","CoreJava"));
    }
}

复制代码

1.4 泛型接口 Generic Interface

如下定义一个泛型接口。

package com.corejava.genericprogramming;

public interface IGeneric<T> {
    public T test();
}
复制代码

泛型接口未传入泛型实参时

package com.corejava.genericprogramming;

public class TestGeneric<T> implements IGeneric<T>{

	@Override
	public T test() {
		return null;
	}

}
复制代码

泛型接口传入泛型实参时

package com.corejava.genericprogramming;

public class TestGeneric implements IGeneric<String>{

	@Override
	public String test() {
		return null;
	}

}
复制代码

1.5 类型变量的限定

有的时候,类或方法需要对类型变量加以约束。 下面是一个计算最小元素的例子。

package com.corejava.genericprogramming;

public class ArrayAlg {
    public static <T> T getMin(T[] a) {
        if( a == null || a.length == 0)
            return null;
        T min = a[0];
        for (T t : a) {
            if(min.compareTo(t) > 0)
            	min = t;
        }
        return min;
    }
}

复制代码

这里有个问题,T是任意类型,但是不能保证T含有compareTo方法,在编写代码过程中会直接报错。

解决方案便是给T加上限定,使其实现Comparable接口。

public static <T extends Comparable> T getMin(T[] a){...}
复制代码

一个类型变量或者通配符可以有多个限定。

无论限定是接口还是类,只用extends来连接。

选择关键字extends的原因是更接近子类的概念。

限定类型用&分隔,而逗号用来分隔类型变量。

类型变量的限定类型中可以有多个接口,但最多一个类。

如果需要用一个类作为限定,它必须是限定列表中的第一个。

T extends Comparable & Serializable
复制代码

1.6 泛型代码和虚拟机

请注意,虚拟机没有泛型类型对象——所有对象属于普通类。

1.6.1 类型擦除

泛型类型的原始类型是删除类型参数后的泛型类型名。

擦除类型变量,并且替换成限定类型。

如果没有限定类型,使用Object。

例如,Pair的原始类型如下所示。

package com.corejava.genericprogramming;

public class Pair {

    private Object first;

    private Object second;

    public Pair(Object first, Object second){
        this.first = first;
        this.second = second;
    }

	public Object getFirst() {
		return first;
	}

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

	public Object getSecond() {
		return second;
	}

	public void setSecond(Object second) {
		this.second = second;
	}
    
}

复制代码

如果泛型类型含有限定的类型变量,那么原始类型可以使用第一个限定的类型变量替换。如果没有限定就用Object替换。

代码示例:

package com.corejava.genericprogramming;

import java.io.Serializable;

public class Interval <T extends Comparable & Serializable> implements Serializable{
	private T lower;
	
	private T upper;
	
	public Interval(T first, T second) {
		if(first.compareTo(second) <= 0) {
			lower = first;
			upper = second;
		}else {
			lower = second;
			upper = first;
		}
	}
}

复制代码

原始类型:

package com.corejava.genericprogramming;

import java.io.Serializable;

public class Interval implements Serializable{
	private Comparable lower;
	
	private Comparable upper;
	
	public Interval(Comparable first, Comparable second) {
		if(first.compareTo(second) <= 0) {
			lower = first;
			upper = second;
		}else {
			lower = second;
			upper = first;
		}
	}
}

复制代码

2. 泛型的约束与局限性

2.1 不能使用基本类型实例化类型参数

以之前的Pair类为例,只有Pair<Double>,没有Pair<double>。

原因:

擦除之后,Pair类含有Object类的域,而Object不能存储double值。

笔者在这里其实有个疑惑,就算擦除之后,为什么Object不能存放double?

double是可以自动装箱成Double,而Double作为一个对象类型是可以被Object存储的。

2.2 运行时类型查询只适用于原始类型

请看下列两段代码。

package com.corejava.genericprogramming;

public class Generic<T> {
	private T first;
	
	private T second;

	public Generic(T first, T second) {
		super();
		this.first = first;
		this.second = second;
	}

	public T getFirst() {
		return first;
	}

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

	public T getSecond() {
		return second;
	}

	public void setSecond(T second) {
		this.second = second;
	}
}

复制代码
package com.corejava.genericprogramming;

import org.junit.jupiter.api.Test;

class GenericTest {

	
	@Test
	void test() {
		Generic<String> genericStr = new Generic<>("first", "second");
		//error:Cannot perform instanceof check against parameterized type Generic<String>. 
		//Use the form Generic<?> instead since further generic type information will be erased at runtime
		//System.out.println(genericStr instanceof Generic<String>);
		
		//error:Cannot perform instanceof check against parameterized type Generic<String>. 
		//Use the form Generic<?> instead since further generic type information will be erased at runtime
		//System.out.println(genericStr instanceof Generic<T>);
		
		//输出:true
		System.out.println(genericStr instanceof Generic);
		
		//输出:class com.corejava.genericprogramming.Generic
		System.out.println(genericStr.getClass());
	}


}

复制代码

由此可见,如果想查询一个对象是否属于某个泛型类型时,使用instanceof会得到一个编译器错误。

如果使用getClass方法则会返回一个原始类型。

2.3 不能创建参数化类型的数组

不能实例化参数化类型的数组

package com.corejava.genericprogramming;

import org.junit.jupiter.api.Test;

class GenericTest {

	
	@Test
	void test() {
		//实际这行代码会报错,报错信息:Cannot create a generic array of Generic<String>
		Generic<String>[] array = new Generic<String>[10];
		//类型擦除后,array的类型即为Generic[]。
		//可以把它转换成Object[]类型。
		Object[] objarray = array;
		//数组会记住它的元素类型,当传入其他类型元素,本应会报错。
		//但是对于泛型来说,擦除使检查机制无效。
		//出于这个原因,参数化类型的数组不允许被创建。
		objarray[0] = "hello";
	}


}

复制代码

需要说明的是,声明类型为Generic[]的变量仍是合法的,但是不能用new Generic[10]进行初始化。

如果需要收集参数类型化对象,只能使用ArrayList。

2.4 不能实例化类型变量

不能使用new T(...)或者new T[...]或T.class这样的表达式中的类型变量。 下面的构造器是非法的。

    public Generic(){
        first = new T();
        second = new T();
    }
复制代码

2.5 不能构造泛型数组

public static <T extends Comparable> T[] minmax(T[] a) {
        //error:
		T[] mm = new T[2];
	}
复制代码

2.6 泛型类的静态上下文中类型变量无效

不能在静态域或方法中引用类型变量。 请看代码:

package com.corejava.genericprogramming;

public class Generic<T> {
	private T first;
	
	private T second;

	public Generic(T first, T second) {
		super();
		this.first = first;
		this.second = second;
	}

	public T getFirst() {
		return first;
	}

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

	public T getSecond() {
		return second;
	}

	public void setSecond(T second) {
		this.second = second;
	}

	public void test(T t) {
		System.out.println("hello");
	}
	//如果不声明T,会给出下列报错信息。
	//error: Cannot make a static reference to the non-static type T
	public static <T> void name(T t) {
		System.out.println("hello");
	}
}
复制代码

2.7 不能抛出或捕获泛型类的实例

既不能抛出也不能捕获泛型类对象。

实际上,甚至泛型类拓展Throwable都是不合法的。

//The generic class Generic<T> may not subclass java.lang.Throwable
public class Generic<T> extends Exception{...}
复制代码

3. 泛型类型的继承规则

无论S与T有什么联系,通常,Pair<E>与Pair<T>没有什么联系。 请看下列代码。

package com.corejava.genericprogramming;

import org.junit.jupiter.api.Test;

class GenericTest {

	
	@Test
	void test() {
		Manager ceo = new Manager();
		Manager cfo = new Manager();
		Employee employee = new Employee();
		Generic<Manager> managerGroup = new Generic<>(ceo, cfo);
		//error:Type mismatch: cannot convert from Generic<Manager> to Generic<Employee>
		Generic<Employee> employeeGroup = managerGroup;
		employeeGroup.setFirst(cfo);
	}


}

复制代码

注意泛型和数组之间的区别。

package com.corejava.genericprogramming;

import org.junit.jupiter.api.Test;

class GenericTest {

	
	@Test
	void test() {
		Manager ceo = new Manager();
		Manager cfo = new Manager();
		Employee lowerEmployee = new Employee();
		Manager[] managerGroup = new Manager[] {ceo, cfo};
		//操作合法
		Employee[] employeeGroup = managerGroup;
		//error: Type mismatch: cannot convert from Employee to Manager
		managerGroup[0] = lowerEmployee;
	}

}

复制代码

4. 通配符类型

4.1 通配符概念

通配符类型中,允许类型参数变化。

例如,通配符类型Pair< ? extends Employee>表示任何泛型Pair类型,它的类型参数是Employee的子类。

假设要编写一个方法。

package com.corejava.genericprogramming;

public class GenericTest {
	public static void printGroup(Generic<Employee> g) {
		System.out.println(g.getFirst().getName() + g.getSecond().getName());
	}
}

复制代码

但是这个方法不能将Generic传入。

public static void main(String[] args) {
		Manager m1 = new Manager();
		Manager m2 = new Manager();
		m1.setName("mat");
		m2.setName("xu");
		Generic<Manager> gm = new Generic<Manager>(m1, m2);
		//error:The method printGroup(Generic<Employee>) in the type GenericTest is not applicable for the arguments (Generic<Manager>)
		printGroup(gm);
	}
复制代码

解决的办法便是使用通配符类型。

public static void printGroup(Generic<? extends Employee> g) {...}
复制代码

但笔者在这里有个疑惑,为什么不能这么写。

public static void printGroup(Generic<T extends Employee> g) {...}
复制代码

Eclipse给出的报错信息是:

Incorrect number of arguments for type Generic<T>; it cannot be parameterized with arguments <T, Employee>
复制代码

也就是说其实<? extends Employee>算一个参数,而算两个参数。 对应的泛型类Generic只包含一个类型参数,所以后者不能通过。

5. 结语

本文参考了大量文章,未来会加入《Java编程思想》和《Effective Java》的内容和观点。目前主要内容还是来自《Java核心技术》,通过这篇博文的撰写,原先比较陌生的Java泛型现在也比较熟悉了。但是仍然未涉及其复杂之处。如果需要转载,请注明出处!如果有什么疑问或者见解,请分享你的观点!谢谢大家!

参考链接

  1. java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
  2. 《Java核心技术卷I》
  3. Java泛型
  4. Java Generics FAQs - Frequently Asked Questions
  5. 《Java编程思想》
  6. 《Effective Java》

猜你喜欢

转载自juejin.im/post/5d2195c26fb9a07eba2c5e4a