[疯狂Java]泛型:泛型方法、泛型方法VS类型通配符(?)

1. 定义泛型方法:

    1) 如果你定义了一个泛型(类、接口),那么Java规定,你不能在所有的静态方法、静态初块等所有静态内容中使用泛型的类型参数!!例如:

class A<T> {
	public static void func(T t) { // 错误!在所有静态内容中不得使用泛型的类型参数
		
	}
}
!!这个原因会在泛型底层原理中详细介绍,这里就只要先记住这个规则就行了!!

    2) 那么问题来了,我要如何在静态内容(特别是静态方法)中使用泛型呢?更一般的问题是,如果我的类(或者接口)没有定义成泛型,但是我就想在其中某几个方法中运用泛型(比如接受一个泛型的参数等)时该怎么办呢?

    3) 针对上述问题,Java提供了一个统一的解决方法,那就是定义单独的泛型方法;

    4) 定义泛型方法:

         i. 就像定义泛型类或接口一样,在定义类名(或者接口名)的时候需要指定我的作用域中谁是泛型参数:public class A<T> { ... }即表明在我A的作用域中,T是我的泛型类型类型参数;

         ii. 定义泛型方法也一样,其格式是:修饰符 <类型参数列表> 返回类型 方法名(形参列表) { 方法体 }

         iii. 例如:public static <T, S> int func(List<T> list, Map<int, S> map) { ... }   // 就表示,在我func这个方法的作用域中,T和S是我的泛型类型参数

         iv. 可以看到,泛型方法的定义和普通方法定义不同的地方在于需要在修饰符和返回类型之间加一个泛型类型参数的声明,表名在我这个方法作用域中谁才是类型参数;

    5) 可以看到,不管是普通的类/接口的泛型定义,还是方法的泛型定义都逃不出两大要素:

         i. 声明哪些是我的泛型类型参数;

         ii. 这些类型参数在哪里使用;

    6) 而类型参数本身有一个非常重要的特点,就是作用域:

         i. 比如class A<T> { ... }中T的作用域就是整个A,而public <T> func(...) { ... }中T的作用域就是方法func;

         ii. 和普通变量一样,类型参数也存在作用域覆盖的问题:你可以在一个泛型模板类/接口中继续定义泛型方法,例如:

class A<T> { // A已经是一个泛型类,其类型参数是T
	public static <T> void func(T t) { // 再在其中定义一个泛型方法,该方法的类型参数也是T
		
	}
}
!!当上述两个类型参数冲突时,在方法中,方法的T会覆盖类的T,即和普通变量的作用域一样,内部覆盖外部,外部的同名变量是不可见的!!

         iii. 除非是一些特殊需求,一定要将局部类型参数和外部类型参数区分开来,避免发生不必要的错误,因此一般正确的定义方式是这样的:

class A<T> {
	public static <S> void func(S s) { 
		
	}
}

!即内外名称要区分开,表示不同的两个类型参数!

    7) 泛型方法的类型参数也可以指定上限(但是不能指定上限!):

         i. 类型上限必须在类型参数声明的地方定义上限!!不能在方法参数中定义上限;

         ii. 例如:

             a. <T extends Xxx> void func(List<T> list);  // 正确

             b. <T extends Xxx> void func(T t);  // 必然正确

             c. <T> void func(List<T extends Xxx> list);   // 编译错误

         iii. 规定了上限就只能在规定范围内指定类型实参,超出这个范围就会直接编译报错!



2. 调用泛型方法:

    1) 有两种方式:

         i. 显式指定方法的类型参数,类型参数要写在尖括号中并放在方法名之前,例如:obj.<String>func(...);

!!这样就显式指定了泛型方法的类型参数为String,那么所有出现类型参数T的地方都将替换成String;

         ii. 隐式地自动推断:那就是不指明泛型参数,让编译器根据传入的实参类型来自动推断类型参数是什么;

             a. 最简单的例如:<T> void func(T t);  这样调用的话,obj.func("lala");   // 那么就会根据"lala"的类型String推断出类型参数T的类型是String

             b. 但是一定要避免歧义,例如:<T> void func(T t1, T t2);  如果这样调用的话,obj.func("lala", 15); 虽然编译不会报错,但是仍然会有很大隐患,T到底应该是String还是Integer存在歧义;

