JAVA | 第1期 - 关于泛型的内容回顾~

createh52个月前 (02-01)技术教程10

释义

Java 泛型(generics)是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

什么是泛型,为什么要使用泛型?

通俗地讲,泛型的本质其实就是“参数化类型”,将需要传入的类型参数化。其实说的再简单一点,泛型就类似于我们的模板代码,我们可以在使用的时候替换成指定的类型,而不需要为涉及到的每一种类型都写一套相同逻辑的代码,并且合理的使用泛型可以使得我们的程序变得更加通用和易于扩展。

一个说烂了的例子

这个例子被很多人用来阐述我们在没有泛型的时候,是如何操作的,比如在 JDK5 以前,我们是这样写的:

 public static void main(String[] args) {
   List list = new ArrayList();
   list.add(1);        // Integer
   list.add("String"); // String
   list.add(...);      // Others
   // 因为 List 存放的是 Object 对象,所以下面强制转换为 String 会抛出异常。
   for (int i = 0, s = list.size(); i < s; i++) {
     System.out.println((String) list.get(i)); // ClassCastException
   }
 }

再比如:

 public class Caller {
   public void call(int index) {
     System.out.printf("index: %d\n", index);
   }
 }
 
 // 没有使用泛型
 // 必须得强转才能操作集合里面的元素
 List callers = Arrays.asList(
   new Caller(),
   new Caller()
 );
 for (Object caller : callers)
   ((Caller) caller).call(i); 
 }
 
 // 当我们用了泛型后,就可以直接对其进行操作了。
 // 少了很多代码,并且编辑器也可以对代码进行提示了,简直不要太方便。
 List callers = Arrays.asList(
   new Caller(),
   new Caller(),
 );
 for (Caller caller: callers) {
   caller.call(i);
 }

试想一下,如果不使用泛型,那么在进行操作的时候是不是每次都需要强制转换,很麻烦对不对。再者使用泛型一定程度上提供了程序的安全性,防止出现类型不匹配的低级错误,也算是一种安全检测机制,毕竟 JAVA 作为一种强类型就需要适当的对参数进行一定约束。

一些约定俗成泛型符号

  • E - 通常用来表示某个元素,比如 List 中就是用了 E 代表了元素的类型 ;
  • T - 通常代表某个参数类型,一般情况下一个泛型入参的时候会高频率的使用这个字符;
  • K - 通常代表一个键的类型,比如 Map 中就使用 K 表示数据键的类型;
  • V - 通常代表一个值的类型,比如 Map 中就使用 V 表示数据值的类型;
  • N - 通常用于表示一种数字类型;
  • ? - 代表不确定的类型(这是一个特殊的类型,需要和 Object 区分开来);

当然了以上这些符号不是固定的,只要你符合变量命名规则都可以使用,但为了更好的识别它是一个泛型,最好统一使用单个有意义的字母或者 大写+下划线 的方式来命名。

泛型的使用

  • 泛型类 - 在类的定义中使用泛型(作用域是整个类);
  • public class Example<T> {}
  • 泛型接口 - 在接口的定义中使用泛型(作用域是接口内部或者实现类中);
  • public interface Example<T> {}
  • 泛型方法 - 在方法的定义中使用泛型(仅在方法内部有效);
  • public <T> void method(T value) {}

高级通配符

 public class Animal {}
 public class Dog extends Animal {}
 public class Wolf extends Animal {}
 public class Rabbit extends Animal {}

6.1、上界通配符,表示的是类型的上界,接受 T 或者 T 的子类;

小提示:extends 顾名思义是继承的意思,? 继承了 T,那 T 就是顶点,依次向下匹配,所以叫做上界通配符。

 List upperBounds = null;
 
 // 正常匹配
 upperBounds = new ArrayList(); // Animal 本身
 upperBounds = new ArrayList();    // Animal 的子类
 upperBounds = new ArrayList();   // Animal 的子类
 upperBounds = new ArrayList(); // Animal 的子类
 
 // 编译报错,超出了限定类型
 upperBounds = new ArrayList();

6.2、下界通配符,表示的是类型的下界,接受类型 T 或者 T 的超类,并且往上直到 Object;

