Java中泛型总结

前天晚上被人问了一个问题:“<?extends SomeClass>与<Textends SomeClass>的区别是什么?” ,思考了下,发现自己并不能解释清楚,于是有了这篇文章。(如果觉得文章太长可以直接看最下面的第七点)

 

0.术语 发现关于泛型的叫法在core java中存在不规范的情况容易误导初学者,在经过查证后整理了基本的术语

  用示例进行描述。ArrayList<E>类、ArrayList<Integer>类:

  • 整个称为ArrayList<E> 泛型类型。
  • ArrayList<E>中的E称为 类型变量 或 类型参数。
  • 整个ArrayList<Integer> 称为 参数化的类型。
  • ArrayList<Integer>中的Integer称为实际类型参数或type argument(不知道怎么翻译)。
  • ArrayList<Integer>中的<>念着typeof。整个ArrayList<Integer>称为"arraylist typof integer"。
  • ArrayList称为原始类型。

 

1.为什么要有泛型

泛型的引入是因为想要将一个类或方法用在不同的对象身上,在java1.5 之前并没有泛型,那是的解决办法是写一个拥有Object对象的类或传入Object对象的方法,再进行强制转换来达到代码复用的目的。

package generics;

public class Reason4G {
	private Object a;		//普通类中的Object对象
	public Reason4G(Object a){
		this.a = a;
	}
	public void set(Object a){
		this.a = a;
	}
	public Object get(){
		return a;
	}
	
	public static void main(String[] args) {
		Reason4G r4g = new Reason4G("string");
		r4g.set("string1");
		String s = (String) r4g.get(); //一个丑陋的强制类型转换警告,因为编译器不知道强制转换是否安全,在运行阶段可能会发生异常java.lang.ClassCastException
	}
	
}

于是为了更安全的考虑,最好是将错误的产生原因提前至编译器,由此产生了泛型

2.泛型类

还是刚才的例子,将上面的代码改用泛型来表示:

package generics;

public class GClass<T> {
	private T a;
	public GClass(T a){
		this.a  = a;
	}
	public void set(T a){
		this.a = a;
	}
	public T get(){
		return a;
	}
	//指定泛型类型与协变的关系
	public static void main(String[] args) {
		GClass<Fruit> fruitGClass = new GClass<>(new Fruit());
		//存入并读取基类
		fruitGClass.set(new Fruit());
		Fruit fruit = fruitGClass.get();
		
		//存入并读取子类对象
		fruitGClass.set(new Apple());
		fruit = fruitGClass.get();
		
		//返回的结果是一个Fruit对象,向下转型是不安全的
		//Apple apple = fruitGClass.get();
		Apple apple = (Apple)fruitGClass.get();
	}

}

class Fruit{
	
}

class Apple extends Fruit{
	
}

在改写成泛型类了以后,对比以前的非泛型类来说,对于set方法来说,将插入其他类型对象的错误时机提前到了编译器;对于get方法来说,不用自己再添加不安全的强制转换,返回参数被认为是创建类时给的泛型参数<Fruit>,经过反编译会发现编译器会在泛型类的get()方法处自动添加一个强制类型转换而不用我们手动添加。

3.泛型方法

泛型方法和定义该方法的类是否是泛型类无关,泛型方法的定义如下:

package generics;

public class GMethodTest {
public static void main(String[] args) {
	GMethod1 gm1 = new GMethod1();
	gm1.f("string");//泛型方法中可以利用类型推断,从传入参数的类型推出泛型方法使用的泛型类型
	gm1.<String>f("string");
	GMethod2.f("String");//泛型方法中可以利用类型推断,从传入参数的类型推出泛型方法使用的泛型类型
	GMethod2.<String>f("String");
}
}

//非泛型类GMethod,泛型方法定义时需要在返回类型前加上类型的参数列表
class GMethod1{
	public <T> void f(T x){
		System.out.println(x.getClass().getSimpleName());
	}
}

class GMethod2<T>{
	//静态方法要使用泛型参数必须成为泛型方法(无法获得泛型类中的泛型参数)
	//public static  void f(T x){
	public static <U> void f(U x){
		System.out.println(x.getClass().getSimpleName());
	}
}

