大众号「稀有猿诉」 原文链接 浅显易懂Java泛型

温故而知新,能够为师矣!

前面的一篇文章中学习了Kotlin的泛型知识,但总感觉还不够深入,由于一些深入的论题和高档的特性并未有讲清楚。但在继续深入之前仍是有必要重温一下Java的泛型知识,这是由于Kotlin是根据JVM的言语,而且与Java联络暧昧,它能够与Java混合运用,能够彼此调用,在某种程度上讲Kotlin能够视为Java的一种『方言』。所以,我们先回顾Java的泛型,夯实根底,并弄清楚Java泛型遗留了哪些问题,然后再看看Kotlin是如何处理这些问题的。

浅显易懂Java泛型

根底运用办法

仍是要从基本的运用办法来谈起。

泛型(Generics)便是在类或许办法界说的时分并不指定其操作数据的具体类型,而是用一个虚拟的姓名<T>代替,类的运用者或许办法的调用在运用时提供具体的类型,以到达类和办法能对一切的类型都能运用的目录。能够把泛型了解为参数化,也便是说界说的时分把其操作的数据类型视为一种参数,由运用者在运用时具体指定(创建目标时或许调用办法时),因而泛型也能够称为参数化类型。有3个当地能够运用泛型,类,接口和办法,接下分别来看一下具体如何运用。

泛型类

泛型类,也即参数化类型的类,是最为常见的一种泛型的运用办法。这些类能够视为元类,它会操作另一个类型,比方存储或许加工,类自身的完成重点在于如何操作,而关于这个『另一个类型』具体是什么,并不关怀。这时就能够用泛型,在界说类的时分并不指定具体的类型,而是用一个虚拟的类型来代替,由类的运用者在运用的时分来指定具体的类型:

class ArrayList<E> {
	public void add(E e) { ... }
	public E get(int index) { ... }
}

这儿ArrayList是一个容器,能够以线性的办法来存储恣意其他类型,具体是啥其实ArrayList并不关怀,所以这儿用泛型,E便是参数化类型,代指某一个类型。运用时需求提供具体的类型,能够Integer,String,或许界说好了的任何一种类型(Class):

ArrayList<String> players = new ArrayList<>();
players.add("James");
players.add("Kevin");
System.out.println("#1 is " + players.get(0));
System.out.println("#2 is " + players.get(1));
// #1 is James
// #2 is Kevin

小结 一下,泛型是为了增强代码的复用,界说时用尖括号<>表明的参数化类型Parameterized type,拼接在类姓名的后边,运用时再指定具体的类型。而且,当编译器能推断出参数类型时,能够用钻石符号(Diamond operator)<>来省略参数类型姓名。

泛型接口

泛型能够用于接口的声明,与类相同,把类型参数化即可:

interface Consumer<T> {
	void consume(T t);
}

泛型办法

除了类和接口,办法也能够运用泛型,把用尖括号表明的参数化类型<T>放在办法的回来类型之前就能够了:

public <T> ArrayList<T> fromArrayToList(T[] a) { ... }
String[] names = {"James", "Kevin", "Harden"};
ArrayList<String> players = fromArrayToList(names);

需求留意的是,由于Java的办法有必要声明在类里边,但这并不意味着办法的泛型必定要与类的类型参数一致,当然了,办法能够直接运用类的类型参数,也能够自己再界说一个别的的类型参数,留意这是办法自界说的泛型与其地点的类的泛型没啥联络,如:

class ArrayList<E> {
	public <T> ArrayList<T> transfer(E e) { ... }
}

留意,为了可读性办法自界说的泛型最好不要与其地点类运用的泛型相同,比方类用T,办法也用T,虽然这是能够的,由于这个代替类型姓名随意取为啥非要弄的简单混杂呢?

多元类型参数

类型参数能够有多个,用不同的代号姓名并用逗号隔开就能够了,就比方哈希表:

class HashMap<K, V> { ... }

便是一个运用二元类型参数的类。

以上便是泛型的根底运用办法。

