Java的泛型之谈

首先

鉴于之前关于Java的静态主题的内容,我打算继续专注地进行Java的学习和实践。

目标

无论是泛型还是非泛型,暂且不论。我们试着在Java中广泛涉猎,附带一些简单的例子和娱乐。

泛型和类型安全

从jdk 1.5开始引入。

在我正在写这篇文章的时候,刚好收到了来自Java的提示,要求更新到jdk 9.0.4,并且也听说即将发布java10,所以我决定升级到jdk 1.5…。

此外,如果再往前追溯的话,

List lst = new ArrayList();
Map map = new HashMap();

这种原始类型的形式据说是用这样的方式描述的。虽然现在仍能运行,但不建议使用。

任何物体都能够放进去,一旦放进去就无法取出来了。

lst.add("sample");
lst.add(11111);

// for-each も1.5以降
// イテレータもまだ raw です
Iterator it = lst.iterator();
while (it.hasNext()) {
  Object o = it.next();
  System.out.print(o); // sample1111
}

如果这样做,无法保证类型安全。
在提取集合中的元素时,必须仔细进行强制转换。

lst.add("sample");
lst.add(11111);

Iterator it = lst.iterator();
while (it.hasNext()) {
  Object o = it.next();
  System.out.print((String) o); // sample と ClassCastException
}

在这种情况下,无论是因为疏忽还是其他原因,如果类型被错误地设定,程序只有在运行时才会意识到异常,因此存在着在意想不到的时间停止程序的风险。

关于异常

在发生类型转换失败时抛出的ClassCastException是继承自RuntimeException的异常类。
RuntimeException被视为运行时异常,异常处理由使用者决定。
换句话说,错误被发现的时机不是在编译时,而是在之后的运行时。

泛型实参

从JDK 1.5开始,Collection接口进化为Collection,只能存储一种类型的元素。
正如Collection的Javadoc所述,E代表”该集合的元素类型”。

继承了 Collection 的 List 也是一样的,变成了 List。虽然还不确定会放入什么,但保证只能放入一种类型,就像暂时先放进去一样。E 表示的应该就是 Element。

所以,实际上在创建列表实例时,可以使用List lst = new ArrayList<>();之类的代码…
终于填充了泛型参数的位置,在这里Integer将成为该列表的实际类型参数。

jdk7及以后的菱形操作符

List lst = new ArrayList();
List lst = new ArrayList<>(); // 可以省略

将这些泛型接口以及它们的实现类称为泛型类型。

另外,在Java中,泛型可以对类、接口、方法和构造函数进行声明。

原始类型和的区别例如,就像原始列表一样,列表可以存储任何类型的对象。

 List<Object> objList = new ArrayList<>();
 objList.add("a");
 objList.add(1);
 objList.add(new Sample());

如果只是到这个程度的话,似乎没有太大的差别。

首先,原始类型的列表

  private static List rawList() {
    return new ArrayList();
  }

  private static void printList(List rawList) {
    rawList.forEach(System.out::println);
  }
  public static void main(String... args) {
    printList(rawList()); // 通る
  } 

从编译器那里没有收到任何提示。
换句话说,由于类型错误出现在运行时才会被注意到,所以不能称之为类型安全。

在中文中,则为 “反面,List是”

List<Object> objList = new ArrayList<String>(); // コンパイルエラー

或者

  private static List<String> strList() {
    return new ArrayList<String>();
  }

  private static void printObjList(List<Object> objList) {
    objList.forEach(System.out::println);
  }

  public static void main(String... args) {
    printObjList(strList()); // コンパイルエラー
  }

其实也是这样的

  private static List<Object> objList() {
    return new ArrayList<Object>();
  }

  private static void printStrList(List<String> strList) {
    strList.forEach(System.out::println);
  }

  public static void main(String... args) {
    printStrList(objList()); // コンパイルエラー
  }

做不到。

理由是List是List的子类型,但不是List的子类型。也就是说,List和List之间没有继承关系。

由于这样的情况,我们将泛型称为”不变”(invariant)。