在定义泛型方法时将参数列表放在返回值前面。这里有2点要注意:1静态方法和静态成员中不能使用泛型类中定义的参数,原因是类中的静态域是属于整个类的,一个泛型类就像一个生产普通类的工厂,如List<T>这个泛型类型可以有很多参数化类型List<String>,List<Integer>等。所以如果需要静态泛型方法必须要重新定义术语此方法的泛型类型。2.类型推导,详细内容在后面会说到。在这里我们可以看出,在调用泛型方法时,如果一个类型参数在方法入参和返回值其中的一处被用到,那么就可以省略指定实际类型参数而由参数推断得到参数。

 

4.泛型类型限定和擦除

4.1 泛型类型限定

考虑一个下面的程序:

 

package generics;
// 一个有2个方法的接口
public interface People {
	void eat();
	void run();
}

package generics;
// Student扩展了People接口
public class Student implements People {

	@Override
	public void eat() {
		System.out.println("Student eat");
		
	}

	@Override
	public void run() {
		System.out.println("Student run");
		
	}

}

package generics;

public class GErase {
	//当没有边界时编译时会有错误:The method eat/run() is undefined for the type T
	//原因是因为泛型的擦除,擦除后为参入参数的类型为Objcet,编译器无法确定是否有方法eat()和run();
	//public static<T> void f(T obj){        
	
	//引入擦除边界,当擦除时会擦除到第一个扩展的边界,意味泛型参数T为People的子类型,当然会有方法eat()和run();
	public static<T extends People> void f(T obj){
		obj.eat();
		obj.run();
	}

	public static void main(String[] args) {
		f(new Student());  //类型推导

	}

}


使用<T extends People>意味着这个方法可以被实现了People接口的类调用,如果只使用泛型变量T,编译器会不知道传入的参数obj是否拥有方法eat()和run()。所以相当于将泛型类型中的范围缩小到了某一个类型的子类中。在编译器的具体实现中,会将泛型参数擦除为People(第一个边界)而不是Object.

4.2类型擦除

泛型是提供给Javac编译器看的,可以限定集合中的输入类型,让编译器挡住源程序中的非法输入,编译器编译带参数类型说明的集合时会去去除掉“泛型类型”信息,使程序运行不受影响,对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样。具体的做法是,当一个对象在进入泛型类/方法时,首先编译器会检查该类是否符合泛型的规定,不符合就在编译器报错。当泛型方法返回对象时,编译器在程序中自动的插入强制转换语句保证我们得到的就是在泛型类型中定义的类型变量。

当泛型参数扩展了多个接口时如<T extends Student & Comparable & Serializable>,类限定放在第一位,标签接口(没有方法)放在最后。如果要调用第二个接口方法,编译器会在调用的地方自动插入一个强制转换。

4.3 由于泛型擦除导致的问题

(1).不能用基本类型当作实际类型参数

(2).不能创建参数化类型数组

A<String>[] table = new A<String>[10]();//错误,不能创建参数化类型数组。如果可以创建的话,那么在虚拟机中将分不出A<String>/A<Interger>..导致数组中存储的并非一个类型,所以不能创建。

(3).不能构造泛型数组

 

class A<T>{

	public T[] test(){
		T[] array = new T[2];//错误
      	        array =  (T[])new Object[2];
		return array;
	}
}

 

在方法中,不能创建泛型数组,理由同上,在执行过程中由于擦除的原因,导致创建时不能创建真实类型数组而是擦除类型数组。后面的那个强制转型其实没有任何作用,只是生成一个警告提示你不安全。

(4).泛型类的静态域中类型变量无效,这点已经在上文说过。
 

5.通配符

5.1上限通配符<? extends T>

限定泛型只能为T类型的子类或者本身

程序中Student继承了People

package generics;

public class GenericsTest<T> {
	private T item;
	public GenericsTest(T t){
		item = t;
	}
	public T get(){
		return item;
	}
	public void set(T t){
		item = t;
	}

