二十、泛型(6)

本章概要

  • 问题
    • 任何基本类型都不能作为类型参数
    • 实现参数化接口
    • 转型和警告
    • 重载
    • 基类劫持接口
  • 自限定的类型
    • 古怪的循环泛型
    • 自限定
    • 参数协变

问题

本节将阐述在使用 Java 泛型时会出现的各类问题。

任何基本类型都不能作为类型参数

正如本章早先提到的,Java 泛型的限制之一是不能将基本类型用作类型参数。因此,不能创建 ArrayList<int> 之类的东西。

解决方法是使用基本类型的包装器类以及自动装箱机制。如果创建一个 ArrayList<Integer>,并将基本类型 int 应用于这个集合,那么你将发现自动装箱机制将自动地实现 intInteger 的双向转换——因此,这几乎就像是有一个 ArrayList<int> 一样:

import java.util.*;
import java.util.stream.*;public class ListOfInt {public static void main(String[] args) {List<Integer> li = IntStream.range(38, 48).boxed() // Converts ints to Integers.collect(Collectors.toList());System.out.println(li);}
}

在这里插入图片描述

通常,这种解决方案工作得很好——能够成功地存储和读取 int,自动装箱隐藏了转换的过程。但是如果性能成为问题的话,就需要使用专门为基本类型适配的特殊版本的集合;一个开源版本的实现是 org.apache.commons.collections.primitives

下面是另外一种方式,它可以创建持有 ByteSet

import java.util.*;public class ByteSet {Byte[] possibles = {1, 2, 3, 4, 5, 6, 7, 8, 9};Set<Byte> mySet = new HashSet<>(Arrays.asList(possibles));// But you can't do this:// Set<Byte> mySet2 = new HashSet<>(// Arrays.<Byte>asList(1,2,3,4,5,6,7,8,9));
}

自动装箱机制解决了一些问题,但并没有解决所有问题。

在下面的示例中,FillArray 接口包含一些通用方法,这些方法使用 Supplier 来用对象填充数组(这使得类泛型在本例中无法工作,因为这个方法是静态的)。Supplier 实现来自 数组 一章,并且在 main() 中,可以看到 FillArray.fill() 使用对象填充了数组:

FillArray.java

import java.util.*;
import java.util.function.*;// Fill an array using a generator:
interface FillArray {static <T> T[] fill(T[] a, Supplier<T> gen) {Arrays.setAll(a, n -> gen.get());return a;}static int[] fill(int[] a, IntSupplier gen) {Arrays.setAll(a, n -> gen.getAsInt());return a;}static long[] fill(long[] a, LongSupplier gen) {Arrays.setAll(a, n -> gen.getAsLong());return a;}static double[] fill(double[] a, DoubleSupplier gen) {Arrays.setAll(a, n -> gen.getAsDouble());return a;}
}public class PrimitiveGenericTest {public static void main(String[] args) {String[] strings = FillArray.fill(new String[5], new Rand.String(9));System.out.println(Arrays.toString(strings));int[] integers = FillArray.fill(new int[9], new Rand.Pint());System.out.println(Arrays.toString(integers));}
}

ConvertTo.java

public interface ConvertTo {static boolean[] primitive(Boolean[] in) {boolean[] result = new boolean[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i]; // Autounboxing}return result;}static char[] primitive(Character[] in) {char[] result = new char[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i];}return result;}static byte[] primitive(Byte[] in) {byte[] result = new byte[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i];}return result;}static short[] primitive(Short[] in) {short[] result = new short[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i];}return result;}static int[] primitive(Integer[] in) {int[] result = new int[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i];}return result;}static long[] primitive(Long[] in) {long[] result = new long[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i];}return result;}static float[] primitive(Float[] in) {float[] result = new float[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i];}return result;}static double[] primitive(Double[] in) {double[] result = new double[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i];}return result;}// Convert from primitive array to wrapped array:static Boolean[] boxed(boolean[] in) {Boolean[] result = new Boolean[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i]; // Autoboxing}return result;}static Character[] boxed(char[] in) {Character[] result = new Character[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i];}return result;}static Byte[] boxed(byte[] in) {Byte[] result = new Byte[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i];}return result;}static Short[] boxed(short[] in) {Short[] result = new Short[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i];}return result;}static Integer[] boxed(int[] in) {Integer[] result = new Integer[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i];}return result;}static Long[] boxed(long[] in) {Long[] result = new Long[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i];}return result;}static Float[] boxed(float[] in) {Float[] result = new Float[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i];}return result;}static Double[] boxed(double[] in) {Double[] result = new Double[in.length];for (int i = 0; i < in.length; i++) {result[i] = in[i];}return result;}
}

Rand.java

import java.util.*;
import java.util.function.*;import static com.example.test.ConvertTo.primitive;public interface Rand {int MOD = 10_000;class Boolean implements Supplier<java.lang.Boolean> {SplittableRandom r = new SplittableRandom(47);@Overridepublic java.lang.Boolean get() {return r.nextBoolean();}public java.lang.Boolean get(int n) {return get();}public java.lang.Boolean[] array(int sz) {java.lang.Boolean[] result =new java.lang.Boolean[sz];Arrays.setAll(result, n -> get());return result;}}class Pboolean {public boolean[] array(int sz) {return primitive(new Boolean().array(sz));}}class Byteimplements Supplier<java.lang.Byte> {SplittableRandom r = new SplittableRandom(47);@Overridepublic java.lang.Byte get() {return (byte) r.nextInt(MOD);}public java.lang.Byte get(int n) {return get();}public java.lang.Byte[] array(int sz) {java.lang.Byte[] result =new java.lang.Byte[sz];Arrays.setAll(result, n -> get());return result;}}class Pbyte {public byte[] array(int sz) {return primitive(new Byte().array(sz));}}class Characterimplements Supplier<java.lang.Character> {SplittableRandom r = new SplittableRandom(47);@Overridepublic java.lang.Character get() {return (char) r.nextInt('a', 'z' + 1);}public java.lang.Character get(int n) {return get();}public java.lang.Character[] array(int sz) {java.lang.Character[] result =new java.lang.Character[sz];Arrays.setAll(result, n -> get());return result;}}class Pchar {public char[] array(int sz) {return primitive(new Character().array(sz));}}class Shortimplements Supplier<java.lang.Short> {SplittableRandom r = new SplittableRandom(47);@Overridepublic java.lang.Short get() {return (short) r.nextInt(MOD);}public java.lang.Short get(int n) {return get();}public java.lang.Short[] array(int sz) {java.lang.Short[] result =new java.lang.Short[sz];Arrays.setAll(result, n -> get());return result;}}class Pshort {public short[] array(int sz) {return primitive(new Short().array(sz));}}class Integerimplements Supplier<java.lang.Integer> {SplittableRandom r = new SplittableRandom(47);@Overridepublic java.lang.Integer get() {return r.nextInt(MOD);}public java.lang.Integer get(int n) {return get();}public java.lang.Integer[] array(int sz) {int[] primitive = new Pint().array(sz);java.lang.Integer[] result = new java.lang.Integer[sz];for (int i = 0; i < sz; i++) {result[i] = primitive[i];}return result;}}class Pint implements IntSupplier {SplittableRandom r = new SplittableRandom(47);@Overridepublic int getAsInt() {return r.nextInt(MOD);}public int get(int n) {return getAsInt();}public int[] array(int sz) {return r.ints(sz, 0, MOD).toArray();}}class Longimplements Supplier<java.lang.Long> {SplittableRandom r = new SplittableRandom(47);@Overridepublic java.lang.Long get() {return r.nextLong(MOD);}public java.lang.Long get(int n) {return get();}public java.lang.Long[] array(int sz) {long[] primitive = new Plong().array(sz);java.lang.Long[] result =new java.lang.Long[sz];for (int i = 0; i < sz; i++) {result[i] = primitive[i];}return result;}}class Plong implements LongSupplier {SplittableRandom r = new SplittableRandom(47);@Overridepublic long getAsLong() {return r.nextLong(MOD);}public long get(int n) {return getAsLong();}public long[] array(int sz) {return r.longs(sz, 0, MOD).toArray();}}class Floatimplements Supplier<java.lang.Float> {SplittableRandom r = new SplittableRandom(47);@Overridepublic java.lang.Float get() {return (float) trim(r.nextDouble());}public java.lang.Float get(int n) {return get();}public java.lang.Float[] array(int sz) {java.lang.Float[] result =new java.lang.Float[sz];Arrays.setAll(result, n -> get());return result;}}class Pfloat {public float[] array(int sz) {return primitive(new Float().array(sz));}}static double trim(double d) {return((double) Math.round(d * 1000.0)) / 100.0;}class Doubleimplements Supplier<java.lang.Double> {SplittableRandom r = new SplittableRandom(47);@Overridepublic java.lang.Double get() {return trim(r.nextDouble());}public java.lang.Double get(int n) {return get();}public java.lang.Double[] array(int sz) {double[] primitive = new Rand.Pdouble().array(sz);java.lang.Double[] result = new java.lang.Double[sz];for (int i = 0; i < sz; i++) {result[i] = primitive[i];}return result;}}class Pdouble implements DoubleSupplier {SplittableRandom r = new SplittableRandom(47);@Overridepublic double getAsDouble() {return trim(r.nextDouble());}public double get(int n) {return getAsDouble();}public double[] array(int sz) {double[] result = r.doubles(sz).toArray();Arrays.setAll(result,n -> result[n] = trim(result[n]));return result;}}class Stringimplements Supplier<java.lang.String> {SplittableRandom r = new SplittableRandom(47);private int strlen = 7; // Default lengthpublic String() {}public String(int strLength) {strlen = strLength;}@Overridepublic java.lang.String get() {return r.ints(strlen, 'a', 'z' + 1).collect(StringBuilder::new,StringBuilder::appendCodePoint,StringBuilder::append).toString();}public java.lang.String get(int n) {return get();}public java.lang.String[] array(int sz) {java.lang.String[] result =new java.lang.String[sz];Arrays.setAll(result, n -> get());return result;}}
}

在这里插入图片描述

自动装箱不适用于数组,因此我们必须创建 FillArray.fill() 的重载版本,或创建产生 Wrapped 输出的生成器。 FillArray 仅比 java.util.Arrays.setAll() 有用一点,因为它返回填充的数组。

实现参数化接口

一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。下面是产生这种冲突的情况:

interface Payable<T> {
}class Employee implements Payable<Employee> {
}class Hourly extends Employee implements Payable<Hourly> {
}

Hourly 不能编译,因为擦除会将 Payable<Employe>Payable<Hourly> 简化为相同的类 Payable,这样,上面的代码就意味着在重复两次地实现相同的接口。十分有趣的是,如果从 Payable 的两种用法中都移除掉泛型参数(就像编译器在擦除阶段所做的那样)这段代码就可以编译。

在使用某些更基本的 Java 接口,例如 Comparable<T> 时,这个问题可能会变得十分令人恼火,就像你在本节稍后看到的那样。

转型和警告

使用带有泛型类型参数的转型或 instanceof 不会有任何效果。下面的集合在内部将各个值存储为 Object,并在获取这些值时,再将它们转型回 T

import java.util.*;
import java.util.stream.*;class FixedSizeStack<T> {private final int size;private Object[] storage;private int index = 0;FixedSizeStack(int size) {this.size = size;storage = new Object[size];}public void push(T item) {if (index < size) {storage[index++] = item;}}@SuppressWarnings("unchecked")public T pop() {return index == 0 ? null : (T) storage[--index];}@SuppressWarnings("unchecked")Stream<T> stream() {return (Stream<T>) Arrays.stream(storage);}
}public class GenericCast {static String[] letters = "ABCDEFGHIJKLMNOPQRS".split("");public static void main(String[] args) {FixedSizeStack<String> strings = new FixedSizeStack<>(letters.length);Arrays.stream("ABCDEFGHIJKLMNOPQRS".split("")).forEach(strings::push);System.out.println(strings.pop());strings.stream().map(s -> s + " ").forEach(System.out::print);}
}

在这里插入图片描述

如果没有 **@SuppressWarnings ** 注解,编译器将对 pop() 产生 “unchecked cast” 警告。由于擦除的原因,编译器无法知道这个转型是否是安全的,并且 pop() 方法实际上并没有执行任何转型。

这是因为,T 被擦除到它的第一个边界,默认情况下是 Object ,因此 pop() 实际上只是将 Object 转型为 Object

有时,泛型没有消除对转型的需要,这就会由编译器产生警告,而这个警告是不恰当的。例如:

NeedCasting.java

import java.io.*;public class NeedCasting {@SuppressWarnings("unchecked")public void f(String[] args) throws Exception {ObjectInputStream in = new ObjectInputStream(new FileInputStream(args[0]));List<Widget> shapes = (List<Widget>) in.readObject();}
}

FactoryConstraint.java

import java.util.*;
import java.util.function.*;class IntegerFactory implements Supplier<Integer> {private int i = 0;@Overridepublic Integer get() {return ++i;}
}class Widget {private int id;Widget(int n) {id = n;}@Overridepublic String toString() {return "Widget " + id;}public staticclass Factory implements Supplier<Widget> {private int i = 0;@Overridepublic Widget get() {return new Widget(++i);}}
}class Fudge {private static int count = 1;private int n = count++;@Overridepublic String toString() {return "Fudge " + n;}
}class Foo2<T> {private List<T> x = new ArrayList<>();Foo2(Supplier<T> factory) {Suppliers.fill(x, factory, 5);}@Overridepublic String toString() {return x.toString();}
}public class FactoryConstraint {public static void main(String[] args) {System.out.println(new Foo2<>(new IntegerFactory()));System.out.println(new Foo2<>(new Widget.Factory()));System.out.println(new Foo2<>(Fudge::new));}
}

Suppliers.java

import java.util.*;
import java.util.function.*;
import java.util.stream.*;public class Suppliers {// Create a collection and fill it:public static <T, C extends Collection<T>> Ccreate(Supplier<C> factory, Supplier<T> gen, int n) {return Stream.generate(gen).limit(n).collect(factory, C::add, C::addAll);}// Fill an existing collection:public static <T, C extends Collection<T>>C fill(C coll, Supplier<T> gen, int n) {Stream.generate(gen).limit(n).forEach(coll::add);return coll;}// Use an unbound method reference to// produce a more general method:public static <H, A> H fill(H holder,BiConsumer<H, A> adder, Supplier<A> gen, int n) {Stream.generate(gen).limit(n).forEach(a -> adder.accept(holder, a));return holder;}
}

readObject() 无法知道它正在读取的是什么,因此它返回的是必须转型的对象。但是当注释掉 **@SuppressWarnings ** 注解并编译这个程序时,就会得到下面的警告。

NeedCasting.java uses unchecked or unsafe operations.
Recompile with -Xlint:unchecked for details.And if you follow the instructions and recompile with  -
Xlint:unchecked :(如果遵循这条指示,使用-Xlint:unchecked来重新编译:)NeedCasting.java:10: warning: [unchecked] unchecked castList<Widget> shapes = (List<Widget>)in.readObject();required: List<Widget>found: Object
1 warning

你会被强制要求转型,但是又被告知不应该转型。为了解决这个问题,必须使用 Java 5 引入的新的转型形式,即通过泛型类来转型:

import java.io.*;
import java.util.*;public class ClassCasting {@SuppressWarnings("unchecked")public void f(String[] args) throws Exception {ObjectInputStream in = new ObjectInputStream(new FileInputStream(args[0]));// Won't Compile://    List<Widget> lw1 =//    List<>.class.cast(in.readObject());List<Widget> lw2 = List.class.cast(in.readObject());}
}

但是,不能转型到实际类型( List<Widget> )。也就是说,不能声明:

List<Widget>.class.cast(in.readobject())

甚至当你添加一个像下面这样的另一个转型时:

(List<Widget>)List.class.cast(in.readobject())

仍旧会得到一个警告。

重载

下面的程序是不能编译的,即使它看起来是合理的:

import java.util.*;public class UseList<W, T> {void f(List<T> v) {}void f(List<W> v) {}
}

因为擦除,所以重载方法产生了相同的类型签名。

因而,当擦除后的参数不能产生唯一的参数列表时,你必须提供不同的方法名:

import java.util.*;public class UseList2<W, T> {void f1(List<T> v) {}void f2(List<W> v) {}
}

幸运的是,编译器可以检测到这类问题。

基类劫持接口

假设你有一个实现了 Comparable 接口的 Pet 类:

public class ComparablePet implements Comparable<ComparablePet> {@Overridepublic int compareTo(ComparablePet o) {return 0;}
}

尝试缩小 ComparablePet 子类的比较类型是有意义的。例如,Cat 类可以与其他的 Cat 比较:

class Cat extends ComparablePet implements Comparable<Cat> {// error: Comparable cannot be inherited with// different arguments: <Cat> and <ComparablePet>// class Cat// ^// 1 errorpublic int compareTo(Cat arg) {return 0;}
}

不幸的是,这不能工作。一旦 Comparable 的类型参数设置为 ComparablePet,其他的实现类只能比较 ComparablePet

public class Hamster extends ComparablePet implements Comparable<ComparablePet> {@Overridepublic int compareTo(ComparablePet arg) {return 0;}
}// Or just:
class Gecko extends ComparablePet {@Overridepublic int compareTo(ComparablePet arg) {return 0;}
}

Hamster 显示了重新实现 ComparablePet 中相同的接口是可能的,只要接口完全相同,包括参数类型。然而正如 Gecko 中所示,这与直接覆写基类的方法完全相同。

自限定的类型

在 Java 泛型中,有一个似乎经常性出现的惯用法,它相当令人费解:

class SelfBounded<T extends SelfBounded<T>> { // ...
}

这就像两面镜子彼此照向对方所引起的目眩效果一样,是一种无限反射。SelfBounded 类接受泛型参数 T,而 T 由一个边界类限定,这个边界就是拥有 T 作为其参数的 SelfBounded

当你首次看到它时,很难去解析它,它强调的是当 extends 关键字用于边界与用来创建子类明显是不同的。

古怪的循环泛型

为了理解自限定类型的含义,我们从这个惯用法的一个简单版本入手,它没有自限定的边界。

不能直接继承一个泛型参数,但是,可以继承在其自己的定义中使用这个泛型参数的类。也就是说,可以声明:

class GenericType<T> {
}public class CuriouslyRecurringGeneric extends GenericType<CuriouslyRecurringGeneric> {
}

这可以按照 Jim Coplien 在 C++ 中的_古怪的循环模版模式_的命名方式,称为古怪的循环泛型(CRG)。“古怪的循环”是指类相当古怪地出现在它自己的基类中这一事实。

为了理解其含义,努力大声说:“我在创建一个新类,它继承自一个泛型类型,这个泛型类型接受我的类的名字作为其参数。”

当给出导出类的名字时,这个泛型基类能够实现什么呢?好吧,Java 中的泛型关乎参数和返回类型,因此它能够产生使用导出类作为其参数和返回类型的基类。它还能将导出类型用作其域类型,尽管这些将被擦除为 Object 的类型。下面是表示了这种情况的一个泛型类:

public class BasicHolder<T> {T element;void set(T arg) {element = arg;}T get() {return element;}void f() {System.out.println(element.getClass().getSimpleName());}
}

这是一个普通的泛型类型,它的一些方法将接受和产生具有其参数类型的对象,还有一个方法在其存储的域上执行操作(尽管只是在这个域上执行 Object 操作)。
我们可以在一个古怪的循环泛型中使用 BasicHolder

class Subtype extends BasicHolder<Subtype> {
}public class CRGWithBasicHolder {public static void main(String[] args) {Subtype st1 = new Subtype(), st2 = new Subtype();st1.set(st2);Subtype st3 = st1.get();st1.f();}
}

在这里插入图片描述

注意,这里有些东西很重要:新类 Subtype 接受的参数和返回的值具有 Subtype 类型而不仅仅是基类 BasicHolder 类型。这就是 CRG 的本质:基类用导出类替代其参数。这意味着泛型基类变成了一种其所有导出类的公共功能的模版,但是这些功能对于其所有参数和返回值,将使用导出类型。

也就是说,在所产生的类中将使用确切类型而不是基类型。因此,在Subtype 中,传递给 set() 的参数和从 get() 返回的类型都是确切的 Subtype

自限定

BasicHolder 可以使用任何类型作为其泛型参数,就像下面看到的那样:

class Other {
}class BasicOther extends BasicHolder<Other> {
}public class Unconstrained {public static void main(String[] args) {BasicOther b = new BasicOther();BasicOther b2 = new BasicOther();b.set(new Other());Other other = b.get();b.f();}
}

限定将采取额外的步骤,强制泛型当作其自身的边界参数来使用。观察所产生的类可以如何使用以及不可以如何使用:

class SelfBounded<T extends SelfBounded<T>> {T element;SelfBounded<T> set(T arg) {element = arg;return this;}T get() {return element;}
}class A extends SelfBounded<A> {
}class B extends SelfBounded<A> {
} // Also OKclass C extends SelfBounded<C> {C setAndGet(C arg) {set(arg);return get();}
}class D {
}
// Can't do this:
// class E extends SelfBounded<D> {}
// Compile error:
//   Type parameter D is not within its bound// Alas, you can do this, so you cannot force the idiom:
class F extends SelfBounded {
}public class SelfBounding {public static void main(String[] args) {A a = new A();a.set(new A());a = a.set(new A()).get();a = a.get();C c = new C();c = c.setAndGet(new C());}
}

自限定所做的,就是要求在继承关系中,像下面这样使用这个类:

class A extends SelfBounded<A>{}

这会强制要求将正在定义的类当作参数传递给基类。

自限定的参数有何意义呢?它可以保证类型参数必须与正在被定义的类相同。正如你在 B 类的定义中所看到的,还可以从使用了另一个 SelfBounded 参数的 SelfBounded 中导出,尽管在 A 类看到的用法看起来是主要的用法。对定义 E 的尝试说明不能使用不是 SelfBounded 的类型参数。

遗憾的是, F 可以编译,不会有任何警告,因此自限定惯用法不是可强制执行的。如果它确实很重要,可以要求一个外部工具来确保不会使用原生类型来替代参数化类型。

注意,可以移除自限定这个限制,这样所有的类仍旧是可以编译的,但是 E 也会因此而变得可编译:

public class NotSelfBounded<T> {T element;NotSelfBounded<T> set(T arg) {element = arg;return this;}T get() {return element;}
}class A2 extends NotSelfBounded<A2> {
}class B2 extends NotSelfBounded<A2> {
}class C2 extends NotSelfBounded<C2> {C2 setAndGet(C2 arg) {set(arg);return get();}
}class D2 {
}// Now this is OK:
class E2 extends NotSelfBounded<D2> {
}

因此很明显,自限定限制只能强制作用于继承关系。如果使用自限定,就应该了解这个类所用的类型参数将与使用这个参数的类具有相同的基类型。这会强制要求使用这个类的每个人都要遵循这种形式。
还可以将自限定用于泛型方法:

public 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());}
}

