Java泛型详解:类型参数化的魅力

引言

对泛型的简明定义

 

在Java中,泛型(generic)是一种强类型机制,允许程序员在类、接口或方法中的编译时指定和检查类型。通俗地讲,泛型就是参数化类型的应用,允许你创建可以处理各种数据类型的代码,同时保持类型检查的优势。使用泛型可以让你的代码更加灵活,同时又保持了类型安全。

 

Java语言中存在泛型的原因和目标

 

存在泛型的原因在于,Java希望确保在编译时刻就可以进行严格的类型检查,以便在开发时期找到和修正可能的错误。在没有泛型之前,我们可以将任何类型的对象添加到Java集合中,编译器也不能对此进行检查。然而在运行时,一旦取出的对象类型不准确,就会抛出ClassCastException,带来严重的后果。

 

引入泛型后,Java能提供编译时类型安全检查,减少运行时类型错误,提供程序的稳定性。同时,它还可以消除源代码中的许多类型强制转换,使代码看上去更加简洁。

 

总结一下,泛型的目标主要有三点:

 
  1. 提供编译时类型检查,防止插入错误类型的数据。
  2. 消除源代码中的类型转换,简化代码。
  3. 使得类和方法能适用于多种类型,提高代码复用性。

Java泛型基础

泛型类和泛型方法

 

泛型类是在类名后面跟一个尖括号,里面是类的类型参数。这些类型参数只存在于编译期,实例化对象时,需要指定具体的类型。例如,ArrayList<E>就是一个泛型类,E是ArrayList内部元素的类型。

public class Box<T> {
    private T t;
    
    public void set(T t) {
        this.t = t;
    }
    
    public T get() {
        return t;
    }
}
 

泛型方法和泛型类类似,是在返回类型之前添加类型参数列表< E >。泛型方法并不需要在一个泛型类中,而且类型参数<E>只在方法声明中定义。

public class Util {
    public static <T> T getLast(T[] array) {
        return array[array.length - 1];
    }
}
 

泛型接口

 

和泛型类一样,泛型接口允许接口的某个或部分或全部方法使用泛型。

public interface Pair<K, V> {
    public K getKey();
    public V getValue();
}
 

泛型和继承

 

Java中的泛型继承规则是,如果类A是类B的子类,那么List<A>不是List<B>的子类。这是因为即使A和B有继承关系,但它们对应的泛型List<A>和List<B>没有继承关系。

 

通配符

 

Java泛型接口、泛型类和泛型方法对类型参数有严格的限制,而通配符就是为了解决这种严格限制的问题。Java提供了两种通配符,'?'代表未知类型;'? extends E' 代表类型上限,表示参数化类型是“E或者E的子类型”; '? super E' 代表类型下限,表示参数化类型是“E或者E的父类型”。

类型参数的边界

明确参数类型

 

在Java泛型中,你可以使用特定的参数类型来创建实例化的对象。例如,“Box<Integer> integerBox = new Box<>()"就是一个明确参数类型的例子,在创建Box实例时已经明确确定了储存的内容类型为Integer。

 

使用extends关键字设定上边界

 

在有些情况下,我们可能需要限制可用于特定类的类型参数,这就需要使用extends关键字来设定类型的上边界。例如,我们想定义一个处理Number类及其子类的泛型,则可以这么定义:"<T extends Number>",那么这个泛型就只能指定Number或者其子类作为类型参数。

public class Box<T extends Number> {
    private T t;
   
    public void set(T t) {
        this.t = t;
    }
    //...
}
 

这样在使用Box类时,我们就不能再使用String或其他非Number的子类作为类型参数了。

 

使用super关键字设定下边界

 

super关键字在泛型中的主要用处是设定类型参数的下限。例如,“List<? super Integer>”就表示只能接受Integer以及其超类(super class)的类型。

 

这种方式非常有用,例如当我们需要从集合中获取元素,同时这些元素应该是某个特定的超类型时。

public void processElements(List<? super Integer> elements){
    for(Object o : elements){
        // can only process elements as Object
    }
}
 

在上述代码示例中,我们可以将任何Integer对象或者其子类对象添加到集合中,而获取的元素只能按Object对象来处理。

泛型的类型擦除

什么是类型擦除

 

类型擦除(Type Erasure)是Java泛型的一个核心概念。它指的是在编译期间,Java编译器会把泛型的具体类型参数信息全部擦除,替换为它的限定类型(没有定义限定类型的用Object代替),生成运行时的字节码。换句话说,到了运行期间,泛型的类型信息已经被擦除了,不再存在。

 

类型擦除如何影响我们的代码

 

类型擦除主要有以下几种影响:

 
  1. 无法使用instanceof关键字判断对象是否是一个特定的泛型类型。
