引子

相信总是有很多同学,总是在抱怨泛型无论怎么学习,都只是停留在一个简单使用的水平,所以一直为此而备受苦恼。

Kotlin 作为一门能和 Java 相互调用的语言,自然也支持泛型,不过 Kotlin 的新关键字 inout 却总能绕晕一部分人,归根结底,还是因为 Java 的泛型基本功没有足够扎实。

很多同学总是会产生这些疑问:

  • Kotlin 泛型和 Java 泛型到底有何区别?
  • Java 泛型存在的意义到底是什么?
  • Java 的类型擦除到底是指什么?
  • Java 泛型的上界、下界、通配符到底有何区别?它们可以实现多重限制么?
  • Java 的 <? extends T><? super T><?> 到底对应了什么?有哪些使用场景?
  • Kotlin 的 inout*where 到底有何魔力?
  • 泛型方法又是什么?

今天,就用一篇文章为大家解除上述疑惑。

泛型:类型安全的利刃

总所周知,Java 在 1.5 之前,是没有泛型这个概念的。那时候的 List 还只是一个可以装下一切的集合。所以我们难免会写上这样的代码:

List list = new ArrayList();
list.add(1);
list.add("nanchen2251");
String str = (String) list.get(0);

上面的代码编译并没有任何问题,但运行的时候一定会出现常见的 ClassCastException 异常:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

这个体验非常糟糕,我们真正需要的是在代码编译的时候就能发现错误,而不是让错误的代码发布到生产环境中。

而如果上述代码我们增加上泛型,就会在编译期就能看到明显的错误啦。

List<String> list = new ArrayList<>();
list.add(1);
// 报错 Required type:String but Provided:int
list.add("nanchen2251");
String str = list.get(0);

很明显,泛型的出现,让类型更加安全,使我们在使用 ListMap 等不再需要去专门编写 StringListStringMap 了,只需要在声明 List 的同时指定参数类型即可。

总的来说,泛型具备以下优势:

  • 类型检查,能在编译时就帮开发检查出错误;
  • 更加语义化,比如我们声明一个 LIst<String>,我们可以很直接知道里面存储的是 String 对象;
  • 能自动进行类型转换,获取数据的时候不需要再做强转操作;
  • 能写出更加通用化的代码。

类型擦除

可能有些同学思考过这样一个问题,既然泛型是和类型相关的,那么是不是也能使用类型的多态呢?

我们知道,一个子类型是可以赋值给父类型的,比如:

Object obj = "nanchen2251";
// 这是多态

Object 作为 String 的父类,自然可以接受 String 对象的赋值,这样的代码我们早已司空见惯,并没有什么问题。

但当我们写下这串代码:

List<String> list = new ArrayList<String>();
List<Object> objects = list;
// 多态用在这里会报错 Required type:List<Object> Provided: List<String>

上面发生了赋值错误,这是因为 Java 的泛型本身具有「不可变性 Invariance」,Java 里面认为 List<String>List<Object> 类型并不一致,也就是说,子类的泛型 List<String> 不属于泛型 List<Object> 的子类。

由于 Java 的泛型本身是一种 「伪泛型」,Java 为了兼容 1.5 以前的版本,不得以在泛型底层实现上使用 Object 引用,所以我们声明的泛型在编译时会发生「类型擦除」,泛型类型会被 Object 类型取代。比如:

class Demo<T> {
void func(T t){
// ...
}
}

会被编译成:

class Demo {
void func(Object t){
// ...
}
}

可能你会好奇,在编译时发生类型擦除后,我们的泛型都被更换成了 Object,那为什么我们在使用的时候,却不需要强转操作呢?比如:

List<String> list = new ArrayList<>();
list.add("nanchen2251");
String str = list.get(0);
// 这里并没有要求我们把 list.get(0) 强转为 String

这是因为编译器会根据我们声明的泛型类型进行提前的类型检查,然后再进行类型擦除,擦除为 Object,但在字节码中其实还存储了我们的泛型的类型信息,在使用到泛型类型的时候会把擦除后的 Object 自动做类型强转操作。所以上面的 list.get(0) 本身就是一个经过强转的 String 对象了。

