Java泛型的总结

首先

由于最近一些日子重新研究了Generics,在简要介绍Generics概要之后,我决定简要记录以下两个内容。

    • Genericsのスコープ

 

    型変数周りの話し

简介:泛型概念是指在编程中使用一种通用的类型或方法来处理多种数据类型的能力。它允许在编译时进行类型检查和类型安全,提高了代码的可重用性和性能。泛型使得程序员能够编写灵活且可靠的代码,同时减少了代码冗余。

无论如何,我想重新整理一下使用Generics带来的好处。它可以在编译时告诉我们当试图插入错误的类型对象时(引用自《Effective Java 第3版》第5章 Generics),这样我们就可以在一定程度上确保类型安全,并在各种不同类型上实现相同的处理逻辑。
现在,我想通过代码给出一些具体例子,来展示使用Generics的优点。这里我们将比较数组和Generics,并意识到使用Generics的好处。
以下是在不使用Generics的情况下定义列表、向列表中添加元素和提取元素的示例。

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

需要明确地进行类型转换以从Object类型中取出,如果有错误(例如用int进行赋值,然后用String进行取出)则无法通过运行来发现错误。←这个非常令人沮丧。

使用泛型的列表的定义,添加元素到列表中,以及从列表中取出元素的代码如下所示。

ArrayList<String> list = new ArrayList<String>(); 
list.add("hoge"); 
String s = list.get(0);

在编译时出现错误。(在执行前的阶段就能够察觉到错误,这真是令人高兴。)

关于泛型的范围

泛型有两个作用域,分别是方法作用域和实例作用域。下面分别给出概述。

方法范围

首先,以下是语法结构的示例。

public class SetInt{
  public <E> Set<E> union(Set<E> s1,Set<E> s2){
    // (例)s1とs2をたす処理
 }
}

首先注意到的是public访问修饰符后面的,它是一个类型变量声明。通过这个声明,可以在该方法范围内定义参数和返回值具有相同类型的对应关系。在方法作用域内,泛型声明可以用于实例方法,当然也可以用于静态方法和构造函数。
在调用时,可以直接调用或者明确指定类型后再调用。

如果直接调用的话
Set<Integer> result = SetInt.union(Set<Integer>,Set<Integer>);
如果需要明确指定类型并调用时
Set<Integer> result = SetInt.<Integer>union(Set<Integer>,Set<Integer>);

此外,还可以通过明确指定类型来区分调用异常处理的方式。

public class Hoge{
    public <E extends Exception> void example() throws E {};
    public  void test() {
        try {
            this.<IOException>example();
        } catch (IOException e) {
            // IOException例外catch時の例外処理
        }
        try {
            this.<SQLException>example();
        } catch (SQLException e) {
            // SQLException例外catch時の例外処理
        }
    }   
}

实例范围

与方法作用域相比,它在实际使用中更常见,并且在入门书籍中也通常被涉及。以下是一个例子。

public class Stack<E> {
  private E elements;
  public void push(E e) {
    // elementsにpushする処理
  }
  public E pop() {
    // elementsからEを取り出す処理
  }
}

我们可以定义多个已声明的实例方法(如上所述的两个)具有相同的参数、返回值和实例字段类型的对应关系。

有关泛型变量的讨论

假设你具备关于”共变、逆变、不变”以及”里斯科夫替换原则”的知识。关于前者,你可以参考这篇文章来理解(https://www.thekingsmuseum.info/entry/2016/02/07/235454)。
在下面的备忘录中,我们将以A、B和C三个类作为例子进行验证,假设它们之间存在继承关系:C继承B,B继承A。

协变

可以将其定义为如下:

List<? extends B> extendsBList = new ArrayList<C>();

上述的extendsAList具有以下特性。

    1. 可以从extendsBList中提取出B型和C型的变量。

 

    extendsBList不能存储除null类型以外的数据。

尽管性质1暂且不论,但对于性质2仍然存在疑问。以下是对性质2的推导过程的总结。

为什么extendsBList不能存储除了null类型以外的元素?

假设我们可以将新的对象B存储在extendsBList中并执行extendsBList.add(new B())。但是请再次参考 extendsBList的初始化代码。它是用ArrayList类型进行初始化的。如果我们可以使用add()方法添加B类型的对象,则会向ArrayList类型中添加B类型的对象,这将导致矛盾。因此,为了保持List<? extends B>的类型安全,只能添加到可分配给List<? extends B>的List和List中的对象。这是因为只有null类型才满足属性2。

反變 –

可以将其定义如下。

List<? super B> superBList = new ArrayList<A>();

上述的superBList具有以下特性:
1. superBList能够存储B型和A型的变量。
2. 从superBList中取出的类型是Object型。

性质2仍然存在疑问。但是如果性质1成立,那么通过get()取出的对象可能是比B类型更高级的对象类型。因此,当使用<? super B>时,只能接收到位于所有类型顶端的Object类型的get()返回对象。这意味着无法确定返回的类型以接受存储,也就是无法确定将要返回的类型。

关于PECS原则。

在《Effective Java》第二版的第28项中,提到了PECS(Producer Extends Consumer Super)原则。以下是引文。

如果在函数内部,泛型参数的角色是”生产者(Producer)”,我们使用”extends”;如果是”消费者(Consumer)”,我们使用”super”。生产者在函数内部产生(提供)某些值的参数。而消费者在函数内部消费(利用)某些值的参数。这个原则通常被称为”PECS”,即Producer-Extends and Consumer-Super。

如果根据协变和逆变的讨论,我们可以理解为什么要这样做。以Hoge类为例。

    • 関数側からすれば値を生成(提供)することは、関数を使う側からすれば値を取得すること、すなわち値を取り出す場合は<? extends Hoge>とする。

 

    関数側からすれば値を消費(利用)することは、関数を使う側からすれば値を設定すること、すなわち値を格納する場合は<? super Hoge>とする。

在某个方法中,如果在该方法的参数中使用extends,在该方法的返回值中使用super,就可以更广泛地采用该方法的实现。

最后

我希望能隨時檢查並修改文章內容。而且,當我能夠寫泛型的時候,我希望能夠進行使用泛型進行設計。

请参考

(https://www.amazon.co.jp/exec/obidos/ASIN/B078H61SCH/xray2000-22/) 的链接是一个日本亚马逊产品页面。

广告
将在 10 秒后关闭
bannerAds