removeIf 方法设计理念及泛型界限限定

ArrayList 中的 removeIf 方法是 Java 8 中引入的集合操作方法之一。它使用了 Predicate 接口作为参数,以便根据指定的条件移除集合中的元素。以下是对 removeIf 方法入参设计的详细解释:

Predicate 接口

Predicate 是一个函数式接口,定义了一个 test 方法,用于接收一个参数并返回一个布尔值。它的签名如下:

java 复制代码
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Predicate 接口通常用于对输入参数进行条件测试。结合 removeIf 方法,这个接口被用来判断集合中的元素是否应该被移除。

removeIf 方法的签名

ArrayList 类中,removeIf 方法的签名如下:

java 复制代码
public boolean removeIf(Predicate<? super E> filter)

其中,E 是集合的泛型类型参数,而 Predicate<? super E> 表示可以接受 E 类型或其父类型的参数。

设计理由

  1. 灵活性 : 使用 Predicate<? super E> 作为参数,使得 removeIf 方法可以接受针对元素类型及其父类型的条件。这种设计提供了更大的灵活性。例如,如果有一个 ArrayList<Number>,可以传入 Predicate<Object>,因为 ObjectNumber 的父类型。

  2. 函数式编程: Java 8 引入了函数式编程的概念,允许使用 lambda 表达式和方法引用。这使得编写条件测试的代码变得更加简洁和直观。例如,可以使用 lambda 表达式来移除所有负数:

    java 复制代码
    ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, -2, 3, -4, 5));
    list.removeIf(n -> n < 0);
  3. 类型安全 : 泛型确保了 Predicate 的参数类型与集合元素类型一致,从而在编译时提供类型安全。这样可以避免传递错误类型的条件测试,导致运行时错误。

removeIf 方法的实现

removeIf 方法的内部实现使用了迭代器来遍历集合,并应用 Predicate 进行条件测试。以下是简化的实现:

java 复制代码
public boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean removed = false;
    final Iterator<E> each = iterator();
    while (each.hasNext()) {
        if (filter.test(each.next())) {
            each.remove();
            removed = true;
        }
    }
    return removed;
}

在这个实现中:

  1. 检查 filter 是否为 null
  2. 使用迭代器遍历集合。
  3. 对每个元素应用 Predicate 进行测试。
  4. 如果 Predicate 返回 true,则移除该元素。

使用示例

以下是一些使用 removeIf 方法的示例:

移除负数
java 复制代码
ArrayList<Integer> numbers = new ArrayList<>(Arrays.asList(1, -2, 3, -4, 5));
numbers.removeIf(n -> n < 0);
System.out.println(numbers); // 输出:[1, 3, 5]
移除空字符串
java 复制代码
ArrayList<String> strings = new ArrayList<>(Arrays.asList("apple", "", "banana", " ", "cherry"));
strings.removeIf(String::isEmpty);
System.out.println(strings); // 输出:[apple,  , banana, cherry]

细品一下 removeIf() 方法入参设计

ArrayListremoveIf 方法中,使用 Predicate<? super E> 而不是 Predicate<E> 是为了增加方法的灵活性和通用性。具体来说,这与 Java 泛型的协变和逆变有关。以下是详细的解释:

泛型的协变和逆变

  • 协变(Covariance) :允许使用某种类型及其子类型。用 <? extends E> 表示。
  • 逆变(Contravariance) :允许使用某种类型及其超类型。用 <? super E> 表示。
  • 不变(Invariance) :只能使用某种特定类型。用 <E> 表示。

removeIf 方法中,Predicate<? super E> 表示这个 Predicate 可以接受类型 EE 的任何超类型。这是一种逆变,用于增加灵活性。

为什么使用 <? super E> 而不是 <E>

  1. 灵活性 : 使用 <? super E> 可以让 Predicate 接受类型 EE 的父类型的对象,从而增加了方法的灵活性。例如,如果 EInteger,那么 Predicate<? super Integer> 可以接受 Integer 及其父类 NumberObjectPredicate

    这意味着你可以传入更多种类的 Predicate,而不仅仅是严格匹配 EPredicate。例如:

    java 复制代码
    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
    Predicate<Object> isEven = obj -> obj instanceof Integer && (Integer) obj % 2 == 0;
    list.removeIf(isEven); // 使用 Predicate<Object> 也可以
  2. 兼容性 : 假设你有一个处理父类 NumberPredicate,比如:

    java 复制代码
    Predicate<Number> isPositive = num -> num.doubleValue() > 0;

    这个 Predicate 可以传递给 ArrayList<Integer>removeIf 方法,因为 NumberInteger 的超类型。如果 removeIf 方法只接受 Predicate<E>,那么你只能传入 Predicate<Integer>

  3. 类型安全 : 使用 <? super E> 仍然确保了类型安全,因为在调用 removeIf 方法时,传入的 Predicate 需要能够处理类型 E。例如,对于 ArrayList<Integer>,传入的 Predicate 需要能够处理 Integer 及其超类型。

注意: ? 无界通配符主要用于不关心元素类型的操作,基本都是公共类操作。

例子

使用 Predicate<E>

如果 removeIf 只接受 Predicate<E>,那么只能传入 Predicate<Integer>

java 复制代码
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
Predicate<Integer> isEven = n -> n % 2 == 0;
list.removeIf(isEven); // 正常工作

但是不能传入 Predicate<Number>