这可以防止这个方法被应用于除上述形式的自限定参数之外的任何事物上。

参数协变

自限定类型的价值在于它们可以产生_协变参数类型_——方法参数类型会随子类而变化。

尽管自限定类型还可以产生与子类类型相同的返回类型,但是这并不十分重要,因为_协变返回类型_是在 Java 5 引入:

class Base {
}class Derived extends Base {
}interface OrdinaryGetter {Base get();
}interface DerivedGetter extends OrdinaryGetter {// Overridden method return type can vary:@OverrideDerived get();
}public class CovariantReturnTypes {void test(DerivedGetter d) {Derived d2 = d.get();}
}

DerivedGetter 中的 get() 方法覆盖了 OrdinaryGetter 中的 get() ,并返回了一个从 OrdinaryGetter.get() 的返回类型中导出的类型。尽管这是完全合乎逻辑的事情(导出类方法应该能够返回比它覆盖的基类方法更具体的类型)但是这在早先的 Java 版本中是不合法的。

自限定泛型事实上将产生确切的导出类型作为其返回值,就像在 get() 中所看到的一样:

interface GenericGetter<T extends GenericGetter<T>> {T get();
}interface Getter extends GenericGetter<Getter> {
}public class GenericsAndReturnTypes {void test(Getter g) {Getter result = g.get();GenericGetter gg = g.get(); // Also the base type}
}

注意,这段代码不能编译,除非是使用囊括了协变返回类型的 Java 5。

然而,在非泛型代码中,参数类型不能随子类型发生变化:

class OrdinarySetter {void set(Base base) {System.out.println("OrdinarySetter.set(Base)");}
}class DerivedSetter extends OrdinarySetter {void set(Derived derived) {System.out.println("DerivedSetter.set(Derived)");}
}public class OrdinaryArguments {public static void main(String[] args) {Base base = new Base();Derived derived = new Derived();DerivedSetter ds = new DerivedSetter();ds.set(derived);// Compiles--overloaded, not overridden!:ds.set(base);}
}

在这里插入图片描述

set(derived)set(base) 都是合法的,因此 DerivedSetter.set() 没有覆盖 OrdinarySetter.set() ,而是重载了这个方法。从输出中可以看到,在 DerivedSetter 中有两个方法,因此基类版本仍旧是可用的,因此可以证明它被重载过。
但是,在使用自限定类型时,在导出类中只有一个方法,并且这个方法接受导出类型而不是基类型为参数:

interface SelfBoundSetter<T extends SelfBoundSetter<T>> {void set(T arg);
}interface Setter extends SelfBoundSetter<Setter> {
}public class SelfBoundingAndCovariantArguments {voidtestA(Setter s1, Setter s2, SelfBoundSetter sbs) {s1.set(s2);//- s1.set(sbs);// error: method set in interface SelfBoundSetter<T>// cannot be applied to given types;//     s1.set(sbs);//       ^//   required: Setter//   found: SelfBoundSetter//   reason: argument mismatch;// SelfBoundSetter cannot be converted to Setter//   where T is a type-variable://     T extends SelfBoundSetter<T> declared in//     interface SelfBoundSetter// 1 error}
}

编译器不能识别将基类型当作参数传递给 set() 的尝试,因为没有任何方法具有这样的签名。实际上,这个参数已经被覆盖。

如果不使用自限定类型,普通的继承机制就会介入,而你将能够重载,就像在非泛型的情况下一样:

class GenericSetter<T> { // Not self-boundedvoid set(T arg) {System.out.println("GenericSetter.set(Base)");}
}class DerivedGS extends GenericSetter<Base> {void set(Derived derived) {System.out.println("DerivedGS.set(Derived)");}
}public class PlainGenericInheritance {public static void main(String[] args) {Base base = new Base();Derived derived = new Derived();DerivedGS dgs = new DerivedGS();dgs.set(derived);dgs.set(base); // Overloaded, not overridden!}
}

在这里插入图片描述

这段代码在模仿 OrdinaryArguments.java;在那个示例中,DerivedSetter 继承自包含一个 set(Base)OrdinarySetter 。而这里,DerivedGS 继承自泛型创建的也包含有一个 set(Base)GenericSetter<Base>

就像 OrdinaryArguments.java 一样,你可以从输出中看到, DerivedGS 包含两个 set() 的重载版本。如果不使用自限定,将重载参数类型。如果使用了自限定,只能获得方法的一个版本,它将接受确切的参数类型。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/171173.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DBever连接PG库

一、简介 DBeaver是一种通用数据库管理工具&#xff0c;适用于需要以专业方式使用数据的每个人&#xff1b;适用于开发人员&#xff0c;数据库管理员&#xff0c;分析师和所有需要使用数据库的人员的 免费(DBeaver Community) 的多平台数据库工具&#xff0c;支持 Windows、Li…

使用JAVA pdf转word

使用spire.pdf 非常简单。 查看 https://mvnrepository.com/artifact/e-iceblue/spire.pdf 注意&#xff0c;这个包在 e-iceblue 下。 下面开始撸代码 先来pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://mav…

高通SDX12:ASoC 音频框架浅析

一、简介 ASoC–ALSA System on Chip ,是建立在标准ALSA驱动层上,为了更好地支持嵌入式处理器和移动设备中的音频Codec的一套软件体系。 本文基于高通SDX12平台,对ASoC框架做一个分析。 二、整体框架 1. 硬件层面 嵌入式Linux设备的Audio subsystem可以划分为Machine(板…

[100天算法】-球会落何处(day 76)

题目描述 用一个大小为 m x n 的二维网格 grid 表示一个箱子。你有 n 颗球。箱子的顶部和底部都是开着的。箱子中的每个单元格都有一个对角线挡板&#xff0c;跨过单元格的两个角&#xff0c;可以将球导向左侧或者右侧。将球导向右侧的挡板跨过左上角和右下角&#xff0c;在网…

【java:牛客每日三十题总结-7】

java:牛客每日三十题总结 总结如下 总结如下 执行流程如下&#xff1a;创建HttpServlet时需要覆盖doGet()和doPost请求 2. request相关知识 request.getParameter()方法传递的数据&#xff0c;会从Web客户端传到Web服务器端&#xff0c;代表HTTP请求数据&#xff1b;request.…

工作记录--(用HTTPS,为啥能被查出浏览记录?如何解决?)---每天学习多一点

由于网络通信有很多层&#xff0c;即使加密通信&#xff0c;仍有很多途径暴露你的访问地址&#xff0c;比如&#xff1a; DNS查询&#xff1a;通常DNS查询是不会加密的&#xff0c;所以&#xff0c;能看到你DNS查询的观察者&#xff08;比如运营商&#xff09;是可以推断出访问…

媒体软文投放的流程与媒体平台的选择

海内外媒体软文&#xff1a;助力信息传播与品牌建设 在当今数字化时代&#xff0c;企业如何在庞大的信息海洋中脱颖而出&#xff0c;成为品牌建设的领军者&#xff1f;媒体软文投放无疑是一项强大的策略&#xff0c;通过选择合适的平台&#xff0c;精准投放&#xff0c;可以实…

nodejs+vue+python+PHP+微信小程序-安卓- 电影在线订票系统的设计与实现-计算机毕业设计推荐

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

深度学习模型基于Python+TensorFlow+Django的垃圾识别系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 要使用Python、TensorFlow和Django构建一个垃圾识别系统&#xff0c;您可以按照以下步骤进行操作&#xff1a; 安装…

基于单片机智能输液器监控系统的设计

**单片机设计介绍&#xff0c; 基于单片机智能输液器监控系统的设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的智能输液器监控系统可以实现对输液过程的实时监测和控制&#xff0c;以下是一个基本的设计介绍&am…

11.10 知识总结(数据的增删改查、如何创建表关系、Django框架的请求生命周期流程图)

一、 数据的增删改查 1.1 用户列表的展示 把数据表中得用户数据都给查询出来展示在页面上 添加数据 id username password gender age action 修改 删除 1.2 修…

你真的会使用 MySQL中EXPLAIN吗

EXPLAIN是MySQL数据库中一个强大的工具&#xff0c;用于查询性能分析和优化。通过EXPLAIN&#xff0c;你可以查看MySQL查询的执行计划&#xff0c;了解MySQL是如何执行你的查询语句的。这篇文章将详细介绍EXPLAIN的使用&#xff0c;帮助你更好地理解和优化MySQL查询。 为什么使…