如果要修改之前的代码的话,只需进行一处修正。

List<Object> objList = new ArrayList<String>(); // コンパイルエラー
List<?> someTypeList = new ArrayList<String>();

我认为会变成这样的形式。关于<?>,我想在后面的通配符部分整理。

泛型与数组的区别

数组是协变的。

泛型是不可变的,这个话题将继续讨论。
关于协变性质,我们之前讲过”继承关系”,但在数组中可以这样做。

  private static void printAry(Object[] objAry) {
    Arrays.stream(objAry).forEach(System.out::println);
  }

  private static String[] strAry(String[] strAry) {
    return strAry;
  }

  printAry(strAry(new String[]{"a", "b", "c"}));

如果String是Object的子类(即String与Object之间存在继承关系),那么String[]也是Object[]的子类(即String[]与Object[]之间也存在继承关系)。

总结起来

Object[] objAry = new String[10];
objAry[0] = 1; 

尽管在编译时无法察觉存储了非法类型。

泛型擦除(类型擦除)

    具現化
    配列は、実行時にはもう要素の型がわかっていて、その型のまま実行することになります.具現化不可能
    一方、ジェネリクスは、コンパイル時に一度チェックし終えたら、その後はイレイジャ(実行時には要素の型情報が消してある)で実装を行います. コンパイルの時点と比べて、実行時には情報が一つ失われている状態です. これにより、jdk 1.5 以前の raw type のコレクションであっても、ジェネリクスを既存の raw type に追加することができたのです.

泛型设计

元组

元组是一种可以存储多个不同元素并有序的数据结构。

public class Pair<V, W> {
  private final V v;
  private final W w;

  Pair(V v, W w) {
    this.v = v;
    this.w = w;
  }

  V getV() {
    return v;
  }

  W getW() {
    return w;
  }
}
Pair<String, Integer> tuple2 = new Pair<>("toilet-score", 100);

String v = tuple2.getV();
Integer w = tuple2.getW();

System.out.println(v + w);

通过扩展存储和提取包含两种基本异构数据的数据对的使用模式进行操作。

public class Tuple {
  public static class Tuple2<A, B> {
    private final A a;
    private final B b;

    public Tuple2(A a, B b) {
      this.a = a;
      this.b = b;
    }

    public A getA() {
      return a;
    }

    public B getB() {
      return b;
    }
  }

  public static class Tuple3<A, B, C> extends Tuple2<A, B> {
    private final C c;

    public Tuple3(A a, B b, C c) {
      super(a, b);
      this.c = c;
    }

    public C getC() {
      return c;
    }
  }

  // Tuple4, Tuple5 ... 
}

我认为可以通过增加静态内部类以提高通用性,并将每个构造函数的访问器更改为public,从而创建一个可以以类型安全的方式存储三个,四个或更多类型的数据的通用元组实用类。

借鉴先人的智慧

由于已存在且广泛使用的库,如Commons Lang的Pair等,可以得到更确切且多样的模式。

请参考官方 GitHub 页面,javatuples 提供了一个简单的结构,从一个类型到十个类型的范围内,虽然规模较小但仍然令人满意。

关于javatuples,我个人认为,它以只有一个类型的Unit元组为起点,然后以一种优美的继承形式展开…我觉得很有意思的是以具有两种类型的Tuple类为中心,同时也让Unit继承Tuple。

通配符

在前文的基础上延伸一下,泛型是不可变的。虽然泛型确保了一定会有某种确定的类型,但并不表示允许任何继承T类型的类型。List和List之间并没有继承关系。换句话说,它不是很灵活。

在这个时候,通配符就在与泛型相同的时间(jdk 1.5)出现了。

非境界线的万能卡片 <?>

如果想要使用泛型,但实际类型参数尚未确定,可以将问号传递给占位符

// T が解決できないため, コンパイルエラー
List<T> someList = new ArrayList<String>(); 

// 実型が決まる前に格納しようとしているため, コンパイルエラー
List<?> anotherList = new ArrayList<>(); anotherList.add("a"); 

