二十、泛型(6)
本章概要
- 问题
- 任何基本类型都不能作为类型参数
- 实现参数化接口
- 转型和警告
- 重载
- 基类劫持接口
- 自限定的类型
- 古怪的循环泛型
- 自限定
- 参数协变
问题
本节将阐述在使用 Java 泛型时会出现的各类问题。
任何基本类型都不能作为类型参数
正如本章早先提到的,Java 泛型的限制之一是不能将基本类型用作类型参数。因此,不能创建 ArrayList<int>
之类的东西。
解决方法是使用基本类型的包装器类以及自动装箱机制。如果创建一个 ArrayList<Integer>
,并将基本类型 int 应用于这个集合,那么你将发现自动装箱机制将自动地实现 int 到 Integer 的双向转换——因此,这几乎就像是有一个 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。
下面是另外一种方式,它可以创建持有 Byte 的 Set:
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()
的重载版本。如果不使用自限定,将重载参数类型。如果使用了自限定,只能获得方法的一个版本,它将接受确切的参数类型。
相关文章:

二十、泛型(6)
本章概要 问题 任何基本类型都不能作为类型参数实现参数化接口转型和警告重载基类劫持接口 自限定的类型 古怪的循环泛型自限定参数协变 问题 本节将阐述在使用 Java 泛型时会出现的各类问题。 任何基本类型都不能作为类型参数 正如本章早先提到的,Java 泛型的…...

Java18新增特性
前言 前面的文章,我们对Java9、Java10、Java11、Java12 、Java13、Java14、Java15、Java16、Java17 的特性进行了介绍,对应的文章如下 Java9新增特性 Java10新增特性 Java11新增特性 Java12新增特性 Java13新增特性 Java14新增特性 Java15新增特性 Java…...

springboot容器
1.主要指的是servlet容器 servlet组件由sevlet Filter Listener等 2.自动配置原理 通过ServletWebServerFactoryAutoConfiguration 配置这些内容 (自动配置类开始分析功能) conditionalOnclass开启条件 ServletRequest类 import导入嵌入式的tomcat Jetty等 这些是配置类&…...

Windows 10 下使用Visual Studio 2017 编译CEF SDK
1.下载CEF SDK 由于需要跑在32位的机器,所以选择下载32位的SDKCEF Automated Builds 选择 Current Stable Build (Preferred) ,这是当前稳定版本,CEF版本118 下载成功解压 2.下载编译工具 CMake 下载地址:CMake 配置CMake指向…...

数字货币swap交易所逻辑系统开发分析方案
随着数字货币市场的快速发展, Swap交易所已成为一种重要的交易方式。本文将对数字货币Swap交易所逻辑系统开发进行分析,并探讨其优势、开发难点和解决方案。 一、数字货币Swap交易所逻辑系统开发的优势 数字货币Swap交易所是一种点对点的交易方式&#x…...