List<String> strList = new ArrayList<>();
if (strList instanceof ArrayList<String>) { }
// 这将会触发编译错误:"Cannot perform instanceof check against parameterized type ArrayList<String>"
 
  1. 在一个类中,不能声明多个同名方法,参数类型仅有泛型参数区别。
public class Test<T> {
    public void setData(T data){}
    public void setData(String data){} // 这将会触发编译错误因为在运行时都为 setData(Object data)
}
 

解决因类型擦除产生的问题

 
  1. 使用特定类型替代泛型类型。
    例如,“List<Integer>”和“List<String>”在类型擦除后都是“List”,如果需要在运行期间判断原始的泛型类型,可以创建一个IntegerList和StringList扩展自List。

  2. 对于方法的重载,可以利用方法的返回类型进行重载,或者改变方法名以避免使用擦除后类型相同的参数列表。

深入泛型

泛型方法

 

泛型方法与泛型类一样,都使用类型参数。泛型方法的类型参数出现在返回值前面,并且仅在该方法中有效。因而,泛型方法可以说是整个类中独立的部分,可以在非泛型类中使用。

 

例如:

public class Utility {
   // 泛型方法 printArray                       
   public static < E > void printArray( E[] inputArray ) {
      // 输出数组元素
      for(E element : inputArray) {
         System.out.printf("%s ", element);
      }
      System.out.println();
   }
}
 

通用类型推断

 

在Java SE 8及更高版本中,类型推断已被加强以便程序员可以省略必要的类型参数。该操作称为钻石操作符 (<>),因为 <> 运算符看起来像是一个钻石。

 

例如:

List<String> list = new ArrayList<>();。
 

在上述代码中,我们并没有在右侧的 expression 显式声明类型,Java编译器能够推断出我们想要创建一个ArrayList<String>对象。

 

泛型嵌套

 

泛型嵌套是指创建的类型或方法本身就是泛型,同时它的类型参数(对于类)或类型变量(对于方法)包含另一个泛型表达式。这可能包括多级嵌套,但无论如何,都要保持明确的可读性和简洁性为优。

 

例如:

Map<String, List<Integer>> map = new HashMap<>(); 
 

在这里,List<Integer>嵌套在Map<String, List<Integer>>之中,是对泛型的嵌套使用。

泛型的限制

泛型不能用于静态成员

 

        我们需要先知道静态成员对于类的所有实例来说是共享的。考虑到泛型类型是在实例化类或调用方法时才被确定,如果允许我们将泛型用于静态成员,那么这个静态成员就得需要同时满足所有可能的泛型类型,这显然是不切实际的。因此,泛型不能用于静态类型或者方法。

 

无法创建泛型数组

 

Java不支持直接创建泛型数组。以下代码是有误的:

List<String>[] array = new List<String>[10];   // 编译错误
 

这是因为会存在类型协变的问题。假设这是可行的,我们会面临下列情况:

Object[] objArray = new List<String>[10];
objArray[0] = new ArrayList<Integer>();
 

第二行使用Object[]引用List<String>[]是合法的,因为数组支持协变。第三行再往所谓的List<String>[]数组存放ArrayList<Integer>,也没有类型问题。然而,这明显是我们不期望的,因此,Java编译器就禁止直接创建泛型数组。

 

泛型异常的限制

 

在处理异常时,Java泛型有如下的限制:

 
  1. 不能捕捉泛型类型的异常,因为在运行时期异常类型需要明确。
public <T extends Exception, J> void execute(J array, Function<J, T> function) {
    try {
        function.apply(array);
    } catch (T e) {  // 编译错误,无法捕捉泛型类型的异常
        //...
    }
}
 
  1. 泛型类不能直接或者间接继承自Throwable(也就是说,异常类型不能是泛型)。这也是因为在抛出与捕捉异常时,需要明确的异常类型信息。

泛型在实践当中的应用

在集合中使用泛型的例子

 

泛型在集合中的使用可以提升程序的灵活性以及类型安全。例如,在使用ArrayList存储数据时,如果不确定存储元素的类型,可以使用泛型来解决问题。

List<String> list = new ArrayList<String>();

list.add("Hello");
list.add("World");

for(String s: list) {
    System.out.println(s);
}
 

在这个例子中,我们定义了一个只能存储String类型数据的ArrayList。当我们试图添加一个非String类型的数据时,编译器会报错。

 

自定义泛型类和方法的使用实例

 

下面是一个自定义的泛型类Pair以及一个泛型方法示例。

public class Pair<T> {

    private T first;
    private T second;

    public Pair() {
        first = null;
        second = null;
    }

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }

    // 泛型方法
    public <U extends Number> void inspect(U u){
        System.out.println("Number: " + u);
    }
}
 

