Java进阶之泛型
引出泛型
上节我们看了Java中的异常处理,知道Java中有很多的异常类型。其中有一种运行时异常叫类型转换异常: ClassCastException 异常。
运行下列代码:
public class ClassCastExceptionExample {
public static void main(String[] args) {
Object obj = new Integer(10);
try {
String str = (String) obj;
} catch (ClassCastException e) {
System.out.println("发生 ClassCastException!");
e.printStackTrace();
}
}
}
我们创建了一个Integer对象,并将其赋值给一个Object类型的变量obj。然后,我们尝试将obj强制转换为String类型,但是由于obj实际上是一个Integer对象,而不是String对象,这个转换操作会失败,并抛出ClassCastException。
报错:
发生 ClassCastException!
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at ClassCastExceptionExample.main(ClassCastExceptionExample.java:6)
在Java中,对象的向下转型都是有安全隐患的,比如上述的Integer强制转换为String。此时就会出现类型转换异常:ClassCastException。
那怎么办呢? 在Java5的时候引入了一个叫泛型的东西,就是为了解决这种问题。
比如我们在使用集合的时候,首先就会定义其泛型<String>:
// 使用泛型 List,指定只能存储 String 类型的元素
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");
// 使用泛型后,编译器会保证类型的正确性
for (String str : stringList) {
System.out.println(str);
}
// 下面这行代码会导致编译错误,因为不能将Integer添加到String类型的List中,因为就和上面一样:Integer强制转换为String就报错
// stringList.add(10);
// 使用泛型,即使在需要类型转换时,也不会出现ClassCastException
// 因为编译器已经保证了类型的正确性
System.out.println(stringList.get(0));
可以看到,上面我们在定义List的时候就声明了泛型是String,这样在下面使用的时候,我们在编译代码的时候就能看到插入的类型是否会报错,就可以避免了运行时发生类型转换异常。
由此可见,使用泛型可以在编译时期捕捉到类型错误,而不是在运行时。根据越早出错代价越小原则,这有助于提高代码的可靠性和稳定性。
什么是泛型?
泛型,即“参数化类型”,允许在定义类、接口、方法时不指定具体的类型,而是使用一个占位符(例如T、E、K、V等)来表示,在使用时再指定具体的类型。
上面也说了,泛型是Java5中引入的,但是前面版本中的类怎么办呢?为了不受影响,默认就将泛型设置为<Object>,这样就实现兼容了,所以在使用原始类的时候,不加泛型也是可以的,但是会有警告,比如List list = new ArrayList<>();就会警告提示。
Java7的时候,泛型可以由List<String> stringList = new ArrayList<String>();简写为List<String> stringList = new ArrayList<>();
泛型分类
泛型有三种使用方式:
泛型类:
在定义类时使用类型参数,这样类就可以被用于各种数据类型。声明泛型类非常简单,只需要在类名后添加一对尖括号,里面写上类型参数即可。
// T1, T2, ..., Tn 是类型参数,它们在实例化泛型类时被替换为具体的类型。
class className<T1, T2, ..., Tn> {
private T1 t1;
private T2 t2;
public void set(T t) {
this.t1 = t;
}
}
泛型接口
public interface Generator<T> {
T next();
}
实现泛型接口时,可以选择指定具体类型,也可以继续保持泛型。
public class NumberGenerator implements Generator<Integer> {
private int index = 0;
public Integer next() {
return index++;
}
}
泛型方法
泛型方法是在方法级别使用泛型,即方法可以在调用时确定泛型参数的具体类型。可以在泛型类使用,也可以再非泛型类中使用。
泛型方法的类型参数放在修饰符(如 public、static 等)之后,返回类型之前。
<类型参数T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T。这个类型参数T可以出现在这个泛型方法的任意位置,泛型的数量也可以为任意多个
语法:
修饰符 <类型参数T> 返回类型 方法名(参数列表) {
// 方法体
}
示例:
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue());
}
}
在这个示例中,compare方法是一个泛型方法,它有两个类型参数K和V,用于比较两个Pair对象的键和值是否相等。
泛型方法可以是静态的,也可以是实例的。在静态泛型方法中,类型参数是在调用方法时确定的,与类实例无关。
泛型的通配符和上下限
占位符:
T是Type的缩写,比如 定义class Animal<T>{},使用的时候: Animal<Dog> dogs = new Animal<>();
K是Key,V是Value,比如Map<K,V>, 使用的时候: HashMap<String, Integer> map = new HashMap<>();
E是Element,常用在集合中,表示集合中的元素类型。
N是Number,表示数字类型。
S是Subtype,表示子类型。
U是Another type,表示另一个类型参数。
这些占位符并没有强制性的规定,开发者可以根据自己的喜好或者上下文来选择合适的字母作为泛型占位符。
无界通配符(Unbounded Wildcards):
无界通配符简单地使用<?>表示未知类型,它表示对类型没有限制。这种通配符通常用于处理泛型类型时不需要具体类型信息的情况。
示例:
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
在这个示例中,printList方法可以接受任何类型的List,但是不能向列表中添加元素,因为编译器不知道列表应该接受哪种类型。
在Java中,通配符可以指定上限和下限.
上限通配符使用 <? extends T> 表示,它指定泛型类型的上界,这意味着这个泛型类型必须是T类型或T类型的子类型。
public class Fruit {}
public class Apple extends Fruit {}
public class Orange extends Fruit {}
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
// 使用上限通配符,可以传入Fruit或Fruit的子类型
printFruitList(apples);
List<Fruit> fruits = new ArrayList<>();
fruits.add(new Fruit());
fruits.add(new Apple());
fruits.add(new Orange());
// 同样可以使用上限通配符
printFruitList(fruits);
}
public static void printFruitList(List<? extends Fruit> fruits) {
for (Fruit fruit : fruits) {
System.out.println(fruit);
}
}
printFruitList 方法接受一个 List,其中包含的类型是 Fruit 或 Fruit 的任意子类型。这种方法可以安全地读取列表中的元素,因为它们至少是 Fruit 类型,但是不能向列表中添加元素,因为编译器不知道列表具体是 Fruit 的哪个子类型。
下限通配符使用 <? super T> 表示,它指定泛型类型的下界,这意味着这个泛型类型必须是T类型或T类型的父类型。
public static void addFruits(List<? super Fruit> fruits) {
fruits.add(new Fruit());
fruits.add(new Apple());
fruits.add(new Orange());
}
public static void main(String[] args) {
List<Fruit> fruits = new ArrayList<>();
addFruits(fruits);
List<Object> objects = new ArrayList<>();
addFruits(objects); // 可以传入Object类型的列表,因为Object是Fruit的父类型
}
addFruits方法接受一个List,其中包含的类型是Fruit或Fruit的任意父类型。这种方法可以安全地向列表中添加Fruit或Fruit的子类型的元素,但是不能保证从列表中读取到的元素的类型,因为它们可能是 Object 类型。
泛型擦除概念
泛型信息只存在于代码编译阶段有效,但是在java的运行期(已经生成字节码文件后)与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
例子:
ArrayList<Integer> l1 = new ArrayList();
ArrayList<String> l2 = new ArrayList();
System.out.println(l1.getClass()==l2.getClass());
运行代码,结果为True
这是因为ArrayList<String>和ArrayList<Integer>在jvm中的Class都是List.class,二者在jvm中等同于List<Object>,也说明泛型只是在编译的时候给我们增加了一个检查而已。
使用泛型的好处:
类型安全:在编译时进行类型检查,确保只能使用指定的类型。
代码重用:同一个泛型类可以用于不同的数据类型,避免为每种数据类型编写重复的代码。
性能提升:由于不需要进行类型转换,因此可以减少运行时错误,提高性能。
定义的时候类型并不确定,在使用的时候才知道具体的类型就可以考虑泛型。
END