关于泛型:TypeScript之泛型

根底阐明如果咱们须要定义一个函数,一个入参和一个返回值,入参和返回值类型须要保持一致,怎么办? 比方规定类型都是字符串,那么: function doit(arg: string): string { return arg;}能够看进去,类型这里写死成string了,那类型可不可以像参数一样定义成变量传递进来?当然能够,咱们革新一下代码: function doit<T>(arg: T): T { return arg;}这里通过<T>定义了一个类型变量T,而这个T具体是什么类型,应用的时候用户传递即可: let result: string = doit<string>("我是一个字符串");事实上,编译器通过查看arg的值的类型,能够推断出T示意什么类型,因而,大部分状况下,上述代码能够简化一下: let result: string = doit("我是一个字符串");泛型类型function doit<T>(arg: T): T { return arg;}let myDoit: Function = doit;咱们想把函数doit赋值给变量myDoit,咱们定义myDoit的类型是Function,那么,可不可以明确一下myDoit的类型细节?答案是必定的: let myDoit: <T>(arg: T) => T = doit;或 let myDoit: { <T>(arg: T): T } = doit;泛型接口聪慧的是必定能够想到,咱们还能够定义一个接口: interface DoitFnType { <T>(arg: T): T;} let myDoit: DoitFnType = doit;类型锁定当初有一个状况,咱们在给myDoit赋值的是,曾经明确了类型T的理论值肯定是string,那么,就能够这样改变: interface DoitFnType<T> { (arg: T): T;} let myDoit: DoitFnType<string> = doit;泛型类也非常简单,咱们间接举个例子: ...

January 17, 2023 · 1 min · jiezi

关于泛型:Java中泛型的基本介绍深入解析泛型的使用方式

泛型的基本概念泛型: 参数化类型 参数: 定义方法时无形参调用办法时传递实参参数化类型: 将类型由原来的具体的类型参数化,相似办法中的变量参数 类型定义成参数模式, 能够称为类型形参在应用或者调用时传入具体的类型,能够称为类型实参泛型的实质是为了参数化类型 在不创立新的类型的状况下,通过泛型指定的不同类型来管制形参具体限度的类型在泛型应用过程中,操作的数据类型被指定为一个参数,这种参数类型能够用在: 类 - 泛型类接口 - 泛型接口办法 - 泛型办法泛型示例: List arrayList = new ArrayList();arrayList.add("aaaa");arrayList.add(100); arrayList.forEach(i -> { String item = (String) arrayList.get(i); Log.d("泛型", "item = " + item);});这样的写法会导致程序出现异常解体完结: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String这里的ArrayList能够寄存任意类型,增加了一个String类型,增加了一个Integer类型,再应用时都以String的形式应用,因而程序解体泛型就是解决这样的问题再探讨另一种状况,如果将第一行申明初始的代码批改一下,那么在编译阶段就能发现问题: List arrayList = new ArrayList<String>();arrayList.add("aaaa");arrayList.add(100); // 这一步在编译阶段,编译器就会报错 arrayList.forEach(i -> { String item = (String) arrayList.get(i); Log.d("泛型", "item = " + item); });泛型只在编译阶段无效: List<String> stringArrayList = new ArrayList<String>();List<Integer> integerArrayList = new ArrayList<Integer>();Class classStringArrayList = stringArrayList.getClass();Class classIntegerArrayList = integerArrayList.getClass();if (classStringArrayList.equals(classIntegerArrayList)) { Log.d("泛型", "类型雷同");}能够发现,在编译过后,程序会采取去泛型化措施.也就是说,Java中的泛型,只在编译阶段无效.在编译过程中,正确测验泛型后果后,会将泛型的相干信息擦除,并且在对象进入和来到办法的边界处增加类型检查和类型转换办法 ...

August 9, 2021 · 5 min · jiezi

关于泛型:Java泛型方法的定义

一 点睛 泛型办法次要用于容器类,Java中任何办法,包含动态的(留神,泛型类不容许在动态环境中应用)和非动态的,均能够用泛型来定义,而且和所在类是否是泛型没有关系。 上面是泛型办法的定义 [public] [static] <T> 返回值类型 办法名(T 参数列表) 二 代码 public class GeneralMethod { public static <U> void print(U[] list) { System.out.println(); for (int i = 0; i < list.length; i++) { System.out.print(" " + list[i]); } System.out.println(); } public static void main(String[] args) { String a[]={"a","b","c","d","e"}; Character b[]={'1','2','3','4','5'}; Integer c[]={1,2,3,4,5}; GeneralMethod.print(a); GeneralMethod.print(b); GeneralMethod.print(c); } } 三 运行 a b c d e 1 2 3 4 5 1 2 3 4 5四 阐明 ...

December 19, 2020 · 1 min · jiezi

Android中泛型在实际项目中的使用小结

前言 为什么要总结泛型的使用?泛型在项目中是如何体现价值的?不多说,总结一波。从实践中到理论,最后回归泛型本质。1.什么是泛型?为什么要用泛型?定义: 泛型:就是“宽泛的数据类型”,任意的数据类型。作用: 泛型可以解决数据类型的安全问题,它的主要原理是:在类声明的时候通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型在开发中常用用于代码的抽象和封装,使其工具化,通用化。可有效降低代码的冗余,使代码的可读性更高。在ArrayList源码中, 可以发现泛型的使用到处存在。 public class ArrayList<E> extends AbstractList<E> public boolean addAll(int index, Collection<? extends E> c) { rangeCheckForAdd(index); boolean modified = false; for (E e : c) { add(index++, e); modified = true; } return modified; }注意点: 泛型通常用<>和一个大写字母表示,而一般常用的就是 <T>、<?>、<? extends Object>、<? super Object>四种方式。 其中,T表示同一种类型,?表示任意类型,<? extends XX>表示xx类型,和xx类型的子<? super XX >表示xx类型,和xx类型的父类型。class<T>:实例化的时候,需要指定具体类型。 class<?>:可以表示所有类型。不建议使用<G>、<Q>这种奇葩定义方式。泛型不能使用基本数据类型,如int,double等,只能使用它们的容器类如Integer、Double。因为Java的泛型是用类型擦除实现的,在运行阶段用的都是Object,而基本数据类型不是继承自Object,也就无法在泛型中使用。Kotlin 中的 out 和 in 和 Java 泛型一样,Kolin 中的泛型本身也是不可变的。 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends。Foo<? extends Bar>对应Foo<out Bar!>! ...

November 5, 2019 · 2 min · jiezi

Java-泛型之上界下界通配符