上述代码首先定义了一个泛型类Pair,该类有两个类型为T的属性。然后,我们实现了一个泛型方法inspect,该方法接受一个继承自Number类的参数U。

 

例如,下面的代码创建了一个Pair<String>对象,并使用了inspect方法:

Pair<Integer> pair = new Pair<>(1, 2);
pair.inspect(3.14);  // 输出: Number: 3.14

泛型与反射

如何通过反射访问泛型信息

 

Java的反射API提供了许多方法,允许我们在运行时访问类和方法的泛型信息。通过Class对象的getDeclaredMethod()方法,可以获取到Method对象。然后可以通过Method对象的getGenericReturnType()getGenericParameterTypes()方法,获取到泛型返回类型和参数类型。

 

以下是一个例子:

public class Test {
    public Map<String, Integer> testMethod(List<String> keys) {
        return null;
    }

    public static void main(String[] args) throws NoSuchMethodException {
        Method method = Test.class.getDeclaredMethod("testMethod", List.class);
        Type returnType = method.getGenericReturnType();
        Type[] paramTypes = method.getGenericParameterTypes();

        if(returnType instanceof ParameterizedType) {
            System.out.println(((ParameterizedType) returnType).getActualTypeArguments()[0]);
            System.out.println(((ParameterizedType) returnType).getActualTypeArguments()[1]);
        }

        if(paramTypes[0] instanceof ParameterizedType) {
            System.out.println(((ParameterizedType) paramTypes[0]).getActualTypeArguments()[0]);
        }
    }
}
 

这个例子将会打印出方法testMethod的参数和返回值的泛型类型。

 

通过反射创建泛型实例

 

由于Java的类型擦除,我们不能直接通过反射来创建一个具有具体参数化类型的泛型实例。但我们可以创建泛型类的一个未参数化的实例,然后在使用时进行类型转换。

 

这是一个例子:

public class Test<T> {
    private T field;

    public Test(T field) {
        this.field = field;
    }

    public T getField() {
        return field;
    }

    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("your.package.Test");
        Test<String> instance = (Test<String>) clazz.getDeclaredConstructor(Object.class).newInstance("Hello");
        System.out.println(instance.getField()); // Prints: Hello
    }
}
 

在这个例子中,我们首先获取了Test类的Class对象,然后通过getDeclaredConstructor方法获取了构造函数,并传入Object.class表示需要一个接收Object的构造函数。然后我们调用newInstance方法创建了一个新的Test实例,并传入一个字符串"Hello"作为参数。最后,我们将这个新创建的无参数化实例统一转型为Test<String>类型。

总结

泛型在Java中的重要性

 
  1. 提高程序的可读性:泛型能够让代码清晰地表达出其工作方式,因为泛型方法包含了显式的参数类型。例如,List<String>,这就明确表示我们在用一个专门存放String类型对象的列表。

  2. 代码重用:通过泛型,你可以编写出可以对多种数据类型进行操作的通用算法。这避免了因类型不同而需要编写多个方法或者类的问题。

  3. 类型安全:在编译阶段,就可以检查类型的匹配性,一旦类型不匹配,编译器就不会通过,这样可以提早发现并解决问题,保证了程序的稳定性和可靠性。

 

泛型用法的最佳实践

 
  1. 使用边界通配符来提高灵活性:如果你需要从一个泛型实例中读取类型T的实例,你应该使用? extends T。如果你需要写入类型T的实例,你应使用? super T。如果你既需要读取又需要写入,你应该直接使用T。

  2. 避免类型警告:即使类型警告通常并不会导致运行时错误,也应尽可能地消除它们。如果无法消除类型警告,但你可以证明引发警告的代码是类型安全的,可以使用一个@SuppressWarnings("unchecked")注解来禁止警告。

  3. 优先使用列表代替数组:泛型和数组是有冲突的,Java中的数组是协变的,而泛型却是可替换的。数组提供了运行时的类型安全,但泛型的类型安全是在编译时提供。所以,在你需要泛型和数组功能的地方,应当优先使用列表。

  4. 注意泛型方法的类型擦除:你要意识到类型参数在运行时可能并不存在,所有的真实对象都必须是未经检查的原始类型。如果因为类型擦除,你的方法声明产生了类型错误,可以通过引入一个Class<T>引用来解决。

  5. 利用有限的通配符实例化泛型:在创建泛型对象时,List<T>可以接受任何类型T,此时如果实例化时没有明确指出具体类型,可能会增加类型推导的难度,所以可以利用有界通配符比如List<? extends Number>来限定接受的类型范围,提高代码的易读性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哎 你看

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值