1.泛型
Java5开始提供的新特性,表示不确定的类型
注意:泛型是提供给javac编译器使用的,它用于限定集合的输入类型,让编译器在源代码级别上,即挡住向集合中插入非法数据。但编译器编译完带有泛形的java程序后,生成的class文件中将不再带有泛形信息,以此使程序运行效率不受到影响,这个过程称之为“擦除”。泛型也可以用在方法上或者是类上。
1.1、集合泛型
class Person{}
class Student extends Person{}
public class Demo1 {
public static void main(String[] args) {
List<String> list1= new ArrayList<String>();
List list2 = new ArrayList();
List list3 = new ArrayList<String>();//正确的
method1( new ArrayList<String>());
List<String> list4 = new ArrayList();//正确的
List<String> list41 = method2();
List<Person> list5 = new ArrayList<Student>();//错误的
List<Student> list6 = new ArrayList<Person>();//错误的
}
关于集合泛型总结:要么两边都没有;或者一边有,一边没有;如果两边都有时,那么要保存“一致”。
1.2自定义的泛型
- 分为:方法上的泛型和类上的泛型
1.2.2、方法上的泛型
我们来考虑这样一个例子:
上帝(神)能够杀死任何生物,我们设计方法的需要为每一个不同的生物重载相似的方法,这样的话需要我们编写非常多的类。思考是否可以设计一个通用的方法:
public Object kill(Object obj){
System.out.println("上帝杀死了"+obj);
return obj;
}
该方法的优点是所有的生物一个方法搞定。
缺点:每次调用该方法还需要强制类型转换。使用不方便。
可以使用泛型:
public <T> T kill(T t){
System.out.println("上帝杀死了"+t);
return t;
}
调用使用方向的方法,不要再强制类型转换了。
*泛型定义: 一个大写的英文字母,一般使用使用T,集合泛型一般使用E、K、V
在修饰符和返回值类型之间定义泛型: <一个大写的英文字母>
定义在方法上的泛型只能在方法签名和方法的内容有效;出了该方法将无效。在方法上使用泛型要注意:需要先定义,再使用。*
例如:
public <T> T kill(T t){
System.out.println("上帝杀死了"+t);
return t;
}
public <T> T save(T t){
System.out.println("上帝救活了"+t);
return t;
}
public static void main(String[] args) {
God god = new God();
Person person = god.kill(new Person());
god.save(person);
god.save(new Dog());
}
1.2.2、类上的泛型
定义在类型的泛型需要先定义在使用。
类上的泛型的定义在类名后面。
定义泛型时通常一个大写的英文字母(sun公司推荐使用T)。
定义在类上泛型有效范围是在当前类的内部有效。
定义在类上的泛型不能在静态方法上使用,所以静态方法上要想使用泛型,需要自己定义。
public class God<T> {
public T kill(T t){
System.out.println("上帝杀死了"+t);
return t;
}
public T save(T t){
System.out.println("上帝救活了"+t);
return t;
}
public static void main(String[] args) {
God<Person> god1 = new God<Person>();
Person person = god1.kill(new Person());
Person person2 = god1.save(person);
}
}
1.3、泛型通配符
因为泛型没有继承关系,所以当需要用一个泛型引用引用不同的泛型实现时,泛型中写他们共同的父类是不行的,这时该怎么做呢?引入一个新的概念,叫做泛型通配符?,注意泛型通配符只能用在泛型引用中,用来引用不同的泛型实现,不能出现在实现中.
问题:该方法只能打印保存了String对象的集合,不能打印其它集合。通配符用于解决此类问题,方法的定义可改写为如下形式:
void print (Collection<?> c){
//Collection<?>(发音为:"collection of unknown")
for (Object e : c) {
System.out.println(e);
}
}
总结:使用?通配符主要用于引用对象,使用了?通配符,就只能调对象与类型无关的方法,不能调用对象与类型有关的方法。
*泛型的边界:
如果没有指定泛型默认可以接受任意的类型,有时希望进一步限制,此时可以使用泛型的边界:
限定通配符的上边界:
extends - 用来指定泛型的上边界,使用在泛型的通配符中和泛型定义中,指定具体的泛型实现必须是指定的类或其子类.
坏处是,在传入对象时,只能传入null
好处是,获取到泛型的对象时,可以调用上边界的方法.
正确:Vector<? extends Number> x = new Vector<Integer>();
错误:Vector<? extends Number> x = new Vector<String>();
class Ani{
public void sayx(){
}
public void say(){
}
}
class Person extends Ani{
public void say(){
}
}
class Teacher extends Person{
public void say() {
}
public void teach(){
}
}
class SomeC<T extends Person>{
public void method1(T t){
t.say();
}
}
public class Demo {
public static void print(List<? extends Person> list){
for (Person person : list) {
person.say();
}
}
public static void main(String[] args) {
Person p = new Person();
/*List<?> list0 = new ArrayList<Teacher>();
list0.get(0);*/
//--上边界:指定泛型必须是某个类或其子孙类
List<? extends Person>list1 = null;
list1 = new ArrayList<Teacher>();
//好处是,获取到泛型类型后,可以直接调用上边界的方法,
//坏处是,当进行传入操作时,只能传入null,其他的值不能传入
//list1.get(0).say();
/*list1.add(new Object());
list1.add(new Ani());
list1.add(new Person());
list1.add(new Teacher());*/
//以上几种添加都不行,只能添加null
list1.add(null);
//Object和Animal不可以添加很好理解,为什么不能添加Person和Teacher类
//?可以是Person、Teacher、Student、Doctor...所以没有办法确定具体是哪个,
//那么上边界还有什么用处?常见用处:1、方法形参
List<Teacher> tealist = new ArrayList<Teacher>();
tealist.add(new Teacher());
tealist.add(new Teacher());
print(tealist);
List<Person> psonList = new ArrayList<Person>();
psonList.add(new Person());
psonList.add(new Person());
print(psonList);
List<Ani> aniList = new ArrayList<Ani>();
//print(aniList);//无法使用
//上边界的使用场景2 class SomeC<T extends Person>{}
SomeC<Person> sc1 = new SomeC<Person>();
SomeC<Teacher> sc2 = new SomeC<Teacher>();
super - 用来指定泛型的下边界,使用在泛型的通配符中,指定具体的泛型实现必须是指定类或其超类.
好处是,可以传入对象时,可以传入下边界的子孙类对象
坏处是,获取到泛型对象时,只能调用Object身上的方法
//-泛型的下边界,用来指定泛型是某个类型或其祖先类型(超类),
//好处是,可以传入指定类的子孙对象,坏处是获取出来后只能当作Object使用
List<? super Person>list2 = null;
list2 = new ArrayList<Ani>();
list2.add(new Person());
list2.add(new Teacher());
public class DemoSuper {
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
public static void main(String[] args) {
List<Object> list1 = new ArrayList<Object>();
addNumbers(list1);
System.out.println(list1);
List<Number> list2 = new ArrayList<Number>();
addNumbers(list2);
System.out.println(list2);
List<Double> list3 = new ArrayList<Double>();
addNumbers(list3); // 编译报错
}
}
我们看到,List<? super E>
是能够调用add方法的, 因为我们在addNumbers所add的元素就是Integer类型的, 而传入的list不管是什么, 都一定是Integer或其父类泛型的List, 这时add一个Integer元素是没有任何疑问的. 但是, 我们不能使用get方法, 请看如下代码:
public static void getTest2(List<? super Integer> list) {
// Integer i = list.get(0); //编译报错
Object o = list.get(1);
}
这个原因也是很简单的, 因为我们所传入的类都是Integer的类或其父类, 所传入的数据类型可能是Integer到Object之间的任何类型, 这是无法预料的, 也就无法接收. 唯一能确定的就是Object, 因为所有类型都是其子类型.
总结:
“in out”原则, 总的来说就是:
in就是你要读取出数据以供随后使用(想象一下List的get), 这时使用extends关键字, 固定上边界的通配符. 你可以将该对象当做一个只读对象;
out就是你要将已有的数据写入对象(想象一下List的add), 这时使用super关键字, 固定下边界的通配符. 你可以将该对象当做一个只能写入的对象;
当你希望in的数据能够使用Object类中的方法访问时, 使用无边界通配符;
当你需要一个既能读又能写的对象时, 就不要使用通配符了.