这个技术看起来还蛮好的,但却有一个弊端。就是既然擦成 Object 了,那么在运行的时候,你根本不能确定这个对象到底是什么类型,虽然你可以通过编译器帮你插入的 checkcast 来获得此对象的类型。但是你并不能把 T 真正的当作一个类型使用:比如这条语句在 Java 中是非法的。

T a = new T();
// 报错:Type parameter 'T' cannot be instantiated directly

同理,因为都被擦成了 Object,你就不能根据类型来做某种区分。

比如 instanceof

if("nanchen2251" instanceof T.class){
// 报错:Identifier expected Unexpected token
}

比如重载:

void func(T t){
// 报错:'func(T)' clashes with 'func(E)'; both methods have same erasure
}
void func(E e){
}

同样,因为基本数据类型不属于 oop,所以也不能被擦除为 Object,所以 Java 的泛型也不能用于基本类型:

List<int> list;
// 报错:Type argument cannot be of primitive type

oop:面向对象的程序设计(Object Oriented Programming)

到这里,是不是可以回答上面的第 3 个问题了:Java 的类型擦除到底是指什么?

首先你要明白一点,一个对象的类型永远不会被擦出的,比如你用一个 Object 去引用一个 Apple 对象,你还是可以获得到它的类型的。比如用 RTTI。

RTTI:运行时类型信息,运行时类型识别 (Run Time Type Identification)

Object object = new Apple();
System.out.println(object.getClass().getName());
// will print Apple

哪怕它是放到泛型里的。

class FruitShop<T>{
private T t; public void set(T t){
this.t = t;
} public void showFruitName(){
System.out.println(t.getClass().getName());
}
}
FruitShop<Apple> appleShop = new FruitShop<Apple>();
appleShop.set(new Apple());
appleShop.showFruitName();
// will print Apple too

为啥?因为引用就是一个用来访问对象的标签而已,对象一直在堆上放着呢。

所以不要断章取义认为类型擦除就是把容器内对象的类型擦掉了,所谓的类型擦除,是指容器类FruitShop<Apple>,对于 Apple 的类型声明在编译期的类型检查之后被擦掉,变为和 FruitShop<Object> 等同效果,也可以说是 FruitShop<Apple>FruitShop<Banana> 被擦为和 FruitShop<Object> 等价,而不是指里面的对象本身的类型被擦掉!

那,Kotlin 中有类型擦除么?

C# 和 Java 在一开始都是不支持泛型的。Java 在 1.5 开始才加入了泛型。为了让一个不支持泛型的语言支持泛型,只有两条路可以走:

  • 以前的非泛型容器保持不变,然后平行的增加一套泛型化的类型。
  • 直接把已有的非泛型容器扩展为泛型,不添加任何新的泛型版本。

Java 由于 1.5 之前市面上一句有大量的代码,所以不得以选择了第 2 种方式,而 C# 比较机智就选择了第一种。

而 Kotlin 本身就是基于 Java 1.6 编写的,一开始就有泛型,不存在兼容老版本代码的问题,那 Kotlin 实现的泛型还具备类型擦除么?

当然具备。上面其实已经说的很清楚了,Kotlin 本身就是基于 Java 1.6 编写的,而且 Kotlin 和 Java 有极强的互调能力,当然也存在类型擦除。

不过...

你还是会发现有意思的点:

val list = ArrayList()
// 报错:Not enough information to infer type variable E

在 Java 中,不指定泛型类型是没问题的,但 Kotlin 这样不好使了。想来也简单,毕竟在 Java 1.5 之前是肯定不存在上述类似代码的,而泛型的设计初衷就不是用来装默认的 Kotlin Any 的。

泛型的上界通配符

前面说到:因为 Java 的泛型本身具有「不可变性 Invariance」,所以即使 Fruit 类是 Apple 类的父类,但 Java 里面认为 List<Fruit>List<Apple> 类型并不一致,也就是说,子类的泛型 List<Apple> 不属于泛型 List<Fruit> 的子类。

所以这样的代码并不被运行。

List<Apple> apples = new ArrayList<Apple>();
List<Fruit> fruits = apples;
// 多态用在这里会报错 Required type:List<Fruit> Provided: List<Apple>

