【JavaSE】泛型

泛型(Generics)是JDK 1.5时增加的编程语言中的一种特性,允许在编写代码时使用一种抽象的类型来代表实际的数据类型。也可以通俗地理解为将数据类型实现参数化,处理的数据类型不是固定的,而是可以作为参数传入。我们可以把“泛型”理解为数据类型的一个占位符(类似:形式参数),即告诉编译器,在调用泛型时必须传入实际类型

类似于cpp中的模板(Templates),异曲同工,旨在“数据类型参数化”;

优点

不使用泛型,可以使用Object类型来实现任意的参数类型,但是在使用时需要我们进行强制类型转换。这要求编写程序的人明确知道实际类型,不然容易引起类型转换错误ClassCastException;这种错误在编译期无法识别,只有在运行期实际执行了语句后才能发现,加大了维护的工作量。

使用泛型,可以在编译期识别出这种错误,有了更好的安全性;同时所有类型转换由编译期完成,在程序员看来都是自动转换,提高了代码的可读性

来看看chat老师的话术:

类型擦除

编码时采用泛型写的参数类型,编译期会在编译时将其去掉,即“类型擦除”。

泛型主要用于编译阶段,编译后生成的字节码.class文件不包含泛型中的类型信息,涉及类型转换仍然是普通的强制类型转换。类型参数在编译后会被替换成Object,运行时虚拟机JVM并不知道泛型。

泛型主要是方便代码的编写,以及更好的安全性检测

一、泛型类


把泛型定义在类上,基本的语法格式:

// 单个泛型标识
public class 类名<泛型标识符号> {
}
// 多个泛型标识
public class 类名<泛型标识符号,泛型标识符号> {
}

其中,定义泛型时,一般采用几个标记:E、T、K、V、N、?。他们约定俗称的含义如下:

泛型标记

对应单词

说明

E

Element

在容器中使用,表示容器中的元素

T

Type

表示普通的JAVA类

K

Key

表示键,例如:Map中的键Key

V

Value

表示值

N

Number

表示数值类型

表示不确定的JAVA类型

举例

public class Generic<T> {
    private T value; // 泛型类中定义的涉及的数据类型都采用T
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }

    public static void main(String[] args) {
        // 指定T为String
        Generic<String> generic1 = new Generic<>();
        generic1.setValue("kk");
        String value1 = generic1.getValue();
        System.out.println(value1);
        // 指定类型为Integer
        Generic<Integer> generic2 = new Generic<>();
        generic2.setValue(25);
        Integer value2 = generic2.getValue();
        System.out.println(value2);
    }
}

二、泛型接口


在接口上定义泛型,其声明方式和泛型类基本一致:

public interface 接口名<泛型标识符号> {
}
public interface 接口名<泛型标识符号,泛型标识符号> {
}

用法

  1. 在实现接口时就传递具体数据类型;
  2. 实现接口时仍然使用泛型作为数据类型,而在真正使用时才传入指定数据类型;

举例

// 定义泛型接口
public interface IGeneric<T> {
    public abstract T getValue(T t);
}
// 用法一:在实现接口时就传递具体数据类型
public class IGenericImpl implements IGeneric<String>{
    @Override
    public String getValue(String name) {
        return name;
    }
}
// 用法二:实现接口时仍然使用泛型作为数据类型,而在真正使用时才传入指定数据类型
public class IGenericImpl1<T> implements IGeneric<T>{
    @Override
    public T getValue(T name) {
        return name;
    }
}
// 测试类
public class IGenericTest {
    public static void main(String[] args) {
        IGeneric<String> ig1 = new IGenericImpl();
        System.out.println(ig1.getValue("kk"));
        IGeneric<String> ig2 = new IGenericImpl1<>();
        System.out.println(ig2.getValue("kkakoka"));
    }
}
// 控制台输出
kk
kkakoka

剖析

可以看到25行和28行的区别,25行后的类型IGenericImpl后是没有泛型<>的,说明这个类在实现了泛型接口IGeneric之后已经被当成一个普通类了(没有泛型加持),而28行由于IGenericImpl1在实现泛型接口时还是没有指定具体的数据类型,所以还是泛型类