// OK
List<?> theOtherList = new ArrayList<String>(); theOtherList.add("a"); 

境界的野牌(上限)
在Functional Interface javadoc中有写明,它是一个只有一个抽象方法的接口。以下是部分包含在java.util.Objects中的Function接口。

@FunctionalInterface
public interface Function<T, R> {
    /**
     * Applies this function to the given argument. 
     * 与えられた引数を受け付けます.
     * 
     * @param t the function argument 
     * パラメータ t 関数の引数
     * 
     * @return the function result 
     * リターン 関数の結果
     */
    R apply(T t);

    // omit 
}

函数接口,如BiFunction、Consumer和Supplier等,表示接受某些东西但尚未确定类型,或者以某种结果返回某些东西但尚未确定类型的结构。

像IntSupplier的getAsInt()方法, 还有其他预先确定返回值只能是int类型的方法, 但基本上我们可以看到, 它们使用了泛型来增加灵活性。

泛型和我 – 类似于泛型方法的游戏

这个奇怪的标题是单纯的玩闹备忘录之类的东西。

前几天,我创建了一个算法。
它接受一个不依赖于元素类型的列表,对该列表进行处理,并将操作结果存入另一个列表中,然后返回这个O(N)的函数。

  private static List<Object> doSomething(List<Object> list) {

    List<Object> result = new ArrayList<>();

    // 処理

    return result;
  }

但是,

  public static void main(String[] args) {
   List<String> strListA = Arrays.asList("c", "a", "b", "b", "c", "a", "d", "b"); 

   List<Object> strListB = Arrays.asList("c", "a", "b", "b", "c", "a", "d", "b");

   // List<Object> ではなく List<String> を渡すためコンパイルエラー
   doSomething(strListA).forEach(System.out::print);

   doSomething(strListB).forEach(System.out::print);
  }

我对于传递给参数的列表类型参数都限制为Object类型并不满意。比如,如果要添加一个生成只接受数字的列表的方法,并且想通过参数传递,这样有点不方便。

  private static List<Number> makeNumberList(Number...nums) {
    List<Number> list = new ArrayList<>();
    for (Number num : nums) list.add(num);
    return list;
  }
  public static void main(String[] args) {
   doSomething(makeNumberList(1, 2, 3, 4, 1)).forEach(System.out::print); // コンパイルエラー
  }

为了在编译时能够进行类型检查,我添加了类型标记,将更改为

// 为了在只有类型参数和类型标记匹配的情况下进行编译,我们考虑将Class字面量作为参数传递给方法,这样在运行时可以传递类型信息。
// 然而,在修正过程中,由于Type在方法中的用法丢失了,所以我们暂时搁置。

  private static <T> List<T> doSomething(List<T> list) {

    List<T> result = new ArrayList<>();

    // 処理

    return result;
  }

尝试创建一个用于数字列表的方法,使其接受 Number 类型及其子类型作为参数…

  private static <T extends Number> List<T> makeNumberList(T...args) {
    List<T> list = new ArrayList<>();
    for (T arg : args) list.add(arg);
    return list;
  }
  public static void main(String[] args) {
    List<Integer> someList = makeNumberList(5, 1, 2, 3, 3, 4, 3, 1, 2, 4, 5);
    doSomething(someList).forEach(System.out::print);
  }

通过这种方式,可以将除了`List`之外的列表作为参数传递。

结束了

在某个突发的时机,我突然对泛型产生了兴趣。
虽然这么说有点奇怪,但是我觉得它有点像是一种安全但不是那么安全的世界中突然降临的安全且灵活的魔法工具(我在说什么呢),可能是因为这种奇妙的地方引起了我的关注。

在我写这个的同时,我意识到这里有很多不清楚的地方,或者我其实不知道这些地方,所以请您在发现错误时给我指点一下。

参考以下内容,仅需一个选项。

《Effective Java》第二版
Oracle Java文档
– https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.1.2
– https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html#package.description

以上

bannerAds