JavaSE : 泛型(Generics)

1. 泛型基础

**泛型(Generics)**是Java语言中的一个重要特性,自Java 5版本引入。

  1. 泛型允许在编译时进行类型检查,提高了代码的可读性和可维护性,同时减少了运行时类型转换错误的风险。
  2. 泛型的主要作用是参数化类型。换句话说,泛型允许在类、接口和方法中定义类型参数,从而使得它们可以在实例化或调用时接受具体的类型。

1.1 定义与使用

泛型的基本使用包括在类、接口和方法中声明类型参数。例如,定义一个泛型类:

public class Box<T> {
    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }
}

实例化泛型类时,可以明确指定类型参数:

Box<String> box = new Box<>();
box.set("Hello");
System.out.println(box.get()); // 输出: Hello

泛型方法的定义允许在方法级别使用泛型参数,例如:

public class Util {
    public static <T> List<T> createList(T element) {
        List<T> list = new ArrayList<>();
        list.add(element);
        return list;
    }
}

调用泛型方法时,编译器会根据传入的参数类型自动推断泛型参数:

List<String> stringList = Util.createList("Hello");

1.2 类型擦除

Java泛型的一个关键特性是类型擦除。这意味着在编译之后,所有具体的泛型类型信息都会被擦除,只保留原始类型(未指定泛型参数时的类型)。例如,List<String>List<Integer> 在编译后都变成 List。这解释了为何不能使用泛型参数来创建数组(因为数组在运行时需要知道其确切类型),同时也意味着泛型的类型检查只发生在编译阶段。

1.3 上限与下限

  • 上限 使用 extends 关键字来指定类型参数必须是某个类或接口的子类型。例如,T extends Number 表示类型T必须是Number或其子类。
public class NumberHolder<T extends Number> {
    public void displayInfo(T t) {
        System.out.println(t.doubleValue());
    }
}
  • 下限 使用 super 关键字指定类型参数必须是某个类或接口的超类型。这在实践中不常见,但语法上是 T super SomeClass。不过,由于类型擦除和Java的类型系统限制,这种方式在泛型参数声明中并不实用,更多是用于方法的形参。

1.4 通配符

  • 无限制通配符 ? 表示可以接受任何类型的泛型参数。这提供了最大的灵活性,但限制了对泛型集合的操作(只能进行读取操作,不能写入)。

  • 上界通配符 ? extends T 允许任何T或T的子类型作为参数。这在需要从集合中读取元素,而不关心具体类型时非常有用。

  • 下界通配符 ? super T 允许T或T的父类型作为参数。当向集合中添加元素,而不关心具体容器类型时使用,体现了生产者与消费者的模式(PECS原则)。

理解并熟练运用这些基础概念,对于编写高效、类型安全的Java泛型代码至关重要。

2. 泛型类与接口

2.1 泛型类

定义泛型类:泛型类是指在类定义中声明一个或多个类型参数的类。这些类型参数在类内部可以作为字段类型、方法参数类型或返回类型使用。

public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

在这个例子中,Pair 类是泛型的,它有两个类型参数 KV,分别代表键和值的类型。在实例化时,我们可以指定具体类型:

Pair<String, Integer> pair = new Pair<>("Hello", 123);

在类中使用泛型参数:泛型参数可以在类的成员变量、方法参数、返回类型中使用,从而使得类变得更加通用和灵活。

2.2 泛型接口

定义泛型接口:与泛型类相似,泛型接口也可以声明类型参数,使得接口的实现能够支持多种数据类型。

public interface Mapper<T> {
    T map(Object source);
}

实现泛型接口的类型绑定规则:当实现一个泛型接口时,可以选择显式地指定类型参数,或者让实现类自身也成为泛型类。

  • 显式指定类型
class StringMapper implements Mapper<String> {
    @Override
    public String map(Object source) {
        return (String) source;
    }
}
  • 使实现类成为泛型类
public class GenericMapper<T> implements Mapper<T> {
    @Override
    public T map(Object source) {
        return (T) source;
    }
}

2.3 类型推断

类型推断机制:Java编译器能够根据上下文自动推断出泛型参数的类型,使得代码更简洁,无需显式指定类型参数。

例如,在实例化泛型类或调用泛型方法时,如果可以从上下文中明确类型,就不需要显示指定类型参数:

List<String> stringList = new ArrayList<>(); // 编译器推断出String类型

同样,在调用泛型方法时:

public static <T> List<T> asList(T... a) {
    return new ArrayList<>(Arrays.asList(a));
}

List<Integer> intList = asList(1, 2, 3); // 编译器推断出Integer类型

类型推断减少了代码中的冗余,提高了代码的可读性和编写效率,是Java泛型使用中非常便利的特性。

3.泛型方法

3.1.声明泛型方法

定义:泛型方法是指在方法的声明中包含类型参数列表的方法。这样的方法可以在任何类中定义,无论是普通的类还是泛型类。泛型方法使得方法能够独立于所归属的类是否为泛型类,拥有自己的泛型参数。

