来了,看到了,概括了:沉浸在Java Generics中

Java泛型是Java语言历史上最重要的变化之一。 Java 5可用的泛型使Java收集框架的使用变得更加容易,便捷和安全。 现在在编译阶段检测到与错误使用类型相关的错误。 是的,Java语言本身已经变得更加安全。 尽管泛型类型看起来很简单,但是许多开发人员仍难以使用它们。 在这篇文章中,我将讨论使用Java泛型的功能,以便使您减少这些困难。 如果您不是通用专家,则很有用,并且可以帮助您避免陷入主题时的很多困难。



处理收藏


假设一家银行需要计算客户帐户中的储蓄金额。 在“泛型”出现之前,计算总和的方法如下所示:

public long getSum(List accounts) {   long sum = 0;   for (int i = 0, n = accounts.size(); i < n; i++) {       Object account = accounts.get(i);       if (account instanceof Account) {           sum += ((Account) account).getAmount();       }   }   return sum; } 

我们进行了迭代,遍历了帐户列表,并检查了该列表中的元素是否确实是Account类的实例-即用户的帐户。 我们将Account类的对象类型和getAmount方法进行了getAmount ,它们返回了该帐户中的金额。 然后他们将它们加总并返回总数。 需要两个步骤:
 if (account instanceof Account) { // (1) 

 sum += ((Account) account).getAmount(); // (2) 

如果不检查( instanceof )是否属于Account类,则在第二阶段可能会发生ClassCastException即程序崩溃。 因此,这种检查是强制性的。

随着泛型的问世,对类型检查和强制转换的需求已消失:
 public long getSum2(List<Account> accounts) {  long sum = 0;  for (Account account : accounts) {      sum += account.getAmount();  }  return sum; } 

现在方法
 getSum2(List<Account> accounts) 
仅接受Account类的对象列表作为参数。 此限制在方法本身中表示,在其签名中,程序员根本无法转移任何其他列表-仅客户帐户列表。

我们不需要检查此列表中的元素的类型:method参数的类型描述暗含了它。
 List<Account> accounts 
(可以读取为 Account )。 如果出现问题,即如果有人尝试将Account类以外的对象列表传递给此方法,则编译器将引发错误。

在检查的第二行中,需求也消失了。 如有必要,将在编译阶段进行转换。

替代原则


Barbara Liskov的替换原理是面向对象编程中子类型的特定定义。 Liskov的“子类型”概念定义了替换的概念:如果ST的子类型,则程序中类型T的对象可以用类型S的对象替换,而无需更改该程序的所需属性。

型式
亚型
编号
整数
列出 <E>
ArrayList <E>
集合 <E>
列出 <E>
可迭代 <E>
集合 <E>

类型/子类型关系示例

这是在Java中使用替换原理的示例:
 Number n = Integer.valueOf(42); List<Number> aList = new ArrayList<>(); Collection<Number> aCollection = aList; Iterable<Number> iterable = aCollection; 

IntegerNumber的子类型,因此,可以为Number类型的变量n分配Integer.valueOf(42)方法返回的值。

协方差,协方差和不变性


首先,一点理论。 协方差是在相同类型的派生类型中保留源类型的继承层次结构。 例如,如果CatAnimals的子类型,则<Cats>集合是<Animals>集合的子类型。 因此,考虑到替代原则,可以执行以下分配:

许多<动物> =许多<猫>

矛盾是派生类型中源类型层次结构的反转。 例如,如果Cat的子类型,则Set <Animals><Cats>Set的子类型。 因此,考虑到替代原则,可以执行以下分配:

许多<猫> =许多<动物>

不变性-派生类型之间缺乏继承。 如果CatAnimals的子类型,则<Cats>集合不是<Animals>集合的子类型,并且<Animals>集合不是<Cats>集合的子类型。

Java中的数组是协变的 。 如果ST[]的子类型,则类型S[]T[]的子类型T 分配示例:
 String[] strings = new String[] {"a", "b", "c"}; Object[] arr = strings; 

我们为变量arr分配了一个字符串数组链接,其类型为« » 。 如果数组不是协变的,我们将无法做到这一点。 Java允许您执行此操作,程序可以编译并运行而不会出错。

 arr[0] = 42; // ArrayStoreException.       

但是,如果我们尝试通过arr变量更改数组的内容并在其中写入数字42,则由于程序不是字符串,而是数字, ArrayStoreException在程序执行阶段将获得ArrayStoreException 。 这是Java数组协方差的缺点:我们无法在编译阶段执行检查,并且某些东西可能在运行时已经损坏。

“泛型”是不变的。 这是一个例子:
 List<Integer> ints = Arrays.asList(1,2,3); List<Number> nums = ints; // compile-time error.      nums.set(2, 3.14); assert ints.toString().equals("[1, 2, 3.14]"); 

如果采用整数列表,则它将不是Number类型的子类型,也不会是任何其他子类型。 他只是他自己的一个亚型。 也就是说, List <Integer>List<Integer> ,仅此而已。 编译器将确保声明为Integer类的对象列表的ints变量仅包含Integer类的对象, 而不包含其他任何对象。 在编译阶段,将执行检查,并且运行时不会出错。

通配符


泛型总是不变的吗? 不行 我将举一些例子:
 List<Integer> ints = new ArrayList<Integer>(); List<? extends Number> nums = ints; 

这就是协方差。 List<Integer> - List<? extends Number>子类型 List<? extends Number>

 List<Number> nums = new ArrayList<Number>(); List<? super Integer> ints = nums; 

这是矛盾的。 List<Number>List<? super Integer>的子类型List<? super Integer> List<? super Integer>

诸如"? extends ...""? super ..."的记录称为通配符或通配符,具有上限( extends )或下限( super )。 List<? extends Number> List<? extends Number>可能包含其类为Number或从Number继承的对象。 List<? super Number> List<? super Number>可能包含其类为NumberNumber为继承者( Number超类型)的对象。


扩展B-具有上限的通配符
超级B-下限通配符
其中B-代表边界

形式为T 2 <= T 1的记录表示T 2描述的类型集合是T 1描述的类型集合的子集


数字<=? 扩展对象
? 扩展数字<=? 扩展对象

? 超级对象<=? 超级号码


对主题的更多数学解释

一对测试知识的任务:

1.为什么在下面的示例中出现编译时错误? 我可以在nums列表中添加什么值?
 List<Integer> ints = new ArrayList<Integer>(); ints.add(1); ints.add(2); List<? extends Number> nums = ints; nums.add(3.14); // compile-time error 

答案
是否应使用通配符声明容器? extends ? extends ,您只能读取值。 除了null之外,什么都不能添加到列表中。 为了将对象添加到列表中,我们需要另一种通配符- ? super ? super


2.为什么我无法从下面的列表中获得商品?
 public static <T> T getFirst(List<? super T> list) {  return list.get(0); // compile-time error } 

答案
无法从带有通配符的容器中读取项目? super ? super ,但Object类的Object除外

 public static <T> Object getFirst(List<? super T> list) {  return list.get(0); } 



获取和放置原则或PECS(生产者扩展了超级消费者)


具有上限和下限的通配符功能提供了与类型的安全使用相关的其他功能。 您只能从一种类型的变量中读取,而只能写入另一种类型(例外是,可以为extends写入null ,为super读取Object )。 为了更容易记住何时使用哪个通配符,有PECS原理-生产者扩展了超级用户。

  • 如果我们声明一个带有extends通配符 ,那么这就是生产者 。 他仅“生产”,从容器中提供元素,并且不接受任何东西。
  • 如果我们宣布使用super作为通配符 ,那么这就是消费者 。 他只接受但不能提供任何东西。

以java.util.Collections类中的copy方法为例,考虑使用通配符和PECS原理。

 public static <T> void copy(List<? super T> dest, List<? extends T> src) { … } 

该方法将元素从原始src列表复制到dest列表。 src使用通配符声明? extends ? extends并且是生产者,并且dest用通配符声明? super ? super ,是消费者。 给定通配符的协变量和协变量,您可以将元素从ints列表复制到nums列表:
 List<Number> nums = Arrays.<Number>asList(4.1F, 0.2F); List<Integer> ints = Arrays.asList(1,2); Collections.copy(nums, ints); 


如果我们错误地误认为复制方法参数,并尝试从nums列表复制到ints列表,编译器将不允许我们这样做:
 Collections.copy(ints, nums); // Compile-time error 


<?>和Raw类型


以下是带有无限通配符的通配符。 我们只放入<?> ,不带superextends关键字:
 static void printCollection(Collection<?> c) {  // a wildcard collection  for (Object o : c) {      System.out.println(o);  } } 


实际上,从上方看,这样的“无限”通配符仍然受到限制。 Collection<?>也是通配符,例如“ ? extends Object ”。 形式为Collection<?>记录等效于Collection<? extends Object> Collection<? extends Object> ,这意味着该集合可以包含任何类的对象,因为Java中的所有类都继承自Object因此替换称为无限制。

例如,如果我们省略类型指示,例如:
 ArrayList arrayList = new ArrayList(); 

然后他们说ArrayList是参数化ArrayList <T>Raw类型。 使用原始类型,我们回到了泛型时代,有意识地放弃了参数化类型固有的所有功能。

如果尝试在Raw类型上调用参数化方法,则编译器将向我们发出警告“未检查的调用”。 如果我们尝试将对参数化Raw类型的引用分配给类型,则编译器将发出警告“未检查的分配”。 稍后我们将看到,忽略这些警告可能会导致我们的应用程序执行期间出错。
 ArrayList<String> strings = new ArrayList<>(); ArrayList arrayList = new ArrayList(); arrayList = strings; // Ok strings = arrayList; // Unchecked assignment arrayList.add(1); //unchecked call 


通配符捕获


现在,让我们尝试实现一种以相反顺序排列列表元素的方法。

 public static void reverse(List<?> list); // ! public static void reverse(List<?> list) { List<Object> tmp = new ArrayList<Object>(list); for (int i = 0; i < list.size(); i++) {   list.set(i, tmp.get(list.size()-i-1)); // compile-time error } } 

发生编译错误是因为reverse方法将具有无限通配符<?>作为参数。
<?>含义与<? extends Object> <? extends Object> 。 因此,根据PECS原理, listproducerproducer只生产元素。 然后我们在for循环中调用set()方法,即 试图写list 。 因此,我们反对Java保护,因为Java保护不允许我们通过索引设置某些值。

怎么办 Wildcard Capture模式将为我们提供帮助。 在这里,我们创建一个通用的rev方法。 使用类型T的变量声明它T 此方法接受T类型的列表,我们可以进行设置。
 public static void reverse(List<?> list) { rev(list); } private static <T> void rev(List<T> list) { List<T> tmp = new ArrayList<T>(list); for (int i = 0; i < list.size(); i++) {   list.set(i, tmp.get(list.size()-i-1)); } } 

现在一切都会与我们一起编译。 通配符捕获在此处捕获。 当调用reverse(List<?> list)方法时reverse(List<?> list) ,一些对象(例如,字符串或整数)的列表作为参数传递。 如果我们可以捕获这些对象的类型并将其分配给类型X的变量,则可以得出TX结论X

您可以在此处此处阅读有关Wildcard Capture更多信息。

结论


如果您需要从容器中读取内容,则使用通配符,其上边框为“ ? extends ”。 如果需要写入容器,请使用下边框为“ ? super ”的通配符。 如果需要记录和读取,请不要使用通配符。

不要使用Raw类型! 如果未定义type参数,则使用通配符<?>

类型变量


当我们在尖括号中写下标识符时,例如,在声明类或方法时,例如<T><E> ,我们将创建一个类型变量 。 类型变量是一种不合格的标识符,可以用作类或方法主体中的类型。 类型变量可以在上面限制。
 public static <T extends Comparable<T>> T max(Collection<T> coll) { T candidate = coll.iterator().next(); for (T elt : coll) {   if (candidate.compareTo(elt) < 0) candidate = elt; } return candidate; } 

在此示例中,表达式T extends Comparable<T>定义了上面由类型Comparable<T>界定的T (类型变量)。 与通配符不同,类型变量只能在顶部限制(仅extends )。 不能写super 。 另外,在此示例中, T依赖于自身,它称为recursive bound -递归边界。

这是Enum类的另一个示例:
 public abstract class Enum<E extends Enum<E>>implements Comparable<E>, Serializable 

在这里,Enum类由类型E(它是Enum<E>的子类型)进行参数化。

多重界限


Multiple Bounds -多个约束。 它是通过“ & ”字符写的,也就是说,我们说由类型T的变量表示的类型应由Object类和Comparable接口从上方限制。

 <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) 

记录Object & Comparable<? super T> Object & Comparable<? super T>形成相交类型“ Multiple Bounds 。 第一个限制-在这种情况下为erasure用于erasure ,即重写类型的过程。 它由编译器在编译阶段执行。

结论


类型变量只能限制在一个或多个类型的顶部。 在多个约束的情况下,在重写(类型擦除)过程中使用左边框(第一个约束)。

类型擦除


类型擦除是类型(可能包括参数化类型和类型变量)到永不参数化类型或变​​量类型的类型的映射。 我们将T型混搭写为|T|

混搭显示的定义如下:
  • 混合参数化类型G < T1 ,..., Tn >是| G |
  • 拼凑嵌套类型TC是| T |。 ç
  • 哈希数组类型T []是| T | []
  • 混搭类型变量会混搭其左边框
  • 混搭任何其他类型就是该类型本身


在执行Type Erasure(类型混搭)期间,编译器执行以下操作:
  • 添加类型转换以在必要时提供类型安全
  • 生成Bridge方法以保持多态


T(类型)
| T | (混搭类型)
列表<整数>,列表<String>,列表<列表<String >>
清单
列出<整数> []
清单[]
清单
清单
整型
整型
整数
整数
<T扩展了可比性<T >>
可比
<T扩展了对象和可比对象<? 超级T >>
对象
LinkedCollection <E> .Node
LinkedCollection.Node

下表显示了混搭过程中不同类型变成什么类型​​“擦除”。

在下面的屏幕截图中,是该程序的两个示例:


两者之间的区别在于,左侧会发生编译时错误,而右侧会编译所有错误而没有错误。 怎么了

答案
在Java中,两个不同的方法不能具有相同的签名。 在类型清除过程中,编译器将添加桥接方法public int compareTo(Object o) 。 但是该类已经包含一个带有签名的方法,它将在编译期间导致错误。

通过删除compareTo(Object o)方法来编译Name类,并使用javap查看生成的字节码:
 # javap Name.class Compiled from "Name.java" public class ru.sberbank.training.generics.Name implements java.lang.Comparable<ru.sberbank.training.generics.Name> { public ru.sberbank.training.generics.Name(java.lang.String); public java.lang.String toString(); public int compareTo(ru.sberbank.training.generics.Name); public int compareTo(java.lang.Object); } 

尽管我们从源代码中删除了该类,但我们看到该类包含一个int compareTo(java.lang.Object)方法int compareTo(java.lang.Object) 。 这是编译器添加的桥接方法。


可更改的类型


在Java中,我们说类型是可reifiable只要它的信息在运行时就可以完全访问。 可更改的类型包括:
  • 基本类型( intlongboolean
  • 非参数化(非泛型)类型( StringInteger
  • 参数化类型,其参数表示为无界通配符(无限通配符)( List <?>Collection <?>
  • 原始 (未格式化的)类型( ListArrayList
  • 组件为可更改类型的数组( int []Number []List <?> []List [ ]


为什么可以获得有关某些类型的信息,而没有有关其他类型的信息? 事实是,由于编译器覆盖类型的过程,有关某些类型的信息可能会丢失。 如果丢失,则此类型将不再是可修复的。 也就是说,它在运行时不可用。 如果可用-分别是可更改的。

不让所有通用类型在运行时可用的决定是Java类型系统中最重要且相互冲突的设计决定之一。 首先,这样做是为了与现有代码兼容。 我必须为迁移兼容性付费-在运行时无法完全访问通用类型的系统。

哪些类型不合理?
  • 类型变量( T
  • 具有指定参数类型的参数化类型( List <Number> ArrayList <String>List <List <String >>
  • 具有指定上限或下限的参数化类型( 列表<?扩展数>,可比较的<?超级字符串> )。 但这是一个保留: 列表<? 扩展对象> - 可靠,但列表<?> -有效


还有一项任务。 为什么在下面的示例中无法创建参数化的Exception?

 class MyException<T> extends Exception {  T t; } 

答案
try-catch中的每个catch表达式分别在程序执行期间检查接收到的异常的类型(等效于instanceof),该类型必须是可更改的。 因此,Throwable及其子类型无法参数化。

 class MyException<T> extends Exception {// Generic class may not extend 'java.lang.Throwable'  T t; } 



未经检查的警告


编译我们的应用程序可能会产生所谓的“ Unchecked Warning ,即警告编译器无法正确确定使用我们的类型的安全级别。 这不是错误,而是警告,因此您可以跳过它。 但是建议将其全部修复以避免将来出现问题。

堆污染


如前所述,将对Raw类型的引用分配给参数化类型的变量会导致警告“未检查的分配”。 如果我们忽略它,则可能会出现“ Heap Pollution ”(堆污染)情况。 这是一个例子:
 static List<String> t() {  List l = new ArrayList<Number>();  l.add(1);  List<String> ls = l; // (1)  ls.add("");  return ls; } 

在第(1)行中,编译器警告“未检查的分配”。

我们需要举另一个“堆污染”的例子-当我们使用参数化对象时。 下面的代码片段清楚地表明,不允许将参数化类型用作使用Varargs的方法的参数。 在这种情况下,方法参数m是List<String>… ,即 实际上,是List<String>类型的元素数组。 给定在混搭期间显示类型的规则,则stringLists类型将变成原始列表的数组( List[] ),即 可以完成分配Object[] array = stringLists; 然后将除字符串列表(1)之外的对象写入array ,这将在字符串(2)中ClassCastException

 static void m(List<String>... stringLists) {  Object[] array = stringLists;  List<Integer> tmpList = Arrays.asList(42);  array[0] = tmpList; // (1)  String s = stringLists[0].get(0); // (2) } 


考虑另一个示例:
 ArrayList<String> strings = new ArrayList<>(); ArrayList arrayList = new ArrayList(); arrayList = strings; // (1) Ok arrayList.add(1); // (2) unchecked call 

Java允许在第(1)行中进行赋值。 这是向后兼容所必需的。 但是,如果尝试执行第(2)行中的add方法,则会收到Unchecked call警告-编译器会警告我们可能的错误。 实际上,我们正在尝试将一个整数添加到字符串列表中。

倒影


尽管在编译过程中,参数化类型会经历类型擦除过程,但是我们可以使用反射来获得一些信息。

  • 所有可修改的内容都可以通过反射机制获得。
  • 有关类字段类型,方法参数以及它们返回的值的信息可通过反射获得。

Reflection Reifiable , . , , , - , :
 java.lang.reflect.Method.getGenericReturnType() 

Generics java.lang.Class . :
 List<Integer> ints = new ArrayList<Integer>(); Class<? extends List> k = ints.getClass(); assert k == ArrayList.class; 


ints List<Integer> ArrayList< Integer> . ints.getClass() Class<ArrayLis> , List<Integer> List . Class<ArrayList> k Class<? extends List> , ? extends . ArrayList.class Class<ArrayList> .

结论


, Reifiable. Reifiable : , , , Raw , reifiable.

Unchecked Warnings « » .

Reflection , Reifiable. Reflection , .

Type Inference


« ». () . :
 List<Integer> list = new ArrayList<Integer>(); 

- Java 7 ArrayList :
 List<Integer> list = new ArrayList<>(); 

ArrayListList<Integer> . type inference .

Java 8 JEP 101.
Type Inference. :
  • (reduction)
  • (incorporation)
  • (resolution)

: , , — .
, . JEP 101 .

, :
 class List<E> {  static <Z> List<Z> nil() { ... };  static <Z> List<Z> cons(Z head, List<Z> tail) { ... };  E head() { ... } } 

List.nil() :
 List<String> ls = List.nil(); 

, List.nil() String — JDK 7, .

, , , :
 List.cons(42, List.nil()); //error: expected List<Integer>, found List<Object> 

JDK 7 compile-time error. JDK 8 . JEP-101, — . JDK 8 — :
 List.cons(42, List.<Integer>nil()); 


JEP-101 , , :
 String s = List.nil().head(); //error: expected String, found Object 

, . , JDK , :
 String s = List.<String>nil().head(); 


JEP 101 StackOverflow . , , 7- , 8- – ? :
 class Test {  static void m(Object o) {      System.out.println("one");  }  static void m(String[] o) {      System.out.println("two");  }  static <T> T g() {      return null;  }  public static void main(String[] args) {      m(g());  } } 


- JDK1.8:
   public static void main(java.lang.String[]);   descriptor: ([Ljava/lang/String;)V   flags: ACC_PUBLIC, ACC_STATIC   Code:     stack=1, locals=1, args_size=1        0: invokestatic  #6   // Method g:()Ljava/lang/Object;        3: checkcast     #7   // class "[Ljava/lang/String;"        6: invokestatic  #8   // Method m:([Ljava/lang/String;)V        9: return     LineNumberTable:       line 15: 0       line 16: 9 


0 g:()Ljava/lang/Object; java.lang.Object . , 3 («») , java.lang.String , 6 m:([Ljava/lang/String;) , «two».

- JDK1.7 – Java 7:
   public static void main(java.lang.String[]);   flags: ACC_PUBLIC, ACC_STATIC   Code:     stack=1, locals=1, args_size=1        0: invokestatic  #6   // Method g:()Ljava/lang/Object;        3: invokestatic  #7   // Method m:(Ljava/lang/Object;)V        6: return            LineNumberTable:       line 15: 0       line 16: 6 


, checkcast , Java 8, m:(Ljava/lang/Object;) , «one». Checkcast – , Java 8.

, Oracle JDK1.7 JDK 1.8 , Java, , .

, Java 8 , Java 7, :

 public static void main(String[] args) { m((Object)g()); } 


结论


Java Generics . , :


  • Bloch, Joshua. Effective Java. Third Edition. Addison-Wesley. ISBN-13: 978-0-13-468599-1

, Java Generics.

Source: https://habr.com/ru/post/zh-CN416413/


All Articles