一、泛型是什么 ,为啥会有泛型
在泛型出现前,Java等强类型语言中方法的参数、返回值、类的成员变量、局部变量都必须是一个特定数据类型。比如:要找出Integer、Double
数组和 Float
数组中的最大值,如果没有泛型,就需要为每种类型编写单独的方法:
// 找出 Integer 数组中的最大值
public static Integer findMax(Integer[] array) {
Integer max = array[0];
for (Float element : array) {
if (element > max) {
max = element;
}
}
return max;
}
// 找出 Float 数组中的最大值
public static Float findMax(Float[] array) {
Float max = array[0];
for (Float element : array) {
if (element > max) {
max = element;
}
}
return max;
}
// 找出 Double 数组中的最大值
public static Double findMax(Double[] array) {
Double max = array[0];
for (Double element : array) {
if (element > max) {
max = element;
}
}
return max;
}
.......
观察这些方法发现,除了方法的入参、返回值、还有关联的局部变量不一样,每个方法的运算步骤可以说是一毛一样。那如何在数据类型不一样的情况下实现思想和步骤复用呢?我们很容易想到向上转型,但向上转型会丢失类型限制,出现诸如返回值不满足类型要求,类型编译时检查失效从而导致的运行时类型转换错误等问题。
这种情况下,泛型出现了,它既实现了方法的复用,又能对数据类型做一致性限制,如下所示:
// 定义一个泛型方法来找出数组中的最大值
// <T extends Comparable<T>> 是泛型声明,声明该泛型的类型必须是Comparable<T>的子类 泛型类型为T
// 泛型类型声明为T之后,后面使用的入参类型T[],局部变量类型、返回值类型都使用了T类型,就是说编译时候会检查这类型是否一致
public static <T extends Comparable<T>> T findMax(T[] array) {
T max = array[0];
for (T element : array) {
if (element.compareTo(max) > 0) {
max = element;
}
}
return max;
}
类型安全限制
如上图,case1 findMax入参是Integer[],即T = Integer,它的返回值是Integer,所以赋值给i没有错;case2 中findMax入参是Float[] 即T = Float,它的返回值就是Float,那么直接赋值给Integer,就会报错,就是通过泛型这种机制实现了数据类型一致性的限制,避免了运行时类型转换出错。
在面向对象语言中,所有方法都在类中,所以泛型不仅能够解决方法的复用,也能够解决类的成员变量、局部变量等的复用(如何用下一个章节介绍),总结下泛型的作用有两点:
1. 类和方法复用:可以使用调用者传入类型;
2. 类型限制: 一致性、类型上下界限制(子类、父类限制);
二、泛型如何使用
泛型使用遵守先声明后使用的步骤,常见的有以下几种:
2.1 泛型类
public class Box<T> { // <T>是泛型声明 跟在类名后面
private T content; // 泛型使用,这里说名了 content是T类型的
public void setContent(T content) {
this.content = content;
}
public T getContent() { // 泛型使用 这里说明 getContent 返回类型是T
return content;
}
public static void main(String[] args) {
Box<String> stringBox = new Box<>(); // 这里Box<> = Box<String> jdk7之后可以省略,编译器根据前面自动推导,通过构造函数将T类型赋值为String类型
stringBox.setContent("Hello");
System.out.println("String content: " + stringBox.getContent()); // 输出:Hello
Box<Integer> integerBox = new Box<>();
integerBox.setContent(123);
System.out.println("Integer content: " + integerBox.getContent()); // 输出:123
}
}
常见的泛型类有Map、Set、List以及他们的子类。
2.2 泛型方法
public class GenericMethodExample {
// 泛型方法,<T> 是泛型声明,需要放在返回类型之前声明
// T[] 是泛型使用,表明入参是T类型的数组
public static <T> T getFirst(T[] array) {
if (array == null || array.length == 0) {
return null;
}
return array[0];
}
public static void main(String[] args) {
String[] stringArray = {"Hello", "World"};
Integer[] intArray = {1, 2, 3};
// <String> 在方法前面显式指出了返回类型
String firstString = GenericMethodExample.<String>getFirst(stringArray);
// intArray 和firstInt类型 隐式推断类型参数
Integer firstInt = getFirst(intArray);
System.out.println("First String: " + firstString); // 输出:First String: Hello
System.out.println("First Integer: " + firstInt); // 输出:First Integer: 1
}
}
接下来是两个参数的泛型方法
public class DualGenericExample {
@RequiresApi(api = Build.VERSION_CODES.N)
// <T, U> 是泛型声明 U是返回值类型 T是一个输入参数类型
// Function<T, U> converter 然后 T U 又传递到一个泛型接口中 当作它的两个参数的类型
public static <T, U> U convertAndPrint(T input, Function<T, U> converter) {
U result = converter.apply(input);
System.out.println("Converted Result: " + result);
return result;
}
@RequiresApi(api = Build.VERSION_CODES.N)
public static void main(String[] args) {
// 示例1:将整数转换为字符串并打印
Integer num = 123;
String numStr = convertAndPrint(num, new Function<Integer, String>() {
@Override
public String apply(Integer integer) {
return String.valueOf(integer);
}
});
// 输出:Converted Result: 123
// 示例2:将字符串转换为其长度并打印
String str = "Hello";
Integer length = convertAndPrint(str, new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return s.length();
}
});
// 输出:Converted Result: 5
}
}
2.3 泛型接口
泛型接口允许我们在接口定义中使用类型参数,从而使接口能够处理不同类型的数据。
// 类似于泛型类
public interface Comparable<T> {
int compareTo(T o);
}
public class GenericInterfaceExample implements Comparable<GenericInterfaceExample> {
private int value;
public GenericInterfaceExample(int value) {
this.value = value;
}
@Override
public int compareTo(GenericInterfaceExample other) {
return Integer.compare(this.value, other.value);
}
public static void main(String[] args) {
GenericInterfaceExample obj1 = new GenericInterfaceExample(10);
GenericInterfaceExample obj2 = new GenericInterfaceExample(20);
System.out.println(obj1.compareTo(obj2)); // 输出:-1(因为 10 < 20)
}
}
2.4 泛型通配符
在未引入泛型通配符时,泛型的限制是类型一致性限制(必须都是声明的类型T);引入通配符之后限制变松了,有三种:无界通配符<?> (无限制)、上界通配符<? extends T >(必须是T类型及其子类型)和下界通配符<? super T>(必须是T类型及父类型),分别举例如下:
import java.util.List;
// 无限制的通配符,不管什么类型都可以打印,一般用于只需要读取数据,而不需要写入数据
public class UnboundedWildcardExample {
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
public static void main(String[] args) {
List<String> stringList = List.of("Hello", "World");
List<Integer> intList = List.of(1, 2, 3);
printList(stringList); // 可以打印 List<String>
printList(intList); // 可以打印 List<Integer>
}
}
import java.util.List;
//上界通配符 限制父类,比如是Number子类的 才能使用,适用于需要读取某种类型及其子类型的数据的情况。
public class UpperBoundedWildcardExample {
public static void printNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num);
}
}
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); // 可以打印 List<Integer>
printNumbers(doubleList); // 可以打印 List<Double>
}
}
import java.util.List;
import java.util.ArrayList;
// 下界通配符 限制子类,必须是Integer的父类才能使用该方法
// 适合写入某种类型及子类型的数据的情况,注意理解下,因为方法是限制的子类型
// 下届通配符 是限制下界,比如本例子中,是限制是必须是Integer父类才能使用该方法,这个父类是
// 使用者给的,比如Number,所以上面说 适合是写入某种类型 Number以及子类Integer写入使用。
public class LowerBoundedWildcardExample {
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
addNumbers(numberList); // 可以向 List<Number> 添加 Integer
List<Object> objectList = new ArrayList<>();
addNumbers(objectList); // 可以向 List<Object> 添加 Integer
}
}
// 类型要求一致性的场景,
public static <T> void addElement(List<T> list, T element) {
list.add(element);
}
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
addElement(stringList, "Hello"); // 可以添加元素
List<Integer> intList = new ArrayList<>();
addElement(intList, 1); // 可以添加元素
}
// 类型要求一致的场景,通配符? 不能使用
public static void addElement(List<?> list, Object element) {
list.add(element); // 编译错误,无法添加元素
}
总结下:泛型通配符就是从原来必须等于T 或者其他类型的限制,放松为无限制或者限制参数的父类和子类,使用更加灵活,类型安全降低。
三、泛型实现原理简介
回顾前文,泛型有两个作用:
-
类和方法复用:类的成员变量、方法参数使用泛型定义后,调用者可以传入类型,从而使用;
-
类型限制:未使用通配符的定义的泛型方法和类,所有使用地方传入的类型必须一致;使用通配符的定义的泛型方法和类,所有使用方不做类型限制,或者有类型上下界限制(子类、父类限制);
类型擦除(泛型擦除)+强制类型转换 是泛型实现的主要原理,具体来说就是在编译时****,Java 编译器****会****将****泛型类型参数替换为它们的非泛型上界,如果没有指定上界,则替换为 Object,并且使用强制类型转换保证类型一致,这样就实现了在方法和类的复用(可以使用各种类型)和运行时候类型一致性。`
除此之外编译器会使用泛型信息(定义、入参、返回值等)进行类型检查和类型推断,以确保代码在编译时候是类型安全的。
所以我们可以说泛型是编译时特性,虚拟机对泛型一无所知。
四、什么时候使用泛型?
当构建通用工具、通用算法、通用数据结构,就是需要复用的时候请考虑泛型,什么时候需要复用,就是当考虑通过规模化降低成本的时候。
网友评论