澳门新葡萄京娱乐场 1

澳门新葡萄京娱乐场Kotlin教程(四)可空性

本文由码农网 –
civic5216原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划!

三十八、检查参数的有效性:

系列文章的知识点会以《Kotlin实战》这本书中顺序编写,在将书中知识点展示出来同时,我也会添加对应的Java代码用于对比学习和更好的理解。

当变换Java代码为Ceylon代码时,有时候我会遇到一些Java类构造器混淆了验证与初始化的情形。让我们使用一个简单但是人为的代码例子来说明我想阐述的意思。

     
绝大多数方法和构造器对于传递给它们的参数值都会有些限制。比如,索引值必须大于等于0,且不能超过其最大值,对象不能为null等。这样就可以在导致错误的源头将错误捕获,从而避免了该错误被延续到今后的某一时刻再被引发,这样就是加大了错误追查的难度。就如同编译期能够报出的错误总比在运行时才发现要更好一些。事实上,我们不仅仅需要在函数的内部开始出进行这些通用的参数有效性检查,还需要在函数的文档中给予明确的说明,如在参数非法的情况下,会抛出那些异常,或导致函数返回哪些错误值等,见如下代码示例:

Kotlin教程(一)基础

澳门新葡萄京娱乐场 1

 1     /**
 2      * Returns a BigInteger whose value is(this mod m). This method
 3      * differs from the remainder method in that it always returns a
 4      * non-negative BigInteger.
 5      * @param m the modulus, which must be positive.
 6      * @return this mod m.
 7      * @throws ArithmeticException if m is less than or equal to 0.
 8 */
 9      public BigInteger mod(BigInteger m) {
10          if (m.signum() <= 0)
11              throw new ArithmeticException("Modulus <= 0: " + m);
12          ... //Do the computation.
13      }

Kotlin教程(二)函数

一些坏代码

考虑下面的Java类。(伙计,不要在家里写这样的代码)

public class Period {
    private final Date startDate;
    private final Date endDate;
    //returns null if the given String
    //does not represent a valid Date
    private Date parseDate(String date) {
       ...
    }
    public Period(String start, String end) {
        startDate = parseDate(start);
        endDate = parseDate(end);
    }
    public boolean isValid() {
        return startDate!=null && endDate!=null;
    }
    public Date getStartDate() {
        if (startDate==null) 
            throw new IllegalStateException();
        return startDate;
    }
    public Date getEndDate() {
        if (endDate==null)
            throw new IllegalStateException();
        return endDate;
    }
}

嘿,我之前已经警告过,它是人为的。但是,在实际Java代码中找个像这样的东西实际上并非不常见。

这里的问题在于,即使输入参数(在隐藏的parseDate()方法中)的验证失败了,我们还是会获得一个Period的实例。但是我们获取的那个Period不是一个“有效的”状态。严格地说,我的意思是什么呢?

好吧,假如一个对象不能有意义地响应公用操作时,我会说它处于一个非有效状态。在这个例子里,getStartDate()
和getEndDate()会抛出一个IllegalStateException异常,这就是我认为不是“有意义的”一种情况。

从另外一方面来看这个例子,在设计Period时,我们这儿出现了类型安全的失败。未检查的异常代表了类型系统中的一个“漏洞”。因此,一个更好的Period的类型安全的设计,会是一个不使用未检查的异常—在这个例子中意味着不抛出IllegalStateException异常。

(实际上,在真实代码中,我更有可能遇到一个getStartDate() 方法它不检查null ,在这个代码行之后就会导致一个NullPointerException异常,这就更加糟糕了。)

我们能够很容易地转换上面的Period类成为Ceylon形式的类:

shared class Period(String start, String end) {
    //returns null if the given String
    //does not represent a valid Date
    Date? parseDate(String date) => ... ;
    value maybeStartDate = parseDate(start);
    value maybeEndDate = parseDate(end);
    shared Boolean valid
        => maybeStartDate exists 
        && maybeEndDate exists;
    shared Date startDate {
        assert (exists maybeStartDate);
        return maybeStartDate;
    }
    shared Date endDate {
        assert (exists maybeEndDate);
        return maybeEndDate;
    }
}

当然了,这段代码也会遇到与原始Java代码同样的问题。两个assert符号冲着我们大喊,在代码的类型安全中有一个问题。

      
是不是我们为所有的方法均需要做出这样的有效性检查呢?对于未被导出的方法,如包方法等,你可以控制这个方法将在哪些情况下被调用,因此这时可以使用断言来帮助进行参数的有效性检查,如:

