专业的编程技术博客社区

网站首页 > 博客文章 正文

java和kotlin泛型得使用(kotlin ::class.java)

baijin 2024-09-06 14:52:44 博客文章 6 ℃ 0 评论

什么是泛型

泛型指的是,在定义类,方法,接口时将类型作为参数,从而使同一份代码能够在不同类型间复用的能力。

为什么要使用泛型

使用泛型的代码比非泛型的代码有许多优点:

  • 在编译时进行更强的类型检查,编译器对泛型代码应用强类型检查,如果代码违反了类型安全,则发出错误。修复编译时错误比修复运行时错误更容易
  • 使程序员能够实现泛型算法。通过使用泛型,程序员可以实现适用于不同类型集合的泛型算法,可以进行定制,并且类型安全且易于阅读
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) -> Iterable<R>): C {
    for (element in this) {
        val list = transform(element)
        destination.addAll(list)
    }
    return destination
}
  • Java中,如果不使用类型,会有强制转换的风险,但kotlin强制要求传递类型``
// 未声明类型,默认为Object
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);   // no cast

Generic Types

Box

泛型类型是通过类型参数化的泛型类或接口。下面的 Box 类将被修改以演示这个概念。

public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

因为它的方法接受或返回一个 Object,所以您可以自由地传入任何您想要的内容,只要它不是基本类型。但是这种写法没有办法在编译时验证类的使用方式。代码的一部分可能会在Box放置一个 Integer 并期望从中获得 Integer 类型的对象,而另一部分代码可能会错误地传入一个 String,导致运行时错误。

Box的泛型版本

泛型类的定义格式如下:

/**
 * Generic version of the Box class.
 * @param <T> the type of the value being boxed
 */
public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

Bounded Type

有时可能希望限制可用作参数化类型中的类型参数的类型。例如,对数字进行操作的方法可能只希望接受 Number 或其子类的实例。 若要声明有界类型参数,需要列出类型参数的名称,然后是 extends 关键字,最后是它的上界,在本例中为 Number。注意,在这个上下文中,extends 在一般意义上被用来表示“ extends”(如类)或“ implements”(如接口)。

public class Box<T> {

    private T t;          

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public <U extends Number> void inspect(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: " + u.getClass().getName());
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(new Integer(10));
        integerBox.inspect("some text"); // error: this is still String!
    }
}

除了限制可用于实例化泛型类型的类型之外,有界类型参数还允许调用边界中定义的方法:

// 在kotlin中 extends对应out in对应super  
public class NaturalNumber<T extends Integer> {

    private T n;

    public NaturalNumber(T n)  { this.n = n; }

    // n是Integer或者其子类,所以存在intValue的方法
    public boolean isEven() {
        return n.intValue() % 2 == 0;
    }
}

多个类型约束

前面的例子演示了一个类型参数的使用,它只有一个边界,但是一个类型参数可以有多个边界:

Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

// kotlin中也可以,需要where关键字
// fun <T> test2() where  T : List<String>, T :Comparable<String> {}
class D <T extends A & B & C> { /* ... */ }

泛型与继承

与继承一样,子类的泛型可以更加细化

interface GenericBase<T:Person>{
    fun set(collection: List<T>)
    fun get(): Collection<T>
}
interface GenericSub<T:Teacher>: GenericBase<T>{
    override fun get(): List<T>
}
interface GenericSub2<T:Doctor>: GenericBase<T>{
    override fun get(): ArrayList<T>
}

逆变与型变

一般原则是:当一个类 C 的类型参数 T 被声明为 out 时,它就只能出现在 C 的成员的输出-位置(方法的返回值),但回报是 C 可以安全地作为 C的超类。 简而言之,他们说类 C 是在参数 T 上是协变的,或者说 T 是一个协变的类型参数。 你可以认为 C 是 T 的生产者,而不是 T 的消费者out修饰符称为型变注解,并且由于它在类型参数声明处提供,所以我们称之为声明处型变。 这与 Java 的使用处型变相反,其类型用途通配符使得类型协变。 另外除了 out,Kotlin 又补充了一个型变注释:in。它使得一个类型参数逆变:只可以被消费而不可以被生产,即只可以出现在方法的输入-位置(方法的入参)

Joshua Bloch 称那些你只能从中读取的对象为生产者,并称那些你只能写入的对象为消费者。他建议:“为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型”,并提出了以下助记符:

PECS 代表生产者-Extends、消费者-Super(Producer-Extends, Consumer-Super)。