了解泛型的本质

通过以上的介绍能够得出泛型的底子意图是加强复用,让类和办法不受类型的约束,能够应用于任何类型,而且是以一种安全的办法,遭到编译器的支持。

泛型的优势

假如不用泛型,想要让类或许办法通用,即对任何目标都能生效,那只能把其参数的类型声明为顶层基类Object,然后在某些当地做手动类型转化(type casting)。很明显,这十分简单犯错,而且十分的不安全, 一旦某些当地忘记了查看,就会有运行时的类型转化异常(ClassCastException)。

运用了泛型后,编译器会协助我们对类型对待查看和主动转化,在完成代码复用的一起,又能确保运行时的类型安全,减少运行时的类型转化过错,所以我们应该尽或许多的运用泛型。

命名规范

虽然说参数化类型能够用任何姓名,但为了可读性仍是要遵从比较盛行的规范:

  • T 类型
  • E 调集里边元素的类型
  • K 哈希表,或许其他有键值的键的类型
  • V 哈希表中值的类型
  • N 数字类型
  • S, U, V等多元参数类型时运用

泛型高档特性

指定参数类型的边界

泛型在界说的时分用虚拟的类型表明参数化的类型,运用的时分传入具体的类型,但有些时分需求对能够传入的具体类型做约束,这时能够用类似<T extends Number>来约束能够运用的类型参数的边界(上界),这儿的Number能够是恣意已知的类型。而且与类的多继承规则相同,这儿能够指定多个类型上限,但只能有一个类且要放在最前面后边的只能是接口,用&来衔接,如<T extends ClassA & IfaceB & IfaceC>,比方:

class Calculator<T extends Number & Runnable & Closeable> {
    private T operand;
    public static <S extends Number & Runnable & Comparable> S plus(S a, S b) {
        //
    }
}

指定泛型中参数型的约束在实际项目中是很有用的,它能够加强代码复用,把一些公共的代码从子类中抽出来,比方像一个列表中的Item有不同的数据类型和不同的布局款式,常规的多态是说让每个子类去完成自己的布局款式,但假如共性太多,这时就能够在创建一个泛型的类或许办法来做,而这个类或许办法就能够指定基类作为泛型类型边界。这样能够加强代码的类型安全,防止调用者传入代码不认识和不能处理的参数类型。

边界通配符来完成协变与逆变

协变与逆变是用来描述目标的继承联络在运用这些目标为类型参数的泛型中的联络。比方说Dog是Animal的子类,那么运用这两个类型为参数的泛型目标之间的联络应该是会么呢?如List<Dog>是否也是List<Animal>的子类?Java中的泛型是不可变的Invariant,即泛型目标之间的联络与它们的类型参数之间的联络是没有联络的,即List<Dog>与List<Animal>之间没联络。

浅显易懂Java泛型

不可变Invariant是为了类型安全,编译器查看泛型类型参数有必要严格匹配,但在有些时分会带来极大的不方便,由于面向目标的两大基本特性继承和多态确保了子类目标能够当作其基类运用,换句话说能用Animal的当地,放一个Dog目标应该彻底合法。但由于泛型不可变,一个声明为addAll(List<Animal>)的办法,是没有办法传入List<Dog>的:

class Animal {}
class Dog extends Animal {}
class List<E> {
	private E[] items;
	private int size;
	public void addAll(List<E> b) {
		for (E x : b) {
			items[size++] = x;
		}
	}
	public void getAll(List<E> b) {
		for (E e : items) {
			b.add(e);
		}
	}
}
List<Animal> animals = new List<>();
List<Dog> dogs = new List<>();
animals.addAll(dogs); // compile error
dogs.getAll(animals); // compile error

但这其实是很安全的,由于我们把Dog从列表中取出,然后当作Animal运用,这是向上转型(Upcasting)是彻底安全的。但由于泛型是不可变的,编译器有必要要确保泛型的类型参数有必要彻底一致,因而会给出编译过错,但这显然不方便,会让泛型的作用大打折扣。再比方Object是一切目标的基类,但是当把Object作为类型参数时,这个泛型并不是其他泛型的父类,如List<String>并不是List<Object>的子类。