Kotlin教程(三)类、对象和接口

使Java代码变得更好

Java里我们怎么改进这段代码呢?好吧,这儿就是一个例子关于Java饱受诟病的已检查异常会是一个非常合理的解决方法!我们可以稍微修改下Period来从它的构造器中抛出一个已检查的异常:

public class Period {
    private final Date startDate;
    private final Date endDate;
    //throws if the given String
    //does not represent a valid Date
    private Date parseDate(String date)
            throws DateFormatException {
       ...
    }
    public Period(String start, String end) 
            throws DateFormatException {
        startDate = parseDate(start);
        endDate = parseDate(end);
    }
    public Date getStartDate() {
        return startDate;
    }
    public Date getEndDate() {
        return endDate;
    }
}

现在,使用这个解决方案,我们就不会获取一个处于非有效状态的Period,实例化Period的代码会由编译器负责去处理无效输入的情形,它会捕获一个DateFormatException异常。

try {
    Period p = new Period(start, end);
    ...
}
catch (DateFormatException dfe) {
    ...
}

这是一个对已检查异常不错的、完美的、正确的使用,不幸的是我几乎很少看到Java代码像上面这样使用已检查异常。

1      private static void sort(long a[],int offset,int length) {
2          assert(a != null);
3          assert(offset >= 0 && offset <= a.length);
4          assert(length >= 0 && length <= a.length - offset);
5          ... //Do the computation
6      }

Kotlin教程(四)可空性

使Ceylon代码变得更好

那么Ceylon怎么样呢?Ceylon没有已检查异常,因而我们需要寻找一个不同的解决方式。典型地,在Java调用一个函数会抛出一个已检查异常的情形中,Ceylon会调用函数返回一个联合类型。因为,一个类的初始化不返回除了类自己外的任何类型,我们需要提取一些混合的初始化/验证的逻辑来使其成为一个工厂函数。

//returns DateFormatError if the given 
//String does not represent a valid Date
Date|DateFormatError parseDate(String date) => ... ;
shared Period|DateFormatError parsePeriod
        (String start, String end) {
    value startDate = parseDate(start);
    if (is DateFormatError startDate) {
        return startDate;
    }
    value endDate = parseDate(end);
    if (is DateFormatError endDate)  {
        return endDate;
    }
    return Period(startDate, endDate);
}
shared class Period(startDate, endDate) {
    shared Date startDate;
    shared Date endDate;
}

根据类型系统,调用者有义务去处理DateFormatError:

value p = parsePeriod(start, end);
if (is DateFormatError p) {
    ...
}
else {
    ...
}

或者,如果我们不关心给定日期格式的实际问题(这是有可能的,假定我们工作的初始化代码丢失了那个信息),我们可以使用Null而不是DateFormatError:

//returns null if the given String 
//does not represent a valid Date
Date? parseDate(String date) => ... ;
shared Period? parsePeriod(String start, String end)
    => if (exists startDate = parseDate(start), 
           exists endDate = parseDate(end))
       then Period(startDate, endDate)
       else null;
shared class Period(startDate, endDate) {
    shared Date startDate;
    shared Date endDate;
}

至少可以说,使用工厂函数的方法是优秀的,因为通常来说在验证逻辑和对象初始化之间它具有更好的隔离。这点在Ceylon中特别有用,在Ceylon中,编译器在对象初始化逻辑中添加了一些非常严厉的限制,以保证对象的所有领域仅被赋值一次。

      
和通用的检查方式不同,断言在其条件为真时,无论外部包得客户端如何使用它。断言都将抛出AssertionError。它们之间的另一个差异在于如果断言没有起到作用,即-ea命令行参数没有传递给java解释器,断言将不会有任何开销,这样我们就可以在调试期间加入该命令行参数,在发布时去掉该命令行选项,而我们的代码则不需要任何改动。
      
需要强调的是,对于有些函数的参数,其在当前函数内并不使用,而是留给该类其他函数内部使用的,比较明显的就是类的构造函数,构造函数中的很多参数都不一样用于构造器内,只是在构造的时候进行有些赋值操作,而这些参数的真正使用者是该类的其他函数,对于这种情况,我们就更需要在构造的时候进行参数的有效性检查,否则一旦将该问题释放到域函数的时候,再追查该问题的根源,将不得不付出更大的代价和更多的调试时间。
      