// 测试类型的上下界
fun testBound() {
    // out 表示 存入的值只能是Teacher或者Teacher的子类,但是不知道其具体类型
    // out 同时也表示producer 即被范性约束的类的与类范型相同的方法的返回值的类型必须为Teacher
    val outTeachers: MutableList<out Teacher> = arrayListOf(Teacher())

    //所以不能加人任何元素
    //    teachers.add(Person()) // compile error
    //    teachers.add(Teacher()) // compile error
    //    teachers.add(Student()) // compile error
    // 但是取出的元素必然为Teach类型或者其子类
    val teacher = outTeachers[0] // compile success
    // 必然为true
    assert(teacher is Teacher)
    // in 表示存入的元素是Teacher或者Teacher的子类
    // 同时也是消费者,即被范性约束的类的与类范型相同的方法入参的类型必须为Teacher
    val inTeachers: MutableList<in Teacher> = ArrayList()
    inTeachers.add(Doctor())
    inTeachers.add(Teacher())

    // 不知道具体的类型,所有范型约束的方法的返回值都变为Any
    val get: Any? = inTeachers.get(0)

    val personList = listOf<Person>()
    val teacherList = listOf<Teacher>()
    println(teacherList)
 val map: MutableMap<in Number, out Any> = mutableMapOf()
    // 应为V 为out 所以只能读
    //Type mismatch.
    //Required:  CapturedType(out Any)
    //Found: String
    //map["sample"] = "sample"
    val values: MutableCollection<out Any> = map.values
    val keys: MutableSet<in Number> = map.keys
    keys.add(1)
    keys.add(1.0)

通配符

有时你想说,你对类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化将是该投影的子类型。 Kotlin 为此提供了所谓的星投影语法:

  • 对于 Foo ,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <> 等价于 Foo 。 这意味着当 T 未知时,你可以安全地从 Foo <> 读取 TUpper 的值。
  • 对于 Foo ,其中 T 是一个逆变类型参数,Foo <> 等价于 Foo 。 这意味着当 T 未知时,没有什么可以以安全的方式写入 Foo <>。
  • 对于 Foo ,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo 而对于写值时等价于 Foo。

如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为 interface Function ,我们可以想象以下星投影:

  • Function<*, String> 表示 Function;
  • Function 表示 Function;
  • Function<*, *> 表示 Function。

注意:星投影非常像 Java 的原始类型,但是安全。

上面摘抄自kotlin官网,没看懂可以直接看代码 @UnsafeVariance 这个注解,可以绕过in只能作为入参,out只能作为返回值的限制

class Test<out T:Number>{
    fun set(t: @UnsafeVariance T){

    }
    fun get():T?{
        return null
    }
}
class Test1<in T:Number>{
    fun set(t:  T){

    }
    fun get(): @UnsafeVariance T?{
        return null
    }
}

class Test3<T : Number>{
    fun set(t:  T){

    }
    fun get():  T?{
        return null
    }
}
fun testStar1() {
    val test: Test<*> = Test<Int>()
    test.set(1)
    val get: Number? = test.get()
    val test1: Test1<*> = Test1<Int>()
//    The integer literal does not conform to the expected type Nothing
//    test1.set(1)
    val number: Number? = test1.get()
    val test3: Test3<*> = Test3<Int>()
//    //    The integer literal does not conform to the expected type Nothing
//    test3.set(1)
    val get3: Number? = test3.get()
}

总结,当以*作为泛型参数时,读取都是正常的,但是写入在非out情况下,都无法编译成功。

泛型的缺点

无法在静态字段上使用类型参数

类的静态字段是一个类级别变量,由类的所有非静态对象共享。因此,类型参数的静态字段是不允许的

class MobileDevice<T> {
    companion object {
//        类的静态字段是一个类级别变量,由类的所有非静态对象共享。因此,类型参数的静态字段是不允许的
//        private val os: T? = null
    }
}

无法创建、捕获或抛出参数化类型的对象

fun <E : Throwable> testError() {
//    try {
//
//        // Type parameter is forbidden for catch parameter
//    }catch (e:E){
//        e.printStackTrace()
//    }
}

// Extends Throwable indirectly
//class MathException<T> : Exception() { /* ... */ }    // compile-time error

// Extends Throwable directly
//class QueueFullException<T> : Throwable() { /* ... */ // compile-time error
// kotlin中无法抛出泛型异常,但是java是可以的
internal class Parser<out T : Exception> {
    //    @Throws(T::class) compile-time error
    fun parse(file: File?) {     // OK
        // ...
    }
}


// class Parser<T extends Exception> {
//    public void parse(File file) throws T {     // OK
//        // ...
//    }
//}

无法重载两个泛型擦出后具有相同签名的方法

class Example {
    // 虽然编译器会擦出泛型信息,但是只要具体的type为ParameterizedType,运行时仍然可以获取到泛型信息
    // Example::class.java.methods[0].parameters[0].parameterizedType
    // rawType Set  typeArguments String
    fun print(strSet: Set<String>) {}
    // 编译错误
//    fun print(intSet: Set<Int>) {}
}

is无法作用与泛型类型

// 因为 Java 编译器会擦除泛型代码中的所有类型参数,所以无法验证在运行时使用的泛型类型的参数化类型:
fun <E> rtti(list: List<E>?) {
    if (list is ArrayList<*>) {  // OK; instanceof requires a reifiable type
        // ...
    }

//    if (list is ArrayList<Int?>) {  // compile-time error
//    }
}

无法创建类型参数的实例 (C# 和typescript可以)

fun <T> create(list: List<T>) {
//    val a =T()

}

无法在注解中使用泛型

//annotation class Test<St> (val value: St)

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表