MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java中的多重通配符使用

2021-03-203.5k 阅读

Java 中的多重通配符使用

通配符基础回顾

在深入探讨多重通配符之前,我们先来回顾一下 Java 泛型中通配符的基本概念。通配符在 Java 泛型中起着至关重要的作用,它允许我们编写更加通用的代码,增强了代码的灵活性和复用性。

Java 中有两种主要的通配符类型:上限通配符(? extends Type)和下限通配符(? super Type)。上限通配符表示类型参数必须是指定类型或其某个子类型。例如,List<? extends Number> 表示一个 List,它可以包含 Number 类型或者 Number 的任何子类型,如 IntegerDouble 等。

import java.util.ArrayList;
import java.util.List;

public class UpperBoundedWildcardExample {
    public static void printList(List<? extends Number> list) {
        for (Number num : list) {
            System.out.println(num);
        }
    }

    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);

        List<Double> doubleList = new ArrayList<>();
        doubleList.add(1.5);
        doubleList.add(2.5);

        printList(intList);
        printList(doubleList);
    }
}

下限通配符则相反,表示类型参数必须是指定类型或其某个超类型。例如,List<? super Integer> 表示一个 List,它可以包含 Integer 类型或者 Integer 的任何超类型,如 NumberObject 等。

import java.util.ArrayList;
import java.util.List;

public class LowerBoundedWildcardExample {
    public static void addNumber(List<? super Integer> list) {
        list.add(10);
    }

    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        List<Number> numberList = new ArrayList<>();
        List<Object> objectList = new ArrayList<>();

        addNumber(intList);
        addNumber(numberList);
        addNumber(objectList);
    }
}

多重通配符场景引入

在实际的编程场景中,我们有时会遇到需要同时使用多个通配符的情况。这种场景通常出现在处理复杂的泛型类型关系时,比如在一些通用的数据处理框架或者工具类中。

假设我们有一个工具类,需要处理不同类型层次结构的数据。例如,我们有一个 Shape 类,CircleRectangle 继承自 Shape,同时有一个 Color 类,RedBlue 继承自 Color。我们想要一个方法,可以处理既包含某种形状又包含某种颜色的组合数据结构。

class Shape {}
class Circle extends Shape {}
class Rectangle extends Shape {}

class Color {}
class Red extends Color {}
class Blue extends Color {}

多重通配符语法及示例

简单多重通配符组合

  1. 在方法参数中的使用 我们可以定义一个方法,其参数使用多重通配符。例如,假设我们有一个 Pair 类,用于存储两个不同类型的对象。
class Pair<F, S> {
    private F first;
    private S second;

    public Pair(F first, S second) {
        this.first = first;
        this.second = second;
    }

    public F getFirst() {
        return first;
    }

    public S getSecond() {
        return second;
    }
}

public class MultipleWildcardExample {
    public static void printPair(Pair<? extends Shape,? extends Color> pair) {
        System.out.println("Shape: " + pair.getFirst().getClass().getSimpleName());
        System.out.println("Color: " + pair.getSecond().getClass().getSimpleName());
    }

    public static void main(String[] args) {
        Pair<Circle, Red> circleRedPair = new Pair<>(new Circle(), new Red());
        Pair<Rectangle, Blue> rectangleBluePair = new Pair<>(new Rectangle(), new Blue());

        printPair(circleRedPair);
        printPair(rectangleBluePair);
    }
}

在这个例子中,printPair 方法接受一个 Pair 对象,其第一个类型参数是 Shape 或其子类型,第二个类型参数是 Color 或其子类型。这使得我们可以处理不同形状和颜色组合的 Pair 对象。

  1. 在集合类型中的使用 考虑一个场景,我们有一个 List,其中每个元素都是一个 Pair,且 Pair 的类型符合特定的通配符要求。
import java.util.ArrayList;
import java.util.List;

public class MultipleWildcardInCollectionExample {
    public static void printListOfPairs(List<Pair<? extends Shape,? extends Color>> list) {
        for (Pair<? extends Shape,? extends Color> pair : list) {
            System.out.println("Shape: " + pair.getFirst().getClass().getSimpleName());
            System.out.println("Color: " + pair.getSecond().getClass().getSimpleName());
        }
    }

