内容


第 20 单元:泛型

使用抽象类型参数来定义类

Comments

开始之前

本单元是 “Java 编程入门” 学习路径的一部分。尽管各单元中讨论的概念具有独立性,但实践组件是在您学习各单元的过程中逐步建立起来的,推荐您在继续学习之前复习 前提条件、设置和单元细节

单元目标

  • 了解使用泛型的优势
  • 了解何时和如何参数化类或方法
  • 了解如何迭代泛型
  • 了解如何使用 enum 数据类型

什么是泛型?

JDK 5.0(2004 年发布)向 Java 语言中引入了泛型类型泛型)和关联的语法。基本上讲,一些当时熟悉的 JDK 类被替换为了等效的泛型。泛型是一种编译器机制,您可通过该机制获取通用的代码并参数化(或模板化)剩余部分,从而以一种一般化方式创建(和使用)一些类型的实体(比如类或接口和方法)。这种编程方法被称为泛型编程

泛型实战

要了解泛型有何作用,可以考虑一个在 JDK 中存在已久的类的示例:java.util.ArrayList,它是一个由数组提供支持的 ObjectList

清单 1 展示了如何实例化 java.util.ArrayList

清单 1. 实例化 ArrayList
ArrayList arrayList = new ArrayList();
arrayList.add("A String");
arrayList.add(new Integer(10));
arrayList.add("Another String");
// So far, so good.

可以看到,ArrayList 具有不同的形式:它包含两个 String 类型和一个 Integer 类型。在 JDK 5.0 之前,Java 语言对此行为没有任何约束,这导致了许多编码错误。例如,在 清单 1 中,目前为止看起来一切正常。但要访问 ArrayList 中的元素应该怎么办,清单 2 会尝试采用哪种方法?

清单 2. 尝试访问 ArrayList 中的元素
ArrayList arrayList = new ArrayList();
arrayList.add("A String");
arrayList.add(new Integer(10));
arrayList.add("Another String");
// So far, so good.
processArrayList(arrayList);
// In some later part of the code...
private void processArrayList(ArrayList theList) {
  for (int aa = 0; aa < theList.size(); aa++) {
    // At some point, this will fail...
    String s = (String)theList.get(aa);
  }
}

如果以前不知道 ArrayList 中包含的内容,则必须检查想要访问的元素,查看是否能处理这种类型的元素,否则您可能会遇到 ClassCastException

借助泛型,您可以指定放入 ArrayList 中的项目类型。清单 3 展示了如何做,以及在您尝试添加一个错误类型的对象(第 3 行)时会发生什么。

清单 3. 使用泛型的第二次尝试
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("A String");
arrayList.add(new Integer(10));// compiler error!
arrayList.add("Another String");
// So far, so good.
processArrayList(arrayList);
// In some later part of the code...
private void processArrayList(ArrayList<String> theList) {
  for (int aa = 0; aa < theList.size(); aa++) {
    // No cast necessary...
    String s = theList.get(aa);
  }
}

迭代泛型

泛型使用处理一些实体(比如 List)的特殊语法增强了 Java 语言,您通常可能希望逐个元素地处理这些实体。举例而言,如果想迭代 ArrayList,可以将 清单 3 中的代码重写为:

private void processArrayList(ArrayList<String> theList) {
  for (String s : theList) {
    String s = theList.get(aa);
  }
}

此语法适用于任何 Iterable(即实现了 Iterable 接口)的对象类型。

参数化的类

参数化的类对集合非常有用,所以集合是以下示例的上下文。考虑 List 接口,它表示一个有序的对象集合。最常见的使用场景为,向 List 添加项,然后按索引或迭代 List 来访问这些项。

如果考虑参数化一个类,请考虑是否满足以下条件:

  • 一个核心类位于某种包装器的中心:也就是说,类中心的 “东西” 可广泛地应用,它周围的特性(例如属性)是相同的。
  • 行为是相同的:无论类中心的 “东西” 是什么,都会执行完全相同的操作。

