13_Java泛型总结

2022/1/3 14:37:18

本文主要是介绍13_Java泛型总结,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

  • 1. 泛型概述
    • 1.1 了解泛型
    • 1.2 泛型的好处:
    • 1.3 泛型标识符
  • 2. 泛型类与接口
    • 2.1 定义泛型类与接口
    • 2.2 使用泛型类与接口
  • 3. 泛型方法
    • 3.1 为什么使用泛型方法
    • 3.2 泛型方法的定义语法
  • 4. 类型擦除
    • 4.1 概念
    • 4.2 类型擦除前后比较
    • 4.3 代码验证:
  • 5. 类型通配符
    • 5.1 通配符的引入
    • 5.2 类型通配符的上限
    • 5.3 类型通配符的下限
  • 6. 泛型与数组

1. 泛型概述

1.1 了解泛型

  • 泛型,即参数化类型

  • 泛型的本质是为了参数化类型

  • 参数化类型,即将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)

  • 在没有泛型之前,我们只能定义一个类型为Object的集合,使得该集合可以存放任意的数据类型对象

    然后在我们使用该集合之后,我们必须明确知道这个集合存储元素的数据类型,否则很容易引发ClassCastException异常,此时只能使用强制类型转换

  • 但有了泛型之后,我们在定义类时使用泛型,这样子也使得这个类可以存放任意的数据类型对象

    当使用这个类时通过给泛型赋值,就确定了这个类中存取的数据类型,这样子如果我们存放的数据类型不正确时,会直接在编译中报错提醒程序员写错了

1.2 泛型的好处:

由上面介绍,很自然的,泛型的好处为:

  1. 类型安全
  2. 消除了强制类型的转换

1.3 泛型标识符

  • E - Element (在集合中使用,因为集合中存放的是元素)
  • T - Type(Java 类)
  • K - Key(键)
  • V - Value(值)
  • N - Number(数值类型)
  • ? - 表示不确定的java类型,在使用泛型的时候才用这个,是实参,以上都是定义的时候使用
  • 定义泛型类或方法时,直接将这些泛型标识符当成具体数据类型使用即可

2. 泛型类与接口

2.1 定义泛型类与接口

  1. 泛型类的定义语法:
class 类名 <泛型标识, 泛型标识...> {
	private 泛型标识 变量名;
	...
}
  1. 泛型接口的定义语法:
interface 接口名 <泛型标识, 泛型标识...> {
	泛型标识 方法名(); // 返回值为泛型的方法
    ...
}

2.2 使用泛型类与接口

  • 泛型类对象的使用语法:
类名<具体的数据类型> 对象名 = new 类名<具体的数据类型>();
// 从jdk1.7开始,后面的<>中具体的数据类型可省略不写:
类名<具体的数据类型> 对象名 = new 类名<>();

// 示例:使用集合类
List<String> list = new ArrayList<>();
  • 泛型类的派生子类:

    1.如果子类也是泛型类,则父类和子类的泛型类型要一致(可以扩容):

    class Child<T> extends Parent<T>
    

    2.如果子类不是泛型类,则子类继承时要指定父类泛型的具体数据类型:

    class Child extends Parent<String>
    
  • 泛型接口的使用(实现类):

    1.实现类也是泛型类,则实现类和接口的泛型类型要一致(可以扩容):

    class Child<T,E> implement 接口名<T> { // 扩容的例子
    	// 可以定义泛型变量与实现方法了   
    }
    

    2.如果实现类不是泛型类,则要明确接口泛型的数据类型:

    class Child implement 接口名<String>
    

泛型类的注意事项:

  1. 如果使用泛型类定义对象时,没有指定具体的数据类型,则默认数据类型为Object

  2. 泛型的类型参数只能是类类型,不能是基本数据类型

  3. 泛型类型在逻辑上可以看成多个不同的类型,但实际上都是相同类型(运行前会进行类型擦除)

    因此也就不能使用泛型类型不一样的方法重载了(也可以理解为类型擦除之后都一样)

  4. 同时不能对确切的泛型类型使用instanceof操作

  • 以ArrayList部分源码作为示例:
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
	public E set(int index, E element) {
        Objects.checkIndex(index, size);
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

	private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }
}

3. 泛型方法

3.1 为什么使用泛型方法

  • 泛型类定义的泛型,在整个类中有效。
  • 泛型类的对象明确要操作的具体类型后,所有要操作的类型就已经固定了,所以如果此时在泛型类中直接定义方法,其实方法里的泛型类型早就确定了
  • 因此引入了泛型方法,泛型方法能使方法独立于类而产生变化,以下是一个定义泛型的基本指导原则:

无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法能将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而言,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。

  • 特殊之处:

静态方法不可以访问类上定义的泛型
如果静态方法操作的应用数据类型不确定,可以将泛型定义在方法上。

  • 泛型类与泛型方法的区别:

    泛型类:是在实例化该类时指定泛型的具体类型

    泛型方法:是在调用方法时指定泛型的具体类型

3.2 泛型方法的定义语法

  • 语法:
权限修饰符 <T,E...> 返回值类型 方法名(形参列表){
	...
}
// 示例:
// 表示返回值类型为E,参数类型也为E的方法:
public <E> E fun(E e){
    
}

4. 类型擦除

4.1 概念

由于泛型是java1.5之后才引入的概念,在此之前是没有泛型的

那为什么泛型代码能向下兼容呢?

原因是泛型信息只存在代码编译阶段,在进入JVM之前与泛型相关的信息都会被擦除,也就是说,泛型信息不会进入到运行时阶段

4.2 类型擦除前后比较

  • 无限制类型的类型擦除:
public class A<T> {
	private T key;
	
	public T getKey(){
		return key;
	}
	
