java的泛型编程的笔记
java5开始引入了参数化类型,使用起来和c++的模板有一些相似之处。java7开始,可以不必在构造器中的尖括号中带类型信息,编译器可以推断出来,即可以这样用:
List books = new ArrayList<>();
这里用了List
接口声明了一个变量,也可以用ArrayList
声明变量,效果是一样的,后面的是前面的实现类。接口经常这么用。
java可以为任何接口、类增加泛型声明。为类增加泛型声明时,其构造器名称还是类名,不需要加泛型声明,而调用时可以传入类型参数:
public class Apple<T>
{
// 使用T类型形参定义实例变量
private T info;
public Apple(){}
// 下面方法中使用T类型形参来定义构造器
public Apple(T info)
{
this.info = info;
}
public void setInfo(T info)
{
this.info = info;
}
public T getInfo()
{
return this.info;
}
public static void main(String[] args)
{
// 由于传给T形参的是String,所以构造器参数只能是String
Apple a1 = new Apple<>("苹果");
System.out.println(a1.getInfo());
// 由于传给T形参的是Double,所以构造器参数只能是Double或double
Apple a2 = new Apple<>(5.67);
System.out.println(a2.getInfo());
}
}
这里,用了概述中说的省略语法。
创建泛型类或接口后,可以为该接口创建实现类、或从该父类派生子类。注意,不能用一个非泛型类去继承一个泛型类或实现一个泛型接口。要么给继承的泛型类传入实际类型,要么子类也是泛型类。
public class A1<T> extends Apple<T>;
public class A1 extends Apple<String>;
上面两种都可以。
public class A2 extends Apple;
其实,这一种也可以。严格的泛型代码中,泛型类总应该带着类型参数。如果没有带类型参数,默认是声明该类型参数时指定的第一个上限类型。即这样做的后果是,编译器把T当做Object处理。
不管为泛型类的类型形参传入哪一种实际类型,依然是同一个类,在内存中也只有一块内存空间。所以,静态方法、静态初始化块或静态变量的声明和初始化中不允许用类型参数。
因为并不会真正生成类,所以instanceof
运算符后面也不能用泛型类。
首先说明一下一个定义:
如果A是B的子类型(子类或者子接口),而G是具有泛型声明的类或接口,G并不是G的子类型!但是对于数组来说,A[]仍然是B[]的子类型。
类型通配符是一个?
,表示可以匹配任何类型。
注意,java集合中,类型必须是一致或相容的(加入的是声明类型的子类型),否则不能被放入集合中。因此,类型通配符的主要作用在于匹配任意类型,但是不知道匹配到的具体类型是啥。
类型通配符可以指定上下限。
public abstract class Shape
{
public abstract void draw(Canvas c);
}
public class Circle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("在画布上画一个圆");
}
}
public class Rectangle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("把一个矩形画在画布上");
}
}
public class Canvas
{
// // 同时在画布上绘制多个形状
// public void drawAll(List shapes)
// {
// for (Shape s : shapes)
// {
// s.draw(this);
// }
// }
// public void drawAll(List> shapes)
// {
// for (Object obj : shapes)
// {
// Shape s = (Shape)obj;
// s.draw(this);
// }
// }
// 同时在画布上绘制多个形状,使用被限制的泛型通配符
public void drawAll(List extends Shape> shapes)
{
for (Shape s : shapes)
{
s.draw(this);
}
}
public static void main(String[] args)
{
List circleList = new ArrayList();
Canvas c = new Canvas();
Circle d=new Circle();
circleList.add(d);
c.drawAll(circleList);//这里Circle是Shape的子类型,所以可以通过。
}
}
输出:
在画布上画一个圆
这里,?表示任意类型,但是必须是Shape的子类型或自身。
public class MyUtils
{
// 下面dest集合元素类型必须与src集合元素类型相同,或是其父类
public static T copy(Collection super T> dest, Collection src)
{
T last = null;
for (T ele : src)
{
last = ele;
dest.add(ele);
}
return last;
}
public static void main(String[] args)
{
List ln = new ArrayList<>();
List li = new ArrayList<>();
li.add(5);
// 此处可准确的知道最后一个被复制的元素是Integer类型
// 与src集合元素的类型相同
Integer last = copy(ln , li); // ①
System.out.println(ln);
}
}
输出[5]
这里,super表示了下限。另外,此处还用了泛型方法。
不仅可以为类型通配符指定上下限,类型参数也可以。
public class Apple<T extends Number>//这里指定类型参数必须是Number或其子类
{
T col;
public static void main(String[] args)
{
Apple ai = new Apple<>();
Apple ad = new Apple<>();
// 下面代码将引起编译异常,下面代码试图把String类型传给T形参
// 但String不是Number的子类型,所以引发编译错误
Apple as = new Apple<>(); // ①
}
}
更极端的情况下,可以至多一个父类上限,多个接口上限:
public class Apple<T extends Number & java.io.Serializable>
通配符相当于一把万能钥匙,但是匹配到啥类型自己也不知道。泛型方法有点像类型参数一般,语法如下:
修饰符<T,S> 返回值类型 方法名(形参列表)
和普通方法比只是多了类型参数声明。
public class GenericMethodTest
{
// 声明一个泛型方法,该泛型方法中带一个T类型形参,
static void fromArrayToCollection(T[] a, Collection c)
{
for (T o : a)
{
c.add(o);
}
}
public static void main(String[] args)
{
Object[] oa = new Object[100];
Collection
方法中的泛型参数不必显式指定,可以由编译器推断出。
泛型方法和通配符主要区别在于:
如果一个形参类型或返回值的类型依赖于另一个形参的类型,那么就应该用泛型方法。因为通配符不知道自己匹配了啥。
一般可以通用混用:
public interface Collection<E>
{
boolean containsAll(Collection> c);
boolean addAll(Collection extends E> c);
}
public interface Collection<E>
{
boolean containsAll(COllection c);
boolean addAll(Collection c);
}
class Foo
{
public Foo(T t)
{
System.out.println(t);
}
}
public class GenericConstructor
{
public static void main(String[] args)
{
// 泛型构造器中的T参数为String。
new Foo("疯狂Java讲义");
// 泛型构造器中的T参数为Integer。
new Foo(200);
// 显式指定泛型构造器中的T参数为String,
// 传给Foo构造器的实参也是String对象,完全正确。
new Foo("疯狂Android讲义");
// 显式指定泛型构造器中的T参数为String,
// 但传给Foo构造器的实参是Double对象,下面代码出错
//new Foo(12.3);
}
}
如上,可以定义泛型构造器,使用时不仅可以显示指定还可以让编译器推断。
class MyClass
{
public MyClass(T t)
{
System.out.println("t参数的值为:" + t);
}
}
public class GenericDiamondTest
{
public static void main(String[] args)
{
// MyClass类声明中的E形参是String类型。
// 泛型构造器中声明的T形参是Integer类型
MyClass mc1 = new MyClass<>(5);
// 显式指定泛型构造器中声明的T形参是Integer类型,
MyClass mc2 = new MyClass(5);
// MyClass类声明中的E形参是String类型。
// 如果显式指定泛型构造器中声明的T形参是Integer类型
// 此时就不能使用"菱形"语法,下面代码是错的。
// MyClass mc3 = new MyClass<>(5);
}
}
如上,当显式指定了泛型构造器实际类型后,不能再用前面的简写法。