根据这两个条件,可以看到集合满足以下要求:

  • 这个 “东西” 就是组成集合的类。
  • 操作(比如 addremovesizeclear)完全相同,无论集合由哪些对象组成。

一个参数化的 List

在泛型语法中,创建 List 的代码类似于:

List<E> listReference = new concreteListClass<E>();

E(表示元素)是我之前提到的 “东西”。concreteListClass 是 JDK 中您实例化的类。该 JDK 包含多个 List<E> 实现,但您使用了 ArrayList<E>。您可能看到的泛型类的另一种形式是 Class<T>,其中 T 表示类型。在 Java 代码中看到 E 时,它通常指的是某种类型的集合。当您看到 T 时,它表示一个参数化的类。

所以,要创建一个由 java.lang.Integer 组成的 ArrayList,可以这样做:

List<Integer> listOfIntegers = new ArrayList<Integer>();

SimpleList:一个参数化的类

现在假设您想要创建自己的参数化类,它名为 SimpleList 且包含 3 个方法:

  • add() 将一个元素添加到 SimpleList 的末尾处。
  • size() 返回 SimpleList 中当前的元素数量。
  • clear() 完全清除 SimpleList 的内容。

清单 4 给出了参数化 SimpleList 的语法。

清单 4. 参数化 SimpleList
package com.makotojava.intro;
import java.util.ArrayList;
import java.util.List;
public class SimpleList<E> {
  private List<E> backingStore;
  public SimpleList() {
    backingStore = new ArrayList<E>();
  }
  public E add(E e) {
    if (backingStore.add(e))
    return e;
    else
    return null;
  }
  public int size() {
    return backingStore.size();
  }
  public void clear() {
    backingStore.clear();
  }
}

SimpleList 可使用任何 Object 子类来参数化。要创建并使用一个由 java.math.BigDecimal 对象组成的 SimpleList,可以这样做:

package com.makotojava.intro;
import java.math.BigDecimal;
import java.util.logging.Logger;
import org.junit.Test;
public class SimpleListTest {
  @Test
  public void testAdd() {
    Logger log = Logger.getLogger(SimpleListTest.class.getName());
    
    SimpleList<BigDecimal> sl = new SimpleList<>();
    sl.add(BigDecimal.ONE);
    log.info("SimpleList size is : " + sl.size());
    sl.add(BigDecimal.ZERO);
    log.info("SimpleList size is : " + sl.size());
    sl.clear();
    log.info("SimpleList size is : " + sl.size());
  }
}

而且您会获得以下输出:

Sep 20, 2015 10:24:33 AM com.makotojava.intro.SimpleListTest testAdd 
INFO: SimpleList size is: 1 Sep 20, 2015 10:24:33 AM com.makotojava.intro.SimpleListTest testAdd 
INFO: SimpleList size is: 2 Sep 20, 
2015 10:24:33 AM com.makotojava.intro.SimpleListTest testAdd 
INFO: SimpleList size is: 0

参数化的方法

有时,您可能不想参数化整个类,而是只想参数化一两个方法。在这种情况下,可以创建一个泛型方法。考虑清单 5 中的实例,其中使用方法 formatArray 来创建数组内容的字符串表示。

清单 5. 一个泛型方法
public class MyClass {
// Other possible stuff... ignore...
  public <E> String formatArray(E[] arrayToFormat) {
    StringBuilder sb = new StringBuilder();

    int index = 0;
    for (E element : arrayToFormat) {
      sb.append("Element ");
      sb.append(index++);
      sb.append(" => ");
      sb.append(element);
      sb.append('\n');
    }

    return sb.toString();
  }
// More possible stuff... ignore...
}

无需参数化 MyClass,可以将您想要使用的一个方法泛型化,创建一个适合任何元素类型的一致的字符串表示。

实际上,您会发现使用参数化的类和接口的频率比方法更高,但现在只需知道可以在需要的时候使用该功能。

enum 类型

