泛型(Generics
),从字面的意思理解就是泛化的类型,即参数化类型。
我们都知道,泛型是JDK5
提供的一个非常重要的新特性,它有非常多优秀的品质
:能够把很多问题从运行期提前到编译器,从而使得程序更加的健壮。
但是因为Java5要保持良好的向下兼容性,所以从推出之际一直到现在,它都是个假东西
:只在编译期存在,编译成.class
文件后就不存在了,这就是所谓的泛型擦除
。
C++里的泛型是真实的,它通过类模版的概念去实现
泛型(generics),从字面的意思理解就是泛化的类型,即参数化类型。
请注意
参数化类型
和方法参数类型
的区别~
对比下面两个类,一个是普通类,一个是泛型类:
class Generics {
Object k;
Object v;
public Generics(Object k, Object v) {
this.k = k;
this.v = v;
}
}
class Generics<K, V> {
K k;
V v;
public Generics(K k, V v) {
this.k = k;
this.v = v;
}
}
泛型类的声明一般放在类名之后,可以有多个泛型参数,用尖括号括起来形成类型参数列表。
public interface Generator<T> {
public T next();
}
这种泛型接口设计,是声明的是一个工厂设计模式常用的生成器接口。比如我们常见的迭代器接口Iterable
就是这样一个接口
public interface Iterable<T> {
Iterator<T> iterator();
}
分为实例泛型方法和静态泛型方法。
public <T> T genericMethod(T t){
return t;
}
// 注意静态方法<T>必须是在static的后面~
public static <T> T genericStaticMethod(T t){
return t;
}
这里需要稍微注意一下:
public class Main<T> {
// 静态方法不能直接使用类的泛型参数T 而需要自己声明的
// 形如这样书写才正确:public static <T> T genericStaticMethod(T t) { ... }
public static T genericStaticMethod(T t) {
return t;
}
// 实例方法可以直接使用类声明的泛型参数
public T genericMethod(T t) {
return t;
}
}
静态方法使用泛型参数需要自己单独声明,否则编译报错。
泛型方法的声明和泛型类的声明略有不同,它是在返回类型之前
用尖括号列出类型参数列表(也可以有多个泛型类型),而函数传入的形参类型可以利用泛型来表示。
总结出如下几种情况,使用泛型的时候务必注意:
ArrayList<int>
这样是非法的,而只能ArrayList<Integer>
1. 请注意:数组表示中int[]
和Integer[]
都是可以的前面指出了,Java的泛型是假的,它是编译期的。下面通过一个示例证明这一点:
public class Main {
public static void main(String[] args) {
List<String> c1 = new ArrayList<>();
List<Integer> c2 = new ArrayList<>();
Class<? extends List> class1 = c1.getClass();
Class<? extends List> class2 = c2.getClass();
System.out.println(class1 == class2);
}
}
输出结果为:true
其实这里可能有不少小伙伴不能理解了,泛型中明明给它传的类型是不一样的,为何Class还是一样的呢???这里就要说到Java语言实现泛型所独有的——泛型擦除。
本例说明了:当我们声明List<String>
和List<Integer>
时,在运行时实际上是相同的,都是List,而具体的类型参数信息String和Integer被擦除了。(具体有兴趣的小伙伴可以去查看它的.class文件
内容(可反编译后查看对比))
由上可知,下面结果是:false
public static void main(String[] args) {
List<String> c1 = new ArrayList<>();
List<Integer> c2 = new LinkedList<>();
Class<? extends List> class1 = c1.getClass();
Class<? extends List> class2 = c2.getClass();
// 一个是ArrayList 一个是LinkedList 所以返回false
System.out.println(class1 == class2); // false
}
从上例可以知道,java的泛型擦除确实给实际编程中带来了一些不便(特别是运行时反射时,有时候无法判断出真实类型)。那Java的设计者为什么要这么干呢?
这是一个历史问题,Java在版本1.0(1.5之前)中是不支持泛型的,这就导致了很大一批原有类库是在不支持泛型的Java版本上创建的。而到后来Java逐渐加入了泛型,为了使得原有的非泛化类库能够在泛化的客户端使用,Java开发者使用了擦除进行了折中
(保持向下兼容)。
所以Java使用这么具有局限性的泛型实现方法就是从非泛化代码到泛化代码的一个过渡,以及不破坏原有类库的情况下,将泛型融入Java语言。
<? super T>
和<? extends T>
和<?>
关于通配符的使用,本文略~
如下使用案例,存在非常大的风险:
List list = new ArrayList();
list.add("1");
list.add(2);
List<String> list2 = list; //不报错 因为类型一样都是List类型
//报错 类型转换异常 这时候里面的元素一碰就报错
list2.forEach(x -> System.out.println(x));
如上由于第一个List没有加上泛型,使得它里面既装了String,又装了Integer,所以最后只要一get出来就报错了(迭代也不行)。
这个也是通过反射完成一些封装的框架,比如MyBatis、Redis序列化值、SpringMVC等处理入参的值普遍会遇到但是无法解决的问题
可变参数
作为入参的坑如下示例:
public class Main {
public static void main(String[] args) {
Integer[] ints1 = new Integer[]{1, 2, 3};
int[] ints2 = new int[]{1, 2, 3};
// 注意下面两种输出结果是不一样的
doSomething(ints1); //输出1,2,3
doSomething(ints2); //输出[I@1f32e575
}
// 静态泛型方法 需要自己申明泛型T
// 静态泛型方法 需要自己申明泛型T
private static <T> void doSomething(T... values) {
//boolean b = values instanceof Object; // 编译不抱错
//boolean b = values instanceof List; // 编译报错:不能转换为List类型
for (T value : values) {
System.out.println(value);
}
}
}
这个结果是由于int[]
这个数组最终就被当作一个数值作为入参了。
通过此例可以总结出如下两点:
Arrays.asList()
在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个List集合。但是很多小伙伴对它有点滥用,它的使用还是存在一些坑的,这里借助泛型,稍微总结一下:
// @since 1.2
public class Arrays {
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable {
...
// 来自父类 它自己并没有复写
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
...
}
}
这是asList()
方法的一个申明,可以看到它接收的也是一个可变参数,这么看来它和我们上面定义的doSomething(T... values)
方法何其相似,因此此处我就不再用代码示例了,总结出下面几条结论即可,具体缘由我建议小伙伴一定要做到心知肚明:
那么问题来了,如何正确的把数组Array转换为List呢???下面介绍几种常用方案: 方案一:自己for循环实现 代码略 方案二:最简便的方法(推荐)
List list = new ArrayList<>(Arrays.asList("a", "b", "c"))
方案三:使用 Java8 的Stream(推荐)
public static void main(String[] args) {
Integer[] myArray = {1, 2, 3};
List<Integer> myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本类型也可以实现转换(依赖boxed的装箱操作) //boxed()是IntStream类的方法
int[] myArray2 = {1, 2, 3};
myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
System.out.println(myList); //[1, 2, 3]
}
方案四:使用 Apache Commons Collections
public static void main(String[] args) {
Integer[] myArray = {1, 2, 3};
List<Integer> list = new ArrayList<>();
CollectionUtils.addAll(list, myArray);
System.out.println(list); //
}
它的addAll()是个重载方法,数组List都行。它内部其实就是把myArray遍历了一遍而已,因此int或者Integer数组都无所谓~
注意:
java.util.Collections
可以没有这个能力把Array转为List,但是它的singleton(T o)
等方法也是非常好用的,但也是只读视图哦~
Java的泛型一直是被吐槽的对象,它确实很渣不好用,容易出错。但是存在即合理,你不能改变它那请好好接受它。 编码的时候遵循一个原则:该写泛型的地方务必写上泛型,能使得你对泛型的理解更加的深刻。这是一个非常良好的编码习惯~