【JavaSE】java中“泛型”的那些事~
泛型在java中是非常常见的技术,下面我会从3个方面介绍一下泛型,即What(什么是泛型?),Why(为什么会有泛型?),How(泛型怎么用呢?),还有泛型的一些其他知识(通配符、泛型擦除)。
1. 什么是泛型?(What)
这个玩意,就是泛型…的一种
是的,没错,只要是用尖括号括起来的引用数据类型,都是泛型
简单来说,泛型就是用尖括号括起来的引用数据类型,注意是引用**数据类型哦
引用数据类型都有什么呢?Integer、String、Char…大写字母开头的,
与之对应的是基本数据类型:int、char、float…都是小写字母开头的
***复杂***来说,泛型(generic type)就是:泛型其本质是将类型参数化,也就是说所操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
2. 为什么会有泛型?(Why)
2.1 规范
我们来看一段代码,这是泛型没出来之前小明同学写的一段代码
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("abc");
list.add("abcd");
list.add(666);
System.out.println(list.get(0));
System.out.println(list.get(1));
System.out.println(list.get(2));
}
我们运行一下,发现是可以正常运行的,但是…
我们想要遍历集合获得每个元素的长度时,智能的IDEA就会给我们报错,这是因为list集合中既有Integer类型,又有String类型,我们知道length()这个方法是Sting类型的元素特有的方法,Integer类型的元素是没有的,所以编译器就会报错,那怎么解决呢?
只要把Integer类型和String类型分开存储不就行了吗,这样我们就可以遍历集合,然后使用String类型的特有方法啦
public static void main(String[] args) {
ArrayList listInt = new ArrayList();
listInt.add(1);
listInt.add(2);
listInt.add(3);
ArrayList listString = new ArrayList();
listString.add("abc");
listString.add("abcde");
listString.add("abedefg");
for (int i = 0; i < listString.size(); i++) {
System.out.println(listString.get(i).length());
}
}
但是,在某天写代码的时候,小明同学又不小心把Integer类型的数据添加到了String类型的集合之中了,这就导致他遍历集合取每个字符串长度的时候又发生了报错,于是小明同学就想,我要怎么才能提前知道我”不小心把Integer类型的数据添加到了String类型的集合之中“了呢,于是,泛型诞生了!!!小明同学规定,集合指定了泛型之后,只能添加一种数据类型,如果添加别的数据类型,就会报错
就像这样
而事实上,人们定义泛型的初衷就是这样:
如果我们没有给集合指定类型,默认认为所有的数据类型都是Object类型
此时可以往集合添加任意的数据类型。
带来一个坏处:我们在遍历获取数据的时候,无法使用他的特有行为。
所以
我们推出了泛型,可以在添加数据的时候就把类型进行统一。
而且我们在获取数据的时候,也省的强转了,非常的方便。
2.2 实用
还有一方面,我们在学数据结构时,几乎都手动实现了ArrayList…了吧…(是吧…)
下面将用两种方式实现ArratList:用泛型和不用泛型
1. 不用泛型
public class MyArrayList {
private int[] array;
private int size; // 有效数据个数
public MyArrayList() {
this.array = new int[10];
this.size = 0;
}
public void add(int x) { // 暂不考虑扩容
this.array[size] = x;
this.size++;
}
}
如果我们想使用我们自己的ArrayList
public static void main(String[] args) {
MyArrayList myArrayList = new MyArrayList();
myArrayList.add(1);
myArrayList.add(2);
myArrayList.add(3);
System.out.println(myArrayList);
}
可以说是十分完美啊,但是…
如果我想创建一个存储字符串的集合呢?
又报错了!!!原因很简单,我们自己定义的ArrayList,存储数据的类型是int,只能存储int型的数据,你要存储String类型的数据,不报错才怪
小明同学说了,那我就要存储String类型的数据,怎么解决呢?很简单,我们重新实现一个String类型的ArrayList不就行了吗?
小明同学又说了,那我还要存储flaot类型的数据。也很简单,我们再重新实现一个flaot类型的ArrayList不就行了吗?
那我还要存储short类型的数据,
我还要存储short类型的数据,
我还要存储char类型的数据…
额。。。。。。
2. 用泛型
为了不那么麻烦的解决小明同学的问题,(泛型诞生了!!!)*2
public class MyArrayList<E> {
// 在类的实现中,可以直接将类当成一种数据类型来使用。在实例化该类的时候这个类型才被确定
private E[] array;
private int size; // 有效数据个数
public MyArrayList2() {
this.array = (E[])new Object[10]; // 注意:Java中泛型不允许定义数组
this.size = 0;
}
public void add(E e) { // 不考虑扩容
this.array[size] = e;
this.size++;
}
}
// 带泛型的顺序表元素类型是一个“变量”
// E就是变量的名称
来看这段代码,我们只实现一种ArrayList,但是在定义中不明确指定要存储的数据类型,而是用代替,这就是大名鼎鼎的:泛型!!!,这样,我们只需要在new一个ArrayList的时候,传入要存储数据的类型,就可以做到一种实现,多种使用了嘻嘻…
可以说是非常的实用呢…
2.3 总结
所以,为什么会有泛型的出现呢?主要就是两种原因:
1.如果我们没有给集合指定类型,默认认为所有的数据类型都是Object类型
此时可以往集合添加任意的数据类型。
带来一个坏处:我们在遍历集合获取数据的时候,无法使用他的特有行为
所以,
人们推出了泛型,可以在添加数据的时候就把类型进行统一规范
而且我们在获取数据的时候,也省的强转了,非常的方便
2.我们定义数据结构时,要想该数据结构能够存储所有的数据类型,就要使用到泛型
在定义数据结构时,不明确指定要存储的数据类型,而是用<E>代替
在创建该数据结构时,再将数据类型传入进来
这样,我们就可以定义一种数据结构来存储多种数据类型的数据了
3. 泛型怎么用呢(How?)
说了这么多,小明同学早就迫不及待了:”那泛型到底有什么用啊???“,泛型的主要应用有3种:泛型类、泛型方法、泛型接口
泛型类:在类名后面定义泛型,创建该类对象的时候,确定类型
泛型方法:在修饰符后面定义方法,调用该方法的时候,确定类型
泛型接口:在接口名后面定义泛型,实现类确定类型
3.1 泛型类
当我们在编写一个类的时候,如果不确定类中要存储数据的类型,那么这个类就可以定义为泛型类。
我们上面定义的“实用型”MyArrayList,就是一种泛型类
public class MyArrayList2<E> {
// 在类的实现中,可以直接将类当成一种数据类型来使用。在实例化该类的时候这个类型才被确定
private E[] array;
private int size; // 有效数据个数
public MyArrayList2() {
this.array = (E[])new Object[10]; // 注意:Java中泛型不允许定义数组
this.size = 0;
}
public void add(E e) { // 不考虑扩容
this.array[size] = e;
this.size++;
}
}
// 带泛型的顺序表元素类型是一个“变量”
// E就是变量的名称
即在定义类的时候,不明确指定成员变量的参数,而使用(、等等都是可以的哦)代替,在实例化时,再指定具体的类型参数
public static void main(String[] args) {
//泛型类的使用
MyArrayList<String> myArrayList = new MyArrayList();
myArrayList.add("a");
myArrayList.add("b");
myArrayList.add("c");
System.out.println(myArrayList);
}
3.2 泛型方法
当我们在编写一个方法的时候,如果不确定方法操作的数据的类型,那么这个方法就可以定义为泛型方法 。
例如:我们定义一个工具类ListUtil,类中定义一个静态方法addAll,用来添加多个集合的元素。
public class ListUtil {
private ListUtil(){}
//类中定义一个静态方法addAll,用来添加多个集合的元素。
/*
* 参数一:集合
* 参数二:要添加的元素
*
* */
public static<E> void addAll(ArrayList<E> list, E e){
list.add(e);
}
}
我们可以调用addAll()方法给String类型的集合添加元素,也可以给Integer类型的数组添加元素
public static void main(String[] args) {
ArrayList<String> listString = new ArrayList<String>();
ArrayList<Integer> listInteger = new ArrayList<Integer>();
ListUtil.addAll(listString, "a");
ListUtil.addAll(listString, "b");
ListUtil.addAll(listString, "c");
System.out.println(listString);
ListUtil.addAll(listInteger, 1);
ListUtil.addAll(listInteger, 2);
ListUtil.addAll(listInteger, 3);
System.out.println(listInteger);
}
3.3 泛型接口
泛型接口是指接口的定义中使用泛型类型参数。与泛型类类似,接口的泛型参数可以在实现接口的类中指定。这样可以让接口更加灵活,能够适应不同的数据类型。
来看下面这段代码
// 定义一个泛型接口
interface Pair<K, V> {
K getKey();
V getValue();
}
// 实现泛型接口
class KeyValuePair<K, V> implements Pair<K, V> {
private K key;
private V value;
public KeyValuePair(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
}
public class Main {
public static void main(String[] args) {
// 创建一个键值对,键是 String 类型,值是 Integer 类型
Pair<String, Integer> pair1 = new KeyValuePair<>("Age", 30);
System.out.println(pair1.getKey() + ": " + pair1.getValue()); // 输出:Age: 30
// 创建一个键值对,键是 Integer 类型,值是 String 类型
Pair<Integer, String> pair2 = new KeyValuePair<>(30, "Age");
System.out.println(pair2.getKey() + ": " + pair2.getValue()); // 输出:30:Age
}
}
我们可以通过泛型接口,实现不同类型的键值对创建,可以是(String,Integer),也可以是(Integer,String)
4. 泛型通配符(了解即可)
在 Java 中,泛型通配符是一种特殊的符号,通常用于指定泛型的类型参数范围。通配符可以让你编写更通用、灵活的代码,尤其是在涉及到泛型的集合操作时,它使得方法可以接受不同类型的参数。Java 提供了三种主要的泛型通配符:?(无界通配符)、? extends T(上界通配符)和 ? super T(下界通配符)。
4.1 无界通配符
无界通配符表示可以接受任何类型。它通常用于当我们不关心具体的类型时,但需要泛型参数的某些特定行为时。
import java.util.List;
public class WildcardExample {
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<String> strList = List.of("A", "B", "C");
// 可以接受任何类型的 List
printList(intList); // 输出:1 2 3
printList(strList); // 输出:A B C
}
- List<?> 是无界通配符,它表示可以是任何类型的 List。
- 使用无界通配符时,无法调用特定类型的方法(如 add()),只能读取对象并转为 Object 类型。
4.2 上界通配符
上界通配符用于表示“某个类型的子类型”,即泛型的类型参数必须是 T
或 T
的某个子类。这通常用于只读的操作,能够确保我们可以安全地访问对象的值,但不能修改它们。
import java.util.List;
public class UpperBoundWildcardExample {
// 使用上界通配符,表示 List 中的元素是某个类型的子类
public static void printNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<Double> doubleList = List.of(1.1, 2.2, 3.3);
printNumbers(intList); // 输出:1 2 3
printNumbers(doubleList); // 输出:1.1 2.2 3.3
}
}
- List<? extends Number> 表示这个 List 中的元素必须是 Number 类型或者 Number 的子类(如 Integer, Double 等)。
- 由于我们使用了上界通配符,因此我们可以在方法中读取 Number 类型的元素,但不能对其进行修改(比如无法调用 add() 方法)。
4.3 下届通配符
下界通配符用于表示“某个类型的父类”,即泛型的类型参数必须是 T
或 T
的某个父类。这通常用于写操作,比如往集合中添加元素。
javaCopy Codeimport java.util.List;
public class LowerBoundWildcardExample {
// 使用下界通配符,表示可以接受 T 或 T 的父类
public static void addNumbers(List<? super Integer> list) {
list.add(10); // 可以安全地添加 Integer 类型
// list.add(3.14); // 错误:不能添加 Double 类型
}
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
addNumbers(numberList); // 合法,因为 Number 是 Integer 的父类
List<Object> objectList = new ArrayList<>();
addNumbers(objectList); // 合法,因为 Object 是 Integer 的父类
// List<Integer> integerList = new ArrayList<>();
// addNumbers(integerList); // 错误:不能向 List<Integer> 中添加其他 Integer 之外的类型
}
}
- List<? super Integer> 表示这个 List 中的元素必须是 Integer 或 Integer 的父类(如 Number 或 Object)。
- 我们可以在方法中安全地向列表中添加 Integer 类型的元素,但不能添加其他类型(如 Double)。
5. 泛型擦除
java中的泛型是伪泛型。Java 的泛型只存在于编译阶段。
如下图,编译时期(java文件),调用list.add()方法时编译器会对传入的参数进行类型检查,如果传入的参数不是String类型的话,编译器会报错,但是编译完生成class文件后,list方法的类型会变成Object类型(所有类型的父类或间接父类),不会保留String泛型的信息。
在 Java 中,泛型是编译时的特性,它通过类型参数化提供了类型安全和灵活性。然而,Java 在运行时并不保留泛型的类型信息,所有泛型的类型信息都会被“擦除”,这就是所谓的泛型擦除(Generic Type Erasure)。泛型擦除的目的是为了兼容 Java 的旧版本(即 Java 1.5以前的版本),从而实现向后兼容。