文章目录
- 一、包装类
- 1.1、基本数据类型和对应的包装类
- 1.2、自动装箱和自动拆箱
- 二、基本介绍
- 2.1、泛型引入背景
- 2.1、什么是泛型
- 2.2、为什么使用泛型
- 三、常见泛型字母含义
- 四、泛型的使用
- 4.1、泛型类
- 4.2、泛型接口
- 4.3、泛型方法
- 五、泛型的继承
- 5.1、泛型不具备继承性
- 5.2、何为数据具备继承性
- 六、泛型通配符
- 6.1 通配符之 <?>
- 6.2 通配符之 <? extend E>、泛型的上界
- 6.3 通配符之 <? super E>、泛型的下界
- 七泛型的上界与下界
- 7.1、泛型的上界
- 八、泛型是如何编译的/泛型底层数据存取的实质
- 九、泛型擦除
一、包装类
在了解泛型之前我们先了解什么是包装类,在Java中由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型。
Q: 为什么泛型不能传入基本数据类型,为什么泛型只支持引用数据类型?
A: 因为泛型底层存储的本质是 在存入之后,容器底层还是会把你存入的所有数据类型当作 Object 类型保存起来,当你取数据的时候,它会做一个强转,再从 Object 类型强转变成泛型对应的类型。这也就是为什么泛型只能写引用数据类型,因为泛型的底层会做一个强转,在存取时会在Object类型与泛型类型之间互相强转 ,显然,int,float,double等基本数据类型是不能强转为Object类型的,所以泛型必须为引用数据类型,如果想存入 int 类型数据,只能写 int 的包装类 Integer。
1.1、基本数据类型和对应的包装类
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
注意:泛型只能接受类,所有的基本数据类型必须使用包装类。若泛型中传入的是基本数据类型,则只能使用对应的包装类
public static void main(String[] args) {
List<String> s = new ArrayList<>();
List<Integer> i = new ArrayList<>();
List<Long> l = new ArrayList<>();
}
1.2、自动装箱和自动拆箱
public class Test {
public static void main(String[] args) {
int a = 10;
Integer i = a;//自动装箱 把一个基本数据类型转变为包装类型
System.out.println(i);
Integer j = new Integer(20);
int b = j;//自动拆箱 把一个包装类型转变为基本数据类型
System.out.println(b);
}
}
3.手动装箱和手动拆箱
public class Test {
public static void main(String[] args) {
int a = 10;
Integer i = Integer.valueOf(a);//手动装箱
System.out.println(i);
Integer j = new Integer(20);
int b = j.intValue();//手动拆箱
System.out.println(b);
}
}
二、基本介绍
Java泛型是J2 SE1.5中引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类
、泛型接口
、泛型方法
。
2.1、泛型引入背景
集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK1.5之前只能把元素类型设计为Object,JDK1.5之后使用泛型来解决。因为这个时候除了元素的类型不确定,其他的部分是确定的,例如关于这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个参数,这个类型参数叫做泛型
。Collection,List,ArrayList 这个就是类型参数,即泛型。
为什么要有泛型(Generic)
为什么要有泛型呢,直接Object不是也可以存储数据吗?
先来看一个例子
List<Object> arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
String item = (String)arrayList.get(i);
Log.d("泛型测试","item = " + item);
}
毫无疑问,程序的运行结果会以崩溃结束:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,使用时都以String接收,Integer被强转成String,因此报错。为了解决类似这样的类型转换问题,在编译阶段就规定只能存放某种类型的数据,泛型应运而生。
2.1、什么是泛型
泛型:是一种把明确类型的工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,而这种参数类型可以用在类、方法和接口中,分别被称为泛型类
、泛型方法
、泛型接口
。
注意:一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为Object类型。
2.2、为什么使用泛型
使用泛型的好处
-
避免了类型强转的麻烦。
若没有指定参数类型,底层在存储时会把数据存储为Object类型,取值时转换为对应类型。有个强制转换的过程。 -
它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现ClassCastException。
-
使用泛型的类型或者返回值的方法,自动进行数据类型转换即自动拆箱。
泛型的细节注意点:
(1)泛型的数据类型只能填写引用数据类型,不可以用基本数据类型,具体原因下面会讲。
(2)指定泛型的具体类型之后,可以传入该类类型或其子类类型;
(3)如果不手动添加泛型,则默认泛型为 Object 。
(4)泛型可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>
(5)泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。
泛型如果不指定,按Object处理,但不等价于Object。即ArrayList
不等价与ArrayList<Object>
,以下为示例:
class GenericTest {
public static void main(String[] args) {
// 1、使用时:类似于Object,不等同于Object
ArrayList list = new ArrayList();
// list.add(new Date());//有风险
list.add("hello");
test(list);// 泛型擦除,编译不会类型检查
// ArrayList<Object> list2 = new ArrayList<Object>();
// test(list2);//一旦指定Object,编译会类型检查,必须按照Object处理
}
public static void test(ArrayList<String> list) {
String str = "";
for (String s : list) {
str += s + ","; }
System.out.println("元素:" + str);
}
}
在使用泛型时,若明确知道参数类型,则应该使用具体的引用数据类型;若不知道具体数据类型,则可使用通配符?
或Object
接收对象。
泛型示例:
public class Test {
public static void main(String[] args) {
//创建Integer集合
List<Integer> list1 = new ArrayList<>();
//创建String集合
List<String> list2 = new ArrayList<>();
//当不确定集合的类型时,可以用通配符? 相当于下面的Object
List<?> list3 = new ArrayList<>();
//创建Object集合
List<Object> list4 = new ArrayList<>();
}
}
三、常见泛型字母含义
泛型字母没有特别的限定,即使你使用A-Z英文字母的任意一个,编译器也不会报错
。之所以有不同的标记符,这是一种约定, 为了便于可阅读性。
常见的泛型字母如下:
- E ----Element(在集合中使用,因为集合中存放的是元素)
- T ----Type(Java类)
- K ----Key(键),一般与V搭配使用
- V ----Value(值)
- N ----Number(数值类型)
- ?----表示不确定的java类型(
泛型通配符
,与Object等同)
四、泛型的使用
泛型的使用主要在于泛型类
、泛型接口
、泛型方法
中。
泛型参数是可以支持多个的,以泛型类为示例,如下图:
4.1、泛型类
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种集合框架容器类,如:List、Set、Map。
我们平常所用的ArrayList类,就是一个泛型类,我们看如下源码
(1)泛型类的定义格式:
# 语法
修饰符 class 类名<代表泛型的变量> { }
示例:
//支持多个泛型,用逗号分隔
public class ClassName<T1, T2, ..., Tn> {
}
public class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
public class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
代码示例:
/**
* @param <T> 这里解释下<T>中的T:
* 此处的T可以随便写为任意标识,常见的有T、E等形式的参数表示泛型
* 泛型在定义的时候不具体,使用的时候才变得具体。
* 在使用的时候确定泛型的具体数据类型。即在创建对象的时候确定泛型。
*/
public class GenericsClassDemo<T> {
//t这个成员变量的类型为T,T的类型由外部指定
private T t;
//泛型构造方法形参t的类型也为T,T的类型由外部指定
public GenericsClassDemo(T t) {
this.t = t;
}
//泛型方法getT的返回值类型为T,T的类型由外部指定
public T getT() {
return t;
}
}
泛型在定义的时候不具体,使用的时候才变得具体。在使用的时候确定泛型的具体数据类型。即:在创建对象的时候确定泛型。
使用示例:
GenericsClassDemo<User> u = new GenericsClassDemo<>(new User());
GenericsClassDemo<Dog> d = new GenericsClassDemo<>(new Dog());
GenericsClassDemo<String> s= new GenericsClassDemo<String>("hello");
注意: 定义的泛型类,就一定要传入泛型类型实参么?
并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。即跟之前的经典案例一样,没有写ArrayList
的泛型类型,容易出现类型强转的问题。
(2)注意事项与细节
- 普通成员可以使用泛型(属性,方法)
- 使用泛型的数组,不能初始化
- 静态方法中不能使用类的泛型
- 泛型类的类型,是在创建对象的时候确定的
- 如果在创建对象时,没有指定类型,默认为Object
示例:
//Tiger 后面的为泛型,Tiger就称为自定义泛型类
//T,R,M 为泛型的标识符,一般是单个的大写字母
//普通成员可以使用泛型(属性,方法)
class Tiger<T,R,M>{
String name;
T t; //属性使用泛型
R r;
M[] m;//可以用数组,但不能new初始化
// T[] t1 = new T[8]; //报错,无法确定数组类型,数组不知道开辟多大空间
public Tiger(String name, T t, R r, M[] m) { //方法使用泛型
this.name = name;
this.t = t;
this.r = r;
this.m = m;
}
//静态方法,静态属性,是在类加载的时候进行的,但是对象还没有创建,JVM就无法初始化
// static R r2;
// public static void m1(T t){}
}
4.2、泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中。
(1)定义格式
修饰符 interface 接口名<代表泛型的变量> { }
代码示例
/**
* 定义一个泛型接口
*/
public interface GenericsInteface<T> {
public abstract void add(T t);
}
# 定义类时确定泛型的类型
public class GenericsImpl<T> implements GenericsInteface<T> {
@Override
public void add(T t) {
System.out.println("没有设置类型");
}
}
始终不确定泛型的类型,直到创建对象时,确定泛型的类型
public class GenericsTest {
public static void main(String[] args) {
GenericsImpl<Integer> gi = new GenericsImp<>();
gi.add(66);
}
}
(4)注意事项与细节
- 接口中,静态成员不能使用泛型
- 泛型接口的类型,在继承接口或者实现接口时确定
- 没有指定类型,默认为Object
示例:
//接口中,成员属性时默认静态的 final static
interface IUsb<U,R>{
//普通方法,可以使用泛型接口
// U name; //报错,成员属性时默认静态的 final static
void run(R r1,U u1);
//在jdk8中,可以在接口中,使用默认方法,也是可以使用泛型
default R method(U u){
return null;
}
}
//继承接口时,指定类型 U->String, R->Double
interface AUsb extends IUsb<String, Double>{}
//实现接口时,指定类型 U->String, R->Double
class BUsb implements IUsb<String, Double>{
@Override
public void run(Double r1, String u1) { //自动填充类型
}
}
//未指定,默认为Object
class CUsb implements IUsb{
@Override
public void run(Object r1, Object u1) {//默认为Object
}
}
4.3、泛型方法
泛型方法,是在调用方法的时候指明泛型的具体类型 。通常情况下,当一个方法的形参不确定的情况下,我们会使用到泛型方法。
(1)定义格式:
修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ }
测试示例:
/**
*
* @param t 传入泛型的参数
* @param <T> 泛型的类型
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E等形式的参数常用于表示泛型。
*/
public <T> T genercMethod(T t){
System.out.println(t.getClass());
System.out.println(t);
return t;
}
//定义一个泛型方法,不返回内容
public <T> void print(T t){
System.out.println(t);
}
//定义一个泛型方法,传入多个泛型
public <T,F> void query(T t,List<F> f){
System.out.println(t);
}
//传入T, F 返回T
public <T,F> T query1(T t,F f){
}
//静态的泛型方法 需要在static后用<>声明泛型类型参数
public static <E> void swap(E[] array, int i, int j) {
E t = array[i];
array[i] = array[j];
array[j] = t;
}
使用示例:
String str = this.genercMethod("hello");//传入的是String类型,返回的也是String类型
Integer i = this.genercMethod(123);//传入的是Integer类型,返回的也是Integer类型
this.print("你好")//调用泛型方法,不返回值
这里我们可以看下结果:
class java.lang.String
hello
class java.lang.Integer
123
你好
这里可以看出,泛型方法随着我们的传入参数类型不同,他得到的类型也不同。泛型方法能使方法独立于类而产生变化。
(2)注意事项和使用方法
- 泛型方法可以定义在普通类中,也可以定义在泛型类中
- 当泛型方法被调用时,类型被确定
- public void eat(E e){},修饰符后面没有<T,R…>,这里的方法不是泛型方法,而是使用了泛型
- 泛型方法可以使用自己的泛型,也可以使用类声明的泛型
使用示例
public class Test{
public static void main(String[] args) {
Car car = new Car();
//调用方法,传入参数,编译器,就会确定类型
car.run("run"); //调用的时候指定泛型的类型
new Fish<Integer>().swim1(2);
Fish fish = new Fish<Integer>();
fish.swim(2,new ArrayList());
}
}
class Car{//普通类的泛型方法
public<E> void run(E e){
System.out.println("泛型方法类型为"+e.getClass());
}
}
class Fish<T>{//泛型类里面的泛型方法
public<U> void swim(U u, T t){ //泛型使用了泛型类的泛型,也可以使用自己方法里面的泛型
System.out.println(u.getClass());
System.out.println(t.getClass());
}
public void swim1(T t){ //使用了泛型,但是不是泛型方法
System.out.println(t.getClass());
}
}
五、泛型的继承
泛型本身并不具备继承性,但是数据具备继承性。
5.1、泛型不具备继承性
如下图,我定义了GrandFathor类,Fathor类,Son类;Fathor类继承GrandFathor类,Son类又继承Fathor类。
我们再定义一个空方法体的 method 方法,方法需要传入一个带泛型的集合,我就写 GrandFathor;
分别创建泛型为 GrandFathor,Fathor,Son 的集合对象 list1,list2,list3;
我们可以得出结论,泛型是不具备继承性的,也就是说,一个方法传入的对象泛型是什么类型,我们不能把参数泛型的子类泛型对象作为参数传递给方法,该泛型是不具备继承性的,传入编译器会报错。
5.2、何为数据具备继承性
刚才我们验证了也演示了泛型不具备继承性,那么接下来我们来说一下,数据具备继承性是什么意思。
还拿刚才的代码举例,
我们把刚才的代码注释,然后往 list1 对象中添加对象;
添加 GrandFathor 类对象,添加成功,这也是当然的,因为该类的泛型指定的就是 GrandFathor;
添加 Fathor 类对象,发现也添加成功;
添加 Son 类对象,发现也添加成功;
执行 main 方法,如下结果,说明没有问题
以上我们可以得出结论
当我们为一个类指定泛并创建对象之后,对象中不仅可以加入泛型所指定的类对象,还可以加入泛型类子类的类对象,这就是数据的继承性。
注意这里说的是对象,上面不具备继承性中说的是参数,不要混为一谈。
六、泛型通配符
上面刚刚说到了使用一个类型来表示泛型类型是必须要申明的,也即 <T> ,那是不是不申明就不能使用泛型呢?当然不是,这小节介绍的 <?> 就是为了解决这个问题的。
当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符
<?>
表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。
在Java中,泛型的通配符是一个 “?”, 通配符 ?
即占位符的意思,也就是在使用期间是无法确定其类型的,只有在将来实际使用时再指明类型,它有三种形式
- <?> 无限定的通配符。支持任意泛型的类型
- <? extends E>有上限的通配符。支持E类以及E类的子类,规定了泛型的上限
- <? super E>有下限的通配符。支持E类以及E类的父类,不限于直接父类,规定了泛型的下限
//支持存放任意类型
List<?> list1 = new ArrayList<>();
//支持Number及Number的子类, 规定了泛型的上限
List<? extends Number> list2 = new ArrayList(Arrays.asList(new Integer[]{1, 2, 3}));
//支持Integer及Integer的父类, 规定了泛型的下限
List<? super Integer> list3 = new ArrayList(Arrays.asList(new Number[]{1.0, 2.0, 3.0}));
6.1 通配符之 <?>
<?> 表示任意类型,那 能不能用 ? 去接收一个对象呢,请看代码中的注释
而又因为任何类型都是 Object 的子类,所以,这里可以使用 Object 来接收,对于 ? 的具体使用会在下面两小节介绍
/**
* ?表示未知类型,若要接收具体对象,需要用Object接收
* @param list
*/
public void test(List<?> list){
Object item = list.get(0);
}
另外,大家要搞明白 泛型和通配符不是一回事
6.2 通配符之 <? extend E>、泛型的上界
<? extend E>
表示有上限的通配符,能接受其类型和其子类的类型 E 指上边界,还是写个例子来说明
public class GenericExtend {
public static void main(String[] args) {
List<Father> listF = new ArrayList<>();
List<Son> listS = new ArrayList<>();
List<Daughter> listD = new ArrayList<>();
testExtend(listF);
testExtend(listS);
testExtend(listD);
}
private static <T> void testExtend(List<? extends Father> list) {}
}
class Father {}
class Daughter extends Father{}
class Son extends Father {
}
这个时候一切都还是很和平的,因为大家都遵守着预定,反正 List 中的泛型要么是 Father 类,要么是 Father 的子类。但是这个时候如果这样子来写(具体原因已经在截图中写明了)
6.3 通配符之 <? super E>、泛型的下界
<? super E>
表示有下限的通配符。也就说能接受指定类型及其父类类型,E 即泛型类型的下边界,直接上来代码然后来解释
public class GenericSuper {
public static void main(String[] args) {
List<Son> listS = new Stack<>();
List<Father> listF = new Stack<>();
List<GrandFather> listG = new Stack<>();
testSuper(listS);
testSuper(listF);
testSuper(listG);
}
private static void testSuper(List<? super Son> list){}
}
class Son extends Father{}
class Father extends GrandFather{}
class GrandFather{}
因为 List<? super Son> list 接受的类型只能是 Son 或者是 Son 的父类,而 Father 和 GrandFather 又都是 Son 的父类,所以以上程序是没有任何问题的,但是如果再来一个类是 Son 的子类(如果不是和 Son 有关联的类那更不行了),那结果会怎么样?看下图,相关重点已经在图中详细说明
七泛型的上界与下界
7.1、泛型的上界
在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束
语法:
class 泛型类名称<类型形参 extends 类型边界> {
...
}
示例:
public class MyArray<E extends Number> {
...
}
只接受 Number 的子类型作为 E 的类型实参
复杂示例:
public class MyArray<E extends Comparable<E>> {
...
}
E必须是实现了Comparable接口的
八、泛型是如何编译的/泛型底层数据存取的实质
刚才我说到了泛型只能用引用数据类型,是有原因的;
当我们往写入泛型的集合中添加元素的时候,泛型其实可以理解成一个看门大爷,你在添加数据之前,它会看看你要添加的数据类型是否与标注的泛型类型相匹配,不匹配则不会让你存入,匹配的话,在存入之后,容器底层还是会把你存入的所有数据类型当作 Object 类型保存起来,当你取数据的时候,它会做一个强转,再从 Object 类型强转变成泛型对应的类型。这也就是为什么泛型只能写引用数据类型,因为泛型的底层会做一个强转,在存取时会在Object类型与泛型类型之间互相强转,显然,int,float,double等基本数据类型是不能强转为Object类型的,所以泛型必须为引用数据类型,如果想存入 int 类型数据,只能写 int 的包装类 Integer。
在编译的过程当中,将所有的T替换为Object这种机制,我们称为:擦除机制
Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息
九、泛型擦除
先来看下泛型擦除的定义
因为泛型的信息只存在于 java 的编译阶段,编译期编译完带有 java 泛型的程序后,其生成的 class 文件中与泛型相关的信息会被擦除掉,以此来保证程序运行的效率并不会受影响,也就说泛型类型在 jvm 中和普通类是一样的。
所有泛型类型参数,若没有设置泛型上限,则编译之后统一擦除为Object类型。若设置了泛型上限,则编译之后统一擦除为相应的泛型上限
看完概念可能还是不明白什么叫泛型擦除,举个例子:
下面示例中我们泛型没有设置上限,擦除后类型为Object类型
当设置了泛型上限,则编译之后统一擦除为相应的泛型上限,具体看下图:
参考文章:
https://blog.csdn.net/bjweimengshu/article/details/117793971
https://baike.baidu.com/item/java%E6%B3%9B%E5%9E%8B/511821?fr=ge_ala