Java 泛型之上界下界通配符Java教程是为JDK 8编写的。本页描述的示例和实践没有利用后续版本中引入的改进。 通配符和子类型如 泛型,继承和子类型中所述,泛型类或接口仅仅因为它们的类型之间存在关系而无关。但是,您可以使用通配符在泛型类或接口之间创建关系。 给定以下两个常规(非泛型)类: class A { /* ... */ } class B extends A { /* ... */ }编写以下代码是合理的: B b = new B(); A a = b;此示例显示常规类的继承遵循此子类型规则:如果B扩展A,则类B是类A的子类型。此规则不适用于泛型类型: List<B> lb = new ArrayList<>();List<A> la = lb; //编译时错误鉴于Integer是Number的子类型,List<Integer> 和 List<Number> 之间的关系是什么? 公共父类是List<?>该图表显示 List<Number> 和 List<Integer> 的公共父级是未知类型的List. 尽管Integer是Number的子类型,但List<Integer>不是List<Number>的子类型,实际上,这两种类型不相关。List<Number>和 List<Integer> 的公共父是 List<?>。 上界(extends)的通配符与下界(super)通配符为了在这些类之间创建关系以便代码可以通过 List<Integer> 的元素访问Number的方法,请使用上界的通配符: List<? extends Integer> intList = new ArrayList<>();List<? extends Number> numList = intList; // OK, List<?extends Integer>是 List< ? extends Number>的子类型因为Integer是Number的子类型,而numList是Number对象的列表,所以intList(是一个Integer对象列表)和numList之间现在存在关系。下图显示了使用上限和下限通配符声明的多个 List 类之间的关系。 ...

June 18, 2019 · 1 min · jiezi

Java系列之泛型

自从 JDK 1.5 提供了泛型概念,泛型使得开发者可以定义较为安全的类型,不至于强制类型转化时出现类型转化异常,在没有反省之前,可以通过 Object 来完成不同类型数据之间的操作,但是强制类型转换(向下转型)在不确定具体类型的情况下会出错,泛型机制的引入就是解决数据类型不明确 的问题。 定义泛型类定义一个泛型类,语法如下: //定义泛型类class 类名<T>{ }其中,T 表示一个类型的名称,T 可以表示成其他名称,一般习惯写成 T,<> 里面的类型可以有多个,中间以逗号隔开,下面是一个泛型类,具体如下: /** * 定义泛型类 * @author jzman * @param <T> */public class GenercityClass<T1,T2> { private T1 score; private T2 desc; public GenercityClass() {} public GenercityClass(T1 score, T2 desc) { this.score = score; this.desc = desc; } public T1 getScore() { return score; } public void setScore(T1 score) { this.score = score; } public T2 getDesc() { return desc; } public void setDesc(T2 desc) { this.desc = desc; } public static void main(String[] args) { //使用时指定具体类型,具体类型只能是引用类型,不能时基本类型,如 int GenercityClass<Integer, String> genercity = new GenercityClass<>(90,"A"); int score = genercity.getScore(); String desc = genercity.getDesc(); System.out.println("score="+score+",desc="+desc); }}显然,使用泛型定义的类可以在使用时根据不同的需求指定 <T1,T2> 所代表的真实类型,这样就不会有类型转换操作,将不会出现 ClassCastException 异常,编译器会提前检查类型是否匹配,下面这样会出错,具体如下: ...

June 10, 2019 · 4 min · jiezi

初探Java类型擦除

本篇博客主要介绍了Java类型擦除的定义,详细的介绍了类型擦除在Java中所出现的场景。 1. 什么是类型擦除为了让你们快速的对类型擦除有一个印象,首先举一个很简单也很经典的例子。 // 指定泛型为StringList<String> list1 = new ArrayList<>();// 指定泛型为IntegerList<Integer> list2 = new ArrayList<>();System.out.println(list1.getClass() == list2.getClass()); // true上面的判断结果是true。代表了两个传入了不同泛型的List最终都编译成了ArrayList,成为了同一种类型,原来的泛型参数String和Integer被擦除掉了。这就是类型擦除的一个典型的例子。 而如果我们说到类型擦除为什么会出现,我们就必须要了解泛型。 2. 泛型2.1. 泛型的定义随着2004年9月30日,工程代号为Tiger的JDK 1.5发布,泛型从此与大家见面。JDK 1.5在Java语法的易用性上作出了非常大的改进。除了泛型,同版本加入的还有自动装箱、动态注解、枚举、可变长参数、foreach循环等等。 而在1.5之前的版本中,为了让Java的类具有通用性,参数类型和返回类型通常都设置为Object,可见,如果需要不用的类型,就需要在相应的地方,对其进行强制转换,程序才可以正常运行,十分麻烦,稍不注意就会出错。 泛型的本质就是参数化类型。也就是,将一个数据类型指定为参数。引入泛型有什么好处呢? 泛型可以将JDK 1.5之前在运行时才能发现的错误,提前到编译期。也就是说,泛型提供了编译时类型安全的检测机制。例如,一个变量本来是Integer类型,我们在代码中设置成了String,没有使用泛型的时候只有在代码运行到这了,才会报错。 而引入泛型之后就不会出现这个问题。这是因为通过泛型可以知道该参数的规定类型,然后在编译时,判断其类型是否符合规定类型。 泛型总共有三种使用方法,分别使用于类、方法和接口。 3. 泛型的使用方法3.1 泛型类3.1.1 定义泛型类简单的泛型类可以定义为如下。 public class Generic<T> { T data; public Generic(T data) { setData(data); } public T getData() { return data; } public void setData(T data) { this.data = data; }}其中的T代表参数类型,代表任何类型。当然,并不是一定要写成T,这只是大家约定俗成的习惯而已。有了上述的泛型类之后我们就可以像如下的方式使用了。 3.1.2 使用泛型类// 假设有这样一个具体的类public class Hello { private Integer id; private String name; private Integer age; private String email;}// 使用泛型类Hello hello = new Hello();Generic<Hello> result = new Generic<>();resule.setData(hello);// 通过泛型类获取数据Hello data = result.getData();当然如果泛型类不传入指定的类型的话,泛型类中的方法或者成员变量定义的类型可以为任意类型,如果打印result.getClass()的话,会得到Generic。 ...

May 27, 2019 · 2 min · jiezi

栈和队列 - Algorithms, Part I, week 2 STACKS AND QUEUES