java 复制代码
Predicate<Number> isPositive = num -> num.doubleValue() > 0;
// list.removeIf(isPositive); // 编译错误
使用 Predicate<? super E>

使用 Predicate<? super E>,可以传入 Predicate<Integer>Predicate<Number>

java 复制代码
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Number> isPositive = num -> num.doubleValue() > 0;
list.removeIf(isEven); // 正常工作
list.removeIf(isPositive); // 也能正常工作

简而言之:

ArrayListremoveIf 方法使用 Predicate<? super E> 而不是 Predicate<E> 是为了增加方法的灵活性和通用性。通过允许传入类型 E 或其超类型的 PredicateremoveIf 方法变得更加通用和灵活,同时仍然保持类型安全。这样的设计使得在处理集合元素时,可以应用更多种类的条件,增加了代码的可重用性和灵活性。

再举例说明逆变的用法

逆变(Contravariance)在 Java 泛型中通过下界通配符(<? super T>)来实现。下界通配符允许使用某个类型及其父类型,这对于需要向集合中添加元素或处理泛型对象的写操作特别有用。以下是一些具体的例子来说明逆变的用法。

1. 使用逆变来添加元素到集合中

逆变可以确保能够向集合中添加类型 TT 的子类型的元素。

例子 1:向集合中添加元素
java 复制代码
import java.util.List;
import java.util.ArrayList;

public class CollectionUtils {
    public static void addAnimals(List<? super Dog> list) {
        list.add(new Dog());
        list.add(new Puppy());
        // list.add(new Animal()); // 编译错误,Animal 不是 Dog 的子类
    }

    public static void main(String[] args) {
        List<Animal> animalList = new ArrayList<>();
        addAnimals(animalList);

        for (Object obj : animalList) {
            System.out.println(obj.getClass().getSimpleName());
        }
    }
}

class Animal {}
class Dog extends Animal {}
class Puppy extends Dog {}

在这个例子中,addAnimals 方法接受一个 List<? super Dog> 类型的参数,这意味着它可以接受 Dog 类型及其超类型(如 AnimalObject)的列表,并向其中添加 Dog 及其子类型(如 Puppy)的元素。

2. 处理通用的数据结构

逆变可以用于处理具有多种元素类型的数据结构,比如在比较器中。

例子 2:使用逆变来编写通用比较器
java 复制代码
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class SortingUtils {
    public static <T> void sortList(List<T> list, Comparator<? super T> comparator) {
        Collections.sort(list, comparator);
    }

    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        intList.add(3);
        intList.add(1);
        intList.add(4);
        intList.add(1);
        intList.add(5);
        
        Comparator<Number> numberComparator = (Number n1, Number n2) -> Double.compare(n1.doubleValue(), n2.doubleValue());
        sortList(intList, numberComparator);

        System.out.println(intList); // 输出: [1, 1, 3, 4, 5]
    }
}

在这个例子中,sortList 方法接受一个 Comparator<? super T> 类型的参数,这意味着它可以接受 T 类型及其超类型的比较器,从而使得比较器更加通用和灵活。

3. 使用逆变来编写通用的消费操作

逆变也可以用于消费操作,例如在处理不同类型的消费者时。

例子 3:使用逆变来编写通用的消费者
java 复制代码
import java.util.List;
import java.util.ArrayList;
import java.util.function.Consumer;

public class ConsumerUtils {
    public static <T> void processElements(List<T> list, Consumer<? super T> consumer) {
        for (T element : list) {
            consumer.accept(element);
        }
    }

    public static void main(String[] args) {
        List<Dog> dogList = new ArrayList<>();
        dogList.add(new Dog());
        dogList.add(new Puppy());

        Consumer<Animal> animalConsumer = animal -> System.out.println(animal.getClass().getSimpleName());
        processElements(dogList, animalConsumer); // 输出: Dog Puppy
    }
}

class Animal {}
class Dog extends Animal {}
class Puppy extends Dog {}

在这个例子中,processElements 方法接受一个 Consumer<? super T> 类型的参数,这意味着它可以接受 T 类型及其超类型的消费者,从而使得消费者更加通用和灵活。

总结(添加、通用/公用、消费类型参数使用逆变)

逆变通过下界通配符(<? super T>)允许使用某个类型 T 及其父类型,主要用于写操作。使用逆变可以:

  1. 添加元素 :确保可以向集合中添加类型 T 及其子类型的元素。
  2. 通用操作:处理具有多种元素类型的数据结构,使得操作更加灵活和通用。
  3. 消费操作:处理不同类型的消费者,使得消费者更加通用和灵活。

通过这些例子,可以看到逆变在编写通用和灵活的代码中发挥了重要作用,确保类型安全的同时增加了代码的灵活性。

相关推荐
苹果醋31 小时前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
Hello.Reader1 小时前
深入解析 Apache APISIX
java·apache
菠萝蚊鸭2 小时前
Dhatim FastExcel 读写 Excel 文件
java·excel·fastexcel
旭东怪2 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
007php0072 小时前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程
∝请叫*我简单先生2 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl
ssr——ssss2 小时前
SSM-期末项目 - 基于SSM的宠物信息管理系统
java·ssm
一棵星2 小时前
Java模拟Mqtt客户端连接Mqtt Broker
java·开发语言
鲤籽鲲3 小时前
C# Random 随机数 全面解析
android·java·c#
zquwei3 小时前
SpringCloudGateway+Nacos注册与转发Netty+WebSocket
java·网络·分布式·后端·websocket·网络协议·spring