那假如我们想突破这层限制,怎么办?使用上界通配符 ? extends

List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;
// 使用上界通配符后,编译不再报错

「上界通配符」,可以使 Java 泛型具有「协变性 Covariance」,协变就是允许上面的赋值是合法的。

在继承关系树中,子类继承自父类,可以认为父类在上,子类在下。extends 限制了泛型类型的父类型,所以叫上界。

它有两层意思:

  • 其中 ? 是个通配符,表示这个 List 的泛型类型是一个未知类型。
  • extends 限制了这个未知类型的上界,也就是泛型类型必须满足这个 extends 的限制条件,这里和定义 classextends 关键字有点不一样:
    • 它的范围不仅是所有直接和间接子类,还包括上界定义的父类本身,也就是 Fruit
    • 它还有 implements 的意思,即这里的上界也可以是 interface

这个突破限制有意义么?

有的有的。

假如我们有一个接口 Fruit

interface Fruit {
float getWeight();
}

有两个水果类实现了 Fruit 接口:

class Banana implements Fruit {
@Override
public float getWeight() {
return 0.5f;
}
} class Apple implements Fruit {
@Override
public float getWeight() {
return 1f;
}
}

假设我们有个需求是需要给水果称重:

List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
float totalWeight = getTotalWeight(apples);
// 报错:Required type: List<Fruit> Provided: List<Apple> private float getTotalWeight(List<Fruit> fruitList) {
float totalWeight = 0;
for (Fruit fruit : fruitList) {
totalWeight += fruit.getWeight();
}
return totalWeight;
}

想来这也是一个非常正常的需求,秤可以称各种水果的重量,但也可以只称苹果。你不能因为我只买苹果就不给我称重吧。所以把上面的代码加上上界通配符就可以啦。

List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
float totalWeight = getTotalWeight(apples);
// 不再报错
// 增加了上界通配符 ? extends
private float getTotalWeight(List<? extends Fruit> fruitList) {
float totalWeight = 0;
for (Fruit fruit : fruitList) {
totalWeight += fruit.getWeight();
}
return totalWeight;
}

不过,上面使用 ? extends 上界通配符突破了一层限制,却被施加了另一层限制:只可输出不可输入

什么意思呢?

比如:

List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;
Fruit fruit = fruits.get(0);
fruits.add(new Apple());
// 报错:Required type: capture of ? extends Fruit Provided: Apple

声明了上界通配符泛型的集合,不再允许 add 新的对象,Apple 不行,Fruit 也不行。拓展开来说:不止是集合,自己编写一个泛型做输入也不行

interface Shop<T> {
void showFruitName(T t);
T getFruit();
} Shop<? extends Fruit> apples = new Shop<Apple>(){
@Override
public void showFruitName(Apple apple) { } @Override
public Apple getFruit() {
return null;
}
};
apples.getFruit();
apples.showFruitName(new Apple());
// 报错:Required type: capture of ? extends Fruit Provided: Apple

泛型的下界通配符

泛型有上界通配符,那有没有下界通配符呢?

有的有的。

与上界通配符 ? extends 对应的就是下界通配符 ? super

下界通配符 ? super 所有情况和 ? extends 上界通配符刚刚相反:

  • 通配符 ? 表示 List 的泛型类型是一个 未知类型
  • super 限制了这个未知类型的下界,也就是泛型类型必须满足这个 super 的限制条件
    • 它的范围不仅是所有直接和间接子父类,还包括下界定义的子类本身。
    • super 同样支持 interface

它被施加的新限制是:只可输入不可输出

Shop<? super Apple> apples = new Shop<Fruit>(){
@Override
public void showFruitName(Fruit apple) { } @Override
public Fruit getFruit() {
return null;
}
};
apples.showFruitName(new Apple());
Apple apple = apples.getFruit();
// 报错:Required type: Apple Provided: capture of ? super Apple

解释下,首先 ? 表示未知类型,编译器是不确定它的类型的。

虽然不知道它的具体类型,不过在 Java 里任何对象都是 Object 的子类,所以这里只能把apples.getFruit() 获取出来的对象赋值给 Object。由于类型未知,所以直接赋值给一个 Apple 对象肯定是不负责任的,需要我们做一层强制转换,不过强制转换本身可能发生错误。