小提示:super 就是代表父类、超类的意思,代表由 T 往上的所有超类,所以 T 就是最下面那个,因此叫做下界通配符。

 public class Animal extends Creature {} // 中间叠加 N 个父类,也可以从下往上一直匹配
 
 // 正常匹配
 List lowerBounds = null;    // 从 Dog 这一级一直往上匹配
 lowerBounds = new ArrayList();      // Dog 本身
 lowerBounds = new ArrayList();   // Dog 的父类 Animal
 lowerBounds = new ArrayList(); // Animal 的父类 Creature
 lowerBounds = new ArrayList();   // 最上面的 Object 类
 
 // 无法匹配,因为指定了最低必须是 Dog
 lowerBounds = new ArrayList();    // 编译报错,超出了限定类型
 lowerBounds = new ArrayList();  // 编译报错,超出了限定类型

6.3、无界通配符,可以表示任何类型;

List bounds = null;
bounds = new ArrayList();
bounds = new ArrayList();
bounds = new ArrayList();
bounds = new ArrayList();
...

注意:通配符限定最好是用在入参或者返回类型上面,如果在变量中使用,则会出现一些奇奇怪怪的问题,因此变量的声明中最好明确指定泛型的具体类型。

// 情况1
// 和下界通配符的解释不相符,此处仅能传入 Dog 类型,按照它的说法,理论上以下代码应该是成立的,但。。。
List bounds = new ArrayList<>();
bounds.add(new Dog());      // 正常
bounds.add(new Animal());   // 编译报错
bounds.add(new Wolf());     // 编译报错
bounds.add(new Creature()); // 编译报错

// 情况2
// 这样写,却达到了上界通配符的效果,实际和它的定义却是相悖的。
List bounds = new ArrayList<>();
bounds.add(new Animal());  // 正常
bounds.add(new Dog());     // 正常
bounds.add(new Wolf());    // 正常
bounds.add(new Rabbit());  // 正常

// 情况3
// 无法直接使用,任何类型都无法传入。
List bounds = new ArrayList<>();
bounds.add(new Animal());  // 编译报错
bounds.add(new Dog());     // 编译报错
bounds.add(new Wolf());    // 编译报错
bounds.add(new Rabbit());  // 编译报错

