泛型的本质是: 变量 数据类型 参数化
泛型标识
常用的标识是:T、E、K、V 其实这个标识只要是字母就行,其只是一个声明数据类型的形参。
要明晰,这个形参可以代表的数据的类型可以是String、Integer等数据类型,也可以是自己定义的类,其不支持基本数据类型。
泛型类
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;
}
}
使用<>表示使用了泛型,而其中的K,V则标示使用了两种数据类型。泛型类中也可以有具体的数据类型,在我的理解看来,泛型类就是部分数据类型不确定的类模板。
在使用泛型时(创建对象时),需要指定数据类型
public class TestPair {
public static void main(String[] args) {
// 正确:为 K 和 V 同时指定类型
Pair<String, Integer> pair = new Pair<>("Age", 30);
System.out.println("Key: " + pair.getKey()); // 输出: Age
System.out.println("Value: " + pair.getValue()); // 输出: 30
}
}
就指定了Pair的K是String V 是Integer
如果不指定,则会按照Object类型来处理
Pairs b = new Pairs("tom",12);
Object key = b.getKey();
Object value = b.getValue();
同时 同一泛型类根据不同数据类型创建的对象,其本质上仍然是同一类型
Pairs <String,Integer> b = new Pairs("tom",12);
Pairs<Integer,String> c = new Pairs(12,"hei");
System.out.println(b.getClass() == c.getClass());
该结果执行的是true.虽然对象b和对象c 其数据类型是不同的,但是其都是class Pairs
泛型类派生子类
- 保持父类的泛型参数:子类仍然是一个泛型类,并且继承父类的泛型参数。
- 指定具体的类型:子类不再是泛型类,父类的泛型参数在子类中被具体化为某个具体类型。
- 子类自定义新的泛型参数:子类可以引入新的泛型参数,或者与父类的泛型参数一起使用。
子类继承父类泛型
// 泛型父类
public class GenericParent<T> {
private T data;
public GenericParent(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
// 泛型子类,保留泛型参数 T
public class GenericChild<T> extends GenericParent<T> {
public GenericChild(T data) {
super(data);//调用父类构造函数
}
// 子类方法可以继续使用泛型参数 T
public void printData() {
System.out.println("Data: " + getData());
}
}
子类指定具体类型
当子类数据类型确认时,需要只当父类的泛型数据类型
// 泛型父类
public class GenericParent<T> {
private T data;
public GenericParent(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
// 子类指定具体的类型为 String,不再是泛型类
public class StringChild extends GenericParent<String> {
public StringChild(String data) {
super(data)//调用父类构造函数,因为传入的是String类型data,所以父类的泛型也需要是String
}
public void printData() {
System.out.println("Data: " + getData());
}
}
子类加入新的泛型参数
// 泛型父类
public class GenericParent<T> {
private T data;
public GenericParent(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
// 子类引入新的泛型参数 U
public class GenericChild<T, U> extends GenericParent<T> {
private U extraData;
public GenericChild(T data, U extraData) {
super(data);
this.extraData = extraData;
}
public U getExtraData() {
return extraData;
}
}
泛型类上界
可以为泛型类的类型参数指定一个上界,即类型参数必须是某个类或接口的子类型。
public class GenericClassExample {
// 这个类的泛型参数 T 必须是 Number 或其子类
static class GenericClass<T extends Number> {
private T value;
public GenericClass(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
public static void main(String[] args) {
// 可以使用 Integer 类型的对象,因为 Integer 是 Number 的子类
GenericClass<Integer> intObject = new GenericClass<>(123);
System.out.println("Value: " + intObject.getValue());
// 可以使用 Double 类型的对象,因为 Double 也是 Number 的子类
GenericClass<Double> doubleObject = new GenericClass<>(456.78);
System.out.println("Value: " + doubleObject.getValue());
// 不能使用 String 类型的对象,因为 String 不是 Number 的子类
// GenericClass<String> stringObject = new GenericClass<>("ABC"); // 编译错误
}
}
但是泛型类没有下界, 在定义泛型类时不能直接使用 super
来限制泛型类型参数,但可以在泛型类的方法参数或方法签名中使用 super
来实现下界通配符。
class GenericClass<T> {
public void addValue(List<? super T> list, T value) {
list.add(value);
}
}
泛型接口
泛型接口的定义方式与泛型类类似,通常在接口名后使用尖括号 <T>
来表示泛型参数。
// 定义一个泛型接口,泛型参数 T
public interface GenericInterface<T> {
T getData();
void setData(T data);
}
泛型接口的实现
- 指定具体类型:实现类直接指定接口中的泛型参数为具体的类型。
- 保持泛型:实现类也可以保留泛型,并在实现时继续使用泛型参数。
指定具体类型
// 实现时将泛型类型 T 具体化为 String
public class StringImplementation implements GenericInterface<String> {
private String data;
@Override
public String getData() {
return data;
}
@Override
public void setData(String data) {
this.data = data;
}
}
保持泛型
// 保持泛型接口中的泛型
public class GenericImplementation<T> implements GenericInterface<T> {
private T data;
@Override
public T getData() {
return data;
}
@Override
public void setData(T data) {
this.data = data;
}
}
实现类的数据类型是可以比接口数据类型多的
// 定义一个只有一个泛型参数的接口
public interface GenericInterface<T> {
T getData();
void setData(T data);
}
// 实现类,增加了第二个泛型参数 U
public class GenericImplementation<T, U> implements GenericInterface<T> {
private T data;
private U additionalData; // 这是额外的泛型参数,仅供实现类使用
// 实现接口的 getData 方法
@Override
public T getData() {
return data;
}
// 实现接口的 setData 方法
@Override
public void setData(T data) {
this.data = data;
}
// 实现类可以自由使用额外的泛型参数 U
public U getAdditionalData() {
return additionalData;
}
public void setAdditionalData(U additionalData) {
this.additionalData = additionalData;
}
}
只不过额外的数据类型及相关的方法是和接口没有关系的,只和当前类有关。
不指定泛型类型
在实现泛型接口时,如果不指定接口的泛型类型,接口中的泛型参数会默认被视为 Object
类型。这意味着实现类将会处理 Object
类型的参数,所有的泛型类型参数在实现类中都会被擦除为 Object
。
// 实现类不指定接口的泛型类型
public class GenericImplementation implements GenericInterface {
private Object data;
@Override
public Object getData() {
return data;
}
@Override
public void setData(Object data) {
this.data = data;
}
}
泛型接口的通配符
使用泛型接口时,还可以结合通配符(如 ? extends T
和 ? super T
)来增强其灵活性。通配符的应用可以让你更方便地处理类型继承的关系。
// 泛型接口
public interface Container<T> {
void add(T item);
T get();
}
// 使用 ? extends Number
public class NumberContainer implements Container<? extends Number> {
private Number number;
@Override
public void add(Number item) {
this.number = item;
}
@Override
public Number get() {
return number;
}
}
在这个例子中,NumberContainer
使用了通配符 ? extends Number
,表示它可以接收 Number
类型或 Number
的子类。这种虽然提高了灵活性,但也牺牲了部分东西,后面会提到。
无论是泛型类派生子类还是泛型接口的实现,都需要保证类型的一致性,符合继承和实现规则。
泛型方法
public <T> T methodName(T param) {
// 方法体
return param;
}
泛型方法在调用方法时指明泛型的具体类型
泛型方法与泛型类的成员方法区别
泛型方法的标识还式<> ,其和泛型类中的成员方法是有区别的,成员方法是没有<>的,而且成员方法的泛型必须和类的泛型相同吗,同时,泛型类的成员方法是不可以定义成static(静态)的,但泛型方法是可以的。如例子,泛型方法可以使用泛型类的泛型,也可以不使用泛型类的泛型,其和泛型类是互相不影响的。
public class GenericClass<T> {
// 使用类的泛型 T 和方法的泛型 E
public <E> void compare(T element1, E element2) {
System.out.println("Element 1: " + element1);
System.out.println("Element 2: " + element2);
}
public static void main(String[] args) {
GenericClass<String> stringInstance = new GenericClass<>();
stringInstance.compare("Hello", 123); // T 是 String,E 是 Integer
stringInstance.compare("Hello", 45.67); // T 是 String,E 是 Double
}
}
下面的例子更能体现其互不影响的特点,即使泛型方法和泛型类都是T,但其依旧是不同的,泛型类的T是String类型,而泛型方法的T是Double类型的 ,这两个泛型参数是有不同的作用域的。
public class GenericClass<T> {
// 定义方法级别的泛型 T,和类级别的泛型 T 无关
public <T> void compare(T element1) {
System.out.println("Element 1: " + element1);
}
public static void main(String[] args) {
GenericClass<String> stringInstance = new GenericClass<>();
// 调用方法级别的泛型,传入 Double 类型的参数
stringInstance.compare(45.67);
}
}
泛型方法与可变参数
结合可变参数,又能进一步提高泛型方法的灵活性,这样可以让方法接收不确定数量的泛型参数。可变参数允许你传递多个同类型的参数,而不需要显式地创建数组。
public class GenericVarargs {
// 泛型方法,使用可变参数
public static <T> void printElements(T... elements) {
for (T element : elements) {
System.out.println(element);
}
}
public static void main(String[] args) {
// 传递多个 Integer 类型参数
printElements(1, 2, 3, 4, 5);
// 传递多个 String 类型参数
printElements("A", "B", "C");
// 混合类型的调用
printElements(1.1, "Hello", 3, true);
}
}
结果
1
2
3
4
5
A
B
C
1.1
Hello
3
true
使用 @SafeVarargs
注解可以避免编译时的警告,但要确保泛型数组不会被非法操作。
import java.util.List;
public class GenericVarargs {
// 泛型方法,接受多个 List 对象并打印
@SafeVarargs
public static <T> void printLists(List<T>... lists) {
for (List<T> list : lists) {
for (T element : list) {
System.out.print(element + " ");
}
System.out.println();
}
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<String> strList = List.of("A", "B", "C");
printLists(intList, strList);
}
}
限定泛型类型
有时候,我们希望限制泛型方法的类型,使它只能接收特定类型或其子类型。这可以通过使用 extends
关键字实现。
public class BoundedGenericMethod {
// 泛型方法,T 只能是 Number 或其子类
public static <T extends Number> void printNumber(T number) {
System.out.println("Number: " + number);
}
public static void main(String[] args) {
printNumber(123); // Integer
printNumber(45.67); // Double
// printNumber("Hello"); // 编译错误,String 不是 Number 的子类
}
}
使用通配符(通配符方法)
import java.util.List;
public class WildcardExample {
// 使用通配符 ? 来接受任何类型的 List
public static void printList(List<?> list) {
for (Object element : list) {
System.out.println("Element: " + element);
}
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<String> strList = List.of("A", "B", "C");
printList(intList); // 接受 Integer 类型的 List
printList(strList); // 接受 String 类型的 List
}
}
通配符 ?
,表示未知类型。通配符在处理泛型集合或不确定类型的对象时非常有用。但严格来说,这并不是泛型方法,因为通配符 ?
仅表示未知类型,并没有引入方法级别的类型参数。泛型方法 的定义在于引入一个类型参数(如 <T>
)来代替某个具体的类型,而通配符 ?
并没有提供这样的灵活性。
public static <T> void printList(List<T> list) {
for (T element : list) {
System.out.println("Element: " + element);
}
}
这个就是泛型方法,其和上面的通配符方法能有相同的作用。
通配符
是在使用泛型时引入的一种特殊符号,用于表示一个不确定的类型。通配符使用 ?
表示,可以与 extends
或 super
结合使用,形成有界通配符。它们主要用于解决泛型中涉及继承和协变/逆变的问题。通配符是一个实参。
- 无界通配符 (
?
):表示可以接受任何类型。 - 有界通配符 (
? extends T
):表示可以接受T
类型及其子类。 - 下界通配符 (
? super T
):表示可以接受T
类型及其父类。
无界通配符
当你只关心操作中的对象,但不关心具体的类型时,可以使用无界通配符。?
表示未知的类型,可以用来接受任何类型的泛型参数。
如通配符方法中的通配符使用就是无界的
有界通配符
又叫上界通配符,? extends T
表示可以接受类型 T
及其子类。它常用于只读场景,即你希望从泛型容器中读取某些对象,但不向其中添加新对象。
import java.util.List;
public class BoundedWildcardExample {
// 使用有界通配符,接受 Number 及其子类的 List
public static void printNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.println("Number: " + number);
}
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<Double> doubleList = List.of(1.1, 2.2, 3.3);
printNumbers(intList); // 可以接受 Integer 类型的 List
printNumbers(doubleList); // 可以接受 Double 类型的 List
}
}
下界通配符
? super T
表示可以接受类型 T
及其父类。下界通配符通常用于写入场景,即你希望向泛型容器中写入某种类型的对象。
import java.util.List;
import java.util.ArrayList;
public class LowerBoundWildcardExample {
// 使用下界通配符,接受 Integer 及其父类的 List
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
public static void main(String[] args) {
List<Number> numList = new ArrayList<>();
addNumbers(numList); // 可以接受 Number 类型的 List
for (Object number : numList) {
System.out.println("Number: " + number);
}
}
}
通配符类型 | 说明 | 适用场景 |
---|---|---|
? | 无界通配符,表示任何类型。 | 只对数据做读取操作,不关心类型。 |
? extends T | 上界通配符,表示类型 T 及其子类。 | 从泛型容器中读取数据,但不进行写入操作。 |
? super T | 下界通配符,表示类型 T 及其父类。 | 向泛型容器中写入数据,保证插入的数据类型是 T 或其子类。 |
PECS原则
- Producer Extends: 如果你想要从集合中获取数据,且集合是一个生产者,使用
? extends
。 - Consumer Super: 如果你想要向集合中写入数据,且集合是一个消费者,使用
? super
。
继承与泛型
普通的类与接口可以通过继承(extends
或 implements
)来形成父子类关系
class Animal {}
class Dog extends Animal {}
Animal animal = new Dog(); // 合法
泛型类之间没有继承的直接关系。即使 Dog
是 Animal
的子类,List<Dog>
并不是 List<Animal>
的子类。这就是泛型中的一大关键点。如下例子是不合法的:
List<Animal> animals = new ArrayList<Dog>(); // 不合法,编译错误
泛型类的继承关系不会随着其类型参数的继承关系而自动形成。
协变与逆变
协变和逆变是泛型类型的行为,用来描述在子类与父类关系中的类型转换行为。主要通过通配符(?
、extends
、super
)来实现。
协变
协变允许你使用一个子类的集合当作父类的集合。这在泛型中使用 ? extends T
表示。
- 协变使得你可以读取集合中的数据,但不能安全地向其中写入数据。
List<? extends Animal> animals = new ArrayList<Dog>();
Animal a = animals.get(0); // 合法,因为 Dog 是 Animal 的子类
animals.add(new Dog()); // 不合法,不能确定集合的具体类型
逆变
逆变允许你使用一个父类的集合当作子类的集合。这在泛型中使用 ? super T
表示。
- 逆变使得你可以写入数据到集合,但不能安全地读取集合中的数据。
List<? super Dog> animals = new ArrayList<Animal>();
animals.add(new Dog()); // 合法,可以向 List 中添加 Dog 或其子类
Animal a = animals.get(0); // 不合法,编译错误,无法确定类型