    public static void main(String[] args) {
        List<Pair<? extends Shape,? extends Color>> list = new ArrayList<>();
        list.add(new Pair<>(new Circle(), new Red()));
        list.add(new Pair<>(new Rectangle(), new Blue()));

        printListOfPairs(list);
    }
}

复杂多重通配符场景

  1. 多层嵌套泛型中的多重通配符 当涉及到多层嵌套的泛型结构时,多重通配符的使用会更加复杂,但也更加灵活。假设我们有一个 NestedPair 类,它内部又包含一个 Pair
class NestedPair<F, S, T> {
    private F first;
    private Pair<S, T> second;

    public NestedPair(F first, Pair<S, T> second) {
        this.first = first;
        this.second = second;
    }

    public F getFirst() {
        return first;
    }

    public Pair<S, T> getSecond() {
        return second;
    }
}

public class ComplexMultipleWildcardExample {
    public static void printNestedPair(NestedPair<? extends Number,? extends Shape,? extends Color> nestedPair) {
        System.out.println("Number: " + nestedPair.getFirst().getClass().getSimpleName());
        System.out.println("Shape: " + nestedPair.getSecond().getFirst().getClass().getSimpleName());
        System.out.println("Color: " + nestedPair.getSecond().getSecond().getClass().getSimpleName());
    }

    public static void main(String[] args) {
        Pair<Shape, Color> shapeColorPair = new Pair<>(new Circle(), new Red());
        NestedPair<Integer, Shape, Color> nestedPair = new NestedPair<>(10, shapeColorPair);

        printNestedPair(nestedPair);
    }
}

在这个例子中,printNestedPair 方法接受一个 NestedPair 对象,其第一个类型参数是 Number 或其子类型,第二个类型参数是 Shape 或其子类型,第三个类型参数是 Color 或其子类型。这种多层嵌套的泛型结构结合多重通配符,使得代码可以处理非常复杂的数据结构。

  1. 多重通配符与方法重载 多重通配符在方法重载的场景中也有独特的应用。考虑以下示例:
public class MultipleWildcardOverloadingExample {
    public static void process(Pair<? extends Shape,? extends Color> pair) {
        System.out.println("Processing general Shape - Color pair");
    }

    public static void process(Pair<? extends Circle,? extends Red> pair) {
        System.out.println("Processing Circle - Red pair specifically");
    }

    public static void main(String[] args) {
        Pair<Circle, Red> circleRedPair = new Pair<>(new Circle(), new Red());
        Pair<Rectangle, Blue> rectangleBluePair = new Pair<>(new Rectangle(), new Blue());

        process(circleRedPair);
        process(rectangleBluePair);
    }
}

在这个例子中,我们有两个 process 方法,一个接受更通用的 Pair<? extends Shape,? extends Color>,另一个接受更具体的 Pair<? extends Circle,? extends Red>。当调用 process 方法时,Java 编译器会根据实际传入的参数类型选择最合适的方法。对于 circleRedPair,会调用第二个更具体的方法;对于 rectangleBluePair,则会调用第一个更通用的方法。

多重通配符的注意事项

  1. 类型擦除影响 在 Java 中,泛型是通过类型擦除实现的。这意味着在运行时,泛型类型信息会被擦除,只保留原始类型。多重通配符同样受到类型擦除的影响。例如,Pair<? extends Shape,? extends Color>Pair<? extends Rectangle,? extends Blue> 在运行时的类型擦除结果是相同的,都是 Pair。这可能会导致在运行时无法区分基于多重通配符的不同泛型类型,从而在一些需要运行时类型判断的场景中带来限制。

  2. 通配符的赋值兼容性 当使用多重通配符时,需要注意赋值兼容性。例如,Pair<? extends Shape,? extends Color> 类型的变量不能直接赋值给 Pair<? extends Circle,? extends Red> 类型的变量,即使 CircleShape 的子类型,RedColor 的子类型。这是因为泛型的协变和逆变规则在多重通配符场景下同样适用,并且更加严格。

