【Java原理系列】java 泛型原理用法分类示例详解

本文围绕Java泛型展开,介绍了使用泛型的好处,如编译时强类型检查、消除类型转换等。详细阐述了泛型类型、方法、有界类型参数等概念,还讲解了类型擦除的原理及影响。此外,指出了泛型在实例化、创建数组等方面存在的限制。

java 泛型

为什么使用泛型?

在简单来说,泛型允许在定义类、接口和方法时将类型(类和接口)作为参数。类似于方法声明中使用的形式参数,类型参数为您提供了一种重用相同代码但使用不同输入的方式。不同之处在于,形式参数的输入是值,而类型参数的输入是类型。

使用泛型的代码相较于非泛型代码具有许多优势:

  • 编译时更强的类型检查。
    Java编译器会对泛型代码进行强类型检查,并在代码违反类型安全性时发出错误。修复编译时错误比修复运行时错误更容易,后者很难找到。

  • 消除了类型转换。
    在没有使用泛型的代码中,需要进行类型转换:

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

当重写为使用泛型时,代码不需要进行类型转换:

List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);   // 无需转换
  • 可以实现泛型算法。
    使用泛型,程序员可以实现适用于不同类型集合的通用算法,这些算法可以进行定制,并且具有类型安全性和更易读的特点。

以上是使用泛型的一些好处。通过利用泛型,可以使代码更加类型安全、简洁且易于维护。

Generic Types(泛型)

泛型类型是参数化类型的通用类或接口。下面的Box类将被修改以演示这个概念。

一个简单的Box类

首先,看一下操作任何类型对象的非泛型Box类。它只需要提供两个方法:set,用于向盒子中添加对象;get,用于获取对象:

public class Box {
   
   
    private Object object;

    public void set(Object object) {
   
    this.object = object; }
    public Object get() {
   
    return object; }
}

由于其方法接受或返回Object,你可以自由地传入任何你想要的内容,只要不是原始类型之一。在编译时,没有办法验证类的使用方式。代码的一部分可能会将Integer放入盒子中,并期望从中获取到整数,而代码的另一部分可能错误地传入了一个String,导致运行时错误。

Box类的泛型版本

泛型类的定义格式如下:

class name<T1, T2, ..., Tn> {
   
    /* ... */ }

类型参数部分由尖括号(<>)包围,紧跟在类名后面。它指定了类型参数(也称为类型变量)T1、T2、…和Tn。

要将Box类更新为使用泛型,只需将代码"public class Box"更改为"public class Box",创建一个泛型类型声明。这引入了类型变量T,在类内部的任何地方都可以使用。

有了这个改变,Box类变成了:

/**
 * Box类的泛型版本。
 * @param <T> 被封装值的类型
 */
public class Box<T> {
   
   
    // T代表“Type”
    private T t;

    public void set(T t) {
   
    this.t = t; }
    public T get() {
   
    return t; }
}

可以看到,所有Object的出现都被替换为T。类型变量可以是你指定的任何非原始类型:任何类类型、任何接口类型、任何数组类型,甚至是另一个类型变量。

这种技术同样适用于创建泛型接口。

类型参数命名约定

按照惯例,类型参数的名称是单个大写字母。这与你已经了解的变量命名约定形成鲜明对比,并且有充分的理由:如果没有这个约定,很难区分类型变量和普通的类或接口名称。

最常用的类型参数名称有:

  • E - 元素(Java集合框架广泛使用)
  • K - 键
  • N - 数字
  • T - 类型
  • V - 值
  • S, U, V等 - 第2个、第3个、第4个类型

你会在Java SE API以及本课程的其余部分中看到这些名称的使用。

调用和实例化泛型类型

要在代码中引用泛型Box类,必须进行泛型类型调用,将T替换为某个具体的值,例如Integer:

Box<Integer> integerBox;

可以将泛型类型调用视为普通方法调用的类比,但是不是将参数传递给方法,而是将类型参数(在本例中为Integer)传递给Box类本身。

类型参数和类型参数的术语:许多开发人员在使用过程中将“类型参数”和“类型参数”这两个术语互换使用,但这些术语并不相同。在编码时,通过提供类型参数来创建参数化类型。因此,Foo中的T是类型参数,Foo f中的String是类型参数。本课程在使用这些术语时遵守这个定义。

与任何其他变量声明一样,这段代码实际上并没有创建一个新的Box对象。它只是声明integerBox将保存对"Box of Integer"的引用,这是如何读取Box的含义。

泛型类型的调用通常称为参数化类型。

要实例化这个类,像平常一样使用new关键字,但是在类名和括号之间加上:

Box<Integer> integerBox = new Box<Integer>();

菱形操作符

在Java SE 7及更高版本中,你可以用一组空的类型参数(<>)替代调用泛型类构造函数所需的类型参数,只要编译器能够从上下文中确定或推断出这些类型参数。这对尖括号(<>)被非正式地称为菱形。例如,你可以使用以下语句创建Box的实例:

