泛型
简单泛型
让一些类方式适用于所有类型,而不是特定的类和接口。
应用:
元祖
问题:
一个方法只能返回一个新的对象,如果想要返回对个对象,只能再包装成一个新对象。每次需要都需要这样处理。
更好的解决办法:使用泛型元祖1
2
3
4
5
6
7
8
9
10
11
12
13public class TwoTuple<A, B> {
public final A first;
public final B second;
public TwoTuple(A a, B b) {
first = a;
second = b;
}
public String toString() {
return "(" + first + ", " + second + ")";
}
}
这是一个二元元祖
注意:使用final,只能一次赋值,之后就只能读。(符合JAVA编程的安全性原则)
增加“元”可以使用继承来实现。
堆栈类
1 | public class LinkedStack<T> { |
最初的一个Node 叫做哨兵节点,用来判空。
泛型接口
在声明接口的时候,加上泛型声明
来看一个具体的应用:1
2
3public interface Blacksmith {
Weapon manufactureWeapon(WeaponType weaponType);
}
这是之前讲过的工厂方法,上面是一个可以制作武器的工厂。
使用泛型可以做一个生成器,这是工厂方法的一个应用:1
2
3public interface Generator<T> {
T next();
}
泛型方法
泛型可以只用在方法上,无论是不是泛型类。
泛型方法的思想是:可以简化和类型的耦合关系,把编程的重心点放到逻辑上!并且可以让这个逻辑适用于所有类型!很强大!
注意点:尽量使用泛型方法来代替泛型类
什么是类型推断?
能通过参数的类型或者返回的结果类型判断出泛型的具体类型。
用法:1
2
3
4
5
6
7
8public class New {
public static <K,V> Map<K,V> map() {
return new HashMap<K,V>();
}
public static void main(String[] args) {
Map<String, List<String>> sls = New.map();
}
}
类型判断只在赋值操作的时候工作,其它时候是无效的,如下:1
2
3
4
5
6
7public class LimitsOfInference {
static void f(Map<Person, List<? extends Pet>> petPeople) {}
public static void main(String[] args) {
//f(New.map()); // Does not compile
}
}
这时候无法做类型判断,为了解决这个问题,需要显示的声明类型,如下:1
2
3
4
5
6public class ExplicitTypeSpecification {
static void f(Map<Person, List<Pet>> petPeople) {}
public static void main(String[] args) {
f(New.<Person, List<Pet>>map());
}
}
这样做似乎没有什么好处。
泛型方法的应用?
还是那句话:尽量使用泛型方法来代替泛型类
- 利用生成器和泛型方法,编写Collection快速填充方法
1 | public class Generators { |
还有一个麻烦,每一个Collection
解决方案:提供一个基本的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14public class BasicGenerator<T> implements Generator<T> {
private Class<T> type;
public BasicGenerator(Class<T> type){
this.type = type;
}
public T next() {
try { // Assumes type is a public class:
return type.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
- 一个更加通用的类生产方法:
1 | public class BasicGenerator<T> implements Generator<T> { |
- 使用泛型方法来优化元祖类的
1 | public class Tuple { |
擦除
什么是擦除?
在运行时期,即便通过反射,也只是获取类型参数标志符号和泛型类型边界的信息,无法获取实际的类型参数。
擦除给泛型带来的影响有?
C++中的泛型:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17template<class T> class Manipulator {
T obj;
public:
Manipulator(T x) { obj = x; }
void manipulate() { obj.f(); }
};
class HasF {
public:
void f() { cout << "HasF::f()" << endl; }
};
int main() {
HasF hf;
Manipulator<HasF> manipulator(hf);
manipulator.manipulate();
};
Manipulator
再来看JAVA:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class HasF {
public void f() {
System.out.println("HasF.f()");
}
}
class Manipulator<T> {
private T obj;
public Manipulator(T x) {
obj = x;
}
// Error: cannot find symbol: method f():
public void manipulate() {
obj.f();
}
}
会报错,因为编译器无法通过Manipulator
若要解决这个问题,要使用extends来限定边界:1
2
3
4
5class Manipulator2<T extends HasF> {
private T obj;
public Manipulator2(T x) { obj = x; }
public void manipulate() { obj.f(); }
}
所谓擦除,实际上就是编译器会把上面的代码变为下面这样:1
2
3
4
5class Manipulator2 {
private HasF obj;
public Manipulator2(HasF x) { obj = x; }
public void manipulate() { obj.f(); }
}
这时候就不会报错了。
但是这样一来,我们完全没有看出泛型带来的作用,因为多态也有一定的泛化作用。
因此,我们可以说:当希望一段代码作用不同的基类,泛型才有更多的意义。
除此之外,泛型还是有类型检查和自动类型转换的功能的。
擦除设计的由来?
JAVA的一个核心理念就是版本兼容,因此,为了使JAVA5之前的版本代码也能完好的执行,JAVA让擦除成为一个解决方案。1
2
3
4
5
6
7
8
9
10
11
12
13
14//-----JAVA5后更新的泛型类
class GenericBase<T> {
private T element; public void set(T arg) {
arg = element;
}
public T get() {
return element;
}
}
//-----JAVA5后的类库
class Derived1<T> extends GenericBase<T> {}
//-----JAVA5前的类库,完美兼容
class Derived2 extends GenericBase {} // No warning
如何理解JAVA泛型的作用?
即使由于擦除,JAVA的泛型并没有传统上的那么有效,但是还有有一些优点的。
所有泛型的动作都发生在边界(进入和离开泛型)处,对传进来值进行额外的编译器检查,对传出去的值进行额外的转型操作。
擦除的补偿措施
由于编译时期的擦除,导致在运行时期无法获取到真正的类型信息,也就是说通过T获取到真正的.class文件(Class对象)。
因此如果是要执行一些需要检查Class对象的方法,就无法通过。譬如:1
2
3
4
5
6
7
8
9
10public class Erased<T> {
private static final int SIZE = 100;
public void f(Object arg) {
if (arg instanceof T) {}// Error
T var = new T();// Error
T[] array = new T[SIZE];// Error
T[] arrays = (T) new Object[SIZE];// Unchecked warning
}
}
这些方法在初始化阶段需要使用到.class文件,但是因为擦除,找不到了。
补偿措施:
- 通过构造器传入Class对象,使得可以获取到累类型信息,从而通过newInstance来创建一个对象。
- 使用ArrayList代替数组。
一个比较奇怪的现象是:1
T var = (T)new Object();
这种情况编译器又是允许的,为什么?
我的想法是,这其实等价于1
Object var = (Object)new String();
依然是没有任何泛型的信息,所以是不会报错的。
泛型和数组的协变性分析
数组是协变的:1
Object[] obj[] = new Integer[];
泛型不是协变的:1
List<Object> oList = new ArrayList<Integer>()[];
泛型的出现远晚于数组。
为什么数组要设计成协变的?
数组的协变性更加适应具有多态特性的编程。1
2
3
4
5
6
7
8
9puilic boolen equle(Object[] objs1,Object[] objs2){
for(Object obj1:objs1){
for(Object obj2:objs2){
if(obj1.equel(obj2)) {
return false;
}
}
}
}
上面用数组作为参数的方法可以适用于所有的数组。
为什么泛型不是协变的?
协变性会破坏数组的安全性:1
2Object[] obj[] = new Integer[];
obj[0] = new String();
为了避免泛型出现这种安全性,因而将泛型设计成不协变的。(这里不分析由强制改变的任何协变关系,因为那是强转带来的问题。)
数组和泛型不能很好的共存,也是因为担心数组的协变性影响了泛型的非协变性:1
2
3
4
5
6List<String>[] lsa = new List<String>[10]; // 假设这一步是可行的!!!
Object[] oa = lsa; // OK because List<String> is a subtype of Object
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[0] = li;
String s = lsa[0].get(0);
使用通配符的意义是?
但是不协变写就意味着不支持多态的编程,而协变又会出现安全的问题,怎么办?
通配符正是为了解决这个问题而出现的:
泛型通配符支持协变:1
List<? extends Object> list = new ArrayList<Integer>()[];
为了解决安全问题:什么都不让存!
因为取的话至少只能用Object来接受,是安全的,所以可以取。
使用泛型和使用原生类型有什么区别?
使用原生类型意味着兼容各种版本,因此会失去所有的编译时期检查,容器中的类型都是Object,也就不存在任何的限制了。
对于老版本的一些类库,必然存在使用原生类型作为参数的一些方法,如下:
1 | static void oldStyleMethod(List probablyDogs) { |
当我们传入泛型容器时,原生容器必然会破坏我们的泛型容器,怎么办?
JAVA考虑到了这种问题,因此定义了一些可以防止破坏的容器(可以进行动态类型检查的容器)。
使用方法:1
2
3
4
5try{
oldStyleMethod(Collections.checkedList(new ArrayList<Dog>(), Dog.class));
}catch(Exception e){
System.out.println(e);
}
同样的还有checkedMap,checkedSet,checkedSortedMap,checkedSortedSet。
使用泛型会出现哪些问题?
- 基本类型并不能作为类型参数
1 | ArrayList<int> |
可以代码中依然可以看到ArrayList<int>这样的写法,为什么?
这是由于 JAVA的自动包装机制,实际上自动转换成ArrayList<Integer>
由于自动包装机制或多或少会影响一点性能,如果考虑性能问题,就需要使用一些其他的一些开源的适用于基本类型的容器版本。
自动包装机制解决所有问题了吗?
并没有,自动包装机制并不会应用到数组上,所以使用int i来遍历Integer的数组的时候会出错。
- 不能实现同一类型参数接口的两个变体
1 | interface Payable<T> {} |
这样有什么问题吗?
一旦基类指定了某个特定的泛型接口,它的子类都必须实现同样的接口。
一个问题的场景是:1
2
3
4
5
6
7public class ComparablePet implements Comparable<ComparablePet>
{
public int compareTo(ComparablePet arg)
{
return 0;
}
}
pet能和其它的pet比
但是
1 | public class ComparableCat extends ComparablePet implements Comparable<ComparableCat> |
上诉代码无法编译
但是pet的子类cat也是和其它的pet比(应该只能和其它cat比才对)
- 使用带有泛型类型参数的实例类型强制转换不会产生任何效果。
1 | List<Shape> shapes = (List<Widget>)in.readObject(); |
why?
因为泛型参数会被擦除到边界,最终无法得知它具体的类型,即便强转也只能到边界,而无法得知本质的类型,因此会得到一个警告。
- 使用不同泛型类型参数的类型来重载方法无法编译
1 | public class UseList<W,T> { |
why?`
因为擦除,不同泛型类型参数的类型在重载时会被认为没有任何区别。
自限定的类型
在上面,我们遇到过一种情况:自定义的一个类,继承了一个基类,与此同时,这个基类的类型参数是这个自定义的类。
1 | public class ComparablePet implements Comparable<ComparablePet> |
这是个循环泛型,如何理解这个现象?
先看一个简单的版本:
上诉基类为什么要把自定义的类(这里称为派生子类)作为类型参数值?
首先我们知道JAVA中的泛型和参数类型和返回类型相关。
这样做的好处是可以将派生类作为基类方法的参数类型或者返回类型,又或者字段的类型,即便他们都将会被擦除为Object。
1 | public class BasicHolder<T> { |
上面是一个普通的泛型,我们将派生类来作为类型参数T使用。
接着再来分析那个古怪的循环有何用意。
1 | class Subtype extends BasicHolder<Subtype> {} |
需要理解的是:子类继承了基类中的方法和字段,但是这些方法的参数和返回类型是子类,这使得基类称为一个所有子类的公共模板,重点是这些模板的返回和参数是派生子类!
这便是CRG的本质:基类使用派生类作为参数
从上面我们可以看出,BasicHolder<T>中的T可以是任意的类型,这说明会出现这样的情况:1
2class Other {}
class BasicOther extends BasicHolder<Other> {}
这表示当前派生类使用了其它的类的模板,这一般没有什么意义。
为了不让上面的情景发生,我们能怎么做?
给类型参数加一个边界,使得类型参数不能是任意的类型。1
2
3
4
5
6
7
8
9
10
11
12public class BasicHolder<T extent Other> {
T element;
void set(T arg) {
element = arg;
}
T get() {
return element;
}
void f() {
System.out.println(element.getClass().getSimpleName());
}
}
即便这样还是可能使用了其它具有共同基类Other的类的模板。
加怎样的一个边界比较合适呢?
有时候会可看到这样的一种比较奇怪的情况,但是也是最常用的:
1 | class SelfBounded<T extends SelfBounded<T>> |
这种奇怪的写法叫做自限定。
自限定的泛型怎么使用?
自限定规定了我们使用的类必须具有下面的继承形式:1
2class A extends SelfBounded<A> {}
class B extends SelfBounded<A> {}
一般只会用到第一种形式,像下面的这种用法就会出错:1
class C extends SelfBounded<B> {}
因为B的基类并不是SelfBounded<B>
然而这里还有一点点小小的问题,如果这样使用:1
class F extends SelfBounded {}
是可行的。如果需要强制使用泛型,需要额外的工具。
自限定有什么意义吗?
新创建的类使用到的基类模板的参数类型只能是自己,或者是有相同的基类(不常用)。
这就防止了当前派生类使用到了其它类的模板的情况。
自限定除了可以用在类上,还可以用在别的地方吗?
还可以用在静态方法上,如下:1
2
3
4
5
6
7
8
9public class SelfBoundingMethods
{
static <T extends SelfBounded<T>> T f(T arg) {
return arg.set(arg).get();
}
public static void main(String[] args) {
A a = f(new A());
}
}
这样限定了可以使用这个静态方法的类型。
参数协变
参数类型协变是泛型模板的一个应用。
什么是参数类型协变?
方法的参数类型会随着子类变化。
注意,这不是重载。1
2
3
4
5
6
7
8
9
10
11
12
13
14//重载:B中有两个同名的方法
class A{
void f(String str);
}
class B extends A{
void f(Integer int);
}
//参数协变:在子类中的方法参数可以更加具体
class SelfBoundSetter<T extends SelfBoundSetter<T>> {
void set(T arg){
};
}
class Setter extends SelfBoundSetter<Setter> {}
方法set的参数会随着子类而变化
为什么要用自限定的泛型,用一般的泛型不行吗?
参考之前自限定的意义既能理解了。
我们说过泛型关乎参数类型和返回类型,为什么没有返回类型协变?
首先,返回类型协变没那么重要,其次,因为JAVA5之后支持返回类型协变。
什么是返回类型协变?
先回忆什么是覆盖(重写):子类重新定义父类中的方法,参数和返回类型都一致就叫重写。
然而这个要求是在JAVA5之前的,JAVA5之后支持返回类型协变,可以让返回类型不一致。
在面向对象的编程语言中,返回类型协变指的是子类中的成员函数的返回值类型不必严格等同于父类中被重写的成员函数的返回值类型,而可以是更 “狭窄” 的类型。
泛型如何支持参数类型协变?
使用如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class SelfBoundSetter<T extends SelfBoundSetter<T>> {
void set(T arg){
};
}
class Setter extends SelfBoundSetter<Setter> {
}
class A extends SelfBoundSetter<A> {
}
class B extends SelfBoundSetter<B> {
}
class SelfBoundingAndCovariantArguments {
void testA(Setter s1, Setter s2, SelfBoundSetter sbs) {
s1.set(s2);
// s1.set(sbs); // Error:set(Setter) in SelfBoundSetter<Setter> cannot be applied to (SelfBoundSetter)
}
}
Setter会把类型参数T给覆盖,Setter变成其他类之后,相应的参数类型也会改变。
混型
就是一个类混合了多个类的能力。
C++中可以用多重继承和继承泛型实现
当想在混型类中修改某些东西,这些修改会应用于混型的所有类型上。
JAVA的这两种方式都不支持。JAVA的实现方法:
- 使用接口,问题是接口中的方法都需要手动实现。
- 使用装饰器,问题是只有最后一个装饰类中的方法可见。
- 使用动态代理,弥补了第一个方法中的手动实现问题。
潜在类型机制
什么是代码的“泛化”?
代码的“泛化”意思是编写 无需修改就可以应用于更多情况的代码。
具体点说就是一段代码,不需要关心它的类型,只要类型符合这段代码的要求(比如参数的类型具有特定的方法),就可以使用,这一段代码就可以用在很多地方,实现“泛化”。
为了实现跨类型的代码,注意这不是实现同一个接口,某些编程语言提供的一种解决的思想是称为“潜在类型机制”。
什么是潜在类型机制?
两个完全不同的类,不需要继承同一个父类或者实现同一个接口,能在同一段代码中编写能够同时运用于这两个类的代码,只要实现了某个方法子集,程序就允许执行,这种机制就是潜在类型机制,这是一个编程语言的思想。
python和c++都可以支持潜在类型机制,前者是动态类型检查(类型检查发生在运行时期),因而运行时转换为具体的类型。后者是静态类型检查(发生在编译时期),编译时转换为具体的类型。
类型检查是语言的一个功能,可以帮我们避免类型错误。
使用JAVA泛型不能支持潜在类型机制吗?
由于擦除的存在,JAVA的泛型和C++的不一样,属于“第二类泛型类型”。所有的具体类型在之后会被擦除为Object类型,无法拥有特定的方法,因而不支持潜在类型机制。
当然,我们的确可以使用带有边界限制的泛型来实现潜在类型机制,但是这样的方式完全可以使用继承类型和实现接口来替代了,也就没有什么意义。
对泛型缺乏潜在类型机制的补偿
JAVA能通过其它方式来支持潜在类型机制吗?
然而,可喜可贺!使用反射可以实现!
如何实现,看代码:
1 | class Mime { |
尽管这样做可以实现,但是这样处理类型检查是在运行时期,
运行时检查的缺陷是?
- 增加了程序运行时间,影响了效率;
- 需要数据具有类型标志;
- 错误发现太晚,不能防止运行错的出现。
有可能使类型检查在编译时期吗?
要想实现编译时期类型检查,最直观的想法是运用泛型。
但是JAVA的泛型并不支持潜在类型机制。
如果使用带有边界的泛型(支持潜在类型机制),之前也说过是没有意义的,因为如果泛型带有边界,还不如用接口和继承来代替。
当然还有其他的一些弥补方式。
关于混型和潜在类型机制仅作了解,以后有需求再进一步学习