前言:
泛型(Generics)是 Java 编程语言中的一个重要特性,它允许我们在定义类、接口和方法时使用一个或多个类型参数,从而使得代码具有更高的复用性和类型安全性。
为什么要使用泛型?
1.从使用层面上来说:
统一数据类型,防止将来的数据类型转换异常
2.从定义层面上来看:
定义带泛型的类,方法等,将来使用的时候给泛型确定什么类型,泛型就会变成什么类型,凡是涉及到泛型的都会变成确定的类型,代码更灵活
举一个例子:
Java中的ArrayList
package a_genericity;
import java.util.ArrayList;
public class Test02 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("aaa");
list.add(1);
list.add(true);
//获取元素中字符串的长度
for (Object o : list) {
String str = (String) o;
System.out.println(str);
}
}
}
这段代码逻辑:首先我定义了一个 ArrayList 并且我没有指定类型(默认就是Object)
我往这个列表中存了三个元素 : aaa 1 true
现在我的需求是需要知道这三个元素的长度
这三个元素不全是字符串,没有直接获取长度的方法
所以我想着转成字符串进行获取长度,然后就报错了ClassCastException 类型转化异常
出现这个问题的原因就是我们没有统一数据类型
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法,下面将正式介绍泛型的相关知识。
泛型类:
定义:
public class 类名<E>{
}
我们常见的ArrayList就是一个泛型类
下面模拟一个ArrayList,自己造一个MyArrayList
package a_genericity;
import java.util.Arrays;
public class MyArrayLista<E> {
Object[] objects = new Object[10];
int size = 0;
public boolean add(E e){
objects[size++] = e;
return true;
}
public E get(int index){
return (E)objects[index];
}
@Override
public String toString() {
return "MyArrayLista{" +
"objects=" + Arrays.toString(objects) +
'}';
}
}
package a_genericity;
import java.util.ArrayList;
public class Test01 {
public static void main(String[] args) {
MyArrayLista<String> myArrayLista = new MyArrayLista<>();
myArrayLista.add("a");
myArrayLista.add("b");
myArrayLista.add("c");
System.out.println(myArrayLista.get(0));
System.out.println(myArrayLista);
}
}
在MyArrayLista中创建了一个Object的数组来模拟底层数组,一个size变量来模拟数组的大小
并且写了三个方法:add get和toString
在测试类中模拟使用ArrayList
泛型方法:
package a_genericity;
import java.util.ArrayList;
public class MyArrayListb {
public static <E> void addAll(ArrayList<E> list,E...e){
for (E e1 : e) {
list.add(e1);
}
}
}
package a_genericity;
import java.util.ArrayList;
public class Test01 {
public static void main(String[] args) {
MyArrayLista<String> myArrayLista = new MyArrayLista<>();
myArrayLista.add("a");
myArrayLista.add("b");
myArrayLista.add("c");
System.out.println(myArrayLista.get(0));
System.out.println(myArrayLista);
System.out.println("==============================");
ArrayList<Integer> integerArrayList = new ArrayList<>();
MyArrayListb.addAll(integerArrayList,1,2,3);
System.out.println(integerArrayList);
}
}
注意:
public static <E> addAll(ArrayList<E> list,E...e){}这里的<E>是声明这个方法是个泛型方法,不代表返回值类型,所以我们自己还得定义返回值类型
可变参数语法糖:
我们注意到这个方法后面出现了E...e,这个代码:
在 Java 中,可变参数(Varargs)是一种方便的语法糖
- 当我们声明一个方法使用可变参数时,编译器会将可变参数转换为一个数组。
- 在调用该方法时,传递给可变参数的参数将被封装成一个数组对象。
- 在方法内部,可以将这个数组当作一个普通的数组进行操作。
泛型接口:
public interface MyList <E>{
public boolean add(E e);
}
public class MyArrayList1<E> implements MyList<E>{
//定义一个数组,充当ArrayList底层的数组,长度直接规定为10
Object[] obj = new Object[10];
//定义size,代表集合元素个数
int size;
/**
* 定义一个add方法,参数类型需要和泛型类型保持一致
*
* 数据类型为E 变量名随便取
*/
public boolean add(E e){
obj[size] = e;
size++;
return true;
}
/**
* 定义一个get方法,根据索引获取元素
*/
public E get(int index){
return (E) obj[index];
}
@Override
public String toString() {
return Arrays.toString(obj);
}
}
public class Demo04Genericity {
public static void main(String[] args) {
MyArrayList1<String> list1 = new MyArrayList1<>();
list1.add("张三");
list1.add("李四");
System.out.println(list1.get(0));
}
}
看到三种泛型的用法,我们需要考虑一个问题:
泛型的类型什么时候被定义?
对于泛型类来说:new对象的时候确定类型
对于泛型方法来说:调用的时候确定类型
对于方法接口来说:
a.在实现类的时候还没有确定类型,只能在new实现类的时候确定类型了 ->比如 ArrayList
b.在实现类的时候直接确定类型了 -> 比如Scanner
我们来一个项目中会用到泛型的案例来加深一下理解。
/**
* 后端统一返回结果
* @param <T>
*/
@Data
public class Result<T> implements Serializable {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}
}
这是项目中用于统一返回响应的Result类
我们重点看:
public static <T> Result<T> success(T object){}
这里有三个T
第一个<T>:表示声明这个方法是一个泛型方法,谁调用这个方法,这个T就是什么类型
第二个<T>:首先我们需要观察到这个是一个构造方法,Result本身是个泛型类,所以第二个<T>表示这个Result这个泛型类。
第三个T:这个T不是<T>了,这是是说明传进来的对象的类型是不确定的。
泛型的高级应用:
泛型通配符 ?
public class Demo01Genericity {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
list1.add("张三");
list1.add("李四");
ArrayList<Integer> list2 = new ArrayList<>();
list2.add(1);
list2.add(2);
method(list1);
method(list2);
}
public static void method(ArrayList<?> list){
for (Object o : list) {
System.out.println(o);
}
}
}
这段代码的逻辑:
首先我创建了两个列表,存储的元素类型分别是String和Integer
然后我创建了一个方法method 想要分别遍历这两个列表
就可以使用:ArrayList<?> list这种传入参数的办法
就不会导致类型冲突
不过其实也可以ArrayList list默认也行
泛型的上限下限
1.作用:可以规定泛型的范围
2.上限:
a.格式:<? extends 类型>
b.含义:?只能接收extends后面的本类类型以及子类类型
3.下限:
a.格式:<? super 类型>
b.含义:?只能接收super后面的本类类型以及父类类型
我们来看一个案例马上就明白了:
/**
* Integer -> Number -> Object
* String -> Object
*/
public class Demo02Genericity {
public static void main(String[] args) {
ArrayList<Integer> list1 = new ArrayList<>();
ArrayList<String> list2 = new ArrayList<>();
ArrayList<Number> list3 = new ArrayList<>();
ArrayList<Object> list4 = new ArrayList<>();
get1(list1);
//get1(list2);错误
get1(list3);
//get1(list4);错误
System.out.println("=================");
//get2(list1);错误
//get2(list2);错误
get2(list3);
get2(list4);
}
//上限 ?只能接收extends后面的本类类型以及子类类型
public static void get1(Collection<? extends Number> collection){
}
//下限 ?只能接收super后面的本类类型以及父类类型
public static void get2(Collection<? super Number> collection){
}
}
首先我这里有四个列表,存储元素的类型分别是:Integer String Number Object
在我们讲这个案例之前,我们需要知道这四个类的继承关系:
Integer 继承 Number 继承 Object
String 继承 Object
知道了这个之后,我们就能发现,当我们在方法get12中限定了泛型的上下限
有的列表根本就不能当作参数传进去。