【Java基础06】泛型

【Java基础系列】
【Java基础01】基础概述
【Java基础02】常用类
【Java基础03】时间类
【Java基础04】异常
【Java基础05】枚举类
【Java基础06】泛型
【Java基础07】注解
【Java基础08】反射
【Java基础09】代理
【Java基础10】IO流

泛型

文章概述:总结描述Java基础中泛型的定义和使用,包括泛型的定义、泛型的使用优缺点和使用方法、泛型及其通配符、及泛型的实现原理。

一、泛型的定义

泛型(generics)是JDK5引入的新特性,是通用设计上必不可少的元素,在开原框架和JDK源码中都能看到它。

泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

二、为什么使用泛型

泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

泛型的作用:

  • 保证类型安全性
  • 消除强制转换
  • 提升性能,避免了不必要的装箱、拆箱操作
  • 提高代码重用性

保证类型安全性

消除强制转换

在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,容易出现类型转换错误。有了泛型后,相当于告诉编译器每个集合接收的对象类型是什么,使得程序更加安全,增强了程序的健壮性。

没有泛型:
ArrayList names = new ArrayList();
names.add("luo");
names.add(123); //编译正常   
String s = (String) list.get(0);
使用泛型:
ArrayList<String> names = new ArrayList<>();
names.add("luo");
names.add(123); //编译不通过
String s = list.get(0); // no cast

提升性能,避免了不必要的装箱、拆箱操作

在非泛型编程中,将筒单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高。

泛型变量固定了类型,使用的时候就已经知道是值类型还是引用类型,避免了不必要的装箱、拆箱操作。

提高代码重用性

三、如何使用泛型

3.1 泛型类

泛型类:把泛型定义在类上

public class 类名 <泛型类型1,...> {
    
}

泛型类型必须是引用类型(非基本数据类型)

定义泛型类,在类名后添加一对尖括号,并在尖括号中填写类型参数,参数可以有多个,多个参数使用逗号分隔:

public class GenericClass<ab,a,c> {}

public static void main(String[] args) {
    ArrayListT<String> listT = new ArrayListT<String>(5);
    listT.add("123");
    System.out.println(listT.get(0));
}

// T是指我们的数据类型,当我们在实例化的时候必须要指明他的类型
static class ArrayListT<T> {
    private Object[] elementDate;
    private int size = 0;
    //initialCapacity初始容量
    public ArrayListT(int initialCapacity){
        this.elementDate = new Object[initialCapacity];
    }

    //这里的T就是我们要添加数据的类型,其必须与最开始定义的类型相同
    public void add(T t) {
        this.elementDate[size++] = t;
    }

    public T get(int num) {
        return (T) this.elementDate[num];
    }
}

3.2 泛型接口

泛型接口概述:把泛型定义在接口上

public interface  类名 <泛型类型,...> {
    
}

方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。

public class GenericsInterfaceDemo {
    public interface GenericInterface<T> {
        void show(T value);
    }

    class StringShowImpl implements GenericsInterfaceDemo.GenericInterface<String> {
        @Override
        public void show(String value) {
            System.out.println(value);
        }
    }

    class NumberShowImpl implements GenericsInterfaceDemo.GenericInterface<Integer> {
        @Override
        public void show(Integer value) {
            System.out.println(value);
        }
    }
}

3.3 泛型方法

泛型方法概述:把泛型定义在方法上

修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ }
public class GenericsFunctionDemo {
    public static void main(String[] args) {
        GenericsFunctionDemo genericsFunctionDemo = new GenericsFunctionDemo();
        String s = genericsFunctionDemo.soutInfo("luo");
        Integer integer = genericsFunctionDemo.soutInfo(123);
        System.out.println(s);
        System.out.println(integer);
    }

    public <T> T soutInfo(T t) {
        System.out.println(t.getClass());
        return t;
    }
}
输出
class java.lang.String
class java.lang.Integer
luo
123

四、泛型及通配符

4.1 常用的 T,E,K,V,?

本质上这些个都是泛型,没啥区别,只不过是编码时的一种约定俗成的东西。代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。

通常情况下,T,E,K,V,? 是这样约定的:

  • ? 表示不确定的 java 类型
  • T (type) 表示具体的一个java类型
  • K V (key value) 分别代表java键值中的Key Value
  • E (element) 代表Element

4.2 泛型通配符

Java泛型的通配符是用于解决泛型之间引用传递问题的特殊语法, 主要有以下三类:

<?> 无边界的通配符 <? extend E> 固定上边界的通配符 <? super E> 固定下边界的通配符 ```java //表示类型参数可以是任何类型 public class Apple<?>{}

//表示类型参数必须是A或者是A的子类
public class Apple{}

//表示类型参数必须是A或者是A的超类型
public class Apple{}


为什么要使用通配符而不是简单的泛型呢?下面看一段代码

```java
static int countLegs(List<? extends Animal> animals) {
    int retVal = 0;
    for (Animal animal : animals) {
        retVal += animal.countLegs();
    }
    return retVal;
}