	public void setKey(T key){
		this.key = key;
	}
}

// 泛型擦除后:
public class A {
	private Object key;
	
	public Object getKey(){
		return key;
	}
	
	public void setKey(Object key){
		this.key = key;
	}
}
  • 有限制类型的类型擦除:
public class A<T extends Number> {
	private T key;
	
	public T getKey(){
		return key;
	}
	
	public void setKey(T key){
		this.key = key;
	}
}

// 泛型擦除后:
public class A {
	private Number key;
	
	public Number getKey(){
		return key;
	}
	
	public void setKey(Number key){
		this.key = key;
	}
}
  • 泛型方法的类型擦除:
public <T extends Number> T getValue(T value){
	return value;
}

// 泛型擦除后:
public Number getValue(Number value){
	return value;
}
  • 非泛型类实现泛型接口后实现方法的类型擦除使用桥接方法:
public interface Info<T> {
 	T info(T var);   
}

public class InfoImpl implement Info<Integer> {
 	@Override
    public Integer info(Integer var) {
     	return var;   
    }
}

// 类型擦除后:
public interface Info {
 	Object info(Object var);   
}

public class InfoImpl implement Info {
    public Integer info(Integer var) {
     	return var;   
    }
    
    // 桥接方法,保证接口与类的实现关系
 	@Override
    public Object info(Object var) {
     	return info( (Integer)var );   
    }
}

4.3 代码验证:

List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

Class classStrList = strList.getClass();
Class classIntList = intList.getClass();

if(classStrList.equals(classIntList)){
    System.out.println("泛型类型实质是相同的");
}

输出结果:

泛型类型实质是相同的

5. 类型通配符

5.1 通配符的引入

我们知道Ingeter是Number的一个子类,同时在类型擦除中我们也验证过Generic<Ingeter>与Generic<Number>实际上是相同的一种基本类型。那么问题来了,在使用Generic<Number>作为形参的方法中,能否使用Generic<Ingeter>的实参传入呢?在逻辑上类似于Generic<Number>和Generic<Ingeter>是否可以看成具有父子关系的泛型类型呢?

实际上,这种类似多态的形参实参传入方法是错误的,因为同一种泛型可以对应多个版本(因为参数类型是不确定的),而不同版本的泛型类实例是不兼容的

对此,Java引入了泛型类型通配符:?,类型通配符可以代替具体的类型实参

也就是说,?是一个类型实参,而不是类型形参

以ArrayList构造器示例:

	public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // defend against c.toArray (incorrectly) not returning Object[]
            // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

我们注意到,在ArrayList构造器中的参数中,写法是<? extends E>,而E是ArrayList定义类时使用的泛型标识符,那这个extends又是什么意思呢?这就涉及了通配符的上限与下限了:

5.2 类型通配符的上限

语法:

类/接口<? extends 实参类型1>

这表示该泛型的类型只能是实参类型1或者是实参类型1的子类,这也就限定了类型通配符的上限

代码示例:

public void show(List<? extends Number> list) {
    ...
    // 如果只使用?,不使用extends,则定义变量list时list中的泛型参数默认为Object,此时list中的元素变成其他类型需要强转
    // 但使用extends之后,要求list中的泛型参数默认为上限Number,下限则可以是任意Number的子类
}

5.3 类型通配符的下限

语法:

类/接口<? super 实参类型2>

表示该泛型的类型只能是实参类型2,或者是实参类型2的父类类型

6. 泛型与数组

  1. 当创建泛型数组的时候,可以说明带泛型的数组引用(即等号前面),但是不能直接创建带泛型的数组对象

    因为数组在编程期需要全程知道数据类型,而泛型在编译期中会有类型擦除

    但是可以通过匿名数组的形式将数组引用传递给泛型数组:

ArrayList<String>[] listarr = new ArrayList<>[5]; // 报错
// 通过匿名数组引用传递给泛型数组:
ArrayList<String>[] listarr = new ArrayList[5];// 后半部分为匿名非泛型数组
// 相当于:
ArrayList[] list = new ArrayList[5];// 注意两个的后半部分
ArrayList<String>[] listarr = list; // 以上相当于这俩句
/**
  但是要使用匿名数组引用传递给泛型数组
  而不能创建普通数组再传递(防止因为类型被改来改去不一致)
*/ 

  1. 可以通过java.lang.reflect.Array的newInstance(Class, int)来创建T<>数组(Array.newInstance)

    (但是在真正开发中尽量使用泛型集合代替泛型数组)

    返回值方法描述
    static ObjectnewInstance(类<?> componentType(成分类型, int length)创建具有指定组件类型和长度的新数组
    static ObjectnewInstance(类<?> componentType, int… dimensions)创建具有指定组件类型和尺寸的新数组

注意: 因为返回Object所以需要强转!

T[] arr = new T(5); // 报错,因为还不知道T类型
// 只能通过Array.newInstance来创建T<>数组
// 示例:
public class Fruit<T> {
    private T[] arr; // 不能直接初始化
    
    // 通过构造方法调用Array.newInstance创建泛型数组
    public Fruit(Class<T> clz, int len){
        arr = (T[])Array.newInstance(clz, len);
    }
    
    // 填充数组放入元素
    public void put(int index, T item){
        arr[index] = item;
    }
    
    // 获取数组元素
    public T get(int index){
        return arr[index];
    }
    
    // 获取数组
    public T[] getArr(){
        return arr;
    }
}

// 使用上面写的类
public class Test{
    public static void main(String[] agrs){
        Fruit<String> fruit = new Fruit<>(Stirng.class, 3);
        // 创建String类型,将String.class传进去
        // 接着通过上面创建的三个方法使用
    }
}


这篇关于13_Java泛型总结的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程