Box<Integer> integerBox = new Box<>();

有关菱形符号和类型推断的更多信息,请参见类型推断。

多个类型参数

如前所述,泛型类可以有多个类型参数。例如,泛型OrderedPair类实现了泛型Pair接口:

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

public class OrderedPair<K, V> implements Pair<K, V> {
   
   

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
   
   
        this.key = key;
        this.value = value;
    }

    public K getKey() {
   
    return key; }
    public V getValue() {
   
    return value; }
}

下面的语句创建了两个OrderedPair类的实例:

Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String>  p2 = new OrderedPair<String, String>("hello", "world");

代码new OrderedPair<String, Integer>将K实例化为String,V实例化为Integer。因此,OrderedPair的构造函数的参数类型分别是String和Integer。由于自动装箱,将String和int传递给这个类是有效的。

如菱形符号中所述,由于Java编译器可以从声明OrderedPair<String, Integer>中推断出K和V的类型,可以使用菱形符号简化这些语句:

OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String>  p2 = new OrderedPair<>("hello", "world");

要创建一个泛型接口,遵循与创建泛型类相同的约定。

参数化类型

你还可以用参数化类型(例如List)替换类型参数(即K或V)。例如,使用OrderedPair<K, V>示例:

OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));

原始类型(Raw Types)

原始类型是没有任何类型参数的泛型类或接口的名称。例如,给定泛型Box类:

public class Box<T> {
   
   
    public void set(T t) {
   
    /* ... */ }
    // ...
}

要创建Box的参数化类型,你需要为形式类型参数T提供实际类型参数:

Box<Integer> intBox = new Box<>();

如果省略实际类型参数,就会创建Box的原始类型:

Box rawBox = new Box();

因此,Box是泛型类型Box的原始类型。然而,非泛型的类或接口类型不是原始类型。

原始类型在旧代码中出现,因为许多API类(如Collections类)在JDK 5.0之前都不是泛型的。当使用原始类型时,实际上获得的是泛型之前的行为——一个Box会给你Objects。出于向后兼容性的考虑,将参数化类型赋值给其原始类型是被允许的:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;               // OK

但是,如果将原始类型赋值给参数化类型,则会得到警告:

Box rawBox = new Box();           // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox;     // warning: unchecked conversion

如果使用原始类型调用相应的泛型类型中定义的泛型方法,也会得到警告:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8);  // warning: unchecked invocation to set(T)

这个警告显示原始类型绕过了泛型类型检查,将不安全的代码检查推迟到运行时。因此,应该避免使用原始类型。

“类型擦除”一节中详细介绍了Java编译器如何使用原始类型。

未检查的错误消息

正如前面提到的,在将旧代码与泛型代码混合使用时,你可能会遇到类似以下的警告消息:

Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

当使用操作原始类型的旧API时,就会出现这种情况,如下面的例子所示:

public class WarningDemo {
   
   
    public static void main(String[] args){
   
   
        Box<Integer> bi;
        bi = createBox();
    }

    static Box createBox(){
   
   
        return new Box();
    }
}

术语"unchecked"表示编译器没有足够的类型信息来执行所有必要的类型检查,以确保类型安全性。默认情况下,“unchecked”警告被禁用,但编译器给出了提示。要查看所有的“unchecked”警告,请使用-Xlint:unchecked重新编译。

使用-Xlint:unchecked重新编译前面的示例,会显示以下附加信息:

WarningDemo.java:4: warning: [unchecked] unchecked conversion
found   : Box
required: Box<java.lang.Integer>
        bi = createBox();
                      ^
1 warning

要完全禁用未检查的警告,请使用-Xlint:-unchecked标志。@SuppressWarnings(“unchecked”)注解可以抑制未检查的警告。如果对@SuppressWarnings语法不熟悉,请参阅注解。

泛型方法(Generic Methods)

泛型方法是引入自己的类型参数的方法。这类似于声明一个泛型类型,但类型参数的范围仅限于声明它的方法内部。静态和非静态的泛型方法都是允许的,还可以有泛型类构造函数。

泛型方法的语法包括一个类型参数列表,在方法的返回类型之前以尖括号内的形式出现。对于静态的泛型方法,类型参数部分必须出现在方法的返回类型之前。

Util类包含一个泛型方法compare,用于比较两个Pair对象:

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());
    }
}

public class Pair<K, V> {
   
   

    private K key;
    private V value;

    public Pair(K key, V value) {
   
   
        this.key = key;
        this.value = value;
    }

    public void setKey(K key) {
   
    this.key = key; }
    public void setValue(V value) {
   
    this.value = value; }
    public K getKey()   {
   
    return key; }
    public V getValue() {
   
    return value; }
}

调用该方法的完整语法如下:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);

类型已经被明确指定,如加粗所示。一般来说,可以省略这部分,编译器会推断需要的类型:

Pair<Integer, String> p1 = new Pair<>(1, "apple")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

BigDataMLApplication

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

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

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

打赏作者

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

抵扣说明:

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

余额充值