static int countLegs1(List<Animal> animals) {
    int retVal = 0;
    for (Animal animal : animals) {
        retVal += animal.countLegs();
    }
    return retVal;
}

public static void main(String[] args) {
    List<Dog> dogs = new ArrayList<>();
    // 不会报错
    countLegs(dogs);
    // 报错
    countLegs1(dogs);
}

class Animal {
    public Integer countLegs() {
        return 1;
    }
}

class Dog extends Animal {
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KSvmPeeB-1677671097919)(images/【Java基础06】泛型/image-20230301163109712.png)]

像 countLegs 方法中,限定了上限,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错。而 countLegs1 就不行。其原因是在于,要是使用了<? extends Animal>玩意的话我们的范围就变成了Animal及其子类

上界通配符 < ? extends E>

用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。

下界通配符 < ? super E>

用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object

4.3 T 和 ?的区别

?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ? 不行

// 可以
T t = operate();
// 不可以
? car = operate();

区别:

通过 T 来 确保 泛型参数的一致性

// 通过 T 来 确保 泛型参数的一致性
public <T extends Number> void
test(List<T> dest, List<T> src)

//通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型
public void
test(List<? extends Number> dest, List<? extends Number> src)

通配符可以使用超类限定而类型参数不行

类型参数 T 只具有 一种 类型限定方式:

T extends A

但是通配符 ? 可以进行 两种限定:

? extends A
? super A

4.4 Class 和 Class<?> 区别

前面介绍了 ? 和 T 的区别,那么对于,Class<T><Class<?> 又有什么区别呢?

常用的反射代码:

// 通过反射的方式生成  multiLimit 
// 对象,这里比较明显的是,我们需要使用强制类型转换
MultiLimit multiLimit = (MultiLimit)
Class.forName("com.glmapper.bridge.boot.generic.MultiLimit").newInstance();

如果反射的类型不是 MultiLimit 类,那么一定会报 java.lang.ClassCastException 错误。

对于这种情况,则可以使用下面的代码来代替,使得在在编译期就能直接 检查到类型的问题:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ysXTwMkq-1677671097921)(images/【Java基础06】泛型/image-20230301193208361.png)]

Class<T> 在实例化的时候,T 要替换成具体类。Class<?> 它是个通配泛型,? 可以代表任何类型,所以主要用于声明时的限制情况。

// 可以
public Class<?> clazz;
// 不可以,因为 T 需要指定类型
public Class<T> clazzT;

所以当不知道定声明什么类型的 Class 的时候可以定义一 个Class<?>。

那如果也想 public Class<T> clazzT; 这样的话,就必须让当前的类也指定 T

public class Test3<T> {
    public Class<?> clazz;
    // 不会报错
    public Class<T> clazzT;
}

五、泛型实现原理

泛型本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间「擦除」泛型语法并相应的做出一些类型转换动作。

例子:

public class Generics<T> {
	private T t;
}

定义了一个泛型类,定义了一个属性成员,该成员的类型是一个泛型类型,这个 T 具体是什么类型,我们也不知道,它只是用于限定类型的。

反编译一下这个类:

public class Generics{
	public Generics(){}
	private Object num;
}

发现编译器擦除 Caculate 类后面的两个尖括号,并且将 num 的类型定义为 Object 类型。

那么是不是所有的泛型类型都以 Object 进行擦除呢?大部分情况下,泛型类型都会以 Object 进行替换,而有一种情况则不是。那就是使用到了extends和super语法的有界类型

public class Generics<T extends String> {
	private T num;
}

这种情况的泛型类型,num 会被替换为 String 而不再是 Object。

这是一个类型限定的语法,它限定 T 是 String 或者 String 的子类,也就是你构建 Generics 实例的时候只能限定 T 为 String 或者 String 的子类,所以无论你限定 T 为什么类型,String 都是父类,不会出现类型不匹配的问题,于是可以使用 String 进行类型擦除。

实际上编译器会正常的将使用泛型的地方编译并进行类型擦除,然后返回实例。但是除此之外的是,如果构建泛型实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。

extends String> {
	private T num;
}

这种情况的泛型类型,num 会被替换为 String 而不再是 Object。

这是一个类型限定的语法,它限定 T 是 String 或者 String 的子类,也就是你构建 Generics 实例的时候只能限定 T 为 String 或者 String 的子类,所以无论你限定 T 为什么类型,String 都是父类,不会出现类型不匹配的问题,于是可以使用 String 进行类型擦除。

实际上编译器会正常的将使用泛型的地方编译并进行类型擦除,然后返回实例。但是除此之外的是,如果构建泛型实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。

实际上编译器不仅关注一个泛型方法的调用,它还会为某些返回值为限定的泛型类型的方法进行强制类型转换,由于类型擦除,返回值为泛型类型的方法都会擦除成 Object 类型,当这些方法被调用后,编译器会额外插入一行 checkcast 指令用于强制类型转换,这一个过程就叫做『泛型翻译』

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值