spring boot中使用Bean Validation做优雅的参数校验
一、Bean Validation简介 Bean Validation是Java定义的一套基于注解的数据校验规范,目前已经从JSR 303的1.0版本升级到JSR 349的1.1版本,再到JSR 380的2.0版本(2.0完成于2017.08),目前最新稳定版2.0.2(201…...

搜索引擎项目
认识搜索引擎 1、有一个主页、有搜索框。在搜索框中输入的内容 称为“查询词” 2、还有搜索结果页,包含了若干条搜索结果 3、针对每一个搜索结果,都会包含查询词或者查询词的一部分或者和查询词具有一定的相关性 4、每个搜索结果包含好几个部分&…...

7.外部存储器,Cache,虚拟存储器
目录 一. 外部存储器 (1)磁盘存储器 1.磁盘的组成 2.磁盘的性能指标 3.磁盘地址 4.硬盘的工作过程 5.磁盘阵列 (2)固态硬盘(SSD) 二. Cache基本概念与原理 三. Cache和主存的映射方式 ÿ…...

UITableView的style是UITableViewStyleGrouped
一般情况下,UITableViewStylePlain和UITableViewStyleGrouped是UITableView常用到的style, 之前都是用到的时候,遇到问题直接用度娘,差不多就够用了,今天在修复UI提出的间隙问题,来回改,总觉得…...

Java17新增特性
前言 前面的文章,我们对Java9、Java10、Java11、Java12 、Java13、Java14、Java15、Java16 的特性进行了介绍,对应的文章如下 Java9新增特性 Java10新增特性 Java11新增特性 Java12新增特性 Java13新增特性 Java14新增特性 Java15新增特性 Java16新增特…...

VR全景技术在城市园区发展中有哪些应用与帮助
引言: 在数字化时代的浪潮中,虚拟现实(VR)全景技术逐渐融入各个领域,也为城市园区展示带来了全新的可能性。 一.VR全景技术简介 虚拟现实全景技术是一种通过全景图像和视频模拟真实环境的技术。通过相关设…...

在 SQL 中,当复合主键成为外键时应该如何被其它表引用
文章目录 当研究一个问题慢慢深入时,一个看起来简单的问题也暗藏玄机。在 SQL 中,主键成为外键这是一个很平常的问题,乍一看没啥值得注意的。但如果这个主键是一种复合主键,而另一个表又引用这个键作为它的复合主键,问…...

Ps:通过显示大小了解图像的打印尺寸
在 Photoshop 中,如果想了解文档窗口中的图像打印出来之后的实质大小,只要知道两个数值即可。 第一个数值是图像分辨率(也称“文档分辨率”)的大小,可在Ps菜单:图像/图像大小 Image Size对话框中查询或设置…...
Linux - 驱动开发 - watchdog - SMP机制下多核确活
说明 理论上:不管IC是单核还是多核,只要watchdog有被循环feed,就不会触发超时重启,因此watchdog在SMP机制下的多核环境显得比较宽松,只要任意核存活(喂狗)就不会重启设备。 实际情况 有客户反…...
概念解析 | LoRA:低秩矩阵分解在神经网络微调中的魔力
注1:本文系“概念解析”系列之一,致力于简洁清晰地解释、辨析复杂而专业的概念。本次辨析的概念是:基于低秩矩阵分解的神经网络微调方法LoRA LoRA:低秩矩阵分解在神经网络微调中的魔力 Low-Rank Adaptation of Large Language Models LoRA由如下论文提出,详细信息请参见论文原…...

量子计算和量子通信技术:引领潜力无限的未来
近年来,随着量子计算和量子通信技术的迅速发展,它们在各个领域的广泛应用前景引起了人们的极大兴趣。本文将深入探讨量子计算和量子通信技术的普遍应用,以及它们预示的未来,同时提出业内人士需要注意的事项。 介绍:量子…...

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技术可行性:…...
LightDB23.4支持mysql aes_encrypt/aes_decrypt/hex/unhex函数
背景介绍 为了兼容mysql数据库的功能,在LightDB23.4版本上支持hex/unhex/aes_encrypt/aes_decrypt函数。 函数原型如下: hex(data bytea) returns textunhex(data varchar) returns byteaaes_encrypt(data bytea, key bytea) returns byteaaes_encryp…...

uniapp——项目day04
购物车页面——商品列表区域 渲染购物车商品列表的标题区域 1. 定义如下的 UI 结构: 2.美化样式 渲染商品列表区域的基本结构 1. 通过 mapState 辅助函数,将 Store 中的 cart 数组映射到当前页面中使用: import badgeMix from /mixins/tab…...

7-爬虫-中间件和下载中间件(加代理,加请求头,加cookie)、scrapy集成selenium、源码去重规则(布隆过滤器)、分布式爬虫
0 持久化(pipelines.py)使用步骤 1 爬虫中间件和下载中间件 1.1 爬虫中间件(一般不用) 1.2 下载中间件(代理,加请求头,加cookie) 1.2.1 加请求头(加到请求对象中) 1.2.2 加cookie 1.2.3 加代理 2 scrapy集成selenium 3 源码去重…...

Chapter03-Authentication vulnerabilities
文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...

智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...

【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...

遍历 Map 类型集合的方法汇总
1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

UDP(Echoserver)
网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法:netstat [选项] 功能:查看网络状态 常用选项: n 拒绝显示别名&#…...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...

html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...

用机器学习破解新能源领域的“弃风”难题
音乐发烧友深有体会,玩音乐的本质就是玩电网。火电声音偏暖,水电偏冷,风电偏空旷。至于太阳能发的电,则略显朦胧和单薄。 不知你是否有感觉,近两年家里的音响声音越来越冷,听起来越来越单薄? —…...
智能AI电话机器人系统的识别能力现状与发展水平
一、引言 随着人工智能技术的飞速发展,AI电话机器人系统已经从简单的自动应答工具演变为具备复杂交互能力的智能助手。这类系统结合了语音识别、自然语言处理、情感计算和机器学习等多项前沿技术,在客户服务、营销推广、信息查询等领域发挥着越来越重要…...

零知开源——STM32F103RBT6驱动 ICM20948 九轴传感器及 vofa + 上位机可视化教程
STM32F1 本教程使用零知标准板(STM32F103RBT6)通过I2C驱动ICM20948九轴传感器,实现姿态解算,并通过串口将数据实时发送至VOFA上位机进行3D可视化。代码基于开源库修改优化,适合嵌入式及物联网开发者。在基础驱动上新增…...