Pair<? extends Shape,? extends Color> generalPair = new Pair<>(new Circle(), new Red());
// 以下代码会编译错误
// Pair<? extends Circle,? extends Red> specificPair = generalPair;
  1. 方法参数的限制 在使用多重通配符作为方法参数时,由于通配符的不确定性,对方法内部操作有一定限制。例如,对于 Pair<? extends Shape,? extends Color> 类型的参数,我们不能向其中添加新的元素,因为我们不知道具体的类型。如果尝试添加元素,会导致编译错误。
public static void wrongAdd(Pair<? extends Shape,? extends Color> pair) {
    // 以下代码会编译错误
    // pair.setFirst(new Circle());
    // pair.setSecond(new Red());
}

多重通配符在实际项目中的应用

  1. 数据处理框架 在数据处理框架中,多重通配符可以用于处理不同类型层次结构的数据。例如,一个通用的数据转换框架可能需要处理不同类型的输入数据,并将其转换为不同类型的输出数据。通过使用多重通配符,框架可以支持多种数据类型组合,提高了框架的通用性和灵活性。

假设我们有一个数据转换接口 DataTransformer,它接受一个输入对和输出对,且输入和输出类型可以是不同类型层次结构中的类型。

interface DataTransformer<IN_F, IN_S, OUT_F, OUT_S> {
    Pair<OUT_F, OUT_S> transform(Pair<IN_F, IN_S> input);
}

class ShapeColorTransformer implements DataTransformer<? extends Shape,? extends Color, String, String> {
    @Override
    public Pair<String, String> transform(Pair<? extends Shape,? extends Color> input) {
        String shapeName = input.getFirst().getClass().getSimpleName();
        String colorName = input.getSecond().getClass().getSimpleName();
        return new Pair<>(shapeName, colorName);
    }
}

public class DataTransformationExample {
    public static void main(String[] args) {
        Pair<Circle, Red> inputPair = new Pair<>(new Circle(), new Red());
        DataTransformer<? extends Shape,? extends Color, String, String> transformer = new ShapeColorTransformer();
        Pair<String, String> outputPair = transformer.transform(inputPair);

        System.out.println("Transformed Shape: " + outputPair.getFirst());
        System.out.println("Transformed Color: " + outputPair.getSecond());
    }
}
  1. 通用工具类库 在通用工具类库中,多重通配符可以用于实现一些通用的操作。例如,一个通用的集合操作工具类可能需要处理不同类型的集合,且集合中的元素类型也有不同的层次结构。通过使用多重通配符,工具类可以支持多种集合类型和元素类型组合,提供更加通用的功能。
import java.util.ArrayList;
import java.util.List;

class CollectionUtils {
    public static <T, U> List<Pair<T, U>> zip(List<T> list1, List<U> list2) {
        List<Pair<T, U>> result = new ArrayList<>();
        int minSize = Math.min(list1.size(), list2.size());
        for (int i = 0; i < minSize; i++) {
            result.add(new Pair<>(list1.get(i), list2.get(ii)));
        }
        return result;
    }

    public static void printListOfPairs(List<Pair<?,?>> list) {
        for (Pair<?,?> pair : list) {
            System.out.println("First: " + pair.getFirst());
            System.out.println("Second: " + pair.getSecond());
        }
    }
}

public class CollectionUtilExample {
    public static void main(String[] args) {
        List<Shape> shapeList = new ArrayList<>();
        shapeList.add(new Circle());
        shapeList.add(new Rectangle());

        List<Color> colorList = new ArrayList<>();
        colorList.add(new Red());
        colorList.add(new Blue());

        List<Pair<? extends Shape,? extends Color>> zippedList = CollectionUtils.zip(shapeList, colorList);
        CollectionUtils.printListOfPairs(zippedList);
    }
}

多重通配符与其他泛型特性结合

  1. 多重通配符与泛型方法 泛型方法可以与多重通配符很好地结合使用。例如,我们可以定义一个泛型方法,它接受多重通配符类型的参数,并根据具体的类型参数执行不同的操作。
public class GenericMethodWithMultipleWildcards {
    public static <F, S> void performAction(Pair<F, S> pair, Action<F, S> action) {
        action.execute(pair);
    }

    interface Action<F, S> {
        void execute(Pair<F, S> pair);
    }