三、泛型方法


类上定义的泛型,在方法中也可以使用。但是,我们经常需要仅仅在某一个方法上使用泛型,这时候可以使用泛型方法。

调用泛型方法时,不需要像泛型类那样告诉编译器是什么类型,编译器可以自动推断出类型;

用法:

  1. 非静态方法可以使用泛型类中所定义的泛型,也可以将泛型定义在方法上。
//无返回值方法
public <泛型标识符号> void getName(泛型标识符号 name){
}
//有返回值方法
public <泛型标识符号> 泛型标识符号 getName(泛型标识符号 name){
}
  • 举例
public class MethodGeneric {
    public <T> void setName(T name){
        System.out.println(name);
    }
    public <T> T getAge(T age){
        return age;
    }
    public static void main(String[] args) {
        MethodGeneric methodGeneric = new MethodGeneric();
        methodGeneric.setName("kk");
        Integer age = methodGeneric.getAge(18);
        System.out.println(age);
    }
}
// 输出结果
kk
18
  1. 静态方法中使用泛型时有一种情况需要注意一下,那就是静态方法无法访问类上定义的泛型,所以必须要将泛型定义在方法上;
//无返回值静态方法
public static <泛型标识符号> void setName(泛型标识符号 name){
}
//有返回值静态方法
public static <泛型标识符号> 泛型表示符号 getName(泛型标识符号 name){
}
  • 要点:静态方法不能访问类上定义的泛型!
  • 举例
public class MethodGeneric1<T> {
    public static <T> T getName(T name){
        return name;
    }
    public static <T> void setAge(T age){
        System.out.println(age);
    }
    public static void main(String[] args) {
        MethodGeneric1.setAge(18);
        String name = MethodGeneric1.getName("kk");
        System.out.println(name);
    }
}
// 控制台输出
18
kk

3.1 泛型方法与可变参数

在泛型方法中,泛型也可以定义可变参数类型。

语法结构

public <泛型标识符号> void showMsg(泛型标识符号... agrs){
}

举例

// 定义参数为可变的泛型方法
public <T> void method(T ...args){
        for (T item : args) {
            System.out.print(item + " ");
        }
        System.out.println();
    }
// 测试泛型方法的可变参数
String[] arr1 = new String[]{"k","kk","kkk"};
Integer[] arr2 = new Integer[]{1,2,3,4,5};
methodGeneric.method(arr1);
methodGeneric.method(arr2);
// 控制台输出
k kk kkk
1 2 3 4 5

四、通配符

4.1 无界通配符


“?” 表示类型通配符,用于代替具体的类型。它只能在“<>”中使用,可以解决当具体类型不确定的问题。

语法结构

public void showFlag(Generic<?> generic){
}
// Generic类
public class Generic<T> {
    private T value;
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }
}
// 工具类ShowMsg
public class ShowMsg {
    // 使用无界通配符
    public void showValue(Generic<?> generic){
        System.out.println(generic.getValue());
    }
    public static void main(String[] args) {
        ShowMsg showMsg = new ShowMsg();
        // String
        Generic<String> generic1 = new Generic<>();
        generic1.setValue("kk");
        showMsg.showValue(generic1);

        // Integer
        Generic<Integer> generic2 = new Generic<>();
        generic2.setValue(18);
        showMsg.showValue(generic2);
        

    }
}
// 控制台输出
kk
18

:如果不使用<?>则会直接编译报错;

4.2 通配符的上限限定


对通配符的上限的限定:<? extends 类型>

?实际类型可以是上限限定中所约定的类型,也可以是约定类型的子类型

public void showFlag(Generic<? extends Number> generic){
}

举例

4.3 通配符的下限限定


对通配符的下限的限定:<? super 类型>

?实际类型可以是下限限定中所约定的类型,也可以是约定类型的父类型;

public void showFlag(Generic<? super Integer> generic){}

注意:super不用于泛型类的定义中!!(存疑)

举例