	public static void main(String[] args) {
		//不能把子类泛型类型容器的对象向上转型为父类泛型类型容器的对象,换句话来说,对于两种确定的泛型参数类型的容器来说,它们之间没有关系
		//GenericsTest<People> gt = new GenericsTest<Student>(new Student());
		GenericsTest<? extends People> gt = new GenericsTest<Student>(new Student());//上限通配符
		// 上限通配符类型的容器不能向里面存放任何类型,因为它只知道要匹配一个子类型,但不知道具体应该是什么类型
		//gt.set(new Student());
		
		People p = gt.get(); // 可以读取一个上界的类型
	}

}

5.2 下限通配符<? super T>

泛型类型只能为T的超类或本身

Student1类继承了People1类

 

package generics;

public class GenericsTest1<T> {
	private T item;
	public GenericsTest1(T t){
		item = t;
	}
	public T get(){
		return item;
	}
	public void set(T t){
		item = t;
	}

	public static void main(String[] args) {
		//不能把超类泛型类型容器的对象向下转型为子类泛型类型容器的对象,换句话来说,对于两种确定的泛型参数类型的容器来说,它们之间没有关系
		//GenericsTest<Student1> gt = new GenericsTest<People1>(new People1());
		GenericsTest<? super People1> gt = new GenericsTest<People1>(new People1());//下限通配符
		// 下限通配符类型的容器能向里面存放任何类型
		gt.set(new Student1());
		gt.set(new People1());
		
		Object p = gt.get(); // 只可以读取一个Object类型,因为他们共有的超类只有Object
	}
}

上下限通配符的总结:下限通配符可以向泛型对象写入,上限通配符可以读取泛型对象


5.3无界通配符<?>

作用

1.告诉了编译器这是一个使用了某种泛型的类/方法,与原生类型<Object>不同的是,无界通配符不能调用set方法,而原生类型可以调用以Object为参数的泛型方法

2.利用类型捕获所捕获的类型来调用其他泛型方法

package generics;

public class CaptureConversion {
	static <T> void f1(GenericsTest<T> g){
		T t = g.get();
		System.out.println(t.getClass().getSimpleName());
	}
	
	//非泛型方法f2(),利用无界通配符捕获实际传入参数的类型,并调用f1()使用这个确切的类型
	static void f2(GenericsTest<?> g){
		f1(g);
	}
	public static void main(String[] args) {
		GenericsTest raw = new GenericsTest<Integer>(1);
		f2(raw);
		GenericsTest rawBase = new GenericsTest();
		rawBase.set(new Object());
		f2(rawBase);
		GenericsTest<?> wildcarded = new GenericsTest<Double>(1.0);
		f2(wildcarded);
	}

}

程序输出为:Integer, Object, Double ,说明f1()方法对传入的具体类型进行了捕获,调用了f2()方法来输出具体类型。(在f1()处没法直接输出,因为是无界通配符?).

6.泛型类型的继承关系

下面程序 Student1 类型 继承了 People1类型

package generics;

public class GInherent<T> {
	T t;
	void set(T t){
		this.t = t;
	}
	T get(){
		return t;
	}
	
}

class GInherentSonClass<T> extends GInherent<T>{
	
	
	
	
	public static void main(String[] args) {
		//可以赋值给自己
		GInherentSonClass<People1> g1 = new GInherentSonClass<People1>();
		//可以赋值给原始类型
		GInherentSonClass g2 = new GInherentSonClass<People1>();
		
		//对于同一个类的泛型来说,GInherentSonClass<People1> 和 GInherentSonClass<Student1> 没有任何继承关系
		//GInherentSonClass<People1> g = new GInherentSonClass<Student1>();
		
		//可以赋值给父类的同一泛型变量的对象
		GInherent<People1> g3 = new GInherentSonClass<People1>();
		//可以赋值给父类原始类型对象
		GInherent g4 = new GInherentSonClass<People1>();
		
	}
}

从上面程序的我们可以把继承的规则看成:1.每种泛型类都可以向其原始类型转换(但不能横跨原始类型转换) 2.子类类型(包括原始类型)可以向父类类型转换。

 