前言上一篇:算法分析下一篇:基本排序本篇内容主要是栈,队列 (和包)的基本数据类型和数据结构在很多应用中,我们需要维护多个对象的集合,而对这个集合的操作也很简单基本数据类型对象的集合操作:insert – 向集合中添加新的对象remove – 去掉集合中的某个元素iterate – 遍历集合中的元素并对他们执行某种操作test if empty – 检查集合是否为空做插入和删除操作时我们要明确以什么样的形式去添加元素,或我们要删除集合中的哪个元素。处理这类问题有两个经典的基础数据结构:栈(stack) 和队列(queue)两者的区别在于去除元素的方式:栈:去除最近加入的元素,遵循后进先出原则(LIFO: last in first out)。插入元素对应的术语是入栈 – push;去掉最近加入的元素叫出栈 – pop队列:去除最开始加入的元素,遵循先进先出原则(FIFO: first in first out)。关注最开始加入队列的元素,为了和栈的操作区分,队列加入元素的操作叫做入队 – enqueue;去除元素的操作叫出队 – dequeue此篇隐含的主题是模块式编程,也是平时开发需要遵守的原则模块化编程这一原则的思想是将接口与实现完全分离。比如我们精确定义了一些数据类型和数据结构(如栈,队列等),我们想要的是把实现这些数据结构的细节完全与客户端分离。客户端可以选择数据结构不同的实现方式,但是客户端代码只能执行基本操作。实现的部分无法知道客户端需求的细节,它所要做的只是实现这些操作,这样,很多不同的客户端都可以使用同一个实现,这使得我们能够用模块式可复用的算法与数据结构库来构建更复杂的算法和数据结构,并在必要的时候更关注算法的效率。Separate client and implementation via API.API:描述数据类型特征的操作Client:使用API操作的客户端程序。Implementation:实现API操作的代码。下面具体看下这两种数据结构的实现栈栈 API假设我们有一个字符串集合,我们想要实现字符串集合的储存,定期取出并且返回最后加入的字符串,并检查集合是否为空。我们需要先写一个客户端然后再看它的实现。字符串数据类型的栈性能要求:所有操作都花费常数时间客户端:从标准输入读取逆序的字符串序列测试客户端import edu.princeton.cs.algs4.StdIn;import edu.princeton.cs.algs4.StdOut;public static void main(String[] args){ StackOfStrings stack = new StackOfStrings(); while (!StdIn.isEmpty()) { //从标准输入获取一些字符串 String s = StdIn.readString(); //如果字符串为"-",则客户端将栈顶的字符串出栈,并打印出栈的字符串 if (s.equals("-")) StdOut.print(stack.pop()); //否则将字符串入栈到栈顶 else stack.push(s); }}客户端输入输出:栈的实现:链表链表(linked-list)连接待添加…我们想保存一个有节点组成的,用来储存字符串的链表。节点包含指向链表中下一个元素的引用(first).维持指针 first 指向链表中的第一个节点Push:入栈,在链表头插入一个新的节点Pop:出栈,去掉链表头处第一个节点Java 实现public class LinkedStackOfStrings{ //栈中唯一的实例变量是链表中的第一个节点的引用 private Node first = null; //内部类,节点对象,构成链表中的元素,由一个字符串和指向另一个节点的引用组成 private class Node { private String item; private Node next; } public boolean isEmpty() { return first == null; } // public void push(String item) { //将指向链表头的指针先保存 Node oldfirst = first; //创建新节点:我们将要插入表头的节点 first = new Node(); first.item = item; //实例变量的next指针指向链表oldfirst元素,现在变成链表的第二个元素 first.next = oldfirst; } //出栈 public String pop() { //将链表中的第一个元素储存在标量 item 中 String item = first.item; //去掉第一个节点:将原先指向第一个元素的指针指向下一个元素,然后第一个节点就等着被垃圾回收处理 first = first.next; //返回链表中原先保存的元素 return item; }}图示:出栈:入栈:性能分析通过分析提供给客户算法和数据结构的性能信息,评估这个实现对以不同客户端程序的资源使用量Proposition 在最坏的情况下,每个操作只需要消耗常数时间(没有循环)。Proposition 具有n个元素的栈使用 ~40n 个字节内存(没有考虑字符串本身的内存,因为这些空间的开销在客户端上)栈的实现:数组栈用链表是实现花费常数的时间,但是栈还有更快的实现另一种实现栈的 natural way 是使用数组储存栈上的元素将栈中的N个元素保存在数组中,索引为 n,n 对应的数组位置即为栈顶的位置,即下一个元素加入的地方使用数组 s[] 在栈上存储n个元素。push():在 s[n] 处添加新元素。pop():从 s[n-1] 中删除元素。在改进前使用数组的一个缺点是必须声明数组的大小,所以栈有确定的容量。如果栈上的元素个数比栈的容量多,我们就必须处理这个问题(调整数组)Java 实现public class FixedCapacityStackOfStrings{ private String[] s; //n 为栈的大小,栈中下一个开放位置,也为下一个元素的索引 private int n = 0; //int capacity:看以下说明 public FixedCapacityStackOfStrings(int capacity) { s = new String[capacity]; } public boolean isEmpty() { return n == 0; } public void push(String item) { //将元素放在 n 索引的位置,然后 n+1 s[n++] = item; } public String pop() { //然后返回数组n-1的元素 return s[–n]; }}int capacity: 在构造函数中加入了容量的参数,破坏了API,需要客户端提供栈的容量。不过实际上我们不会这么做,因为大多数情况下,客户端也无法确定需要多大栈,而且客户端也可能需要同时维护很多栈,这些栈又不同时间到达最大容量,同时还有其他因素的影响。这里只是为了简化。在调整数组中会处理可变容量的问题,避免溢出对于两种实现的思考上述的实现中我们暂时没有处理的问题:Overflow and underflowUnderflow :客户端从空栈中出栈我们没有抛出异常Overflow :使用数组实现,当客户端入栈超过容量发生栈溢出的问题Null item:客户端是否能像数据结构中插入空元素Loitering 对象游离:即在栈的数组中,我们有一个对象的引用,可是我们已经不再使用这个引用了数组中当我们减小 n 时,在数组中仍然有我们已经出栈的对象的指针,尽管我们不再使用它,但是Java系统并不知道。所以为了避免这个问题,有效地利用内存,最好将去除元素对应的项设为 null,这样就不会剩下旧元素的引用指针,接下来就等着垃圾回收机制去回收这些内存。这个问题比较细节化,但是却很重要。public String pop(){ String item = s[–n]; s[n] = null; return item;} 调整数组队列泛型迭代器栈与队列的应用附录 ...

March 13, 2019 · 2 min · jiezi

Java泛型 - 如何破解Enum<E extends Enum<E>>?

以下内容翻译自链接内容其中一段章节:How do I decrypt “Enum<E extends Enum<E>>"?public abstract class Enum<E extends Enum<E>> { …}Enum类是Java内所有枚举类型的通用基础类。例如enum Color {}会被编译成class Color extends Enum<Color>。Enum基类存在的目的是为这些所有枚举类型提供基础的方法及功能。以下是Euum类的骨架:public abstract class Enum< E extends Enum<E>> implements Comparable<E>, Serializable { private final String name; public final String name() { … } private final int ordinal; public final int ordinal() { … } protected Enum(String name, int ordinal) { … } public String toString() { … } public final boolean equals(Object other) { … } public final int hashCode() { … } protected final Object clone() throws CloneNotSupportedException { … } public final int compareTo( E o) { … } public final Class<E> getDeclaringClass() { … } public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { … } }以下是实际使用中的enum Color:enum Color {RED, BLUE, GREEN}Java编译器会将它编译成:public final class Color extends Enum<Color> { public static final Color[] values() { return (Color[])$VALUES.clone(); } public static Color valueOf(String name) { … } private Color(String s, int i) { super(s, i); } public static final Color RED; public static final Color BLUE; public static final Color GREEN; private static final Color $VALUES[]; static { RED = new Color(“RED”, 0); BLUE = new Color(“BLUE”, 1); GREEN = new Color(“GREEN”, 2); $VALUES = (new Color[] { RED, BLUE, GREEN }); } }Color类继承了所有Enum<Color>所实现了的方法。compareTo方法就是其中之一。如果加上类的限制,那么Color.compareTo应该接收的参数是Color类型实例。为了让这能够实现,Enum设置了泛型<E>并在Enum.compareTo实现中接收E类型的实例作为方法参数。而作为继承的结果,Color类型从Enum<Color>中派生出来的compareTo方法实际上接收的参数就是Color类型的实例,这完美地达成了设计目标。如果我们继续深入解剖类声明Enum<E extends Enum<E>>,我们可以看到它有以下几方面的意义:第一,Enum的泛型E的上界为Enum自身。这确保了只有Enum的子类才被允许成为泛型参数。(理论上,Enum可以被它自己具现化,例如Enum<Enum>,但这没有意义,并且很难想象这会有对应的应用场景。)第二,泛型E的上界被进一步限定为extends Enum<E>,这确保了Enum<子类A>和Enum<子类A>的子类A的继承关系一定满足子类A extends Enum<子类A>。类似子类A extends Enum<子类B>这样的声明是会被编译器拒绝的,因为这个声明并不匹配泛型参数的上界。第三,基于Enum被设计为泛型,这意味着Enum类中的某些方法的方法参数及返回类型在运行时是未知类型(又或者说是依赖于某未知类型)。而根据E extends Enum<E>,我们可以知道E肯定会是Enum<?>的子类。所以,在具象化类型Enum<某具体类>中,这些泛型方法的参数及返回类型就会被编译器转换为某具体类型。(compareTo就是一个例子)。总结来说,E extends Enum<E>保证了每个Enum<E>的子类中都能够接收并返回该子类类型。 ...

March 5, 2019 · 2 min · jiezi

Java泛型 - 能否取出Map<String, Integer>的泛型参数?

这两天有一个小需求:在运行时取出Map对象实例的泛型参数。Google过后,第一条就是StackOverflow的相关问题:如何取出Map的泛型参数在这做一下记录,原文问题及回答都非常有趣清晰。问题:public Object[] convertTo(Map source, Object[] destination) { …}是否有可能通过反射来获取到Map的泛型参数?回答1:被采纳的答案对于任意的Map<Key, Value>,要在运行时获取到它的Key和Value类型是不可能的。这是因为类型擦除。然而,可以在运行时通过getClass()检查map里每个对象的真实类型。再一次说明,这仍然不会告诉你Key和Value的类型。(笔者注:考虑接口与实现的关系)回答2:未被采纳你可以轻松地通过反射获取到泛型参数。private Map<String, Integer> genericTestMap = new HashMap<String, Integer>();public static void main(String[] args) { try { Field testMap = Test.class.getDeclaredField(“genericTestMap”); testMap.setAccessible(true); ParameterizedType type = (ParameterizedType) testMap.getGenericType(); Type key = type.getActualTypeArguments()[0]; System.out.println(“Key: " + key); Type value = type.getActualTypeArguments()[1]; System.out.println(“Value: " + value); } catch (Exception e) { e.printStackTrace(); }}评论最精彩回答2贴出的代码非常有效,能够完整地取出Map<Key, Value>的泛型参数。于是有人质疑为何回答2没有被采纳。于是有人解释道:被采纳的回答1才是正确的。回答2只是抽取了Field字段中的类型声明。这与在运行时获取对象实例的泛型参数是不一样的操作。尤其题主的代码中的Map并没有带任何泛型参数,这种情况甚至无法从参数声明中获取泛型。所以回答2的代码仅在特定情况下有效。

March 4, 2019 · 1 min · jiezi

Java泛型进阶 - 如何取出泛型类型参数

在JDK5引入了泛型特性之后,她迅速地成为Java编程中不可或缺的元素。然而,就跟泛型乍一看似乎非常容易一样,许多开发者也非常容易就迷失在这项特性里。多数Java开发者都会注意到Java编译器的类型擦除实现方式,Type Erasure会导致关于某个Class的所有泛型信息都会在源代码编译时消失掉。在一个Java应用中,可以认为所有的泛型实现类,都共享同一个基础类(注意与继承区分开来)。这是为了兼容JDK5之前的所有JDK版本,就是人们经常说的向后兼容性。向后兼容性译者注:原文较为琐碎,大致意思是。在JVM整个内存空间中,只会存在一个ArrayList.class。为了能够区分ArrayList<String>和ArrayList<Integer>,现在假想的实现方式是在Class文件信息表(函数表+字段表)里添加额外的泛型信息。那这个时候JVM的内存空间中就会存在(假设)ArrayList&String.class和(假设)ArrayList&Integer.class文件。顺着这种情况延续下去的话,就必须要修改JDK5之前所有版本的JVM对Class文件的识别逻辑,因为它破坏了JVM内部只有一个Class只有唯一一个.class这条规则。这也是人们常说的: 破坏了向后兼容性。注:参考Python3舍弃掉Python2的例子,也是放弃了对2的兼容,Python3才能发展并构造更多的新特性。As a consequence既然Java团队选择了兼容JDK5之前的版本,那就不能在JVM里做手脚了,但是还可以在Java编译器做手脚的嘛。于是,Java编译器在编译时把泛型信息都擦除之后,以下的比较在JVM里运行时会永远为真。assert new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass();对JVM来说,上述代码等同于assert new ArrayList.class == ArrayList.class到目前为止,上述内容这都是大家所熟知的事情。然而,与普遍印象相反的是,某些情况下在运行时获取到泛型类型信息是可行的。举个栗子:class MyGenericClass<T> { }class MyStringSubClass extends MyGenericClass<String> { }MyStringSubClass相当于对MyGenericClass<T>做了类型参数赋值T = String。于是,Java编译器可以把这部分泛型信息(父类MyGenericClass的泛型参数是String),存储在它的子类MyStringSubClass的字节码区域中。并且因为这部分泛型信息在被编译后仅仅会存储在被老版JVM所忽略的字节码区域中,所以这种方式没有破坏向后兼容性。与此同时,因为T已经被赋值为String,所有的MyStringSubClass类的对象实例仍然共享同一个MyStringSubClass.class。如何获取这块泛型信息?但是我们应该如何获取到被存储在byte code区域的这块泛型信息呢?Java API提供了Class.getGenericSuperClass()方法,来取出一个Type类型的实例。如果直接父类的实际类型就是泛型类型的话,那取出的Type类型实例就可以被显示地转换为ParameterizeType。(Type只是一个标记型接口,它里面仅包含一个方法:getTypeName()。所以取出的实例的实际类型会是ParameterizedTypeImpl,但不应直接暴露实际类型,应一直暴露Type接口)。感谢ParameterizedType接口,现在我们可以直接调用ParameterizeType.getActualTypeArguments()取出又一个Type类型实例数组。父类所有的泛型类型参数都会被包含在这个数组里,并且以被声明的顺序放在数组对应的下标中。当数组中的类型参数为非泛型类型时,我们就可以简单地把它显示转换为Class<?>。为了保持文章的简洁性,我们跳过了GenericArrayType的情况。现在我们可以使用以上知识编写一个工具类了:public static Class<?> findSuperClassParameterType(Object instance, Class<?> classOfInterest, int parameterIndex) { Class<?> subClass = instance.getClass(); while (subClass != subClass.getSuperClass()) { // instance.getClass()不是classOfInterest的子类, // 或者,instance就是classOfInterest的直接实例 subClass = subClass.getSuperClass(); if (subClass == null) throw new IllegalArgumentException(); } ParameterizedType pt = (ParameterizedType) subClass.getGenericSuperClass(); return (Class<?>) pt.getActualTypeArguments()[parameterIndex];}public static void main(String[] args) { Class<?> genericType = findSuperClassParameterType(new MyStringSubClass(), MyGenericClass.class, 0); assert genericType == String.class;}然而,请注意到findSuperClassParamerterType(new MyGenericClass<String>(), MyGenericClass.class, 0)这个方法的实现会抛出异常。像之前说过的:泛型信息仅有在子类的帮助下才能被取出。然而,MyGenericClass<String>仅是一个拥有泛型参数的实例,并不是MyGenericClass.class的子类。没有显式的子类的话,就没有地方存储String类型参数。因此这一次,上述调用不可避免地会被Java编译器进行类型擦除。于是乎,如果你预见到你项目中会出现这种情况,为了避免之,一种良好的编程实践是将MyGenericClass声明为abstract。不过,我们还没有解决问题,毕竟我们目前为止还有许多坑没有填。为了说明,想象下列类继承层次:class MyGenericClass<T> {}class MyGenericSubClass<U> extends MyGenericClass<U> {}class MyStringSubSubClass extends MyGenericSubClass<String> {}如果现在调用findSuperClassParameterType(new MyStringSubClass(), MyGenericClass.class, 0);仍然会抛出异常。这次又是为什么呢?到目前为止,我们的假设都是MyGenericClass的类型参数T的相关信息会存储在它的直接子类中,结合第一个例子,就是MyStringSubClass会将T映射成String。但凡是总有无赖,现在MyStringSubSubClass将U映射成String,此时MyGenerciSubClass仅仅知道U = T。U甚至都不是一个实际类型,仅仅是Java TypeVariable类型的类型变量,如果我们想要解析这种继承关系,就必须解析它们之间所有的依赖关系。当然还是能做到的:public static Class<?> findSubClassParameterType(Object instance, Class<?> classOfInterest, int parameterIndex) { Map<Type, Type> typeMap = new HashMap<Type, Type>(); Class<?> instanceClass = instance.getClass(); while (classOfInterest != instanceClass.getSuperclass()) { extractTypeArguments(typeMap, instanceClass); instanceClass = instanceClass.getSuperclass(); if (instanceClass == null) throw new IllegalArgumentException(); } ParameterizedType parameterizedType = (ParameterizedType) instanceClass.getGenericSuperclass(); Type actualType = parameterizedType.getActualTypeArguments()[parameterIndex]; if (typeMap.containsKey(actualType)) { actualType = typeMap.get(actualType); } if (actualType instanceof Class) { return (Class<?>) actualType; } else { throw new IllegalArgumentException(); } private static void extractTypeArguments(Map<Type, Type> typeMap, Class<?> clazz) { Type genericSuperclass = clazz.getGenericSuperclass(); if (!(genericSuperclass instanceof ParameterizedType)) { return; } ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; Type[] typeParameter = ((Class<?>) parameterizedType.getRawType()).getTypeParameters(); Type[] actualTypeArgument = parameterizedType.getActualTypeArguments(); for (int i = 0; i < typeParameter.length; i++) { if(typeMap.containsKey(actualTypeArgument[i])) { actualTypeArgument[i] = typeMap.get(actualTypeArgument[i]); } typeMap.put(typeParameter[i], actualTypeArgument[i]); }} ...

February 27, 2019 · 2 min · jiezi

关于List、List<?>、List<Object>的区别

定义:声明中具有一个或者多个类型参数(type parameter)的类或者接口,就是泛型类或者接口。泛型类和接口统称为泛型(generic type)。每种泛型定义一组类型形参(formal type parameters),这些类型形参有时也被简称为类型参数(type parameter),例如对于泛型(generic type)List<E>而言,List<String>就是一个参数化的类型(parameterized type),String就是对应于类型形参(formal type parameters)的类型实参(actual type parameter)。每个泛型定义一个原生类型(raw type),即不带任何类型参数的类型名称,例如,与List<String>对应的原生类型是List。原生类型就像从类型声明中删除了所有泛型信息一样。实际上原生类型List与Java平台在有泛型之前的接口类型List完全一样。容器类使用泛型的好处:安全性:在对参数化类型的容器中放入了错误即不匹配的类型的时候,编译器将会强制性进行错误提示。便利性:当从容器中取出元素的时候不用自己手动将Object转换为元素的实际类型了,编译器将隐式地进行自动转换。表述性:带有类型实参的泛型即参数化类型,可以让人看到实参就知道里面的元素E都是什么类型。所以,不应该使用原生类型的原因如下:虽然使用原生类型是合法的,但不提倡这样做,因为如果使用原生类型,就失掉了泛型在安全性和表述性方面的所有优势;安全性:比如我们可能会不小心把一个java.util.Date实例错误地放进一个原本包含java.sql.Date实例的集合当中,虽然在编译期不会出现任何错误,但在运行期一旦尝试类型转换就会发生ClassCastException,而泛型原本就是为了避免这种问题而出现的;表述性:不像带有类型实参的泛型即参数化类型那样,让人看到实参就知道里面的元素E都是什么类型。泛型的子类型化的原则:List<String>类型是原生类型List的一个子类型,而不是参数化类型List<Object>的子类型List、List<?>、List<Object>的区别List,即原始类型,其引用变量可以接受任何对应List<E>的参数化类型, 包括List<?>,并且可以添加任意类型的元素。但其缺点在于不安全性、不便利性、不表述性(不应该使用原生类型的原因)。List<?>,即通配符类型,其引用变量,同样可以接受任何对应List<E>的参数化类型,包括List,但不能添加任何元素,保证了安全性和表述性。但不具有表述性,从中取出的元素时Object类型,要通过手动转换才能得到原本的类型。List<Object>,即实际类型参数为Object的参数化类型,其引用变量可以接受List,可以添加元素,但不能接受除了其本身外的任何参数化类型(泛型的子类型化原则)。引用变量的类型名称可以接受的类型能否添加元素安全性便利性表述性List原始类型任何对应List<E>的参数化类型, 包括List<?>可以添加任意类型的元素无无无List<?>通配符类型以接受任何对应List<E>的参数化类型,包括List不能添加任何元素有无有List<Object>实际类型参数为Object的参数化类型仅可以接受List和其本身类型可以添加任意类型元素有有有可以看到相比参数化类型的List<Object>,List<?>缺点在于不能添加任何元素并且不具有便利性,如果这无法满足功能要求可以考虑使用泛型方法和有边界的通配符。根据The Java™ Tutorials,原生类型对象可以被赋给参数化类型(包括有界无界通配符参数化类型),同样,参数化类型(包括有界无界通配符参数化类型)对象也能被赋给原生类型

February 18, 2019 · 1 min · jiezi

我理解的 Java 泛型 (一)

前言在学习 Spring 的依赖注入时, 被 Google 导流到了 Java Generics FAQs. 这篇文章深入讲解了 Java 中泛型相关的方方面面, 阅读完毕后, 整理了自己的一些理解.概念与名词在进入具体的讨论之前, 我们需要先明确几个名词的含义.Generic Typegeneric type && type parameterA generic type is a type with formal type parameters.以 interface List<E> {} 为例, List<E> 是 generic type, E 是 type parameter.parameterized type && type argumentA pameterized type is an instantation of a generic type with type argument.以 List<String> stringList; 为例, List<String> 是 parameterized type, 是 List<E> 的一个实例, String 是 type argument.Wildcard泛型中将通配符(wildcard)分为三类:? - unbound, 不做任何限定的通配符.? extends Number - upper bounded, 限定必须是 Number 或其子类.? super Integer - lower bounded, 限定必须是 Integer 或其父类.后两者也被统称为 bounded wildcard.结合通配符, parameterized type 也可以划分为三类. conceret type argumentgeneric type —————————> conceret parameterized type unbound type argumentgeneric type —————————> unbound parameterized type bounded type argumentgeneric type —————————> bounded parameterized typeRaw TypeRaw type 的存在是为了兼容引入泛型之前的版本, 大多数时候你都可以不用考虑它.Type Erasure严格说来, 泛型只存在于编译期间, JVM 并不感知泛型. 在编译时, 编译器通过 type erasure 来消除 type parameter 和 type argument.具体的处理方式是:Generic type 中的 type parameter 都会被其上确界(leftmost bound)所代替.Parameterized type 中的 type argument 被直接移除, parameterized type 转变为对应的 raw type.何为上确界, 对于 upper bounded type paramter 而言, 是指其公共父类, <? extends Number> 对应的就是 Number.对于其他类型的 type paramter 而言, 因为 type argument 只能是引用类型(reference type), 而引用类型的公共父类是 Object, 所以其上确界都是 Object.我们可以从 Java Generics FAQs 的例子中看到具体的转换过程. 左边是原始的代码, 右边是经过 type erasure 转换后的结果.这个例子同时也反应了, 在 type erasure 的过程中, 编译器可能会按需加入 bridge method 和 type cast.泛型的类型系统泛型的引入使得对象之间的继承关系变得更复杂, 如下这个例子中的一部分就是错误的.public class SuperDemo { public static void main(String args[]) { List<Number> a = new ArrayList<Number>(); ArrayList<Number> b = new ArrayList<Integer>(); List<? extends Number> c = new ArrayList<Integer>(); List<? super Number> d = new ArrayList<Object>(); List<? super Integer> e = d; }}理论上, 泛型相关的继承关系判断需要从两个纬度考虑:generic type 之间是否有继承关系type argument 之间是否有超集关系具体而言. 对于 type argument 相同的情况, generic type 之间的继承关系决定两个 parameterized type 的父子关系, 所以 List<Number> 是 ArrayList<Number> 的父类.但 ArrayList<Number> 不是 ArrayList<Integer> 的父类, type argument 不同的情况下, 泛型之间的继承关系判断会很复杂. 主要是由于 wildcard 的存在, 导致 type argument 可以代表一类类型, 所以要引入集合中的超集(superset)概念, 即一方所代表的所有类型完全包含在以一方内.最终的判断标准是, 在 type argument 不相同的情况下, 如果 type argument 是对方的超集, 而且 generic type 与对方相同或者是对方的父类, 那么当前的 parameterized type 才是对方的父类.这时候再来回答以下的问题就会比较简单了:How do instantiations of a generic type relate to instantiations of other generic types that have the same type argument?How do unbounded wildcard instantiations of a generic type relate to other instantiations of the same generic type?How do wildcard instantiations with an upper bound relate to other instantiations of the same generic type?How do wildcard instantiations with a lower bound relate to other instantiations of the same generic type?泛型的特殊使用姿势观察泛型在异常处理和数组中的使用限制, 思考是什么导致了这些限制, 在一定程度上可以验证自己之前的理解是否正确.泛型与异常Java 的异常处理在运行时生效, 而 type erasure 发生在编译期间, 所以大多数时候, 泛型在异常处理中并没有用武之地.Java 不允许任何 Throwable 的子类是泛型.这主要是由于 type erasure 导致 catch 不能在运行时区分一个 generic type 的不同实例, 所以把 error 或者 exception 定义为泛型不具有任何实际意义.同样由于 type erasure, catch 语句也不接受 type parameter.throws 语句可以接受 type parameter, 编译器在编译时其会将 type parameter 的替换为具体的异常.你可以 throw 类型为 type parameter 的异常, 但实际上你基本没有机会这么做, 因为我们无法新建一个类型为 type parameter 的对象.泛型与数组与异常处理类似, 数组在运行时保存了每个元素的类型信息, 所以泛型数组也是一个没有太大意义的概念. 虽然可以定义一个数据的元素为泛型, 但我们仅能新建元素为 unbound parameterized type 的泛型数组. 具体而言, 下例子中 line 1 和 line 2 合法, 但 line 3 是错误的.List<String>[] a;List<?>[] b = new List<?>[10];a = new List<String>[10]; // error究其根本, 是因为数据的组成元素都应该是同一类型的: An array is a container object that holds a fixed number of values of a single type. 而同一 generic type 对应的不同实例实质上并不等价, 但经过 type erasure 后, 并不能在运行时区分出这些.假如能新建元素为 concerete parameterized type 的数组, 考虑如下案例.List<String>[] stringLists = new List<String>[10];stringLists[0] = new List<String>();stringLists[1] = new List<Integer>(); ...

January 3, 2019 · 3 min · jiezi

异构列表(DslAdapter开发日志)

异构列表(DslAdapter开发日志)函数范式, 或者说Haskell的终极追求是尽量将错误"扼杀"在编译期, 使用了大量的手法和技术: 使用大量不可变扼杀异步的不可预计, 以及静态类型和高阶类型 说到静态类型大家应该都不会陌生, 它是程序正确性的强大保证, 这也是本人为什么一直不太喜欢Python, js等动态类型语言的原因静态类型: 编译时即知道每一个变量的类型,因此,若存在类型错误编译是无法通过的。 动态类型: 编译时不知道每一个变量的类型,因此,若存在类型错误会在运行时发生错误。类型检查, 即在编译期通过对类型进行检查的方式过滤程序的错误, 这是我们在使用Java和Kotlin等语言时常用的技术, 但这种技术是有限的, 它并不能通用于所有情况, 因此我们常常反而会回到动态类型, 采用动态类型的方式处理某些问题 本文聚焦于常见的列表容器在某些情况下如何用静态类型的手法进行开发进行讨论编译期错误检查对于函数(方法)的输入错误有两种方式:编译期检查, 比如List<String>中不能保存Integer类型的数据运行期检查, 比如对于列表的下标是否正确, 我们可以在运行的时候检查运行期检查是必须要运行到相应的代码时才会进行相应的检查(无论是实际程序还是测试代码), 这是不安全并且效率低下的, 所以能在编译期检查的问题都尽量在编译期排除掉 编译期的检查中除了语法问题之外最重要的就是类型检查, 但这要求我们提供足够的类型信息DslAdapter实现中遇到的问题DslAdapter是个人开发的一个针对Android RecyclerView的一个扩展库, 专注于静态类型和Dsl的手法, 希望创造一个基于组合子的灵活易用同时又非常安全的Adapter 在早期版本中已经实现了通过Dsl进行混合Adapter的创建:val adapter = RendererAdapter.multipleBuild() .add(layout<Unit>(R.layout.list_header)) .add(none<List<Option<ItemModel>>>(), optionRenderer( noneItemRenderer = LayoutRenderer.dataBindingItem<Unit, ItemLayoutBinding>( count = 5, layout = R.layout.item_layout, bindBinding = { ItemLayoutBinding.bind(it) }, binder = { bind, item, _ -> bind.content = “this is empty item” }, recycleFun = { it.model = null; it.content = null; it.click = null }), itemRenderer = LayoutRenderer.dataBindingItem<Option<ItemModel>, ItemLayoutBinding>( count = 5, layout = R.layout.item_layout, bindBinding = { ItemLayoutBinding.bind(it) }, binder = { bind, item, _ -> bind.content = “this is some item” }, recycleFun = { it.model = null; it.content = null; it.click = null }) .forList() )) .add(provideData(index).let { HListK.singleId(it).putF(it) }, ComposeRenderer.startBuild .add(LayoutRenderer<ItemModel>(layout = R.layout.simple_item, stableIdForItem = { item, index -> item.id }, binder = { view, itemModel, index -> view.findViewById<TextView>(R.id.simple_text_view).text = itemModel.title }, recycleFun = { view -> view.findViewById<TextView>(R.id.simple_text_view).text = "" }) .forList({ i, index -> index })) .add(databindingOf<ItemModel>(R.layout.item_layout) .onRecycle(CLEAR_ALL) .itemId(BR.model) .itemId(BR.content, { m -> m.content + “xxxx” }) .stableIdForItem { it.id } .forList()) .build()) .add(DateFormat.getInstance().format(Date()), databindingOf<String>(R.layout.list_footer) .itemId(BR.text) .forItem()) .build()以上代码实现了一个混合Adapter的创建:|–LayoutRenderer header||–SealedItemRenderer| |–none -> LayoutRenderer placeholder count 5| | | |–some -> ListRenderer| |–DataBindingRenderer 1| |–DataBindingRenderer 2| |–… ||–ComposeRenderer| |–ListRenderer| | |–LayoutRenderer simple item1| | |–LayoutRenderer simple item2| | |–…| || |–ListRenderer| |–DataBindingRenderer item with content1| |–DataBindingRenderer item with content2| |–…||–DataBindingRenderer footer即: Build Dsl –> Adapter, 最后生成了一个混合的val adapter而在使用的时候希望能通过这个val adapter对结构中某些部分进行部分更新 比如上面构造的结构中, 我们希望只在ComposeRenderer中第二个ListRendererinsert 一个元素进去, 并合理调用Adapter的notifyItemRangeInserted(position, count)方法, 并且希望这个操作可以通过Dsl的方式实现, 比如:adapter.updateNow { // 定位ComposeRenderer getLast2().up { // 定位第二个ListRenderer getLast1().up { insert(2, listOf(ItemModel(189, “Subs Title1”, “subs Content1”))) } }}以上Dsl必然是希望有一定的限定的, 比如不能在只有两个元素的Adapter中getLast3(), 也不能在非列表中执行insert() 而这些限制需要被从val adapter推出, 即adapter –> Update Dsl, 这意味着adapter中需要保存其结构的所有信息, 由于我们需要在编译期对结构信息进行提取, 也意味着应该在类型信息中保存所有的结构信息 对于通常的Renderer没有太大的问题, 但对于部分组合其他Renderer的Renderer, (比如ComposeRenderer, 它的作用是按顺序将任意的Renderer组合在一起), 通常的实现方式是将他们统统还原为共通父类(BaseRenderer), 然后看做同样的东西进行操作, 但这个还原操作也同时将各自独特的类型信息给丢失了, 那应该怎么办才能即保证组合的多样性, 同时又不会丢失各自的类型信息?换一种方式描述问题推广到其他领域, 这个问题实际挺常见的, 比如:我们现在有一个用于绘制的基类RenderableBase, 而有两个实现, 一个是绘制圆形的Circle和绘制矩形的Rectangle:graph TBA[RenderableBase]A1[Circle]A2[Rectangle]A –> A1A –> A2我们有一个共通的用于绘制的类Canvas, 保存有所有需要绘制的RenderableBase, 一般情况下我们会通过一个List<RenderableBase>容器的方式保存它们, 将它们还原为通用的父类 但这种方式的问题是这种容器的类型信息中已经丢失了每个元素各自的特征信息, 我们没法在编译期知道或者限定子元素的类型(比如我们并不知道其中有多少个Circle, 也不能限定第一个元素必须为Rectangle) 那是否有办法即保证容器的多样性, 同时又不会丢失各自的类型信息?再换一种方式描述问题对于一个函数(方法), 比如:fun test(s: String): List<String>它其实可以看做声明了两个部分的函数:值函数: 描述了元素s到列表list的态射类型函数: 描述了从类型String到类型List<String>的态射即包括s -> list和String -> List<String> 一般而言这两者是同步的, 或者说类型信息中包括了足够的值相关的信息(值的类型), 但请注意以下函数:fun test2(s: String, i: Int): List<Any?> = listOf(s, i)它声明了(s, i) -> list和(String, Int) -> List<Any?>, 它没有将足够的类型信息保存下来:List中只包括String和Int两种元素List的Size为2List中第一个元素是String, 第二个元素是Int那是否有办法将以上这些信息也合理的保存到容器的类型中呢?一种解决方案异构列表以上的问题注意原因是在于List容器本身, 它本身就是一个保存相同元素的容器, 而我们需要是一个可以保存不同元素的容器 Haskell中有一种这种类型的容器: Heterogeneous List(异构列表), 就实现上来说很简单:Tip: arrow中的实现sealed class HListdata class HCons<out H, out T : HList>(val head: H, val tail: T) : HList()object HNil : HList()我们来看看使用它来构造上一节我们所说的函数应该如何构造:// 原函数fun test2(s: String, i: Int): List<Any?> = listOf(s, i)// 异构列表fun test2(s: String, i: Int): HCons<Int, HCons<String, HNil>> = HCons(i, HCons(s, HNil))同样是构建列表, 异构列表包含了更丰富的类型信息:容器的size为2容器中第一个元素为String, 第二个为Int相比传统列表异构列表的优势完整保存所有元素的类型信息自带容器的size信息完整保存每个元素的位置信息比如, 我们可以限定只能传入一个保存两个元素的列表, 其中第一个元素是String, 第二个是Int:fun test(l: HCons<Int, HCons<String, HNil>>)同时我们也可以确定第几个元素是什么类型:val l: HCons<Int, HCons<String, HNil>> = …l.get0() // 此元素一定是Int类型的由于Size信息被固定了, 传统必须在运行期才能检查的下标是否越界的问题也可以在编译期被检查出来:val l: HCons<Int, HCons<String, HNil>> = …l.get3() // 编译错误, 因为只有两个元素 相比传统列表的难点由于Size信息和元素类型信息是绑定的, 抛弃Size信息的同时就会抛弃元素类型的限制注意类型信息中的元素信息和实际保存的元素顺序是相反的, 因为异构列表是一个FILO(先进后出)的列表由于Size信息是限定的, 针对不同Size的列表的处理需要分开编写对于第一点, 以上面的RenderableBase为例, 比如我们有一个函数可以处理任意Size的异构列表:fun <L : HList> test(l: L)我们反而无法限定每个元素都应该是继承自RenderableBase的, 这意味着HCons<Int, HCons<String, HNil>>这种列表也可以传进来, 这在某些情况下是很麻烦的异构列表中附加高阶类型的处理Tip: 关于高阶类型的内容可以参考这篇文章高阶类型带来了什么继承是OOP的一大难点, 它的缺点在程序抽象度越来越高的过程的越来越凸显. 函数范式中是以组合代替继承, 使得程序有着更强的灵活性由于采用函数范式, 我们不再讨论异构列表如何限定父类, 而是改为讨论异构列表如何限定高阶类型对HList稍作修改即可附加高阶类型的支持:Tip: DslAdapter中的详细实现: HListKsealed class HListK<F, A: HListK<F, A>>class HNilK<F> : HListK<F, HNilK<F>>()data class HConsK<F, E, L: HListK<F, L>>(val head: Kind<F, E>, val tail: L) : HListK<F, HConsK<F, E, L>>()以Option(可选类型)为例:arrow中的详细实现: Optionsealed class Option<out A> : arrow.Kind<ForOption, A>object None : Option<Nothing>()data class Some<out T>(val t: T) : Option<T>()通过修改后的HListK我们可以限定每个元素都是Option, 但并不限定Option内容的类型:// [Option<Int>, Option<String>]val l: HConsK<ForOption, String, HConsK<ForOption, Int, HNilK<ForOption>>> = HConsK(Some(“string”), HConsK(199, HNilK()))修改后的列表即可做到即保留每个元素的类型信息又可以对元素类型进行部分限定它即等价于原生的HList, 同时又有更丰富的功能比如:// 1. 定义一个单位类型data class Id<T>(val a: T) : arrow.Kind<ForId, A>// 类型HListK<ForId, L>即等同于原始的HListfun <L : HListK<ForId, L>> test()// 2. 定义一个特殊类型data class FakeType<T, K : T>(val a: K) : arrow.Kind2<ForFakeType, T, K>// 即可限定列表中每个元素必须继承自RenderableBasefun <L : HListK<Kind<ForFakeType, RenderableBase>, L>> test(l: L) = …fun test2() { val t = FakeType<RenderableBase, Circle>(Circle()) val l = HListK.single(t) test(l)}回到DslAdapter的实现上文中提到的异构列表已经足够我们用来解决文章开头的DslAdapter实现问题了 异构问题解决起来就非常顺理成章了, 以ComposeRenderer为例, 我们使用将子Renderer装入ComposeItem容器的方式限定传入的容器每个元素必须是BaseRenderer的实现, 同时ComposeItem通过泛型的方式尽最大可能保留Renderer的类型信息:data class ComposeItem<T, VD : ViewData<T>, UP : Updatable<T, VD>, BR : BaseRenderer<T, VD, UP>>( val renderer: BR) : Kind<ForComposeItem, Pair<T, BR>>其中可以注意到类型声明中的Kind<ForComposeItem, Pair<T, BR>>, arrow默认的三元高阶类型为Kind<Kind<ForComposeItem, T>, BR>, 这并不符合我们在这里对高阶类型的期望: 我们这里只想限制ForComposeItem, 而T我们希望和BR绑定在一起限定, 所以使用了积类型 Pair将T和BR两个类型绑定到了一起. 换句话说, Pair在这里只起到一个组合类型T和BR的类型粘合剂的作用, 实际并不会被使用到 ComposeItem保存的是在build之后不会改变的数据(比如Renderer), 而使用中会改变的数据以ViewData的形式保存在ComposeItemData:data class ComposeItemData<T, VD : ViewData<T>, UP : Updatable<T, VD>, BR : BaseRenderer<T, VD, UP>>( val viewData: VD, val item: ComposeItem<T, VD, UP, BR>) : Kind<ForComposeItemData, Pair<T, BR>>这里同样使用了Pair作为类型粘结剂的技巧 对于一个ComposeRenderer而言应该保存以下信息:可以渲染的数据类型子Renderer的所有类型信息当前Renderer的ViewData信息以及子Renderer的ViewData信息其中2. 子Renderer的所有类型信息由IL : HListK<ForComposeItem, IL>泛型信息保存3. 当前Renderer的ViewData信息以及子Renderer的ViewData信息由VDL : HListK<ForComposeItemData, VDL>泛型信息保存而1. 可以渲染的数据类型由DL : HListK<ForIdT, DL>(ForIdT等同于上文提到的单位类型Id)于是我们可以得到ComposeRenderer的类型声明:class ComposeRenderer<DL : HListK<ForIdT, DL>, IL : HListK<ForComposeItem, IL>, VDL : HListK<ForComposeItemData, VDL>>子Renderer的所有类型信息(Size, 下标等等)被完整保留, 也就意味着从类型信息我们可以还原出每个子Renderer的完整类型信息一个栗子:构造两个子Renderer:// LayoutRendererval stringRenderer = LayoutRenderer<String>(layout = R.layout.simple_item, count = 3, binder = { view, title, index -> view.findViewById<TextView>(R.id.simple_text_view).text = title + index }, recycleFun = { view -> view.findViewById<TextView>(R.id.simple_text_view).text = "" }) // DataBindingRendererval itemRenderer = databindingOf<ItemModel>(R.layout.item_layout) .onRecycle(CLEAR_ALL) .itemId(BR.model) .itemId(BR.content, { m -> m.content + “xxxx” }) .stableIdForItem { it.id } .forItem()使用ComposeRenderer组合两个Renderer:val composeRenderer = ComposeRenderer.startBuild .add(itemRenderer) .add(stringRenderer) .build()你可以猜出这里composeRenderer的类型是什么吗?答案是:ComposeRenderer< HConsK<ForIdT, String, HConsK<ForIdT, ItemModel, HNilK<ForIdT>>>, HConsK<ForComposeItem, Pair<String, LayoutRenderer<String>>, HConsK<ForComposeItem, Pair<ItemModel, DataBindingRenderer<ItemModel, ItemModel>>, HNilK<ForComposeItem>>>, HConsK<ForComposeItemData, Pair<String, LayoutRenderer<String>>, HConsK<ForComposeItemData, Pair<ItemModel, DataBindingRenderer<ItemModel, ItemModel>>, HNilK<ForComposeItemData>>>>其中完整保留了所有我们需要的类型信息, 因此我们可以通过composeRenderer还原出原来的数据结构:composeRenderer.updater .updateBy { getLast1().up { update(“New String”) } }这里的update(“New String”)方法知道当前定位的是一个stringRenderer, 所以可以使用String更新数据, 如果传入ItemModel就会出错虽然泛型信息非常多而长, 但实际大部分可以通过编译系统自动推测出来, 而对于某些无法被推测的部分也可以通过一些小技巧来简化, 你可以猜到用了什么技巧吗?结语以前我们常常更聚焦于面向过程编程, 但对函数范式或者说Haskell的学习, 类型编程其实也是一个很有趣并且很有用的思考方向 没错, 类型是有相应的计算规则的, 甚至有的编程语言会将类型作为一等对象, 可以进行相互计算(积类型, 和类型, 类型的幂等) 虽然Java或者Kotlin的类型系统并没有如此的强大, 但只要改变一下思想, 通过一些技巧还是可以实现很多像魔法一样的事情(比如另一篇文章中对高阶类型的实现)将Haskell的对类型系统编程应用到Kotlin上有很多有趣的技巧, DslAdapter只是在实用领域上一点小小的探索, 而fpinkotlin则是在实验领域的另外一些探索成果(尤其是第四部分 15.流式处理与增量I/O), 希望之后能有机会分享更多的一些技巧和经验, 也欢迎感兴趣的朋友一同探讨 ...

December 24, 2018 · 4 min · jiezi