public class Utility {
    // 泛型方法示例
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

在这个例子中,printArray 是一个泛型方法,它接受一个泛型数组 T[] 作为参数。类型参数 <T> 在方法声明中定义,表明这个方法可以用于打印任何类型的数组元素。

3.2.静态泛型方法

特性和使用方式:静态泛型方法与非静态泛型方法的主要区别在于它们的访问权限和静态方法的特性。静态泛型方法属于类本身,不依赖于类的实例,因此不能直接访问类的非静态泛型参数(如果类是泛型类的话)。

静态泛型方法的一个典型应用场景是提供工具方法,这些方法可以根据传入的参数类型执行特定操作,而不需要创建类的实例。

public class StaticGenericMethods {
    // 静态泛型方法示例
    public static <T> T findFirstNonNull(T... elements) {
        for (T element : elements) {
            if (element != null) {
                return element;
            }
        }
        return null;
    }
}

上述代码中,findFirstNonNull 是一个静态泛型方法,它接受可变数量的参数,并返回第一个非空的元素。这里,<T> 位于方法名之前,表明这是一个与类无关的泛型声明,适用于任何类型的参数。

使用静态泛型方法的优点包括:

  • 灵活性:无需创建类的实例即可调用,更加灵活方便。
  • 代码复用:可以提供通用的工具方法,不依赖于特定对象的状态。
  • 清晰的职责分离:使代码组织更加清晰,区分静态工具方法与对象行为。

总之,泛型方法和静态泛型方法极大地增强了Java代码的泛型编程能力,提高了代码的灵活性和重用性。

4. 边界与约束

4.1.类型界限

类型界限(也称为边界)允许你在定义泛型时限制类型参数可以表示的类型范围。这是通过在类型参数声明后面使用 extends 关键字(用于指定上界)来实现的。如果需要限制类型参数至少是某个类或接口的子类/实现类,或者就是那个类/接口本身,类型界限就显得尤为重要。

public class BoundedExample<T extends Number> {
    public void process(T t) {
        System.out.println(t.doubleValue());
    }
}

在这个例子中,T 必须是 Number 或其子类,这样就可以安全地调用 doubleValue() 方法。

4.2.多重边界(Java 8+)

从Java 8开始,泛型支持同时指定多个边界,使用 & 符号分隔。这意味着类型参数必须满足所有指定的边界条件。这种特性对于需要类型参数同时实现多个接口或继承特定类的情况非常有用。

public class MultipleBoundsExample<T extends Comparable<T> & Serializable> {
    // ...
}

在这个例子中,类型参数 T 必须实现 Comparable<T> 接口(自我比较)并且还要实现 Serializable 接口,这在设计需要排序和序列化的泛型类时非常有用。

多重边界为泛型增加了更多的灵活性和精确性,使得泛型类和方法能够适应更广泛的类型要求,同时保持类型安全。然而,需要注意的是,多重边界中只能有一个类作为上界(如果有),其余必须是接口,因为Java不支持多重继承。

5. 泛型与数组

限制:Java不允许创建具体泛型类型的数组,这是因为数组和泛型在内存中的处理方式存在根本的不同。数组是协变的,意味着 Object[] 可以赋值给 String[](尽管这样做不安全),而泛型则不是协变的,以保证类型安全。直接创建如 List<String>[] 这样的泛型数组会导致编译错误,因为这可能导致类型安全漏洞。

绕过限制:尽管直接创建具体泛型类型的数组不被允许,但可以通过以下两种方式间接实现类似功能:

  1. 使用对象数组:创建 Object[] 数组,然后手动类型检查和转换。

    List<String>[] stringLists = (List<String>[]) new List<?>[10]; // 编译警告
    

    这种方式会产生未检查的警告,且在运行时可能导致ClassCastException

  2. 使用泛型集合:更推荐的方式是使用如 ArrayList<List<String>> 这样的集合来代替数组,以保持类型安全。

6. 通配符深入 - PECS原则

PECS原则 是指导何时使用上界通配符(? extends T)和下界通配符(? super T)的简单记忆法则,分别对应“生产者(Producer)”和“消费者(Consumer)”。

  • Producer Extends(生产者上界):当你只需要从集合中读取数据(生产数据),不需要插入新元素时,应使用上界通配符 ? extends T。这意味着集合可以是类型 T 或其任何子类型。

    public void printList(List<? extends Number> numbers) {
        for (Number num : numbers) {
            System.out.println(num);
        }
    }
    
  • Consumer Super(消费者下界):如果你要向集合中添加元素,但不需要从中读取特定类型的数据(消费数据),则使用下界通配符 ? super T。这表示集合可以接受类型 T 或其任何父类型。

    public void addNumber(List<? super Number> list, Number n) {
        list.add(n);
    }
    

PECS原则帮助设计更灵活的API,既能确保类型安全,又能最大化泛型的重用性。记住,当你既是生产者又是消费者(既读又写)时,通常需要具体类型而不是通配符。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值