首页 » 技术分享 » 一场由Java堆污染(Heap Pollution)引发的思考

一场由Java堆污染(Heap Pollution)引发的思考

 

Heap Pollution

首先来看下什么是Heap Pollution(堆污染)。

在Java编程语言中, 当一个 可变泛型参数 指向一个 无泛型参数 时,堆污染(Heap Pollution)就有可能发生。

举个栗子, 创建一个方法,空实现即可,如下所示

    public static void varagMethod(Set<Integer> objects) {

    }

此方法接受的是一个泛型Integer类型的Set集合,假如我们将一个没有泛型Set对象传给此方法时,则有可能造成堆污染,如下所示

    public static void main(String[] args){
        Set set = new TreeSet();

        set.add("abc");

        varagMethod(set);
    }

以上代码,我们将一个无泛型的Set传递给了varagMethod方法,此时就有可能造成堆污染。当发生这种情况时,编译器可以检测到,并给我们 Warning,如下
这里写图片描述

而Heap Pollution有可能导致更严重的后果 : ClassCastException, 我们在刚才的varagMethod方法中添加几行代码如下:

public static void varagMethod(Set<Integer> objects) {
        objects.add(new Integer(10));
}

然后修改下main方法

public static void main(String[] args){
        Set set = new TreeSet();

        varagMethod(set);

        Iterator<String> iter = set.iterator();
        while (iter.hasNext())
        {
            String str = iter.next();   // ClassCastException thrown
            System.out.println(str);
        }
}

然后重新运行一下,程序直接 Crash!!!
这里写图片描述

可以看到,程序报了一个ClassCastException(类型转换错误),这是因为Java允许我们将一个无泛型参数的Set对象传递给varagMethod方法,并且我们在varagMethod方法中,给传入的Set对象添加了一个Integer对象,之后我们遍历set集合时,将Set中第一个元素强转成String,很明显Integer不能转化为String对象

思考

在了解了Heap Pollution之后,顺其自然的想到在Android SDK提供给我们的API中,也存在一个这样类似的方法。 这个方法就是 ActivityOptions 中的 makeSceneTransitionAnimation 方法

ActivityOptions makeSceneTransitionAnimation (Activity activity, 
                Pair...<View, String> sharedElements)

以上方法被设计的目的是实现在两个 Activity 之间切换时,添加Activity之间的共享元素,具体效果参考以下图片
这里写图片描述

具体介绍可以参考 Activity & Fragment Transition学习

今天的主要目标是来看一下 makeSceneTransitionAnimation 这个方法有什么问题 ? 这个方法主要存在以下问题

Java数组泛型类型是不安全的

你会发现遇到的第一个问题就是,无论将什么样的参数传递给 makeSceneTransitionAnimation 方法,编译器都会给我们弹出警告Warning

// 调用以下方法可以编译通过,但是会弹出警告:
// "unchecked generics array creation for varargs parameter"
ActivityOptions.makeSceneTransitionAnimation(this, elem1, elem2);

之所以弹出这个 Warning 的原因是 :Java不允许创建带泛型类型的数组对象 也就是说以下代码是编译不通的

// 编译报错
Pair<View, String>[] array = new Pair<View, String>[2];

这种写法在Java中是被禁止的, 因为在Java中数组类型是 协变(covariant) 的,如果不了解 协变, 请参考深入理解Java与Kotlin的泛型(Generic Type)和型变(Variance)。这也就意味着Java是在运行时才去检查写入数组中的数据类型,但是又由于数组在运行时的泛型会被消除generic types have been erased,因此在运行时,JVM无法区分Pair<View, String>Pair<TextView, String>.也就相当于我们将一个无泛型参数传递给了一个可变泛型参数的方法, 进而导致堆污染(Heap Pollution)

结论

简而言之,在Java中只要是将泛型类型的对象放入数组中,那么就要由程序员自己来保证代码的安全性,并尽量避免 ClassCastException 的发生。这也是为什么在Java中当需要处理泛型类型的时候,集合 总是首选. 比如将 makeSceneTransitionAnimation中的参数改为 List<T> 而不是 T... 将会阻止编译器弹出警告提示并且在运行时保证类型安全

List<Pair<View, String>> args = new ArrayList<>(2);
args.add(elem1);
args.add(elem2);
ActivityOptions.makeSceneTransitionAnimation(this, args);

彩蛋 Kotlin

如果使用 Kotlin 实现以上方法,则问题将会完美解决 !

Kotlin的数组比Java的更加安全

在Kotlin中,数组的行为和Java的数组行为不太一致,主要表现在以下几点

1 编译器在编译时期就确定类型安全
2 数组允许泛型类型
3 调用可变泛型类型参数不会弹出 Warning 信息

另外,可变数组在Kotlin中默认是自动被认为 协变(covariant) 的,因此在一个接受可变数组为参数的函数中,他们是只读(Read-Only)的

fun someFunction(vararg words: String) {
    if (words.isNotEmpty()) {
        // 无法编译通过: read-only
        words[0] = "Test"
    }
}

Kotlin支持使用端协变(Use-site variance) 和 声明端协变(Declaration-site variance)

Use-site variance

Kotlin的使用端协变用法跟Java的不太一样,在Java中是使用通配符 <? extendd Object> 的方式,而在Kotlin中使用的是 类型注入(Type Projection). 也就是使用 out (read-only ) 和 in (write-only) 关键字. 具体如下所示:

// 通过out和in关键字,创建更加安全的数组copy方法
fun <T> copy (from: Array<out T?>, to: Array<in T?>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

因为 out 是只读关键字,所以from数组不能被改动, 同理被in关键字修饰的to数组只能被修改

Declaration-site variance

在Kotlin语言中,关于泛型这一快真正的亮点是在 Declaration-site variance, 这使得开发者可以在声明类或者接口时就指定泛型的型变

拿Kotlin提供给我们的官方Pair类举例

data class Pair<out A, out B>(
        val first: A,
        val second: B
) : Serializable

通过以上方式在 Pair 中的 first: Asecond: B 都是只读不可变的,就相当于Java中的 final 关键字,也就使得 Pair<A, B> 总是协变的,因此以下代码可以正常编译通过

// 创建一个 Pair<TextView, String> 对象
val pair = Pair(textView, "elem1")
// 编译OK: 因为默认Pair<A, B>是协变的
val elem1: Pair<View, String> = pair

最后,回到Android中的 makeSceneTransitionAnimation 方法,如果使用Kotlin语言的话,就可以使用如下方式书写

fun Activity.makeSceneTransitionAnimation(
    vararg sharedElements: Pair<View, String>): ActivityOptions {
    ...
}

调用此方法也变得极为简单 (需要使用到在 Pair 类中定义的 to 函数)

val options = makeSceneTransitionAnimation(
              textView to "elem1", imageView to "elem2").toBundle()

后感

1 Kotlin的数组比Java的更加安全,可以避免Heap Pollution
2 Kotlin代码比Java更加简洁
3 结论:赶紧Kotlin搞起

转载自原文链接, 如需删除请联系管理员。

原文链接:一场由Java堆污染(Heap Pollution)引发的思考,转载请注明来源!

0