在 JDK 5.0 中,向 Java 语言添加了一种名为 enum 的新数据类型(不要与 java.util.Enumeration 混淆)。enum 表示一组与某个特定概念相关的常量对象,每个对象都表示该集合中的一个不同的常量值。在将 enum 引入到该语言中之前,需要按照以下方法为一个概念(比如性别)定义一组常量值:

public class Person {
  public static final String MALE = "male";
  public static final String FEMALE = "female";
  public static final String OTHER = "other";
}

需要引用该常量值的代码可以像这样编写:

public void myMethod() {
  //. . .
  String genderMale = Person.MALE;
  //. . .
}

使用 enum 定义常量

使用 enum 类型会使常量的定义更加正式 — 而且更加强大。这是 Genderenum 定义:

public enum Gender {
  MALE,
  FEMALE,
  OTHER
}

此示例仅介绍了 enum 用途的很少一部分。事实上,enum 非常像类,所以它们可拥有构造方法、属性和方法:

package com.makotojava.intro;

public enum Gender {
  MALE("male"),
  FEMALE("female"),
  OTHER("other");

  private String displayName;
  private Gender(String displayName) {
    this.displayName = displayName;
  }

  public String getDisplayName() {
    return this.displayName;
  }
}

类与 enum 的一个区别是,enum 的构造方法必须声明为 private,而且它无法扩展(或继承)其他 enum。但是,一个 enum可以 实现一个接口。

一个实现接口 的 enum

假设您定义了一个接口 Displayable

package com.makotojava.intro;
public interface Displayable {
  public String getDisplayName();
}

您的 Gender enum 可以实现这个接口(其他任何需要生成友好的显示名称的 enum),方法如下:

package com.makotojava.intro;

public enum Gender implements Displayable {
  MALE("male"),
  FEMALE("female"),
  OTHER("other");

  private String displayName;
  private Gender(String displayName) {
    this.displayName = displayName;
  }
  @Override
  public String getDisplayName() {
    return this.displayName;
  }
}

测试您的理解情况

  1. 哪句话最恰当地描述了泛型?
    1. 泛型引入到 Java 语言中是为了实现泛型编程。
    2. 泛型是一种编译器机制,允许将类型指定为参数,这会得到方便代码重用的代码模板。
    3. 泛型是 Java 语言中的一种创建可重用的类的编程方法,在泛型中,您在实例化对象时需要指定它的类。
    4. 上述所有选项。
  2. 以下那段 for 循环语句正确地使用了新的泛型编译器缩写来访问以下 String[]
    String[] stringArray = { "1", "2", "3", "Four" };
    1. for (int a = 0; a < stringArray.length)
    2. for (int a = 0; a < stringArray.length; a++)
    3. for (String s : stringArray)
    4. for (stringArray[s] = a)
    5. 上述选项都不是
  3. enum 类型最好描述为:
    1. 一种定义常量而不是声明静态最终 String 的新的编译器快捷方式。
    2. 一种可拥有属性、构造方法、甚至方法的新数据类型,旨在定义具有可由编译器检查的类型的静态值。
    3. 一种旨在创建数字常量的新数据类型。
    4. Java 中不存在 enum。
    5. 上述选项都不是。
  4. 对或错:Java List 接口是 JDK 中的泛型的一个例子。
  5. 对或错:enum 无法实现接口。
  6. 定义一个名为 EyeColor 的新 enum 来表示以下眼睛颜色:
    • 蓝色
    • 绿色
    • 棕色
    • 灰色
    • 金色
    • 黑色
    您的 enum 应该有一个接受眼睛颜色的 String 描述的构造方法。包含一个名为 getDescription() 的方法来检索该描述。
  7. 实现 清单 5 中的 MyClass,创建一个 JUnit 测试案例(用作测试平台)来格式化一个包含 4 个元素(您喜欢的任何 String 值)的 String 数组。使用 JDK Logger 类打印结果。

核对您的答案。

进一步探索

JDK 5.0 中泛型类型简介

Java 理论与实践:掌握泛型概念

Java - 泛型

Java - 泛型

上一单元:正则表达式下一单元:I/O


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=1039319
ArticleTitle=第 20 单元:泛型
publish-date=11012016