五、类型擦除与桥方法


贴上参考链接:

80%人不懂的泛型知识4-你真的了解泛型擦除吗_哔哩哔哩_bilibili

一文搞懂 java 泛型,也有可能搞不懂,毕竟讲得太全面了 - 掘金

5.1 协变与逆变


协变可以让泛型的约束更加宽松,但无法往原集合中添加元素。

逆变会使得无法获取原类型的对象。也即是:

因此在实际涉及泛型的编程中,我们要规避这两种情况,最大限度使用泛型。

PECS原则全称为Producer Extends, Consumer Super,是一种在泛型编程中的约定,用于确定泛型类型参数的上界和下界。这个原则在Java等编程语言中,通过通配符(Wildcard)来表示不确定的类型参数,以指导我们在使用通配符时,如何为生产者(Producer)和消费者(Consumer)选择合适的边界。

在PECS原则中,“Producer Extends”意味着当你需要从一个容器中取出数据时,应该使用带有extends关键字的通配符,这样你可以取出该容器及其父类中的任何对象(extends的类型及其所有子类型对象)。“Consumer Super”则意味着当你需要向一个容器中添加数据时,应该使用带有super关键字的通配符,这样你可以向该容器及其子类中添加任何对象(super的类型及其所有父类型对象)

这个原则的核心思想是基于Liskov替换原则(LSP),即所有出现基类(父类)的地方都可以用子类进行替换。在泛型编程中,通过使用PECS原则,我们可以更加灵活地处理不同的数据类型,同时保证类型安全。

在泛型编程实际情况中,PECS对应的情况只是其中一部分(不一定所有都是取出、添加元素);

  • Producer Extends
public class GenericTest3 {
    public static void main(String[] args) {

        List<Double> doubleList = new ArrayList<>();
        doubleList.add(2.0d);
        doubleList.add(4.0d);
        doubleList.add(8.0d);
        sum(doubleList);
    }
    // producer
    public static double sum(List<? extends Number> list){
        // list.add(4.0F);
        double res = 0;
        for (Number number : list){
            res += number.doubleValue();
        }
        return res;
    }
}
  • Consumer Super
public class GenericTest3 {
    public static void main(String[] args) {

        List<Double> doubleList = new ArrayList<>();
        doubleList.add(2.0d);
        doubleList.add(4.0d);
        doubleList.add(8.0d);
        //sum(doubleList);
        add(doubleList);
    }
    // Consumer
    public static void add(List<? super Double> list){
        list.add(16.0d);
        System.out.println(list);
    }
}

实例1:ArrayList的拷贝构造方法

/**
 * Constructs a list containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.
 *
 * @param c the collection whose elements are to be placed into this list
 * @throws NullPointerException if the specified collection is null
 */
public ArrayList(Collection<? extends E> c) {
    Object[] a = c.toArray();
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            elementData = a;
        } else {
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        // replace with empty array.
        elementData = EMPTY_ELEMENTDATA;
    }
}

遵循着我们的PE原则,由于构造方法只需要取出传入的泛型参数类型集合的每一个元素,因此可以采用extends关键字来放宽传入类型限制,使得当前ArrayList所指定的泛型类型E及其子类型的集合对象都可以传入为其自身构造。

实例2:ArrayList中的removeIf()方法

@Override
public boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    // figure out which elements are to be removed
    // any exception thrown from the filter predicate at this stage
    // will leave the collection unmodified
    int removeCount = 0;
    final BitSet removeSet = new BitSet(size);
    final int expectedModCount = modCount;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        @SuppressWarnings("unchecked")
        final E element = (E) elementData[i];
        if (filter.test(element)) {
            removeSet.set(i);
            removeCount++;
        }
    }
    ...
}

由于该方法需要调用传入的泛型类型参数的方法,思考,若是传入的是当前ArrayList指定的泛型类型E的子类型,那么就会出现不适配的情况(用子类的方法操作父类的对象),因此这里的关键字应该是super(父类的方法操作子类的对象是兼容的)。

5.2 类型擦除