    public static void main(String[] args) {
        Pair<Shape, Color> shapeColorPair = new Pair<>(new Circle(), new Red());
        performAction(shapeColorPair, new Action<Shape, Color>() {
            @Override
            public void execute(Pair<Shape, Color> pair) {
                System.out.println("Shape: " + pair.getFirst().getClass().getSimpleName());
                System.out.println("Color: " + pair.getSecond().getClass().getSimpleName());
            }
        });
    }
}

在这个例子中,performAction 方法是一个泛型方法,它接受一个 Pair 对象和一个 Action 接口的实现。通过使用泛型方法,我们可以在运行时根据具体的 Pair 类型参数执行不同的操作,而多重通配符可以进一步增强方法的通用性。

  1. 多重通配符与类型边界 类型边界可以与多重通配符结合使用,进一步限制类型参数的范围。例如,我们可以定义一个泛型类,它的类型参数既有上限通配符又有下限通配符。
class BoundExample<F extends Number & Comparable<F>, S super Integer> {
    private F first;
    private S second;

    public BoundExample(F first, S second) {
        this.first = first;
        this.second = second;
    }

    public F getFirst() {
        return first;
    }

    public S getSecond() {
        return second;
    }
}

public class MultipleWildcardAndBoundsExample {
    public static void main(String[] args) {
        BoundExample<Integer, Number> example = new BoundExample<>(10, 20);
        System.out.println("First: " + example.getFirst());
        System.out.println("Second: " + example.getSecond());
    }
}

在这个例子中,BoundExample 类的第一个类型参数 F 必须是 Number 的子类型且实现了 Comparable<F> 接口,第二个类型参数 S 必须是 Integer 的超类型。这种结合方式使得我们可以更加精确地控制泛型类型参数的范围,同时利用多重通配符的灵活性。

总结多重通配符使用要点

  1. 理解通配符本质 多重通配符本质上是对 Java 泛型通配符概念的扩展,通过同时使用多个通配符,我们可以在处理复杂泛型类型关系时实现更高的灵活性和通用性。要深入理解多重通配符,首先要对基本的上限通配符和下限通配符有清晰的认识,明确它们在类型匹配和操作限制上的特点。

  2. 注意类型擦除 由于 Java 的类型擦除机制,多重通配符在运行时会失去具体的类型信息。这意味着在编写代码时,我们不能依赖运行时的类型判断来区分基于多重通配符的不同泛型类型。在设计和实现涉及多重通配符的代码时,需要充分考虑这一点,避免在运行时出现类型相关的错误。

  3. 掌握赋值兼容性 多重通配符的赋值兼容性遵循泛型的协变和逆变规则,但在多重通配符场景下会更加复杂。要清楚不同通配符组合之间的赋值关系,避免在代码中出现编译错误。例如,不能简单地将一个更通用的多重通配符类型赋值给一个更具体的多重通配符类型,除非满足特定的类型继承关系。

  4. 合理运用方法重载 在方法重载的场景中,多重通配符可以根据参数的具体类型提供更精确的方法匹配。通过定义不同通配符组合的重载方法,我们可以在代码中实现针对不同类型层次结构数据的差异化处理。但要注意方法重载的优先级和冲突问题,确保编译器能够正确选择合适的方法。

  5. 结合实际项目场景 在实际项目中,多重通配符常用于数据处理框架、通用工具类库等场景,以提高代码的通用性和可复用性。了解这些实际应用场景,并通过实践将多重通配符运用到项目中,可以更好地发挥其优势,解决复杂的编程问题。

  6. 与其他泛型特性结合 多重通配符可以与泛型方法、类型边界等其他泛型特性结合使用,进一步增强代码的表达能力和灵活性。在实际编程中,要善于将这些特性结合起来,根据具体的需求设计出高效、通用的代码结构。

通过深入理解和熟练运用多重通配符,Java 开发者可以在处理复杂泛型类型关系时更加得心应手,编写出更加健壮、通用和灵活的代码。无论是在大型企业级应用开发,还是在开源项目中,多重通配符都能为我们提供强大的工具,帮助我们解决各种复杂的编程挑战。希望通过本文的介绍和示例,读者对 Java 中的多重通配符使用有了更深入的理解,并能在实际编程中充分发挥其优势。