本文首发于个人网站:Java中的泛型(一)

本文主要介绍Java泛型的基本知识,包括目的、泛型类的基本用法和场景应用场景。

目的

  1. 编写更加“泛化”的代码,编写可以应付多重类型的代码
  2. Java中的泛型,用于实现“参数化类型”的概念
  3. 创造可以放不同类型对象的容器类,通过编译器来保证类型的正确;
  4. 能够简单&安全得创建复杂的模型

泛型类

定义

利用Java开发的时候,肯定会有一个类持有另一个或几个类的情况,在编写一些比较基础的组件,例如缓存操作组件,这类组件的逻辑差不多,但是希望能够处理不同的类型。

在Java SE5之前,我们可以通过Object这个类型来实现,举个例子:

  1. package org.java.learn.generics;
  2. /**
  3. * 作用: User: duqi Date: 2017/11/19 Time: 15:45
  4. */
  5. public class Holder2 {
  6. private Object a;
  7. public Holder2(Object a) {
  8. this.a = a;
  9. }
  10. public void setA(Object a) {
  11. this.a = a;
  12. }
  13. public Object getA() {
  14. return a;
  15. }
  16. public static void main(String[] args) {
  17. Holder2 h2 = new Holder2(new Automobile());
  18. //这里需要进行类型的强制转换
  19. Automobile automobile = (Automobile) h2.getA();
  20. h2.setA("Not a Automobile");
  21. String s = (String) h2.getA();
  22. h2.setA(1); //这里发生了自动装箱
  23. Integer x = (Integer) h2.getA();
  24. }
  25. }

上面这段代码,确实满足了一个组件持有不同类型对象的需求,但是也有两个问题:(1)取出对象的时候,需要进行类型的强制转换;(2)同一个容器对象,可以随意存取不同类型的对象,有出错的风险。JavaSE5引入了“泛型”的概念,使得代码可以应用于多个类型,同时还能避免上述我说的两个问题,上面的代码,如果用Java泛型实现,则如下所示:

  1. package org.java.learn.generics;
  2. /**
  3. * 作用: User: duqi Date: 2017/11/19 Time: 15:48
  4. */
  5. public class Holder3<T> {
  6. private T a;
  7. public Holder3(T a) {
  8. this.a = a;
  9. }
  10. public void setA(T a) {
  11. this.a = a;
  12. }
  13. public T getA() {
  14. return a;
  15. }
  16. public static void main(String[] args) {
  17. Holder3<Automobile> h3 = new Holder3<>(new Automobile());
  18. Automobile automobile = h3.getA(); //这里不需要强制转换类型
  19. // h3.setA("Not an Automobile"); Error
  20. // h3.setA(1); Error
  21. }
  22. }

上面的Holder3是持有一个类型参数T,还可以持有三个相同类型(或不同类型的)类型参数,代码如下:

  1. package org.java.learn.generics;
  2. /**
  3. * 作用: User: duqi Date: 2017/11/19 Time: 15:51
  4. */
  5. public class Holder4<T> {
  6. private T a;
  7. private T b;
  8. private T c;
  9. public Holder4(T a, T b, T c) {
  10. this.a = a;
  11. this.b = b;
  12. this.c = c;
  13. }
  14. public T getA() {
  15. return a;
  16. }
  17. public T getB() {
  18. return b;
  19. }
  20. public T getC() {
  21. return c;
  22. }
  23. public void setA(T a) {
  24. this.a = a;
  25. }
  26. public void setB(T b) {
  27. this.b = b;
  28. }
  29. public void setC(T c) {
  30. this.c = c;
  31. }
  32. public static void main(String[] args) {
  33. Holder4<Automobile> holder4 = new Holder4<>(new Automobile(),new Automobile(), new Automobile());
  34. Automobile a = holder4.getA();
  35. Automobile b = holder4.getB();
  36. Automobile c = holder4.getC();
  37. }
  38. }

应用场景

元组

在实际开发中,常常会有“一次方法调用返回多个对象的需求”,我现在有时候会为每个返回值创建一个Class的写法,但是这块不太符合领域模型的思想——混淆了Entity和Object Value的概念。利用元组,可以实现Object Value的概念。

  1. package org.java.learn.util;
  2. /**
  3. * 作用: User: duqi Date: 2017/11/19 Time: 16:18
  4. */
  5. public class TwoTuple<A, B> {
  6. public final A first;
  7. public final B second;
  8. public TwoTuple(A first, B second) {
  9. this.first = first;
  10. this.second = second;
  11. }
  12. @Override
  13. public String toString() {
  14. //这里隐含表示了:元祖中的元素是有顺序的
  15. return "(" + first + ", " + second + ")";
  16. }
  17. }

这个例子中的final关键字用的非常漂亮,有两个设计上的考虑

  • 访问安全,客户端可以读取first和second两个对象,但是无法修改它们;
  • 体现Object Value的含义,如果开发者需要一个不同元素的元组,必须重新构建一个;

利用继承机制,我们还可以实现元素更多的元组,下面的代码展示了三元组和四元组的实现。不过,从另一个角度考虑——如果一个方法调用需要返回四个以上元素的元组,是不是需要考虑下这个方法本身的设计是否合理了呢。