所谓类型擦除,指的是在Java编译器在编译带泛型类型参数时会擦除其类型信息,转化为它们的上界或者Object类型。是在编译期间将泛型类型转换为非泛型类型的一种机制。

实例1:验证编译阶段会对泛型类型参数进行类型擦除

public class GenericTest1 {

    public static void main(String[] args) throws NoSuchMethodException {
        new GenericTest1().testType();
    }

    public void testType(){
        ArrayList<Integer> collection1 = new ArrayList<>();
        ArrayList<String> collection2 = new ArrayList<>();

        // judge if bytecodes is the same during compile
        System.out.println( collection1.getClass() == collection2.getClass() );

        System.out.println(collection1.getClass().getName());
        System.out.println(collection2.getClass().getName());
    }

输出结果:

true
java.util.ArrayList
java.util.ArrayList

可以看到,在编译后输出的类型collection1.getClass()已经擦除了泛型。

分析:不管泛型的类型形参传入时是哪一种类型实参,对于Java而言,它们依然被当做同一类型进行处理,在内存中也只会占用一块空间。我们讨论Java泛型时,其作用域是代码的编译阶段。在编译过程中,对于正确校验泛型的结果后会将泛型的相关信息擦除。也即是说,成功编译后的class文件中是不包含任何泛型信息的。

实例2:javap命令分析编译后的字节码

public static void main(String[] args){
    List<String> stringList = new ArrayList<>();
    }
}

对编译后的字节码用javap命令进行分析:

  • Code中,关于new方法的描述可以看到有关泛型的信息已经被擦除;
  • LocalVariableTypeTable中,Signature字段记录着泛型的具体信息;

又如:

class IntList extends ArrayList<Integer>{
    List<String> toStringList(){
        return new ArrayList<>();
    }
}

通过javap对字节码进行分析可以得到:

Signature中,额外记录了泛型的实际类型;

注意

  • 在静态方法、静态初始化块和静态变量的声明和初始化中不允许使用类型形参;
  • 由于系统中不会真正生成泛型类,所有instanceof运算符后不能使用泛型类;

不同位置的泛型信息获取(反射):

5.3 桥方法


由于Java的泛型在运行时会发生类型擦除(Type Erasure),这可能导致在某些情况下,如使用反射调用泛型方法时,出现类型不匹配的问题。为了解决这个问题,Java编译器会自动生成一些桥接方法来确保类型安全。

具体而言,桥接方法是一种由编译器自动插入的方法,用于保持多态的正确性。当泛型类继承自非泛型类,并且重写了非泛型类中的方法时,由于Java的泛型类型擦除机制,直接在泛型类中添加这个方法会导致类型不匹配。为此编译器生成一个桥接方法,确保类型安全和多态的正确性。

作用:

  1. 类型安全:泛型桥方法的主要作用是保持类型安全性。通过添加桥方法,可以在运行时防止对不兼容的类型进行访问。这样可以避免在编译期间无法检测到的类型错误;
  2. 维护继承关系:泛型桥方法还用于维护泛型类或接口之间的继承关系。它们确保子类或实现类能够正确地覆盖父类或接口的泛型方法,并使用正确的类型参数;

实例1

public class MyList<T> {
    public void add(T element) {
        // 添加元素的逻辑
    }
}

// 子类继承泛型类,并覆盖泛型方法
public class StringList extends MyList<String> {
    @Override
    public void add(String element) {
        // 添加元素的逻辑
    }
}

在这个示例中,由于Java的泛型类型擦除机制,编译器会生成一个桥方法来确保类型安全性和兼容性。上述代码实际上被编译器转换为以下内容:

public class MyList {
    public void add(Object element) {
        // 添加元素的逻辑
    }
}

public class StringList extends MyList {
    @Override
    public void add(Object element) {
        add((String) element);
    }

    public void add(String element) {
        // 添加元素的逻辑
    }
}

在这个转换后的代码中,StringList 类包含了一个桥方法 add(Object element),它调用了真正的泛型方法 add(String element)。这样就保持了类型安全性,并且与父类的非泛型方法兼容。