Apple 对象一定是这个未知类型的子类型,根据多态的特性,这里通过 showFruitName 输入 Button 对象是合法的。

小结下,Java 的泛型本身是不支持协变和逆变的:

  • 可以使用泛型通配符 ? extends 来使泛型支持协变,但是「只能读取不能修改」,这里的修改仅指对泛型集合添加元素,如果是 remove(int index) 以及 clear 当然是可以的。
  • 可以使用泛型通配符 ? super 来使泛型支持逆变,但是「只能修改不能读取」,这里说的不能读取是指不能按照泛型类型读取,你如果按照 Object 读出来再强转当然也是可以的。

理解了 Java 的泛型之后,再理解 Kotlin 中的泛型,就比较容易了。

Kotlin 的 out 和 in

和 Java 泛型一样,Kolin 中的泛型本身也是不可变的。

不过换了一种表现形式:

  • 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends
  • 使用关键字 in 来支持逆变,等同于 Java 中的下界通配符 ? super
val appleShop: Shop<out Fruit>
val fruitShop: Shop<in Apple>

它们完全等价于:

Shop<? extends Fruit> appleShop;
Shop<? super Apple> fruitShop;

换了个写法,但作用是完全一样的。out 表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;in 就反过来,表示它只用来输入,不用来输出,你只能写我不能读我。

泛型的上下界约束

上面讲的都是在使用的时候再对泛型进行限制,我们称之为「上界通配符」和「下界通配符」。那我们可以在函数设计的时候,就设置这个限制么?

可以的可以的。

比如:

open class Animal
class PetShop<T : Animal?>(val t: T)

等同于 Java 的:

class PetShop<T extends Animal> {
private T t; PetShop(T t) {
this.t = t;
}
}

这样,我们在设计宠物店类 PetShop 就给支持的泛型设置了上界约束,支持的泛型类型必须是 Animal 的之类。所以我们使用的话:

class Cat : Animal()

val catShop = PetShop(Cat())
val appleShop = PetShop(Apple())
// 报错:Type mismatch. Required: Animal? Found: Apple

很明显,Apple 并不是 Animal 的子类,当然不满足 PetShop 泛型类型的上界约束。

那....可以设置多个上界约束么?

当然可以,在 Java 中,给一个泛型参数声明多个约束的方式是,使用 &

class PetShop<T extends Animal & Serializable> {
// 通过 & 实现了两个上界,必须是 Animal 和 Serializable 的子类或实现类
private T t; PetShop(T t) {
this.t = t;
}
}

而在 Kotlin 中舍弃了 & 这种方式,而是增加了 where 关键字:

open class Animal
class PetShop<T>(val t: T) where T : Animal?, T : Serializable

通过上面的方式,就实现了多个上界的约束。

Kotlin 的通配符 *

前面我们说的泛型类型都是在我们需要知道参数类型是什么类型的,那如果我们对泛型参数的类型不感兴趣,有没有一种方式处理这个情况呢?

有的有的。

在 Kotlin 中,可以用通配符 * 来替代泛型参数。比如:

val list: MutableList<*> = mutableListOf(1, "nanchen2251")
list.add("nanchen2251")
// 报错:Type mismatch. Required: Nothing Found: String

这个报错确实让人匪夷所思,上面用通配符代表了 MutableList 的泛型参数类型。初始化里面也加入了 String 类型,但在新 add 字符串的时候,却发生了编译错误。

而如果是这样的代码:

val list: MutableList<Any> = mutableListOf(1, "nanchen2251")
list.add("nanchen2251")
// 不再报错

看来,所谓的通配符作为泛型参数并不等价于 Any 作为泛型参数。MutableList<*>MutableList<Any> 并不是同一种列表,后者的类型是确定的,而前者的类型并不确定,编译器并不能知道这是一种什么类型。所以它不被允许添加元素,因为会导致类型不安全。

不过细心的同学肯定发现了,这个和前面泛型的协变非常类似。其实通配符 * 不过是一种语法糖,背后也是用协变来实现的。所以:MutableList<*> 等价于 MutableList<out Any?>,使用通配符与协变有着一样的特性。