7.<? extends SomeClass>与<extends SomeClass>的区别是什么?

7.1<T extends SomeClass> 叫做有限制类型参数 ,产生它的原因是为了缩小泛型类型的范围,表示在SomeClass子类中的一个确定的类型。可以看作是一个具体类型T(受到SomeClass的限制)。

7.2<? extends SomeClass> 叫做通配符,产生它的原因是为了在泛型中将子类类型定义的泛型类型赋值给父类类型定义的泛型类型。表示在SomeClass子类中的任何类型,可以看作是一族类型<?>(SomeClass的子类的集合)

 

8.参数推导

 

  • 当某个类型变量只在整个参数列表中的所有参数和返回值中的一处被应用了,那么根据调用方法时该处的实际应用类型来确定,这很容易凭着感觉推断出来,即直接根据调用方法时传递的参数类型或返回值来决定泛型参数的类型。
    例: swap(new String[3],3,4) ---> static <E> void swap(E[] a, int i, int j)
  • 当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型都对应同一种类型来确定,这很容易凭着感觉推断出来。
    例:add(3,5) ---> static <T> T add(T a, T b)
  • 当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型对应到了不同的类型,且没有使用返回值,这时候取多个参数中的最大交集类型
    例:下面语句实际对应的类型就是Number了,编译没问题,只是运行时出问题。
    fill(new Integer[3],3.5f) ---> static <T> void fill(T[] a, T v)
  • 当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型对应到了不同的类型, 并且使用返回值,这时候优先考虑返回值的类型。
  • 例如,下面语句实际对应的类型就是Integer了,编译将报告错误,将变量x的类型改为float,对比eclipse报告的错误提示,接着再将变量x类型改为Number,则没有了错误。int x =(3,3.5f) ---> static <T> T add(T a, T b)

 

9.自限定类型

class SelfBounded<T extends SelfBounded<T>> 就是自限定类型,我们先看看在什么情况下能使用它,再说它有什么用:

//自限定泛型
public class SelfBounded<T extends SelfBounded<T>> {
	T element;
	SelfBounded<T> set(T arg){
		element = arg;
		return this;
	}
	T get(){
		return element;
	}
	
	public static void main(String[] args) {
		A a = new A();
		a.set(a);
		a = a.set(new A()).get();
		a = a.get();
		C c = new C();
		c = c.setAndGet(new C());
	}
}

class A extends SelfBounded<A>{
	
}

class B extends SelfBounded<A>{
	
}

class C extends SelfBounded<C>{
	C setAndGet(C arg){
		set(arg);
		return get();
	}
}

class D{
	
}
//错误,不能编译,要使用自限定类型,必须使用在一个继承树上的类型。如:class A extends SelfBounded<A> 常用 和 class B extends SelfBounded<A>不常用
//class E extends SelfBounded<D>{
//	
//}

可以看出,自限定类型有2种用法,主要的形式为Class A extends SelfBounded<A> ,它让新定义的类A作为其基类的边界。可以看作产生了一个新类型的基类SelfBounded,它使用导出类A作为它的参数。

下面我们看看使用自限定范围的用法和作用:

package generics;

public class GenericsAndReturnTypes {

	void testReturn(Getter g){
		Getter result = g.get();	//自限定类型Getter 的get方法产生了可协变的返回类型
		GenericGetter gg = g.get();
	}
	
	void testPara(Setter s1, Setter s2, GenericSetter gs){
		s1.set(s2);
		//s1.set(gs); //自限定类型Setter的set方法没有set(GenericSetter)方法
	}
	
	
}

interface GenericGetter<T extends GenericGetter<T>>{
	T get();
}

interface Getter extends GenericGetter<Getter>{
	
}

interface GenericSetter<T extends GenericSetter<T>>{
	void set(T arg);
}

interface Setter extends GenericSetter<Setter>{
	
}

在Getter中 get方法的返回值为Getter.而不是GenericGetter.

在Setter中 set方法的入参类型为Setter而不是GenericSetter.

 

 

 


 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值