Java泛型

一:泛型是什么?

泛型就是给类或方法加上一个“类型标签”

没有泛型之前,集合里可以放任意类型,取出来必须强转,容易报错。
有了泛型,编译器帮我们检查类型,放错类型直接编译报错,取出来直接是目标类型,不用强转。

一句话:泛型是把“运行时的ClassCastException”提前到“编译时报错”。


为什么要用泛型?

  1. 类型安全——放错类型编译期就报错,不会等到线上崩了才发现

  2. 消除强转——取出来直接用,不用写(String)list.get(0)

  3. 通用代码——写一个类/方法,支持所有类型(比如List<T>Box<T>


二:泛型的基本语法

1. 泛型类

// 定义:T是类型参数,用的时候指定
public class Box<T> {
    private T item;
    
    public void set(T item) {
        this.item = item;
    }
    
    public T get() {
        return item;
    }
}

// 使用
Box<String> box = new Box<>();  // JDK1.7以后右边可以省略类型
box.set("苹果");
String str = box.get();        // 不用强转

记住:

  • T是Type,E是Element,K是Key,V是Value——约定俗成,不是强制

  • 创建对象用new Box<>()——菱形语法


2. 泛型方法

public class Util {
    //  错误:E没有声明
    public static void method(E e) {}
    
    //  正确:<E>写在返回值前面
    public static <E> void method(E e) {}
    
    //  有返回值的泛型方法
    public static <E> E getLast(E[] arr) {
        return arr[arr.length - 1];
    }
}

// 调用
String last = Util.getLast(new String[]{"a", "b", "c"});

死规矩:

方法里用到的泛型字母,要么来自类,要么自己在返回值前面声明。


3. 泛型接口

// 定义
public interface List<E> {
    boolean add(E e);
    E get(int index);
}

// 实现方式1:实现类指定具体类型
public class StringList implements List<String> {
    private String[] arr;
    public boolean add(String e) { ... }
    public String get(int index) { ... }
}

// 实现方式2:实现类继续泛型
public class ArrayList<E> implements List<E> {
    private Object[] arr;
    public boolean add(E e) { ... }
    public E get(int index) { ... }
}

三:泛型通配符

List<Object> 和 List<?> 有什么区别?

List<Object>List<?>
含义这个List明确是Object类型这个List是某种类型,但我不知道具体是什么
可以存什么?可以存任何类型(String、Integer...)不能存任何东西(除了null)
可以取什么?取出来是Object取出来是Object

代码演示:

// List<Object>
List<Object> list1 = new ArrayList<>();
list1.add("字符串");      // 
list1.add(123);          // 
Object obj = list1.get(0); // 取出来是Object

// List<?>
List<?> list2 = new ArrayList<String>();
list2.add("abc");        //  编译报错 不能放
list2.add(null);         //  唯一能放的是null
Object obj2 = list2.get(0); // 可以取,但类型是Object

为什么不能放?

因为list2可能是List<String>,也可能是List<Integer>
编译器不敢让你放任何东西,万一是List<Integer>,你放String就崩了。


<? extends T> 和 <? super T> 是什么?

这是泛型的上限和下限

<? extends T> —— 上限通配符(T或T的子类)

// 这个list里的元素,都是Number或Number的子类
List<? extends Number> list = new ArrayList<Integer>(); // Integer是Number子类

// 只能读,不能写
Number n = list.get(0);  //  可以读,因为一定是Number子类
list.add(123);           //  不能写 万一是List<Double>呢?

场景: 方法只读数据,不修改数据。


<? super T> —— 下限通配符(T或T的父类)

// 这个list里的元素,都是Integer或Integer的父类
List<? super Integer> list = new ArrayList<Number>(); //  Number是Integer父类

// 可以写,读出来是Object
list.add(123);           //  可以写,Integer一定能放进去
Object obj = list.get(0); // 读出来只能是Object,不知道具体类型

场景: 方法只写数据,不关心读出来的具体类型。

PECS原则:Producer Extends, Consumer Super。
生产者用extends,消费者用super。

什么意思?

  • 你从里面取数据(它是生产者)→ <? extends T>

  • 你往里面放数据(它是消费者)→ <? super T>


四:泛型擦除

Java的泛型是真正的泛型吗?

你答:

不是。Java的泛型是伪泛型,或者说编译期泛型

泛型信息只在编译期存在,编译成字节码时会被擦除,所有类型参数替换为Object或上限,并自动插入强转代码。

代码证明:

ArrayList<String> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();

System.out.println(list1.getClass() == list2.getClass()); // true
// 运行时都是ArrayList.class,泛型信息没了

泛型擦除带来什么问题?

1. 不能用基本类型

List<int> list = new ArrayList<>(); //  编译报错
List<Integer> list = new ArrayList<>(); //  必须用包装类

擦除后变成Object,int不是Object的子类。


2. 不能重载签名一样的方法

public void method(List<String> list) {}
public void method(List<Integer> list) {} // 编译报错

擦除后都是method(List list),签名冲突。


3. 不能通过反射获取泛型具体类型(正常途径)

List<String> list = new ArrayList<>();
// 运行时不知道这个list原本是String类型

但可以通过父类的泛型获取(TypeReference)


五:项目里怎么用的

 场景1:统一返回结果(最常用)

public class Result<T> {
    private int code;
    private String msg;
    private T data;
    
    public static <T> Result<T> success(T data) {
        Result<T> r = new Result<>();
        r.code = 200;
        r.msg = "成功";
        r.data = data;
        return r;
    }
}

// 调用
Result<User> r1 = Result.success(new User());
Result<List<User>> r2 = Result.success(userList);
Result<Void> r3 = Result.success(null);

为什么T不用Object?
答: 用Object的话,前端调用要强转,而且文档生成工具(Swagger)能识别Result<User>里的User,识别不了Result里的Object。


场景2:泛型BaseDao/BaseService

public class BaseService<T> {
    @Autowired
    private BaseMapper<T> mapper;
    
    public T getById(Long id) {
        return mapper.selectById(id);
    }
    
    public int save(T entity) {
        return mapper.insert(entity);
    }
}

@Service
public class UserService extends BaseService<User> {
    // 继承getById和save,泛型自动变成User
}

 场景3:泛型工具方法

// 把数组转成List
public static <T> List<T> arrayToList(T[] arr) {
    List<T> list = new ArrayList<>();
    for (T t : arr) {
        list.add(t);
    }
    return list;
}

// 获取集合第一个元素
public static <T> T getFirst(List<T> list) {
    return list.isEmpty() ? null : list.get(0);
}

六:

Q1:什么是泛型?为什么要用泛型?

泛型是JDK1.5引入的特性,把类型参数化。
主要作用是类型安全消除强制转换
没有泛型的话,集合里能放任意对象,取出来必须强转,容易出现ClassCastException。
泛型让编译器在编译期就检查类型,放错类型直接报错,取出来直接用。


Q2:泛型类、泛型方法、泛型接口的区别?

  • 泛型类:类名后面带<T>,整个类都可以使用这个T

  • 泛型方法:返回值前面带<T>,T只在当前方法有效

  • 泛型接口:和泛型类类似,实现类可以指定具体类型,也可以继续泛型


Q3:List<T>List<Object>List<?>的区别?

  • List<T>:正在定义泛型时使用,T是类型参数

  • List<Object>:具体类型是Object,可以放任何对象

  • List<?>:通配符,表示未知类型,只能读不能写


Q4:什么是泛型擦除?

Java的泛型是编译期实现的,编译后会擦除类型信息。
比如List<String>编译成List,所有类型参数替换为Object,并自动插入强转。
所以运行时无法获取泛型的具体类型


Q5:可以声明泛型数组吗?

//  不能直接创建
T[] arr = new T[10]; // 编译报错

//  只能通过反射或强转
T[] arr = (T[]) new Object[10];

因为泛型擦除后,T[]变成Object[],存的时候可能类型错误。

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值