开发公司经营范围,排名轻松seo 网站,wordpress 数据库下载,沙井网站推广文章目录 第二十章 泛型5. 泛型擦除5.1 泛型擦除5.2 迁移兼容性5.3 擦除的问题5.4 边界处的动作 6. 补偿擦除7. 边界8. 通配符8.1 通配符8.2 逆变 9. 问题10. 动态类型安全11. 泛型异常 第二十章 泛型 普通的类和方法只能使用特定的类型#xff1a;基本数据类型或类类型。如果… 文章目录 第二十章 泛型5. 泛型擦除5.1 泛型擦除5.2 迁移兼容性5.3 擦除的问题5.4 边界处的动作 6. 补偿擦除7. 边界8. 通配符8.1 通配符8.2 逆变 9. 问题10. 动态类型安全11. 泛型异常 第二十章 泛型 普通的类和方法只能使用特定的类型基本数据类型或类类型。如果编写的代码需要应用于多种类型这种严苛的限制对代码的束缚就会很大。 多态是一种面向对象思想的泛化机制。你可以将方法的参数类型设为基类这样的方法就可以接受任何派生类作为参数包括暂时还不存在的类。
拘泥于单一的继承体系太过局限如果方法以接口而不是类作为参数限制就宽松多了只要实现了接口就可以。
即便是接口也还是有诸多限制。一旦指定了接口它就要求你的代码必须使用特定的接口。而我们希望编写更通用的代码能够适用“非特定的类型”而不是一个具体的接口或类。
这就是泛型的概念是 Java 5 的重大变化之一。在很多情况下它可以使代码更直接更优雅。
5. 泛型擦除
5.1 泛型擦除
package generics;import java.util.ArrayList;public class ErasedTypeEquivalence {public static void main(String[] args) {Class c1 new ArrayListString().getClass();Class c2 new ArrayListInteger().getClass();System.out.println(c1 c2);}
}输出
trueArrayList 和 ArrayList 应该是不同的类型。不同的类型会有不同的行为。例如如果尝试向 ArrayList 中放入一个 Integer 所得到的行为失败和向ArrayList 中放入一个 Integer 所得到的行为成功完全不同。然而上面的程序认为它们是相同的类型。
下面的例子是对该谜题的补充
package generics;import java.util.*;class Frob {
}class Fnorkle {
}class QuarkQ {
}class ParticlePOSITION, MOMENTUM {
}public class LostInformation {public static void main(String[] args) {ListFrob list new ArrayList();MapFrob, Fnorkle map new HashMap();QuarkFnorkle quark new Quark();ParticleLong, Double p new Particle();// Class.getTypeParameters() “返回一个 TypeVariable 对象数组// 表示泛型声明中声明的类型参数...”System.out.println(Arrays.toString(list.getClass().getTypeParameters()));System.out.println(Arrays.toString(map.getClass().getTypeParameters()));System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));System.out.println(Arrays.toString(p.getClass().getTypeParameters()));}
}输出
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]残酷的现实是
在泛型代码内部无法获取任何有关泛型参数类型的信息。
因此你可以知道如类型参数标识符和泛型边界这些信息但无法得知实际的类型参数从而用来创建特定的实例。
Java 泛型是使用擦除实现的。这意味着当你在使用泛型时任何具体的类型信息都被擦除了你唯一知道的就是你在使用一个对象。因此 List 和 List 在运行时实际上是相同的类型。它们都被擦除成原生类型 List 。
package generics;public class HasF {public void f() {System.out.println(HasF.f());}
}package generics;
class ManipulatorT {private T obj;public Manipulator(T obj) {this.obj obj;}public void manipulate() {obj.f(); // 报错没有 f()}
}
public class Manipulation {public static void main(String[] args) {HasF hf new HasF();ManipulatorHasF manipulator new Manipulator(hf);manipulator.manipulate();}
}因为擦除Java 编译器无法将 manipulate() 方法中调用 obj 的 f() 方法这一需求映射到HasF 的 f() 方法。为了调用 f() 我们必须协助泛型类给定泛型类一个边界以此告诉编译器只能接受遵循这个边界的类型。这里重用了 extends 关键字。由于有了边界下面的代码就能通过编译
package generics;public class Manipulator2T extends HasF {private T obj;Manipulator2(T x) {obj x;}public void manipulate() {obj.f();}
}边界 声明 T 必须是 HasF 类型或其子类。
你可能认为泛型在 Manipulator2.java 中没有贡献任何事。你可以很轻松地自己去执行擦除生成没有泛型的类
package generics;public class Manipulator3 {private HasF obj;Manipulator3(HasF x) {obj x;}public void manipulate() {obj.f();}
}泛型只有在类型参数比某个具体类型以及其子类更加“泛化”——代码能跨多个类工作时才有用。
如果某个类有一个返回 T 的方法那么泛型就有所帮助因为它们之后将返回确切的类型
package generics;public class ReturnGenericTypeT extends HasF {private T obj;public ReturnGenericType(T x) {this.obj x;}public T get(){return obj;}
}5.2 迁移兼容性
擦除不是一个语言特性它是 Java 实现泛型的一种妥协因为泛型不是 Java 语言出现时就有的。泛型在 Java 中仍然是有用的只是不如它们本来设想的那么有用而原因就是擦除。
擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库反之亦然。这经常被称为“迁移兼容性”。在理想情况下所有事物将在指定的某天被泛化。在现实中即使程序员只编写泛型代码他们也必须处理 Java 5 之前编写的非泛型类库。这些类库的作者可能从没想过要泛化他们的代码或许他们可能刚刚开始接触泛型。
因此 Java 泛型不仅必须支持向后兼容性——现有的代码和类文件仍然合法继续保持之前的含义——而且还必须支持迁移兼容性使得类库能按照它们自己的步调变为泛型当某个类库变为泛型时不会破坏依赖于它的代码和应用。在确定了这个目标后Java 设计者们和从事此问题相关工作的各个团队决策认为擦除是唯一可行的解决方案。擦除使得这种向泛型的迁移成为可能允许非泛型的代码和泛型代码共存。
—PS擦除虽然有弊端但它是解决泛型向后兼容性的唯一可行方案
5.3 擦除的问题
因此擦除主要的正当理由是从非泛化代码到泛化代码的转变过程以及在不破坏现有类库的情况下将泛型融入到语言中。
泛型不能用于显式地引用运行时类型的操作中例如转型、instanceof 操作和 new 表达式。因为所有关于参数的类型信息都丢失了当你在编写泛型代码时必须时刻提醒自己你只是看起来拥有有关参数的类型信息而已。
public class FooT {T var;
}看上去当你创建一个 Foo 实例时
FooCat f new Foo();class Foo 中的代码应该知道现在工作于 Cat 之上。泛型语法也在强烈暗示整个类中所有 T 出现的地方都被替换就像在 C 中一样。但是事实并非如此当你在编写这个类的代码时必须提醒自己“不这只是一个 Object“。
另外擦除和迁移兼容性意味着使用泛型并不是强制的
package generics;class GenericBaseT {private T element;public void set(T arg) {element arg;}public T get() {return element;}
}class Derived1T extends GenericBaseT {
}class Derived2 extends GenericBase {
}// Derived3 产生的错误意味着编译器期望得到一个原生基类
//class Derived3 extends GenericBase? {}public class ErasureAndInteritance {public static void main(String[] args) {Derived2 d2 new Derived2();Object obj d2.get();d2.set(obj);}
}5.4 边界处的动作
边界即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。这就告诉我们泛型的所有动作都发生在边界处——对入参的编译器检查和对返回值的转型。这有助于澄清对擦除的困惑记住“边界就是动作发生的地方”。
6. 补偿擦除
因为擦除我们将失去执行泛型代码中某些操作的能力。无法在运行时知道确切类型
package generics;public class ErasedT {private final int SIZE 100;public void f(Object arg) {if (arg instanceof T) {} // 报错T var new T(); // 报错T[] array new T[SIZE]; // 报错T[] array2 (T[]) new Object[SIZE]; // 书上说这个地方应该有警告但是没有不知道为啥}
}—PS上文有泛型不能用于显式地引用运行时类型的操作中例如转型、instanceof 操作和 new 表达式。
有时我们可以对这些问题进行编程但是有时必须通过引入类型标签来补偿擦除。这意味着为所需的类型显式传递一个 Class 对象以在类型表达式中使用它。
例如由于擦除了类型信息因此在上一个程序中尝试使用 instanceof 将会失败。类型标签可以使用动态 isInstance()
package generics;class Building {
}class House extends Building {
}public class ClassTypeCaptureT {ClassT kind;public ClassTypeCapture(ClassT kind) {this.kind kind;}public boolean f(Object arg) {return kind.isInstance(arg);}public static void main(String[] args) {ClassTypeCaptureBuilding ctt1 new ClassTypeCapture(Building.class);System.out.println(ctt1.f(new Building()));System.out.println(ctt1.f(new House()));ClassTypeCaptureHouse ctt2 new ClassTypeCapture(House.class);System.out.println(ctt2.f(new Building()));System.out.println(ctt2.f(new House()));}
}输出
true
true
false
true7. 边界
边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则但潜在的更重要的效果是我们可以在绑定的类型中调用方法。
由于擦除会删除类型信息因此唯一可用于无限制泛型参数的方法是那些 Object 可用的方法。但是如果将该参数限制为某类型的子集则可以调用该子集中的方法。为了应用约束Java 泛型使用了extends 关键字。
重要的是要理解当用于限定泛型类型时 extends 的含义与通常的意义截然不同。此示例展示边界的基础应用
package generics;import java.awt.*;interface HasColor {java.awt.Color getColor();
}class WithColorT extends HasColor {T item;WithColor(T item) {this.item item;}T getItem() {return item;}java.awt.Color color() {return item.getColor();}
}class Coord {public int x, y, z;
}// 类需要放接口前面
// class WithColorCoordT extends HasColor Coord {}
class WithColorCoordT extends Coord HasColor {T item;WithColorCoord(T item) {this.item item;}T getItem() {return item;}java.awt.Color color() {return item.getColor();}int getX() {return item.x;}int getY() {return item.y;}int getZ() {return item.z;}
}interface Weight {int weight();
}// 泛型只能 extends 一个类但是可以 extends 多个接口
class SolidT extends Coord HasColor Weight {T item;Solid(T item) {this.item item;}T getItem() {return item;}java.awt.Color color() {return item.getColor();}int getX() {return item.x;}int getY() {return item.y;}int getZ() {return item.z;}int weight() {return item.weight();}
}class Bounded extends Coord implements HasColor, Weight {Overridepublic Color getColor() {return null;}Overridepublic int weight() {return 0;}
}public class BasicBounds {public static void main(String[] args) {SolidBounded solid new Solid(new Bounded());solid.color();solid.getY();solid.weight();}
}你可能会观察到 BasicBounds.java 中似乎包含一些冗余它们可以通过继承来消除。在这里每个继承级别还添加了边界约束
package generics;class HoldItemT {T item;HoldItem(T item) {this.item item;}T getItem() {return item;}
}class WithColor2T extends HasColor extends HoldItemT {WithColor2(T item) {super(item);}java.awt.Color color() {return item.getColor();}
}class WithColorCoord2T extends Coord HasColor extends WithColor2T {WithColorCoord2(T item) {super(item);}int getX() {return item.x;}int getY() {return item.y;}int getZ() {return item.z;}
}class Solid2T extends Coord HasColor Weight extends WithColorCoord2T {Solid2(T item) {super(item);}int weight() {return item.weight();}
}public class InheritBounds {public static void main(String[] args) {Solid2Bounded solid2 new Solid2(new Bounded());solid2.color();solid2.getY();solid2.color();}
}HoldItem 拥有一个对象因此此行为将继承到 WithColor2 中这也需要其参数符合 HasColor。
WithColorCoord2 和 Solid2 进一步扩展了层次结构并在每个级别添加了边界。现在这些方法已被继承并且在每个类中不再重复。
—PS边界由 T extends 限定每个类或接口中有独有的方法在最后的继承类中都能引用
8. 通配符
8.1 通配符
起始示例要展示数组的一种特殊行为将派生类的数组赋值给基类的引用
package generics;class 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];fruit[0] new Apple();fruit[1] new Jonathan();try {// 编译不报错fruit[0] new Fruit(); // 运行报错-ArrayStoreException} catch (Exception e) {System.out.println(e);}try {// 编译不报错fruit[0] new Orange(); // 运行报错-ArrayStoreException} catch (Exception e) {System.out.println(e);}}
}输出
java.lang.ArrayStoreException: generics.Fruit
java.lang.ArrayStoreException: generics.Orange看起来就像数组对它们持有的对象是有意识的因此在编译期检查和运行时检查之间你不能滥用它们。
数组的这种赋值并不是那么可怕因为在运行时你可以发现插入了错误的类型。但是泛型的主要目标之一是将这种错误检测移到编译期。所以当我们试图使用泛型集合代替数组时会发生什么呢
package generics;import java.util.ArrayList;
import java.util.List;public class NonCovariantGenerics {public static void main(String[] args) {ListFruit fruit new ArrayListApple();}
}阅读这段代码时会认为“不能将一个 Apple 集合赋值给一个 Fruit 集合”。记住泛型不仅仅是关于集合它真正要表达的是“不能把一个涉及 Apple 的泛型赋值给一个涉及 Fruit 的泛型”。
与数组不同泛型没有内建的协变类型。这是因为数组是完全在语言中定义的因此可以具有编译期和运行时的内建检查但是在使用泛型时编译器和运行时系统不知道你想用类型做什么以及应该采用什么规则。
但是有时你想在两个类型间建立某种向上转型关系。通配符可以产生这种关系。
package generics;import java.util.ArrayList;
import java.util.List;public class GenericsAndCovariance {public static void main(String[] args) {List? extends Fruit flist new ArrayList();
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());flist.add(null);Fruit f flist.get(0);}
}展示一个简单的 Holder 类
package generics;import java.util.Objects;public class HolderT {private T value;public Holder() {}public Holder(T value) {this.value value;}public T get() {return value;}public void set(T value) {this.value value;}Overridepublic boolean equals(Object o) {return o instanceof Holder Objects.equals(value, ((Holder) o).value);}Overridepublic int hashCode() {return Objects.hashCode(value);}public static void main(String[] args) {HolderApple apple new Holder(new Apple());Apple d apple.get();apple.set(d);
// HolderFruit fruit apple; // 不能向上转型Holder? extends Fruit fruit apple;Fruit p fruit.get();d (Apple) fruit.get();try {Orange c (Orange) fruit.get(); // No warning} catch (Exception e) {System.out.println(e);}
// fruit.set(new Apple());
// fruit.set(new Fruit());System.out.println(fruit.equals(d));}
}输出
java.lang.ClassCastException: generics.Apple cannot be cast to generics.Orange
false以上得到的信息
1、创建了一个 Holder 就不能将其向上转型为 Holder 但是可以向上转型为 Holder? extends Fruit 。
2、如果调用 get() 只能返回一个 Fruit——这就是在给定“任何额扩展自 Fruit 的对象”这一边界后它所能知道的一切了。如果你知道更多的信息就可以将其转型到某种具体的 Fruit 而不会导致任何警告但是存在得到ClassCastException 的风险。
3、set() 方法不能工作在 Apple 和 Fruit 上因为 set() 的参数也是?
extends Fruit意味着它可以是任何事物编译器无法验证“任何事物”的类型安全性。
8.2 逆变
还可以走另外一条路即使用超类型通配符。这里可以声明通配符是由某个特定类的任何基类来界定的方法是指定 super MyClass 或者甚至使用类型参数 super T 。
package generics;import java.util.List;public class SuperTypeWildcards {static void writeTo(List? super Apple apples) {apples.add(new Apple());apples.add(new Jonathan());
// apples.add(new Fruit()); // 报错}
}参数 apples 是 Apple 的某种基类型的 List这样你就知道向其中添加 Apple 或 Apple 的子类型是安全的。但是因为 Apple 是下界所以你知道向这样的 List 中添加 Fruit 是不安全的因为这将使这个List 敞开口子从而可以向其中添加非 Apple 类型的对象而这是违反静态类型安全的。 下面的示例复习了一下逆变和通配符的的使用
package generics;import java.util.Arrays;
import java.util.List;public class GenericReading {static ListApple apples Arrays.asList(new Apple());static ListFruit fruits Arrays.asList(new Fruit());static T T readExact(ListT list) {return list.get(0);}static void f1() {Apple a readExact(apples);Fruit f readExact(fruits);f readExact(apples); // 向上转型}// 内部类static class ReaderT {T readExact(ListT list) {return list.get(0);}}static void f2() {ReaderFruit fruitReader new Reader();Fruit f fruitReader.readExact(fruits);
// Fruit a fruitReader.readExact(apples);}static class CovariantReaderT {T readCovariant(List? extends T list) {return list.get(0);}}static void f3() {CovariantReaderFruit fruitReader new CovariantReader();Fruit f fruitReader.readCovariant(fruits);Fruit a fruitReader.readCovariant(apples);}public static void main(String[] args) {f1();f2();f3();}
}看到这里有点费劲还积攒了几个问题没关系带着疑惑找答案才能有收获推荐大佬文章Java 中的泛型两万字超全详解
看完大佬文章下面的别看了我是随便摘了几个。
9. 问题
任何基本类型都不能作为类型参数
Java 泛型的限制之一是不能将基本类型用作类型参数。因此不能创建ArrayList 之类的东西。
实现参数化接口
一个类不能实现同一个泛型接口的两种变体由于擦除的原因这两个变体会成为相同的接口。下面是产生这种冲突的情况
package generics;interface PayableT {}class Employee implements PayableEmployee {}// Hourly 不能编译因为擦除会将 PayableEmploye 和 PayableHourly 简化为相同的类
// Payable这样上面的代码就意味着在重复两次地实现相同的接口。
class Hourly extends Employee implements PayableHourly {}public class MultipleInterfaceVariants {
}转型和警告
使用带有泛型类型参数的转型或 instanceof 不会有任何效果。
重载
package generics;public class UseList {void f(ListT v) {}void f(ListW v) {}
}因为擦除所以重载方法产生了相同的类型签名。
基类劫持接口
package generics;public class ComparablePet implements ComparableComparablePet{Overridepublic int compareTo(ComparablePet o) {return 0;}
}class Cat extends ComparablePet implements ComparableCat {}—PS就是上面提到的 擦除 引起的
自限定的类型
class SelfBoundedT extends SelfBoundedT { // ...这就像两面镜子彼此照向对方所引起的目眩效果一样是一种无限反射。SelfBounded 类接受泛型参数T而 T 由一个边界类限定这个边界就是拥有 T 作为其参数的 SelfBounded。
当你首次看到它时很难去解析它它强调的是当 extends 关键字用于边界与用来创建子类明显是不同的。
10. 动态类型安全
因为可以向 Java 5 之前的代码传递泛型集合所以旧式代码仍旧有可能会破坏你的集合。Java 5 的java.util.Collections 中有一组便利工具可以解决在这种情况下的类型检查问题它们是静态方法 checkedCollection() 、 checkedList() 、 checkedMap() 、 checkedSet()、 checkedSortedMap() 和 checkedSortedSet() 。这些方法每一个都会将你希望动态检查的集合当作第一个参数接受并将你希望强制要求的类型作为第二个参数接受。
11. 泛型异常
由于擦除的原因catch 语句不能捕获泛型类型的异常因为在编译期和运行时都必须知道异常的确切类型。泛型类也不能直接或间接继承自 Throwable这将进一步阻止你去定义不能捕获的泛型异常。 但是类型参数可能会在一个方法的 throws 子句中用到。 图网侵删