二十、泛型(6)

news2024/11/17 7:57:02

本章概要

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

问题

本节将阐述在使用 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);

        @Override
        public 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 Byte
            implements Supplier<java.lang.Byte> {
        SplittableRandom r = new SplittableRandom(47);

        @Override
        public 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 Character
            implements Supplier<java.lang.Character> {
        SplittableRandom r = new SplittableRandom(47);

        @Override
        public 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 Short
            implements Supplier<java.lang.Short> {
        SplittableRandom r = new SplittableRandom(47);

        @Override
        public 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 Integer
            implements Supplier<java.lang.Integer> {
        SplittableRandom r = new SplittableRandom(47);

        @Override
        public 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);

        @Override
        public 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 Long
            implements Supplier<java.lang.Long> {
        SplittableRandom r = new SplittableRandom(47);

        @Override
        public 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);

        @Override
        public 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 Float
            implements Supplier<java.lang.Float> {
        SplittableRandom r = new SplittableRandom(47);

        @Override
        public 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 Double
            implements Supplier<java.lang.Double> {
        SplittableRandom r = new SplittableRandom(47);

        @Override
        public 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);

        @Override
        public 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 String
            implements Supplier<java.lang.String> {
        SplittableRandom r = new SplittableRandom(47);
        private int strlen = 7; // Default length

        public String() {
        }

        public String(int strLength) {
            strlen = strLength;
        }

        @Override
        public 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;

    @Override
    public Integer get() {
        return ++i;
    }
}

class Widget {
    private int id;

    Widget(int n) {
        id = n;
    }

    @Override
    public String toString() {
        return "Widget " + id;
    }

    public static
    class Factory implements Supplier<Widget> {
        private int i = 0;

        @Override
        public Widget get() {
            return new Widget(++i);
        }
    }
}

class Fudge {
    private static int count = 1;
    private int n = count++;

    @Override
    public String toString() {
        return "Fudge " + n;
    }
}

class Foo2<T> {
    private List<T> x = new ArrayList<>();

    Foo2(Supplier<T> factory) {
        Suppliers.fill(x, factory, 5);
    }

    @Override
    public 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>> C
    create(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 cast
    List<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> {
    @Override
    public 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 error
    public int compareTo(Cat arg) {
        return 0;
    }
}

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

public class Hamster extends ComparablePet implements Comparable<ComparablePet> {

    @Override
    public int compareTo(ComparablePet arg) {
        return 0;
    }
}

// Or just:
class Gecko extends ComparablePet {
    @Override
    public 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 OK

class 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:
    @Override
    Derived 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 {
    void
    testA(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-bounded
    void 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.coloradmin.cn/o/1205453.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenCV 笔记(6):像素间的基本关系——邻域、邻接、通路、连通、距离

像素是图像的基本元素&#xff0c;像素与像素之间存在着某些联系&#xff0c;理解像素间的基本关系是数字图像处理的基础。常见的像素间的基本关系包括&#xff1a;邻域、邻接、通路、连通、距离。 Part11. 邻域 邻域表示了像素之间的连接关系。 像素(x,y)的邻域&#xff0c;是…

Linux技能篇-软链接和硬链接

文章目录 前言一、硬链接是什么&#xff1f;二、软链接是什么&#xff1f;三、硬链接和软链接的区别和共性1.区别2.共同点 总结 前言 在Linux系统中&#xff0c;有两个容易混淆的概念&#xff0c;就是软链接&#xff08;Soft Link&#xff09;和硬链接&#xff08;Hard Link&a…

android studio新版本gradle Tasks找不到assemble

最近需要打包arr&#xff0c;但android studio新版本为了加快编译速度&#xff0c;取消了gradle下的assemble任务&#xff0c;网上还没有博主更新解决方案&#xff0c;因此一直找不到解决方案&#xff0c;后来尝试如下操作才解决&#xff0c;方便后来者解决。 先将这里勾选上&…

vscode远程linux安装codelldb

在windows上使用vscode通过ssh远程连接linux进行c调试时&#xff0c;在线安装codelldb-x86_64-linux.vsix扩展插件失败&#xff0c;原因是linux服务器上的网络问题&#xff0c;所以需要进行手动安装。 首先在windows上下载&#xff1a; codelldb-x86_64-linux.vsix&#xff1b;…

GoldWave v6.78 绿色免费便携版功能介绍及使用说明

GoldWave v6.78 绿色免费便携版是一款集声音编辑、播放、录制与转换为一体的音频编辑工具&#xff0c;还可以对音频内容进行转换格式等处理。该软件支持许多格式的音频文件&#xff0c;包括WAV, OGG, VOC, IFF, AIF, AFC, AU, SND, MP3,MAT, DWD, SMP, VOX, SDS, AVI, MOV等音频…

vue v-model

一、为什么使用v-model&#xff1f; v-model指令可以在表单input、textarea以及select元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。本质上是语法糖&#xff0c;负责监听用户的输入事件来更新数据。 二、什么场景下会使用v-model&#xff1f; ①…

7.外部存储器,Cache,虚拟存储器

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

RabbitMQ-基础篇-黑马程序员

代码&#xff1a; 链接&#xff1a; https://pan.baidu.com/s/1nQBIgB_SbzoKu_XMWZ3JoA?pwdaeoe 提取码&#xff1a;aeoe 微服务一旦拆分&#xff0c;必然涉及到服务之间的相互调用&#xff0c;目前我们服务之间调用采用的都是基于OpenFeign的调用。这种调用中&#xff0c;调…

OCR转换技巧:如何避免图片转Word时出现多余的换行?

在将图片中的文字识别转换为Word文档时&#xff0c;我们很多时候时会遇到识别内容的一个自然段还没结束就换行的问题&#xff0c;这些就是我们常说的多余换行的问题。为什么会产生这个问题呢&#xff1f;主要是由于OCR返回的识别结果是按图片上的文字换行而换行&#xff0c;而不…

解决Mac配置maven环境后,关闭终端后环境失效的问题(适用于所有终端关闭后环境失效的问题)

目录 问题的原因 解决方式一、每次打开终端时输入&#xff1a;"source ~/.bash_profile"&#xff0c;这个方式比较繁琐 解决方式二、我们终端输入"vim ~/.zshrc"打开".zshrc"文件 1、我们输入以下代码&#xff1a; 2、首先需要按 " i…

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

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

保姆级使用vuedraggable三方组件

第一步 引入vuedraggable npm i vuedraggable -S 第二步 直接使用&#xff0c;源码如下 <template><draggableclass"list-group"tag"ul"v-model"list"v-bind"{animation: 1000,group: description,disabled: false,ghostClass:…

软路由R4S+iStoreOS实现公网远程桌面局域网内电脑

软路由R4SiStoreOS实现公网远程桌面局域网内电脑 文章目录 软路由R4SiStoreOS实现公网远程桌面局域网内电脑简介 一、配置远程桌面公网地址配置隧道 二、家中使用永久固定地址 访问公司电脑具体操作方法是&#xff1a;2.1 登录页面2.2 再次配置隧道2.3 查看访问效果 简介 上篇…

Load-balanced-online-OJ-system 负载均衡的OJ系统项目

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量博客汇总 本项目Github地址 - Load-balanced-o…

pdf增强插件 Enfocus PitStop Pro 2022 mac中文版功能介绍

Enfocus PitStop Pro mac是一款 Acrobat 插件&#xff0c;主要用于 PDF 预检和编辑。这个软件可以帮助用户检查和修复 PDF 文件中的错误&#xff0c;例如字体问题、颜色设置、图像分辨率等。同时&#xff0c;Enfocus PitStop Pro 还提供了丰富的编辑工具&#xff0c;可以让用户…

ESP32网络开发实例-BME280传感器数据保存到InfluxDB时序数据库

BME280传感器数据保存到InfluxDB时序数据库 文章目录 BME280传感器数据保存到InfluxDB时序数据库1、BM280和InfluxDB介绍2、软件准备3、硬件准备4、代码实现在本文中,将详细介绍如何将BME280传感器数据上传到InfluxDB中,方便后期数据处理。 1、BM280和InfluxDB介绍 InfluxDB…

08【保姆级】-GO语言的函数、包、错误处理

08【保姆级】-GO语言的函数、包、错误处理 一、 函数基本介绍1.1 基本概念1.2 包的概念1.3 包使用的注意事项和细节1.4 函数的调用机制1.5 函数的递归调用1.6 函数使用的注意事项和细节讨论1.7 init函数1.8 匿名函数1.8.1 匿名函数使用方式1.8.2 全局匿名函数 1.9 闭包1.9.1 闭…

基于flask+bootstrap4实现的注重创作的轻博客系统项目源码

一个注重创作的轻博客系统 作为一名技术人员一定要有自己的博客&#xff0c;用来记录平时技术上遇到的问题&#xff0c;把技术分享出去就像滚雪球一样会越來越大&#xff0c;于是我在何三博客的基础上开发了[l4blog]&#xff0c;一个使用python开发的轻量博客系统&#xff0c;…

微信小程序广告banner、滚动屏怎么做?

使用滑块视图容器swiper和swiper-item可以制作滚动屏&#xff0c;代码如下&#xff1a; wxml: <swiper indicator-dots indicator-color"rgba(255,255,255,0.5)" indicator-active-color"white" autoplay interval"3000"><swiper-ite…

达梦数据库安装

一、官网参考文档 达梦数据库官网&#xff1a;https://www.dameng.com/ &#xff0c;参考文档如下&#xff1a; 最后的文档地址为&#xff1a;Docker安装 | 达梦技术文档 二、dcoker安装 docker基本按照官网来就行&#xff0c;点击相应的链接下载镜像包。 复制到linux中&#x…