// 情况4
// 以下的代码直接报错,没有任何意义。
List bounds = new ArrayList<>();
bounds.add(1);           // 编译报错   
bounds.add(1.0F);        // 编译报错
bounds.add(1.0D);        // 编译报错
bounds.add("String");    // 编译报错
bounds.add(new Animal(); // 编译报错

"?" 与 "Object" 是同一个东西吗?

Object:代表所有的类型(但不包括 "?" );

?:表示无限制的类型,包括 Object,它是一个特殊的类型,通常用在 函数入参和返回 中,单独使用没啥意义,其次就是用于 通配符限定 中。

泛型擦除

泛型虽然很好用,但是,我们需要明白的是,泛型只是辅助我们开发的一种方式,说白了也是一种语法糖,它的存在只是为了在开发过程中配合编辑器提早的给出提示,在最终程序运行中其实是抹除了泛型,没有任何约束存在的,底层的操作也还是通过类型的强制转换来完成的,只不过由编译器来帮我们做了这个操作。

所以在 JAVA 中,泛型其实也可以叫做伪泛型,因此在程序代码中无法通过泛型去拿到它的一些信息,比如:

T.class 			 // 异常,T 不是一个数据类型
T t = new T(); // 异常,T 不是一个数据类型

虽说存在泛型擦除,但是我们还是可以通过反射去来拿到部分泛型的信息,就像下面这样:

public class Example {
  public final Class type;
  public Example() {
    ParameterizedType pt = (ParameterizedType) getClass().getGenericSuperclass();
    this.type = (Class) pt.getActualTypeArguments()[0];
  }
}

public class ChildA extends Example {}
public class ChildB extends Example {}

public static void main(String[] args) {
  // 可以通过继承拿到泛型的具体类型,因为继承可以保留泛型的基础信息
  System.out.println(new ChildA().type); // class java.lang.Integer
  System.out.println(new ChildA().type); // class java.lang.Integer
  
  // 但是无法通过类直接去获取它的泛型,因为存在泛型擦除,是获取不到的。
  // Expected a Class, ParameterizedType, 
  // but  is of type java.lang.Class
  System.out.println(new Example().type);
}

如何正确地使用?

首先,如果单纯的使用一个泛型其实是毫无意义的,我们需要配合一些场景才能发挥出它的正确用法。

比如:单纯的像以下这样使用,意义不大。

public  void doSomething(List values) {
  for (T element : values) {
    System.out.println(element);
  }
}

场景一:类型转换器

public interface Converter {
  OUT convert(IN input);
  default OUT doConvert(IN input) {
    return input == null ? null : convert(input);
  }
}

public class IntConverter implements Converter {
  @Override
  public Integer convert(String input) {
    return Integer.parseInt(input);
  }
}

public class LocalDateConverter implements Converter {
  private static final DateTimeFormatter formatter = 
    DateTimeFormatter.ofPattern("yyyy-MM-DD HH:mm:ss");
  
  @Override
  public LocalDate convert(String input) {
    return LocalDate.parse(input, formatter);
  }
}

public class UserConverter extends Converter {
  @Override
  public User convert(String input) {
    return JSON.parseObject(input, User.class);
  }
}

public static void main(String[] args) {
  Object input = XXX; // 外部值
  
  List> converters = Arrays.asList(
    new IntConverter(),
    new LocalDateConverter(),
    new UserConverter(),
  );
  for (Converter converter : converters) {
    Object output = converter.doConvert(input);
    System.out.printf("input: %s, output: %s\n", value, output);
  }
}

场景二:校验器

public interface Validator {
  boolean validate(T input);
}

public class Required implements Validator {
  @Override
  public boolean validate(Object input) {
    return input != null;
  }
}

public class NotBlank implements Validator {
  @Override
  public boolean validate(Object input) {
    return StringUtils.isNotBlank(input.toString());
  }
}

public class Before implements Validator {
  @Override
  public boolean validate(LocalDate input) {
    return input.isBefore(LocalDate.now());
  }
}

public class After implements Validator {
  @Override
  public boolean validate(LocalDate input) {
    return input.isAfter(LocalDate.now());
  }
}

public static void main(String[] args) {
  Object target = null; // 给定一个值
  
  List> validators = Arrays.asList(
    new Required(),
    new NotBlank(),
    new Before(),
    new After(),
  );
  
  for (Validator validator : validators) {
    boolean result = validator.validate(target);
    if (!result) {
      System.out.printf("target: %s, valid: %s\n", target, result);
      break;
    }
  }
}

场景三:Spring 中的事件通知,就可以根据具体的类型去监听不同的事件。

public class MyEvent extends ApplicationEvent {
  private final Object payload;
  public MyEvent(Object payload) {
    this.payload = payload;
  }
  public Object payload() {
    return this.payload;
  }
}

@Component
public class MyEventListener implements ApplicationListener {
  @Override
  public void onApplicationEvent(MyEvent event) {
    System.out.println(event.payload());
  }
}

// 模拟 Spring 环境
@Autowired
private ApplicationEventPublisher publisher;
public static void main(String[] args) {
  publisher.publishEvent(new MyEvent("Message"));
}

相关文章

大小写敏感容易忽视的注意点(大小写的作用)

DOS/Windows与众不同,默认不区分大小写,影响了批处理大小写行为。与Linux有别,大部分Unix like操作系统均是大小写敏感。macOS可以选择在制作分区时设定大小写敏感。Windows...

Java标识符和关键字(java标识符关键字题)

标识符Java 中标识符是为方法、变量或其他用户定义项所定义的名称。标识符可以有一个或多个字符。在 Java 语言中,标识符的构成规则如下。 标识符由数字(0~9)和字母(A~Z 和 a~z)、美元符...

Java 近期更新:OpenJDK JDK Jakarta EE Spring等

OpenJDKJEP 485流收集器已从候选提升为提议,并成为 JDK 24 的目标。此 JEP 提议在两轮预览之后完成此功能,即:JEP 473:流收集器(第二预览),在 JDK 23 中交付;以及...

Javadoc(文档注释)详解(java文档注释怎么注释)

Java 支持 3 种注释,分别是单行注释、多行注释和文档注释。文档注释以/**开头,并以*/结束,可以通过 Javadoc 生成 API 帮助文档,Java 帮助文档主要用来说明类、成员变量和方法的...

java判断时间格式--格式必须为“YYYY-MM-dd”

java中的的日期格式为:yyyy-MM-dd HH:mm:ss:代表将时间转换为24小时制,例: 2018-06-27 15:24:21 yyyy-MM-dd hh:mm:ss:代表将时间转换为12...