Java泛型(重点)
这些时候科研任务比较重,没啥时间学Java,慢慢来加油!
为什么会出现泛型?
最让人值得注意的一个原因就是:为了能够更好地创建容器类。 -------《Java编程思想》
有些类似C++中的Templates(模板),实际思想就是:参数化类型。
通俗解释什么是参数化类型和泛型的目的
参数化类型的意思就是将类型当做一种参数来对待,具体类型(如int、double)就是该参数的值。
泛型的出现避免了强转的操作,在编译器完成类型转化,也就避免了运行时错误出现。
注:意思就是说如果不用泛型,那么开发者必须要知道实际参数类型,不然的话进行强转可能会出现错误,而这种错误编译的时候没有问题,因为Object可以转为任何类型,但是转错了之后后续程序在运行司就会出现问题。
从某种意义上来说,泛型也算是一种语法糖(就是使用泛型对程序功能本身并没有什么影响,只是方便了程序员进行代码编写)
JDK1.5增加了泛型,在很大的程度上方便了在集合上的使用。
public class Test_Template {
//不使用泛型
public static void main(String[] args) {
List list = new ArrayList();
list.add(592);
list.add("amyth");
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
}
}
因为list是Object类型的,所以int string可以放入也可以取出,所以里边有不同类型的数据进行强转的时候就会报错
//使用泛型
public static void main(String[] args) {
List<String> list = new ArrayList();
list.add("123");
list.add("amyth");
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
}
泛型的使用方式
-
泛型类:将泛型定义到类上
public class 类名 <泛型类型1,...> { }
注意事项:泛型类型必须是引用类型(非基本数据类型)
-
泛型方法
泛型方法概述:把泛型定义在方法上
定义格式:
package com.amyth.JavaEE; class Demo{ public <T> T fun(T t){ // 可以接收任意类型的数据 return t ; // 直接把参数返回 } }; public class Test_Template2{ public static void main(String args[]){ Demo d = new Demo() ; // 实例化Demo对象 String str = d.fun("汤姆") ; // 传递字符串 int i = d.fun(30) ; // 传递数字,自动装箱 System.out.println(str) ; // 输出内容 System.out.println(i) ; // 输出内容 } };
-
泛型接口
泛型接口概述:把泛型定义在接口
定义格式:
public interface 接口名<泛型类型> {
}
实例:
/**
* 泛型接口的定义格式: 修饰符 interface 接口名<数据类型> {}
*/
public interface Inter<T> {
public abstract void show(T t) ;
}
/**
* 子类是泛型类
*/
public class InterImpl<E> implements Inter<E> {
@Override
public void show(E t) {
System.out.println(t);
}
}
Inter<String> inter = new InterImpl<String>() ;
inter.show("hello") ;
源码中泛型的使用:List接口和ArrayList类的源码实现
//定义接口时指定了一个类型形参,该形参名为E
public interface List<E> extends Collection<E> {
//在该接口里,E可以作为类型使用
public E get(int index) {}
public void add(E e) {}
}
//定义类时指定了一个类型形参,该形参名为E
public class ArrayList<E> extends AbstractList<E> implements List<E> {
//在该类里,E可以作为类型使用
public void set(E e) {
.......................
}
}
父类派生子类的时候不能包含类型形参,需要传入具体的类型
意思就是继承带有泛型的父类的时候,要指明具体的类型
public class A extends Container<Integer, String> {}
也可以不指定具体的类型,系统默认为Object类型
public class A extends Container {}
泛型构造器
- 构造器也是一种方法,所以也就产生了所谓的泛型构造器。
- 和使用普通方法一样没有区别,一种是显示指定泛型参数,另一种是隐式推断
public class Person {
public <T> Person(T t) {
System.out.println(t);
}
}
public static void main(String[] args) {
new Person(22);// 隐式
new <String> Person("hello");//显示
}
特殊说明:
如果构造器是泛型构造器,同时该类也是一个泛型类的情况下应该如何使用泛型构造器:因为泛型构造器可以显式指定自己的类型参数(需要用到菱形,放在构造器之前),而泛型类自己的类型实参也需要指定(菱形放在构造器之后),这就同时出现了两个菱形了,这就会有一些小问题,具体用法再这里总结一下。 以下面这个例子为代表
public class Person<E> {
public <T> Person(T t) {
System.out.println(t);
}
}
public static void main(String[] args) {
Person<String> person = new Person("sss");
} //当程序显示指定了泛型构造器中声明的形参的实际类型,则不可以使用菱形语法
Person<String> person = new <Integer>Person<>("sss"); //这就是错误用法
通配符
常用的T,E,K,V,?:
本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?是这样约定的:
- ?表示不确定的 java 类型
- T (type) 表示具体的一个java类型
- K V (key value) 分别代表java键值中的Key Value
- E (element) 代表Element
数组协变
子类对象数组可以向上转型为父类对象数组的引用。由于java里的数组在初始化后一定会记住元素的类型,虽然数组协变会带来一些问题(下例就会演示),但有了数组的运行时元素插入的类型检查保护,使得造成的问题不会那么严重。所以即使数组可以协变,它也是足够安全的。
Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
//Jonathan[] jonathans = (Jonathan[])fruit;//运行时报错ClassCastException: [LApple; cannot be cast to [LJonathan;
Apple[] apples = (Apple[])fruit;
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
// Runtime type is Apple[], not Fruit[] or Orange[]:
try {
// Compiler allows you to add Fruit:
fruit[0] = new Fruit(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
try {
// Compiler allows you to add Oranges:
fruit[0] = new Orange(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
}
Fruit[] fruit = new Apple[10]此句便是数组协变的表现。
Jonathan[] jonathans = (Jonathan[])fruit,数组强转为其他数组和正常的强转表现一样,jvm会检测对象的真实类型,从而判断是否可以强转,所以此句报错ClassCastException。Apple[] apples = (Apple[])fruit同理,根据真实类型,此句强转是可以的。
由于fruit这个引用的类型是Fruit[],所以可以数组的各个元素赋值以Fruit或者Fruit的子类;但由于fruit这个引用它引用的对象的真正类型是Apple[],当在赋值的时候,数组运行时的检测会判断赋值进来的对象的类型是否正确,当赋值类型不符合真正类型时,报错ArrayStoreException。
泛型擦除
指定泛型类型的集合在编译成功之后,运行时并不会带有指定的类型信息
public class GenericTest {
public static void main(String[] args) {
new GenericTest().testType();
}
public void testType(){
ArrayList<Integer> collection1 = new ArrayList<Integer>();
ArrayList<String> collection2= new ArrayList<String>();
System.out.println(collection1.getClass()==collection2.getClass());
//两者class类型一样,即字节码一致
System.out.println(collection2.getClass().getName());
//class均为java.util.ArrayList,并无实际类型参数信息
}
}
true
java.util.ArrayList
运行时分为无限制擦除和有限制擦除,没有设置上限的时候就是无限制擦除,运行时泛型类型用Object代替,有上限的擦除就会运行时用上限类型代替泛型。
接口实现类泛型擦除时编译器还会默认产生一个桥接方法
- 分析:
- 这是因为不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一类处理,在内存中也只占用一块内存空间。从Java泛型这一概念提出的目的来看,其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
- 在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类
对比之前的数组协变,你却发现List flist = new ArrayList()这样的行为都无法通过编译。之所以java这么设计,是因为泛型不像数组,泛型没有内建的协变类型。具体地说,类型信息在编译过后就被类型擦除掉了,运行时也就没法检查了(不像数组,有运行时的插入检查)
上界通配符 <? extends T>
与下界通配符<? super T>
上界通配符的意思就是,所指定的形参T类型就是形参的类型上界(包含本身),意思就是这里传入的类型实参只能是形参本身或者形参类型的子类。(运行时类的信息可以用反射拿到)
为了获得泛型类的“协变”,可以将引用类型设为? extends 类型
:
List<? extends Apple> extendsList = new ArrayList<Apple>();
extendsList = new ArrayList<Jonathan>();
//extendsList = new ArrayList<Fruit>();//编译报错
有得必有失,虽然上一章的例子让引用获得协变和逆变的效果,但这会对泛型类的读写操作产生限制。
对于<? super 类型>,编译器将只允许写操作,不允许读操作。即只可以设值(比如set操作),不可以取值(比如get操作)。
对于<? extends 类型>,编译器将只允许读操作,不允许写操作。即只可以取值,不可以设值。
以上两点都是针对于源码里涉及到了类型参数的函数而言的。比如对于List而言,不允许的写操作有add函数,因为它的函数签名是boolean add(E e);,此时这个形参E就变成了一个涉及了通配符的类型;而不允许的读操作有get函数,因为它的函数签名是E get(int index);,此时这个返回值E就变成了一个涉及了通配符的类型。
注:泛型表面上还是容易理解,但有很多容易忽视的细节,目前也只是粗粗学习一下,以后找工作前再补补!