实际上这儿需求的是协变(Covariance)与逆变(Contravariance),也便是让运用类型参数的泛型具有其类型参数一致的继承联络,就要用到边界通配符(Bounded Wildcards)。一共有三种:

  • 上界进行协变Covariant,参数化类型<? extends T>表明能够是以T为基类的恣意子类类型,当然也包括T自身,泛型<S>会变成<? extends T>的子类,假如S是T的子类。
  • 下界进行逆变Contravariant,参数化类型<? super T>表明能够是T或许T的基类类型泛型<B>会变成<? super T>的基类,假如B是T的基类。
  • 无界,参数化类型<?>表明能够是任何类型,能够了解为泛型里的顶层基类(就像Object之于其他目标相同)。

运用边界通配符来修正上述:

class List<E> {
	public void addAll(List<? extends E> b) { ... }
	public void getAll(List<? super E> b) { ... }
}
List<Animal> animals = new List<>();
List<Dog> dogs = new List<>();
animals.addAll(dogs); // 0 warnings, 0 errors!
dogs.getAll(animals); // 0 warnings, 0 errors!

需求特别留意的是边界通配符处理的问题是协变与逆变,也即让两个泛型之间的联络与其参数类型保持一致,但具体的这一对类型参数仍能够是任何类型。这与前一末节讨论的参数类型边界是彻底不同的概念,不是同一码事儿,参数类型边界是约束运用泛型时能够传入的类型的约束。

边界通配符处理的是泛型之间的联络,每当需求进行协变与逆变的时分就需求用到通配符,以让代码更通用更合理。还需求特别留意的边界通配符只能用于办法的参数,大神Joshua Bloch在《Effective Java》中给出的主张通配符要用于办法的输入泛型参数,假如参数是生产者用extends(即从里边读取目标),假如是消费者用super(即往里边写数据)

运行时的泛型擦除

泛型是为了以类型安全的办法完成代码复用,但是在Java 1.5版别时引入的,为了保持向后兼容性,编译器会对泛型的类型信息进行擦除(type erasure),使其变成常规的目标,这样运行时(JVM)就不用处理新添加的类型了,保持了字节码的兼容性。比方List<String>与List<Integer>在运行时都变成了List目标,JVM并不知道它们的参数类型。泛型的类型参数查看,以及类型的转化都是发生在编译时,是编译器做的事情。

泛型擦除带来的一个问题便是泛型不能运用类型判别符(instanceof),以及不能进行强制类型转化,比方这样写是不合法的:

// Compile error: Illegal 	generic type for instanceof
if (list instanceof List<Dog>) {
	List<Dog> ld = (List<Dog>) list;
}

很显然,反射(Reflect)是彻底没有办法用泛型的,由于反射是在运行时,这时泛型都被擦除了。假如非要运用泛型,有必要要把其类型参数的Class传入作为参数(也即把T的具体参数的class目标传入如String.class),以此来区分不同的泛型,能够参阅泛型工厂办法的完成

Java泛型的问题

泛型不支持根底类型

Java为了效率和兼容性保留了根底数据类型,如int, boolean, float,但它们并不是目标。而泛型的类型参数有必要是目标,因而根底类型是不能用在泛型上面的,比方不能用List<int>,而只能用List<Integer>,好在有主动装箱autoboxinng和拆箱unboxing,所以List<Integer>也能够能够直接用于整数类型的。

泛型不支持数组

这儿的意思是指不能用泛型去声明数组,比方List<String>[],这是不允许的。(不要搞混混杂了,数组当作泛型的类型参数是彻底能够的,如List<int[]>,由于数组是一个类型。)

参阅资料

欢迎搜索并重视 大众号「稀有猿诉」 获取更多的优质文章!

原创不易,「打赏」「点赞」「在看」「收藏」「共享」 总要有一个吧!