在 Java 中,也有一样意义的通配符,不过使用的是 ? 作为通配。

List<?> list = new ArrayList<Apple>();

Java 中的通配符 ? 也等价于 ? extends Object

多个泛型参数声明

那可以声明多个泛型么?

可以的可以的。

HashMap 不就是一个典型的例子么?

class HashMap<K,V>

多个泛型,可以通过 , 进行分割,多个声明,上面是两个,实际上多个都是可以的。

class HashMap<K: Animal, V, T, M, Z : Serializable>

泛型方法

上面讲的都是都是在类上声明泛型类型,那可以声明在方法上么?

可以的可以的。

如果你是一名 Android 开发,ViewfindViewById 不就是最好的例子么?

public final <T extends View> T findViewById(@IdRes int id) {
if (id == NO_ID) {
return null;
}
return findViewTraversal(id);
}

很明显,View 是没有泛型参数类型的,但其 findViewById 就是典型的泛型方法,泛型声明就在方法上。

上述写法改写成 Kotlin 也非常简单:

fun <T : View?> findViewById(@IdRes id: Int): T? {
return if (id == View.NO_ID) {
null
} else findViewTraversal(id)
}

Kotlin 的 reified

前面有说到,由于 Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。比如你不能检查一个对象是否为泛型类型 T 的实例:

<T> void printIfTypeMatch(Object item) {
if (item instanceof T) { // IDE 会提示错误,illegal generic type for instanceof }
}

Kotlin 里同样也不行:

fun <T> printIfTypeMatch(item: Any) {
if (item is T) { // IDE 会提示错误,Cannot check for instance of erased type: T
println(item)
}
}

这个问题,在 Java 中的解决方案通常是额外传递一个 Class<T> 类型的参数,然后通过 Class#isInstance 方法来检查:


<T> void check(Object item, Class<T> type) {
if (type.isInstance(item)) { }
}

Kotlin 中同样可以这么解决,不过还有一个更方便的做法:使用关键字 reified 配合 inline 来解决:


inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) { // 这里就不会在提示错误了 }
}

上面的 Gson 解析的时候用的非常广泛,比如咱们项目里就有这样的扩展方法:

inline fun <reified T> String?.toObject(type: Type? = null): T? {
return if (type != null) {
GsonFactory.GSON.fromJson(this, type)
} else {
GsonFactory.GSON.fromJson(this, T::class.java)
}
}

总结

本文花了非常大的篇幅来讲 Kotlin 的泛型和 Java 的泛型,现在再回过头去回答文首的几个问题,同学你有谱了吗?如果还是感觉一知半解,不妨多看几遍。

文章中有比较多的参考「码上开学」的文章:Kotlin 的泛型

甚至有一部分直接截取过来,主要本意是不想重复造轮子。文章中如有疏漏,欢迎在评论区进行留言。