!!Java存在一套机制来推断这种情况下到底应该把T当成什么,但是这种机制非常不可靠,通常会发生一些意想不到的错误,因此一定要避免这种歧义,平时编程的时候就应该把这种歧义当成是错误!!

             c. 但是有些歧义Java是会直接当成编译错误的,即所有和泛型参数有关的歧义,例如:<T> void func(List<T> l1, List<T> l2); 如果这样调用的话,obj.func(new List<String>(), new List<Integer>()); 这里当然会有歧义,编译器无法知道T到底应该是String还是Integer,但是这种歧义会直接报错的!!编译都无法通过;

!!即泛型要比普通类型的检查要严很多,很多在普通类型上行得通的擦边球,在泛型上都无法通过!!

!!即泛型方法中,如果类型参数刚好就是泛型参数的类型实参,那么这个类型实参不得有歧义!!否则直接编译报错;


3. 泛型方法VS类型通配符(两者可以混用):

    1) 你会发现所有能用类型通配符(?)解决的问题都能用泛型方法解决,并且泛型方法可以解决的更好:

         !!最典型的一个例子就是:

            a. 类型通配符:void func(List<? extends A> list);

            b. 完全可以用泛型方法完美解决:<T extends A> void func(List<T> list);

!!上面两种方法可以达到相同的效果(?可以代表范围内任意类型,而T也可以传入范围内的任意类型实参),并且泛型方法更进一步,?泛型对象是只读的,而泛型方法里的泛型对象是可修改的,即List<T> list中的list是可修改的!!

    2) 要说两者最明显的区别就是:

         i. ?泛型对象是只读的,不可修改,因为?类型是不确定的,可以代表范围内任意类型;

         ii. 而泛型方法中的泛型参数对象是可修改的,因为类型参数T是确定的(在调用方法时确定),因为T可以用范围内任意类型指定;

!!注意,前者是代表,后者是指定,指定就是确定的意思,而代表却不知道代表谁,可以代表范围内所有类型;

    3) 这样好像说的通配符?一无是处,但是并不是这样,Java设计类型通配符?是有道理的,首先一个最明显的优点就是?的书写要比泛型方法简洁,无需先声明类型参数,其次它们有各自的应用场景:

         i. 一般只读就用?,要修改就用泛型方法,例如一个进行修改的典型的泛型方法的例子:

public <T> void func(List<T> list, T t) {
	list.add(t);
}
         ii. 在多个参数、返回值之间存在类型依赖关系就应该使用泛型方法,否则就应该是通配符?:

             a. 具体讲就是,如果一个方法的返回值、某些参数的类型依赖另一个参数的类型就应该使用泛型方法,因为被依赖的类型如果是不确定的?,那么其他元素就无法依赖它),例如:<T> void func(List<? extends T> list, T t);  即第一个参数依赖第二个参数的类型(第一个参数list的类型参数必须是第二个参数的类型或者其子类);

!!可以看到,Java支持泛型方法和?混用;

!这个方法也可以写成:<T, E extends T> void func(List<E> list, T t);  // 明显意义是一样的,只不过这个list可以修改,而上一个list无法修改

!!总之就是一旦返回值、形参之间存在类型依赖关系就只能使用泛型方法;

             b. 否则就应该使用? ;
    4) 对泛型方法的类型参数进行规约:即有时候可能不必使用泛型方法的地方你不小心麻烦地写成了泛型方法,而此时你可以将其规约成使用?的最简形式

         i. 总结地来讲就是一句话:只出现一次 & 对它没有任何依赖

         ii. 例如:<T, E extends T> void func(List<T> l1, List<E> l2);  // 这里E只在形参中出现了一次(类型参数声明不算),并且没有任何其他东西(方法形参、返回值)依赖它,那么就可以把E规约成?

!!最终规约的结果就是:<T> void func(List<T> l1, List<? extends T> l2);

    5) 一个最典型的应用就是容器赋值方法(Java的API):public static <T> void Collections.copy(List<T> dest, List<? extends T> src) { ... }

!!从src拷贝到dest,那么dest最好是src的类型或者其父类,因为这样才能类型兼容,并且src只是读取,没必要做修改,因此使用?还可以强制避免你对src做不必要的修改,增加的安全性;

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值