三元组

  1. package org.java.learn.util;
  2. /**
  3. * 作用: User: duqi Date: 2017/11/19 Time: 16:32
  4. */
  5. public class ThreeTuple<A, B, C> extends TwoTuple<A, B> {
  6. public final C third;
  7. public ThreeTuple(A first, B second, C third) {
  8. super(first, second);
  9. this.third = third;
  10. }
  11. @Override
  12. public String toString() {
  13. return "(" + first + ", " + second + ", " + third + ")";
  14. }
  15. }

四元组

  1. package org.java.learn.util;
  2. /**
  3. * 作用: User: duqi Date: 2017/11/19 Time: 16:36
  4. */
  5. public class FourTuple<A, B, C, D> extends ThreeTuple<A, B, C> {
  6. public final D forth;
  7. public FourTuple(A first, B second, C third, D forth) {
  8. super(first, second, third);
  9. this.forth = forth;
  10. }
  11. @Override
  12. public String toString() {
  13. return "(" + first + ", " + second + ", " + third + "," + forth + ")";
  14. }
  15. }

堆栈

再看一个比元祖复杂一点的例子,这里利用自己实现的链表存储机制构建了一个下推堆栈。

  1. package org.java.learn.util;
  2. /**
  3. * 作用: User: duqi Date: 2017/11/19 Time: 16:40
  4. */
  5. public class LinkedStack<T> {
  6. private static class Node<U> {
  7. U item;
  8. Node<U> next;
  9. public Node() {
  10. item = null;
  11. next = null;
  12. }
  13. public Node(U item, Node<U> next) {
  14. this.item = item;
  15. this.next = next;
  16. }
  17. /**
  18. * 是否到达堆栈底部
  19. *
  20. * @return
  21. */
  22. boolean end() {
  23. return item == null || next == null;
  24. }
  25. }
  26. /**
  27. * 末端哨兵
  28. */
  29. private Node<T> top = new Node<>();
  30. public void push(T item) {
  31. top = new Node<>(item, top);
  32. }
  33. public T pop() {
  34. T res = top.item;
  35. if (!top.end()) {
  36. top = top.next;
  37. }
  38. return res;
  39. }
  40. public static void main(String[] args) {
  41. LinkedStack<String> lss = new LinkedStack<>();
  42. for (String s: "Phasers on stun!".split(" ")) {
  43. lss.push(s);
  44. }
  45. String s;
  46. while ((s = lss.pop()) != null) {
  47. System.out.println(s);
  48. }
  49. }
  50. }

书中的练习题5:移除Node类上的类型参数,并修改LinkedStack.java的代码,说明内部类可以访问外部类的类型参数,我自己试了下,代码如下:

  1. package org.java.learn.util;
  2. /**
  3. * 作用: User: duqi Date: 2017/11/19 Time: 16:40
  4. */
  5. public class LinkedStack2<T> {
  6. /**
  7. * 这里不能使用静态内部类
  8. */
  9. private class Node {
  10. T item;
  11. Node next;
  12. public Node() {
  13. item = null;
  14. next = null;
  15. }
  16. public Node(T item, Node next) {
  17. this.item = item;
  18. this.next = next;
  19. }
  20. /**
  21. * 是否到达堆栈底部
  22. *
  23. * @return
  24. */
  25. boolean end() {
  26. return item == null || next == null;
  27. }
  28. }
  29. /**
  30. * 末端哨兵
  31. */
  32. private Node top = new Node();
  33. public void push(T item) {
  34. top = new Node(item, top);
  35. }
  36. public T pop() {
  37. T res = top.item;
  38. if (!top.end()) {
  39. top = top.next;
  40. }
  41. return res;
  42. }
  43. public static void main(String[] args) {
  44. LinkedStack<String> lss = new LinkedStack<>();
  45. for (String s: "Phasers on stun!".split(" ")) {
  46. lss.push(s);
  47. }
  48. String s;
  49. while ((s = lss.pop()) != null) {
  50. System.out.println(s);
  51. }
  52. }
  53. }

代码显示,静态内部类,无法访问其外部类的类型参数,但是非静态内部类可以。

RandomList

书中还给出一个随机列表的例子——一个持有特定类型对象的列表,select()方法可以随机取出列表中的一个元素。这是一个抽奖demo,或者我们可以用它决定每天中午吃什么😜。

  1. package org.java.learn.generics;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. import java.util.Random;
  5. /**
  6. * 作用: 持有特定类型对象的列表,select()方法可以随机取一个元素
  7. * User: duqi
  8. * Date: 2017/11/19
  9. * Time: 17:08
  10. */
  11. public class RandomList<T> {
  12. private ArrayList<T> storage = new ArrayList<>();
  13. private Random random = new Random(47);
  14. public void add(T item) {
  15. storage.add(item);
  16. }
  17. public T select() {
  18. return storage.get(random.nextInt(storage.size()));
  19. }
  20. public static void main(String[] args) {
  21. RandomList<String> randomList = new RandomList<>();
  22. for (String s: "The queick brown for jumped over the lazy brown dog".split(" ")) {
  23. randomList.add(s);
  24. }
  25. for (int i = 0; i < 11; i++) {
  26. System.out.print(randomList.select() + " ");
  27. }
  28. }
  29. }

总结

泛型的东西很多,我之前看《Java核心技术》学过一遍,最近为了给团队的同学做分享,准备再结合《Java编程思想》复习一遍,发现《Java编程思想》这本书写得非常有深度,需要思考、实践和回味,才能准确得get到作者想要表达的意思。


本号专注于后端技术、JVM问题排查和优化、Java面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。
Java泛型基础(一):基本概念 - 图1