通过生成泛型桥方法,Java编译器可以在继承和实现泛型类型时保持类型安全性和兼容性。这些桥方法在内部转换和维护泛型类型擦除的同时,提供了更好的类型检查和运行时类型安全性。

实例2:把桥方法揪出来

public class GenericTest4 {
    public static void main(String[] args) {
        Class<ServiceImpl> serviceClass = ServiceImpl.class;
        Method[] methods = serviceClass.getDeclaredMethods();
        for(Method method : methods){
            System.out.println(method.getName() + " : "  +method.getReturnType());
        }
    }
}

interface Service<T>{
    T getData();
}
class ServiceImpl implements Service<Integer>{
    @Override
    public Integer getData() {
        return Integer.MAX_VALUE;
    }
}

输出结果:

getData : class java.lang.Integer
getData : class java.lang.Object

分析:可以看到实际上类中存在的方法有两个,除了我们自己写的类型为Integer的外,还有类型为Object的方法。这个Object方法即为桥接方法。

六、避雷区


 下面是一些不能踩的雷点

  • 基本类型不能用于泛型Test<int> t; 这样写法是错误,我们可以使用对应的包装类Test<Integer> t ;
  • 不能通过类型参数创建对象T elm = new T(); 运行时类型参数T会被替换成Object,无法创建T类型的对象,容易引起误解,java干脆禁止这种写法;
  • 模糊性错误:

:对于泛型类User<K,V>而言,声明了两个泛型类参数。在类中根据不同的类型参数重载show方法:

public class User<K, V> {

    public void show(K k) { // 报错信息:'show(K)' clashes with 'show(V)'; both methods have same erasure

    }
    public void show(V t) {
    }
}

由于泛型擦除,二者本质上都是Obejct类型。方法是一样的,所以编译器会报错。

  • 对静态方法的限制:静态方法无法访问类上定义的泛型,所以必须要将泛型定义在方法上;
  • 对泛型数组的限制:不能实例化元素类型为类型参数的数组,但是可以将数组指向类型兼容的数组的引用:
public class User<T> {
    private T[] values;
    public User(T[] values) {

        //错误,不能实例化元素类型为类型参数的数组
        this.values = new T[5];

        //正确,可以将values 指向类型兼容的数组的引用
        this.values = values;
    }
}

Java中的数组是协变的:数组的协变(covariance)是指可以向子类型的数组赋予基类型的数组引用。如果A是B的子类,那么一个A类型的数组可以被当作B类型的数组来使用。这种特性在Java的泛型中尤为重要,因为它允许我们在不丧失类型安全性的情况下,更加灵活地操作数组。

例如,如果我们有一个Integer类型的数组,我们可以将它赋值给一个Object类型的数组引用,因为Integer是Object的子类。这就是数组的协变特性。然而,需要注意的是,虽然这种赋值在编译时可以通过,但在运行时可能会引发错误,因为如果我们试图通过这个Object类型的数组引用向数组中存储一个非Integer类型的对象(如String),就会在运行时抛出异常。

数组是固定长度的,并且它们的元素类型在创建时就已确定。如果你有一个特定类型的数组(比如 Integer[]),那么你不能向这个数组中添加不是该类型或其子类型的元素。但如果你有一个 Object[] 类型的数组,由于Object是Java中所有类的超类,所以你可以向这个数组中添加任何类型的对象;

然而,如果你有一个 Integer[] 类型的数组,并将其赋值给一个 Object[] 类型的变量,虽然可以通过这个 Object[] 类型的变量来访问数组中的元素,但你仍然不能通过这个变量向数组中存储非Integer类型的对象。如果你尝试这样做,编译器会抛出一个 ArrayStoreException。

还有一些问题目前李家将还没解决,期待看客老爷们评论解惑!

Q:协变与多态的关系?

Q:泛型类中不能用下限限定??

参考

java 泛型全解 - 绝对最详细 - 掘金

致谢&部分资源出处:百战程序员 、B站@程序员c兄 (视频讲解得真的好深刻啊!

  • 13
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值