对该条目的说法确实存在着一种例外情况,在有些情况下有效性检查工作的开销是非常大的,或者根本不切实际,因为这些检查已经隐含在计算过程中完成了,如Collections.sort(List),容器中对象的所有比较操作均在该函数执行时完成,一旦比较操作失败将会抛出ClassCastException异常。因此对于sort来讲,如果我们提前做出有效性检查将是毫无意义的。
    
三十九、必要时进行保护性拷贝:

[Kotlin教程(五)类型]

     
如果你的对象没有做很好的隔离,那么对于调用者而言,则有机会破坏该对象的内部约束条件,因此我们需要保护性的设计程序。该破坏行为一般由两种情况引起,首先就是恶心的破坏,再有就是调用者无意识的误用,这两种条件下均有可能给你的类带来一定的破坏性,见如下代码:

[Kotlin教程(六)Lambda]

 1     public final class Period {
 2         private final Date start;
 3         private final Date end;
 4         public Period(Date start,Date end) {
 5             if (start.compareTo(end) > 0) {
 6                 throw new IllegalArgumentException(start + "After " + end);
 7             this.start = start;
 8             this.end = end;
 9         }
10         public Date start() {
11             return start;
12         }
13         public Date end() {
14             return end;
15         }
16     }

这一章实际上在《Kotlin实战》中是第六章,在Lambda之后,但是这一章的内容实际上是Kotlin的一大特色之一。因此,我将此章的内容提到了前面汇总。

     
从表面上看,该类的实现确实对约束性的条件进行了验证,然而由于Date类本身是可变了,因此很容易违反这个约束,见如下代码:

可空性

可空性是Kotlin类型系统中帮助你避免NullPointerException错误的特性。

1     public void testPeriod() {
2         Date start = new Date();
3         Date end = new Date();
4         Period p = new Period(start,end);
5         end.setYear(78);  //该修改将直接影响Period内部的end对象。
6     }

可空类型

如果一个变量可能为null,对变量的方法的调用就是不安全的,因为这样会导致NullPointerException。例如这样一个Java函数:

int strLen(String s) {
    return s.length();
}

如果这个函数被调用的时候,传给它的是一个null实参,它就会抛出NullPointerException。那么你是否需要在方法中增加对null的检查呢?这取决与你是否期望这个函数被调用的时候传给它的实参可以为null。如果不可以的话,我们用Kotlin可以这样定义:

fun strLen(s: String) = s.length

看上去与Java没有区别,但是你尝试调用strLen(null)
就会发现在编译期就会被标记成错误。因为在Kotlin中String
只能表示字符串,而不能表示null,如果你想支持这个方法可以传null,则需要在类型后面加上?

fun strLen(s: String?) = if(s != null) s.length else 0

?
可以加在任何类型的后面来表示这个类型的变量可以存储null引用:String?
Int?MyCustomType?等。

一旦你有一个可空类型的值,能对它进行的操作也会受到限制。例如不能直接调用它的方法:

    val s: String? = ""
//    s.length  //错误,only safe(?.) or non-null asserted (!!.) calls are allowed
    s?.length   //表示如果s不为null则调用length属性
    s!!.length  //表示断言s不为null,直接调用length属性,如果s运行时为null,则同样会crash

也不能把它赋值给非空类型的变量:

    val x: String? = null
//    val y: String = x  //Type mismatch

也就是说,加?
和不加可以看做是两种类型,只有与null进行比较后,编译器才会智能转换这个类型。

fun strLen(s: String?) = if(s != null) s.length else 0  

这个例子就与null进行比较,于是String? 类型被智能转换成String
类型,所以可以直接获取length属性。

Java有一些帮助解决NullPointerException问题的工具。比如,有些人会使用注解(@Nullable和@NotNull)来表达值得可空性。有些工具可以利用这些注解来发现可能抛出NullPointerException的位置,但这些工具不是标准Java编译过程的一部分,所以很难保证他们自始至终都被应用。而且在整个代码库中很难使用注解标记所有可能发生错误的地方,让他们都被探测到。

Kotlin的可空类型完美得解决了空指针的发生。
注意,可空的和非空的对象在运行时没有什么区别:可空类型并不是非空类型的包装。所有的检查都发生在编译器。这意味着使用Kotlin的可空类型并不会在运行时带来额外的开销。

     
为了避免这样的攻击,我们需要对Period的构造函数进行相应的修改,即对每个可变参数进行保护性拷贝。

安全调用运算符:”?.”

Kotlin的弹药库中最有效的一种工具就是安全调用运算符:?.
,它允许你爸一次null检查和一次方法调用合并成一个操作。例如表达式s?.toUpperCase()
等同于if (s != null) s.toUpperCase() else null
换句话说,如果你视图调用一个非空值得方法,这次方法调用会被正常地执行。但如果值是null,这次调用不会发生,而整个表达式的值为null。因此表达式s?.toUpperCase()
的返回类型是String?

安全调用同样也能用来访问属性,并且可以连续获取多层属性:

class Address(val street: String, val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun Person.countryName(): String {
    val country = this.company?.address?.country  //多个安全调用链接在一起
    return if (country != null) country else "Unknown"
}

Kotlin
可以让null检查的变得非常简洁。在这个例子中你用一个值和null比较,如果这个值不为空就返回这个值,否则返回其他的值。在Kotlin中有更简单的写法。

1     public Period(Date start,Date end) {
2         this.start = new Date(start.getTime());
3         this.end = new Date(end.getTime());
4         if (start.compareTo(end) > 0) {
5             throw new IllegalArgumentException(start + "After " + end);
6     }

Elvis运算符:”?:”

if (country != null) country else "Unknown" 通过Elvis运算符改写成:

country ?: "Unknown"

Elvis运算符接受两个运算数,如果第一个运算数不为null,运算结果就是第一个运算数,如果第一个运算数为null,运算结果就是第二个运算数。
fun strLen(s: String?) = if(s != null) s.length else 0
这个例子也可以用Elvis运算符简写:fun strLen(s: String?) = s?.length ?: 0

     
需要说明的是,保护性拷贝是在坚持参数有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象的。这主要是为了避免在this.start
= new Date(start.getTime())到if (start.compareTo(end) >
0)这个时间窗口内,参数start和end可能会被其他线程修改。
     
现在构造函数已经安全了,后面我们需要用同样的方式继续修改另外两个对象访问函数。

安全转换:”as?”

之前我们学习了as
运算符用于Kotlin中的类型转换。和Java一样,如果被转换的值不是你试图转换的类型,就会抛出ClassCastException异常。当然你可以结合is
检查来确保这个值拥有合适的类型。但Kotlin作为一种安全简洁的语言,有优雅的解决方案。
as? 运算符尝试把值转换成指定的类型,如果值不合适的类型就返回null。
一种常见的模式是把安全转换和Elvis
运算符结合使用。例如equals方法的时候这样的用法非常方便:

class Person(val name: String, val company: Company?) {
    override fun equals(other: Any?): Boolean {
        val o = other as? Person ?: return false  //检查类型不匹配直接返回false
        return o.name == name && o.company == company //在安全转换后o被智能地转换为Person类型
    }

    override fun hashCode(): Int = name.hashCode() * 31 + (company?.hashCode() ?: 0)
}
1     public Date start() {
2         return new Date(start.getTime());
3     }
4     public Date end() {
5         return new Date(end.getTime());
6     }

非空断言:”!!”

非空断言是Kotlin提供的最简单直接的处理可空类型值得工具,它可以把任何值转换成非空类型。如果对null值做非空断言,则会抛出异常。
之前我们也演示过非空断言的用法了:s!!.length

你可能注意到双感叹号看起来有点粗暴,就像你冲着编译器咆哮。这是有意为之的,Kotlin的设计设视图说服你思考更好的解决方案,这些方案不会使用断言这种编译器无法验证的方式。

但是确实存在这样的情况,某些问题适合用非空断言来解决。当你在一个函数中检查一个值是否为null。而在另一个函数中使用这个值时,这种情况下编译器无法识别这种用是否安全。如果你确信这样的检查一定在其他某个函数中存在,你可能不想在使用这个值之前重复检查。这时你就可以使用非空断言。

     
经过这一番修改之后,Period成为了不可变类,其内部的“周期的起始时间不能落后于结束时间”约束条件也不会再被破坏。
     
参数的保护性拷贝并不仅仅针对不可变类。每当编写方法或者构造器时,如果它要允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是可变的。如果是,就要考虑你的类是否能够容忍对象进入数据结构之后发生变化。如果答案是否定的,就必须对该对象进行保护性拷贝,并且让拷贝之后的对象而不是原始对象进入到数据结构中。例如,如果你正在考虑使用有客户提供的对象引用作为内部Set实例的元素,或者作为内部Map实例的键(Key),就应该意识到,如果这个对象在插入之后再被修改,Set或者Map的约束条件就会遭到破坏。
    
四十一、谨慎重载:

“let” 函数

let函数让处理可空表达式变得更容易。和安全调用运算符一起,它允许你对表达式求值,检查求值结果是否为null,并把结果保存为一个变量。所有这些动作都砸系统一个简洁的表达式中。
可空参数最常见的一种用法应该就是被传递给一个接受非空参数的函数。比如说下面这个函数,它接收一个String类型的参数并向这个地址发送一封邮件,这个函数在Kotlin中是这样写的:

fun sendEmailTo(email: String) { ... }

不能把null传给这个函数,因此通常需要先判断一下然后调用函数:
if(email != null) sendEmailTo(email)
但我们有另一种方式:使用let函数,并通过安全调用来调用它。let函数做的所有事情就是把一个调用它的对象变成lambda表达式的参数:
email?.let{ email -> sendEmailTo(email) }
let函数只有在email的值非空时才被调用,如果email值为null则{}
的代码不会执行。
使用自动生成的名字it
这种简明语法之后,可以写成:email?.let{ sendEmailTo(it) }
。(Lambda的语法在只有章节会详细讲)

      见下面一个函数重载的例子:

延迟初始化的属性

很多框架会在对象实例创建之后用专门的方法来初始化对象。例如Android中,Activity的初始化就发生在onCreate方法中。而JUnit则要求你把初始化的逻辑放在用@Brefore注解的方法中。
但是你不能再狗仔方法中完全放弃非空属性的初始化器。仅仅在一个特殊的方法里初始化它。Kotlin通常要求你在构造方法中初始化所有属性,如果某个属性时非空类型,你就必须提供非空的初始化值。否则,你就必须使用可空类型。如果你这样做,该属性的每次访问都需要null检查或者!!
运算符。

class Activity {
    var view: View? = null

    fun onCreate() {
        view = View()
    }

    fun other() {
        //use view
        view!!.onLongClickListener = ...
    }
}

这样使用起来比较麻烦,为了解决这个麻烦,使用lateinit
修饰符来声明一个不需要初始化器的非空类型的属性:

class Activity {
    lateinit var view: View

    fun onCreate() {
        view = View()
    }

    fun other() {
        //use view
        view.onLongClickListener = ...
    }
}

注意,延迟初始化的属性都是var 因为需要在构造方法外修改它的值,而val
属性会被编译成必须在构造方法中初始化的final字段。尽管这个属性时非空类型,但是你不需要再构造方法中初始化它。如果在属性被初始化之前就访问了它,会得到异常”lateinit
property xx has not been initialized” ,说明属性还没有被初始化。

注意lateinit属性常见的一种用法是依赖注入。在这种情况下,lateinit属性的值是被依赖注入框架从外部设置的。为了保证和各种Java框架的兼容性,Kotlin会自动生成一个和lateinit属性具有相同可见性的字段,如果属性的可见性是public,申城字段的可见性也是public。

public final class Activity {
   public View view;

   public final View getView() {
      View var10000 = this.view;
      if(this.view == null) {
         Intrinsics.throwUninitializedPropertyAccessException("view");
      }
      return var10000;
   }

   public final void setView(@NotNull View var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.view = var1;
   }

   public final void onCreate() {
      this.view = new View();
   }

   public final void other() {
   }
}
 1     public class CollectionClassfier {
 2         public static String classify(Set<?> s) {
 3             return "Set";
 4         }
 5         public static String classify(List<?> l) {
 6             return "List";
 7         }
 8         public static String classify(Collection<?> c) {
 9             return "Unknown collection";
10         }
11         public static void main(String[] args) {
12             Collection<?>[] collections = {
13                 new HashSet<String>(),
14                 new ArrayList<BigInteger>(),
15                 new HashMap<String,String>().values()
16             };
17             for (Collection<?> c : collections)
18                 System.out.println(classify(c));
19         }
20     }

可空性的扩展

为可空类型定义扩展函数是一种更强大的处理null值的方式。可以允许接收者为null的(扩展函数)调用,并在该函数中处理null,而不是在确保变量为null之后再调用它的方法。
Kotlin标准库中定义的String的两个扩展函数isEmptyisBlank
就是这样的例子。第一个函数判断字符串是否是一个空的字符串""
。第二个函数判断它是否是空的或则只包含空白字符。通常用这些函数来检查字符串是有价值的,以确保对它的操作是有意义的。你可能意识到,像处理无意义的空字符串和空白字符串这样处理null也很有用。事实上,你的确可以这样做:函数isEmptyOrNull
isNullOrBlank 就可以由String? 类型的接收者调用。

fun verifyUserInput(input: String?) {
    if (input.isNullOrBlank()) { //此方法是String?的方法,不需要安全调用
        println("Please fill in the required fields")
    }
}

无论input是null还是字符串都不会导致任何异常。我们来看下isNullOrBlank
函数的定义:

public inline fun CharSequence?.isNullOrBlank(): Boolean = this == null || this.isBlank()

可以看到扩展函数是定义给CharSequence?
(String的父类),因此不像调用String的方法那样需要安全调用。
当你为一个可空类型定义扩展函数时,这以为这你可以对可空的值调用这个函数;并且函数体中this可能为null,所以你必须显示地检查。在Java中,this永远是非空的,因为他引用的时当前你所在这个类的实例。而在Kotlin中,这并不永远成立:在可空类型的扩展函数中,this可以为null。
之前讨论的let
函数也能被可空的接收者调用,但它并不检查值是否为null。如果你在一个可空类型直接调用let
函数,而没有使用安全调用运算符,lambda的实参将会是可空的:

val person: Person? = ...
person.let { sendEmailTo(it) }  //没有安全调用,所以it是可空类型

ERROR: Type mismatch:inferred type is Person? but Person was expected

因此,如果想要使用let来检查非空的实参,你就必须使用安全调用运算符?.
就像之前看到的代码一样:person?.let{ sentEmailTo(it) }

当你定义自己的扩展函数时,需要考虑该扩展是否需要可空类型定义。默认情况下,应该把它定义成非空类型的扩展函数。如果发现大部分情况下需要在可空类型上使用这个函数,你可以稍后再安全地修改他(不会破坏其他代码)。

      这里你可能会期望程序打印出
      //Set
      //List
      //Unknown Collection
      然而实际上却不是这样,输出的结果是3个”Unknown
Collection”。为什么会是这样呢?因为函数重载后,需要调用哪个函数是在编译期决定的,这不同于多态的运行时动态绑定。针对此种情形,该条目给出了一个修正的方法,如下:

类型参数的可空性

Kotlin中所有泛型和泛型函数的类型参数默认都是可空的。任何类型,包括可空类型在内,都可以替换类型参数。这种情况下,使用类型参数作为类型声明都允许为null,尽管类型参数T并没有用问号结尾。

fun <T> printHashCode(t: T) {
    println(t?.hashCode())
}

在该函数中,类型参数T推导出的类型是可空类型Any?
因此,尽管没有用问号结尾。实参t依然允许持有null。
要使用类型参数非空,必须要为它指定一个非空的上界,那样泛型会拒绝可空值作为实参:

fun <T: Any> printHashCode(t: T) {
    println(t.hashCode())
}

后续章节会讲更多的泛型细节,这里你只需要记得这一点就可以了。

1     public static String classify(Collection<?> c) {
2         return c instanceof Set ? "Set" : c instanceof List 
3             ? "List" : "Unknown Collection";
4     }

可空性和Java

我们在Kotlin中通过可空性可以完美地处理null了,但是如果是与Java交叉的项目中呢?Java的类型系统是不支持可空性的,那么该如果处理呢?
Java中可空性信息通常是通过注解来表达的,当代码中出现这种信息时,Kotlin就会识别它,转换成对应的Kotlin类型。例如:@Nullable String
-> String?@NotNull String -> String
Kotlin可以识别多种不同风格的可空性注解,包括JSR-305标准的注解(javax.annotation包下)、Android的注解(android.support.annitation)
和JetBrans工具支持的注解(org.jetbrains.annotations)。那么还剩下一个问题,如果没有注解怎么办呢?

     
和override不同,重载机制不会像override那样规范,并且每次都能得到期望的结果。因此在使用时需要非常谨慎,否则一旦出了问题,就会需要更多的时间去调试。该条目给出以下几种尽量不要使用重载的情形:
      1.    函数的参数中包含可变参数;
      2.  
 当函数参数数目相同时,你无法准确的确定哪一个方法该被调用时;
      3.    在Java 1.5 之后,需要对自动装箱机制保持警惕。
     
我们先简单说一下第二种情形。比如两个重载函数均有一个参数,其中一个是整型,另一个是Collection<?>,对于这种情况,int和Collection<?>之间没有任何关联,也无法在两者之间做任何的类型转换,否则将会抛出ClassCastException的异常,因此对于这种函数重载,我们是可以准确确定的。反之,如果两个参数分别是int和short,他们之间的差异就不是这么明显。
      对于第三种情形,该条目给出了一个非常典型的用例代码,如下:

平台类型

没有注解的Java类型会变成Kotlin中的平台类型
。平台类型本质上就是Kotlin不知道可空性信息的类型。即可以把它当做可空类型处理,也可以当做非空类型处理。这意味着,你要像在Java中一样,对你在这个类型上做的操作负有全部责任。编译器将会允许所有操作,它不会把对这些值得空安全操作高亮成多余的,但它平时却是这样对待非空类型值上的空安全操作的。
比如我们在Java中定义一个Person类:

public class Person {
    private  String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

我们在Kotlin中使用这个类:

fun yellAt(person: Person) {
    println(person.name.toUpperCase()) //不考虑null情况,但是如果为null则抛出异常
    println((person.name ?: "Anyone").toUpperCase()) //考虑null的可能
}

我们即可以当成非空类型处理,也可以当成可空类型处理。

Kotlin平台类型在表现为:Type!

val i: Int = person.name

ERROR: Type mistach: inferred type is String! but Int was expected

但是你不能声明一个平台类型的变量,这些类型只能来自Java代码。你可以用你喜欢的方式来解释平台类型:

val person = Person()
val name: String = person.name
val name2: String? = person.name

当然如果平台类型是null,赋值给非空类型时还是会抛出异常。

为什么需要平台类型?
对Kotlin来说,把来自Java的所有值都当成可空的是不是更安全?这种设计也许可行,但是这需要对永远不为空的值做大量冗余的null检查,因为Kotlin编译器无法了解到这些信息。
涉及泛型的话这样情况就更糟糕了。例如,在Kotlin中,每个来自Java的ArrayList<String>
都被当作ArrayList<String?>?,每次访问或者转换类型都需要检查这些值是否为null,这将抵消掉安全性带来的好处。编写这样的检查非常令人厌烦,所以Kotlin的设计者作出了更实用的选择,让开发者负责正确处理来自Java的值。

 1     public class SetList {
 2         public static void main(String[] args) {
 3             Set<Integer> s = new TreeSet<Integer>();
 4             List<Integer> l = new ArrayList<Integer>();
 5             for (int i = -3; i < 3; ++i) {
 6                 s.add(i);
 7                 l.add(i);
 8             }
 9             for (int i = 0; i < 3; ++i) {
10                 s.remove(i);
11                 l.remove(i);
12             }
13             System.out.println(s + " " + l);
14         }
15     }

继承

当在Kotlin中重写Java的方法时,可以选择把参数和返回类型定义成可空的,也可以选择把它们定义成非空的。例如,我们来看一个例子:

/* Java */
interface StringProcessor {
    void process(String value);
}

Kotlin中下面两种实现编译器都可以接收:

class StringPrinter : StringProcessor {
    override fun process(value: String) {
        println(value)
    }
}

class NullableStringPrinter : StringProcessor {
    override fun process(value: String?) {
        if (value != null) {
            println(value)
        }
    }
}

注意,在实现Java类或者接口的方法时一定要搞清楚它的可空性。因为方法的实现可以在非Kotlin的代码中被调用,Kotlin编译器会为你声明的每一个非空的参数生成非空断言。如果Java代码传给这个方法一个null值,断言将会触发,你会得到一个异常,即便你从没有在你的实现中访问过这个参数的值。

因此,建议你只有在确保调用该方法时绝对不会出现空值时,才用非空类型取接收平台类型。

     
在执行该段代码前,我们期望的结果是Set和List集合中大于等于的元素均被移除出容器,然而在执行后却发现事实并非如此,其结果为:
      [-3,-2,-1] [-2,0,2]
     
这个结果和我们的期望还是有很大差异的,为什么Set中的元素是正确的,而List则不是,是什么导致了这一结果的发生呢?下面给出具体的解释:
      1.
s.remove(i)调用的是Set中的remove(E),这里的E表示Integer,Java的编译器会将i自动装箱到Integer中,因此我们得到了想要的结果。
      2. l.remove(i)实际调用的是List中的remove(int
index)重载方法,而该方法的行为是删除集合中指定索引的元素。这里分别对应第0个,第1个和第2个。
     
为了解决这个问题,我们需要让List明确的知道,我们需要调用的是remove(E)重载函数,而不是其他的,这样我们就需要对原有代码进行如下的修改:

 1     public class SetList {
 2         public static void main(String[] args) {
 3             Set<Integer> s = new TreeSet<Integer>();
 4             List<Integer> l = new ArrayList<Integer>();
 5             for (int i = -3; i < 3; ++i) {
 6                 s.add(i);
 7                 l.add(i);
 8             }
 9             for (int i = 0; i < 3; ++i) {
10                 s.remove(i);
11                 l.remove((Integer)i); //or remove(Integer.valueOf(i));
12             }
13             System.out.println(s + " " + l);
14         }
15     }

     
该条目还介绍了一种实现函数重载,同时又尽可能避免上述错误发生的方式。即其中的一个重载函数,在其内部通过一定的转换逻辑转换之后,再通过转换后的参数类型调用其他的重载函数,从而确保即便使用者在使用过程中出现重载误用的情况,也因两者可以得到相同的结果而规避了潜在错误的发生。

四十二、慎用可变参数:

     
可变参数方法接受0个或者多个指定类型的参数。可变参数机制通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法,如:

1     static int sum(int...args) {
2         int sum = 0;
3         for (int arg : args)
4             sum += arg;
5         retrun sum;
6     }

     
上面的方法可以正常的工作,但是在有的时候,我们可能需要至少一个或者多个某种类型参数的方法,如:

 1     static int min(int...args) {
 2         if (args.length == 0)
 3             throw new IllegalArgumentException("Too few arguments.");
 4         int min = args[0];
 5         for (int i = 0; i < args.length; ++i) {
 6             if (args[i] < min)
 7                 min = args[i];
 8         }
 9         return min;
10     }

     
对于上面的代码主要存在两个问题,一是如果调用者没有传递参数是,该函数将会在运行时抛出异常,而不是在编译期报错。另一个问题是这样的写法也是非常不美观的,函数内部必须做参数的数量验证,不仅如此,这也影响了效率。将编译期可以完成的事情推到了运行期。下面提供了一种较好的修改方式,如下:

1     static int min(int firstArg,int...remainingArgs) {
2         int min = firstArgs;
3         for (int arg : remainingArgs) {
4             if (arg < min)
5                 min = arg;
6         }
7         return min;
8     }

     
由此可见,当你真正需要让一个方法带有不定数量的参数时,可变参数就非常有效。
     
有的时候在重视性能的情况下,使用可变参数机制要特别小心。可变参数方法的每次调用都会导致进行一次数组分配和初始化。如果确定确实无法承受这一成本,但又需要可变参数的灵活性,还有一种模式可以弥补这一不足。假设确定对某个方法95%的调用会有3个或者更少的参数,就声明该方法的5个重载,每个重载方法带有0个至3个普通参数,当参数的数目超过3个时,就使用一个可变参数方法:

1     public void foo() {}
2     public void foo(int a1) {}
3     public void foo(int a1,int a2) {}
4     public void foo(int a1,int a2,int a3) {}
5     public void foo(int a1,int a2,int a3,int...rest) {}

     
所有调用中只有5%参数数量超过3个的调用需要创建数组。就像大多数的性能优化一样,这种方法通常不恰当,但是一旦真正需要它时,还是非常有用处的。
    
四十三、返回零长度的数组或者集合,而不是null:

      见如下代码:

1     public class CheesesShop {
2         private final List<Cheese> cheesesInStock = new List<Cheese>();
3         public Cheese[] getCheeses() {
4             if (cheesesInStock.size() == 0)
5                 return null;
6             return cheeseInStock.toArray(null);
7         }
8     }

     
从以上代码可以看出,当没有Cheese的时候,getCheeses()函数返回一种特例情况null。这样做的结果会使所有的调用代码在使用前均需对返回值数组做null的判断,如下:

1     public void testGetCheeses(CheesesShop shop) {
2         Cheese[] cheeses = shop.getCheeses();
3         if (cheese != null && Array.asList(cheeses).contains(Cheese.STILTON))
4             System.out.println("Jolly good, just the thing.");
5     }

     
对于一个返回null而不是零长度数组或者集合的方法,几乎每次用到该方法时都需要这种曲折的处理方式。很显然,这样是比较容易出错的。如果我们使getCheeses()函数在没有Cheese的时候不再返回null,而是返回一个零长度的数组,那么我的调用代码将会变得更加简洁,如下:

1     public void testGetCheeses2(CheesesShop shop) {
2         if (Array.asList(shop.getCheeses()).contains(Cheese.STILTON))
3             System.out.println("Jolly good, just the thing.");
4     }

      相比于数组,集合亦是如此。

发表评论

电子邮件地址不会被公开。 必填项已用*标注