扫盲:Kotlin 的泛型的更多相关文章

  1. Kotlin介绍

    Kotlin介绍 转 https://www.jianshu.com/p/d30406daaf25 Google在今年的IO大会上宣布,将Android开发的官方语言更换为Kotlin,作为跟着Goo ...

  2. Kotlin开发语言文档(官方文档)-- 目录

    开始阅读Kotlin官方文档.先上文档目录.有些内容还未阅读,有些目录标目翻译还需琢磨琢磨.后续再将具体内容的链接逐步加上. 文档链接:https://kotlinlang.org/docs/kotl ...

  3. 【Bugly 技术干货】Android开发必备知识:为什么说Kotlin值得一试

    1.Hello, Kotlin Bugly 技术干货系列内容主要涉及移动开发方向,是由 Bugly邀请腾讯内部各位技术大咖,通过日常工作经验的总结以及感悟撰写而成,内容均属原创,转载请标明出处. 1. ...

  4. Android平台的Swift—Kotlin

    WeTest 导读 Kotlin 已经出来较长一段时间了,有些同学已经对Kotlin进行了深入的学习,甚至已经运用到了自己的项目当中,但是还有较多同学可能只是听过Kotlin或简单了解过,这篇文章的目 ...

  5. Kotlin——高级篇(二):高阶函数详解与标准的高阶函数使用

    在上面一个章节中,详细的讲解了Kotlin中关于Lambda表达式的语法以及运用,如果还您对其还不甚理解,请参见Kotlin--高级篇(一):Lambda表达式详解.在这篇文章中,多次提到了Kotli ...

  6. Kotlin学习笔记

    Kotlin的注释 Kotlin 的代码注释和Java一模一样 Kotlin的运行方式也是先kotlinc生成字节码,再kotlin字节码 如果一行里面只有一条语句,那么可以不写分号.但如果打算在同一 ...

  7. Android Weekly Notes Issue #290

    Android Weekly Issue #290 December 31st, 2017 Android Weekly Issue #290 本期内容包括介绍Kotlin逆变协变的一篇(虽然没说清楚 ...

  8. Kotlin 泛型

    泛型,即 "参数化类型",将类型参数化,可以用在类,接口,方法上. 与 Java 一样,Kotlin 也提供泛型,为类型安全提供保证,消除类型强转的烦恼. 声明一个泛型类: cla ...

  9. Kotlin中的“忍者”函数 —— 理解泛型的能力(KAD 12)

    作者:Antonio Leiva 时间:Feb 8, 2017 原文链接:https://antonioleiva.com/generic-functions-kotlin/ Kotlin的一些特性组 ...

随机推荐

  1. 如何在word中插入代码

    本文使用的是word2007,在网上查阅资料,可以使用如下方法: 1. 插入一个1行1列的表格,然后将代码写在里面,完成之后选中表格: 2. 将样式改为"HTML代码". 其实只是 ...

  2. .Net编码规范整理

    前言 此处只是整理并记录下.Net开发规范以便加深编码规范.一个好的编程规范可以让自己程序可读性,让自己编码更规范,分为两部分:通用规范..Net开发规范. 微软通用编程规范 明确性和一致性 库的使用 ...

  3. 第11.7节 Python正则表达式的字符串结尾匹配模式及元字符“$”功能介绍

    符号"$"表示匹配字符串的结尾,即字符串的结尾满足匹配模式的要求. 在 MULTILINE 模式(搜索标记中包含re.MULTILINE,关于搜索标记的含义请见<第11.2节 ...

  4. 关于将Linux中默认的OpenJDK替换为JDK的方法

    首先下载需要的jdk安装包,后缀建议.tar.gz,本文中以jdk-8u212-linux-x64.tar.gz为例,地址就在oracle官网. 将安装包下载到linux环境后,使用命令tar -xz ...

  5. 记阿里云 RDS MySQL 的一个大坑

    花了一个下午的时间,终于把一个阿里云 RDS MySQL 的一个大坑填上了,解决方法令人匪夷所思!绝对会让各位看官感到大吃一惊,阿里云 RDS MySQL 居然有这样 xx 的大坑! 问题 最近应业务 ...

  6. dm8数据库的安装 for linux

    目录 dm8数据库的安装 for linux 1.创建用户 2.修改limit的文件 3.解压文件安装包 4.挂载iso镜像 5.对于安装介质和目录进行权限授予 6.切换用户安装数据库软件 7.dm数 ...

  7. Linux内核源码分析之setup_arch (二)

    1. 概述 接着上一篇<Linux内核源码分析之setup_arch (一)>继续分析,本文首先分析arm_memblock_init函数,然后分析内核启动阶段的是如何进行内存管理的. 2 ...

  8. js onreadystatechange 和 onload的区别

    IE的script 元素只支持onreadystatechange事件,不支持onload事件. FF的script 元素不支持onreadystatechange事件,只支持onload事件. 如果 ...

  9. 阿里云服务器搭建java环境(jdk+tomcat+oracle11g)

    一.JDK配置 1.在centos 7的更新源中有JDK,使用yum即可下载安装 查看库中版本 [root@localhost ~]# yum search java|grep jdk 选择需要版本进 ...

  10. web前端常用js插件

    第一款:截图插件html2Canvas.js html2是一款强大的截图插件,只需引入js文件,依照官方给定的截图方法,就能截取对应DOM区域的内容.对于有些截图出现模糊偏移的问题,网上也有一堆解决方 ...