Java泛型
- 一、泛型 Generics的意义
- 1.1 在没有泛型的时候,集合如何存储数据
- 1.2 引入泛型的好处
- 1.3 注意事项
- 1.3.1 泛型不支持基本数据类型
- 1.3.2 当泛型指定类型,传递数据时可传入该类及其子类类型
- 1.3.3 如果不写泛型,类型默认是Object
- 二、泛型程序设计
- 2.1 类型参数的含义
- 2.2 泛型类(普通类的工厂)
- 2.2.1 泛型类的格式
- 2.2.2 注意事项
- 泛型类与静态成员
- 错误示例
- 正确示例
- 2.2.3 泛型类使用的例子
- 2.2.4 泛型类的继承
- (1)子类保持父类的泛型性
- 2. 子类为父类的泛型类型指定具体类型
- 3.泛型方法的继承
- 2.3 泛型方法
- 2.3.1 格式
- 2.3.2 可变参数(Varargs)
- 2.3.3 泛型方法的使用例子
- 方式1:使用类名后面定义的泛型
- 方式2:单独定义泛型
- 区别总结
- 2.4 泛型接口
- 2.4.1 格式:
- 2.4.2 示例
- 三、类型擦除(Type Erasure)
- 3.1 为什么使用类型擦除
- 四、泛型通配符
- 4.1 ? extends T (上界通配符)
- 4.2 ? super T(下界通配符)
- 4.3 无界通配符 `?`
- 4.4 详细解释
- `? extends T`
- `? super T`
- 无界通配符 `?`
- 4.5 PECS 原则
一、泛型 Generics的意义
在泛型这个概念出现之前,程序员必须使用Object编写适用于多种类型的代码。
1.1 在没有泛型的时候,集合如何存储数据
在 Java 中,如果我们没有给集合指定类型,默认情况下,集合中的所有数据类型都会被认为是 Object
类型。这意味着我们可以向集合中添加任何类型的数据,例如 Integer
、String
、Double
等。
List list = new ArrayList();
list.add(1); // 添加 Integer 类型
list.add("hello"); // 添加 String 类型
list.add(3.14); // 添加 Double 类型
虽然这样使用集合很灵活,但也带来了一些问题:
-
类型安全问题:
由于集合中可以存储任意类型的数据,我们在取出数据时无法确定其实际类型。这可能会导致类型转换错误(ClassCastException
)。Object obj = list.get(0); // 获取集合中的第一个元素,类型是 Object Integer num = (Integer) obj; // 需要强制类型转换
-
丧失类型特有行为:
由于所有元素都被视为Object
类型,我们无法直接调用其特有的方法。由于
Object
类型不包含特定类型的方法或行为,所以无法直接调用这些对象的特有方法。需要进行类型转换(强制类型转换)来使用具体类型的方法,这样不仅麻烦,还可能导致运行时错误(如ClassCastException
)。// 运行时错误示例:如果类型转换不正确,会抛出 ClassCastException // 尝试将 Integer 转换为 String String str2 = (String) list.get(1); // 运行时异常:ClassCastException
例如,如果集合中存储的是
String
类型,我们不能直接调用String
的方法,而必须先进行类型转换。Object obj = list.get(1); String str = (String) obj; System.out.println(str.length()); // 调用 String 的方法
1.2 引入泛型的好处
核心意义在于 类属性或方法的参数在定义数据类型时,可以直接使用一个标记进行占位 ,在具体使用时才设置其对应的实际数据类型,这样当设置的数据类型出现错误后,就可以在程序编译时检测 来。
为了克服上述问题,Java 5 引入了泛型。通过使用泛型,我们可以在创建集合时指定其存储的数据类型,从而在编译时就能进行类型检查,确保类型安全。
List<String> stringList = new ArrayList<>();
stringList.add("hello");
// stringList.add(1); // 编译时会报错
String str = stringList.get(0); // 不需要强制类型转换
System.out.println(str.length());
使用泛型的好处如下:
-
类型安全:
在添加元素时,编译器会检查类型是否匹配,不匹配的类型会在编译时报错,避免了运行时的类型转换错误。 -
减少强制类型转换:
在获取集合中的元素时,不需要进行强制类型转换,代码更加简洁和安全。(把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的异常)
-
提高可读性:
通过指定集合中元素的类型,代码的可读性和维护性得到提高,因为开发者可以明确知道集合中应该存储什么类型的数据。
总结:
-
在没有泛型之前,集合可以存储任意类型的对象,但这带来了类型安全和使用上的不便。
-
泛型允许在编译时指定集合中的元素类型,从而提高了类型安全性和代码的可读性、可维护性。
1.3 注意事项
1.3.1 泛型不支持基本数据类型
泛型的类型参数只能使用引用类型的,其对于基本数据类型(int
,char
,double
)等是不支持直接转化为Object
的。
解决方案:
可以使用基本数据类型对应的包装类(如
Integer
、Character
等)来替代。
1.3.2 当泛型指定类型,传递数据时可传入该类及其子类类型
**泛型的本质是将类型的参数化。**通过将类型作为参数引入,泛型允许在编写代码时不必指定具体的数据类型,而是在使用时才确定具体的类型。
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
List<Animal> animalList = new ArrayList<>();
animalList.add(new Dog()); // 正确:Dog 是 Animal 的子类
animalList.add(new Cat()); // 正确:Cat 是 Animal 的子类
1.3.3 如果不写泛型,类型默认是Object
二、泛型程序设计
泛型不具备继承性,但数据具备
泛型程序设计可以被分为三种:泛型类、泛型接口、泛型方法
2.1 类型参数的含义
在编程中使用字母如 T、E、K、V 等来表示变量通常是为了提高代码的可读性和通用性。
- T (Type): 用来表示一个类型,可以是任意类型。通常用于泛型编程中。
- E (Element): 用来表示一个元素类型,通常用于集合类的数据结构(如列表、集合等)。
- K (Key): 用来表示键的类型,通常用于映射类型的数据结构(如字典、映射等)。
- V (Value): 用来表示值的类型,通常与 K 一起使用,表示字典或映射中的值。
2.2 泛型类(普通类的工厂)
泛型类是一个允许使用或者多个类型参数(类型变量)的类。
例如:一个简单的泛型类可以表示一个容器类,用于存储和检索不同类型的对象。
2.2.1 泛型类的格式
这个类可以使得我们可以只关注泛型,而可以不再为数据存储的细节而分心
修饰符 class 类名<泛型> {
}
// 例如
public class ClassName<T1, T2, ..., Tn> {
// 类体
}
在类名后面定义泛型,在创建该类对象时确定类型。
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
Box<Integer> integerBox = new Box<>();
integerBox.set(10);
Integer value = integerBox.get();
2.2.2 注意事项
泛型类与静态成员
在Java中,静态成员是指用
static
关键字修饰的类成员。静态成员包括静态变量(或静态字段)、静态方法、静态块和静态内部类。
-
静态成员属于类本身,而非类的实例:
由于静态成员在类加载时就存在,而此时泛型参数还未被实例化为具体类型,不与类的实例关联的。
-
静态成员在类加载时就已经初始化,而此时泛型参数尚未被具体化,静态成员无法知道或引用泛型参数的具体类型。
因此,静态成员(如静态变量、静态常量、静态方法等)不能直接访问或使用类的泛型参数
-
静态方法可以有自己的泛型参数,静态内部类也可以定义自己的泛型参数,这些参数独立于外部类的泛型参数。这允许在静态上下文中使用泛型,只要这些泛型参数是在调用静态方法或创建静态内部类的实例时具体化的。
-
-
静态的成员不能使用类的泛型:
- 静态成员在类加载时就存在,而此时泛型参数尚未被具体化(因为没有创建实例),因此静态成员无法知道泛型参数的具体类型。
- 如果静态成员能够使用泛型参数,那么在类加载时就必须确定泛型参数的类型,但这与泛型参数的设计目标相冲突,即在实例化时才确定类型。
错误示例
public class GenericClass<T> {
// 静态变量
// private static int staticVar; // 这个不是关于泛型的错误
// 静态方法
public static void staticMethod() {
// T temp; // 编译错误:无法从静态上下文访问泛型类型T
}
// ...
// 静态内部类
static class StaticNestedClass {
// 静态内部类不能使用外部类的泛型类型参数T
// T temp; // 编译错误:无法从静态上下文访问泛型类型T
public void print() {
// 这里会报编译错误
// T temp = null; // 编译错误:无法从静态上下文访问泛型类型T
System.out.println(static-class);
}
}
// 静态代码块
static{
// T temp; // 编译错误:无法从静态上下文访问泛型类型T
}
}
正确示例
public class GenericClass<T> {
// 实例变量
private T instanceVar;
// 静态方法
public static <U> U staticMethod(U u) { // 使用独立的泛型参数U
return u;
}
// 可以进行正常的构造方法和setter和getter方法
// 静态内部类
static class StaticNestedClass<U> { // 使用独立的泛型参数U
private U temp;
public StaticNestedClass(U temp) {
this.temp = temp;
}
// ...
}
// 实例方法
public void instanceMethod(){
T temp = instanceVar;
System.out.println(temp);
}
// 静态代码块
static{
// 使用原始类型或其他方式初始化静态成员
List list = new ArrayList(); // 假设list是静态成员
}
}
-
静态方法:尝试使用类的泛型参数T,这是不允许的。修改后,静态方法使用了一个独立的泛型参数U,这样就避免了依赖于类的泛型参数。
-
静态内部类:尝试使用外部类的泛型参数T,这也是不允许的。修改后,静态内部类使用了自己的泛型参数U
-
静态代码块:尝试使用泛型参数T,这是不允许的。修改后,静态代码块中使用了原始类型或其他方式来初始化静态成员,避免了直接使用泛型参数。
2.2.3 泛型类使用的例子
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;
}
public void setKey(K key) {
this.key = key;
}
public void setValue(V value) {
this.value = value;
}
}
传入什么样的类型就会转变为该类型输出
2.2.4 泛型类的继承
(1)子类保持父类的泛型性
// 泛型父类
class Animal<T> {
private T food;
public Animal(T food) {
this.food = food;
}
public T getFood() {
return food;
}
public void setFood(T food) {
this.food = food;
}
}
// 泛型子类,保持父类的泛型性
class Person<T> extends Animal<T> {
public Person(T food) {
super(food);
}
}
// 使用
Person<String> person = new Person<>("apple");
System.out.println(person.getFood()); // 输出: apple
2. 子类为父类的泛型类型指定具体类型
// 泛型父类
class Animal<T> {
// ...(与上面相同)
}
// 非泛型子类,为父类的泛型类型指定具体类型
class Dog extends Animal<String> {
public Dog(String food) {
super(food);
}
}
// 使用
Dog dog = new Dog("bone");
System.out.println(dog.getFood()); // 输出: bone
3.泛型方法的继承
// 父类
class Parent {
// 泛型方法
public <T> void print(T item) {
System.out.println(item);
}
}
// 子类
class Child extends Parent {
// 子类可以调用父类的泛型方法
public void test() {
print("Hello, World!"); // 调用继承自Parent的泛型方法
}
// 如果子类需要定义与父类相同签名的泛型方法,则实际上是覆盖父类的方法
// 但在这个例子中,我们没有这样做
}
// 使用
Child child = new Child();
child.test(); // 输出: Hello, World!
2.3 泛型方法
泛型方法在Java中是用于处理多种数据类型的灵活工具。泛型方法允许在方法定义中使用类型参数。
通过使用泛型,可以在方法中处理不同类型的数据,而不需要重载多个方法。
2.3.1 格式
`修饰符 <泛型> 返回值类型 方法名(形参列表){ }`
public <T> void show(T t) {}
在修饰符后面定义泛型,在调用该方法时确定类型。
public class GenericMethodExample {
public <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
}
GenericMethodExample example = new GenericMethodExample();
Integer[] intArray = {1, 2, 3, 4, 5};
example.<Integer>printArray(intArray); // 可以省略 <Integer>
2.3.2 可变参数(Varargs)
可变参数:方法参数个数不固定,用…表示,其底层实现是通过数组来实现的
形参列表中可变参数只能有一个X
可变参数必须放在形参列表的最后面
- 泛型方法
addAll
来动态地向ArrayList
中添加不同类型的元素
public class CC {
private CC() {
}
// 可变参数
public static <E> void addAll(ArrayList<E> list, E... e) {
for (E e1 : e) {
list.add(e1);
}
}
}
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
CC.addAll(list, "a", "b", "c", "d");
System.out.println(list);
}
- 多个参数加法
public class Test {
public static void main(String[] args) {
int x=test(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
System.out.println(x);
}
public static int test(int... args) {
int sum=0;
// 可变参数
for (int arg : args) {
sum += arg;
System.out.println(arg);
}
return sum;
}
}
2.3.3 泛型方法的使用例子
一个简单的泛型方法可以用来交换两个对象的值。
public class Utils {
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4};
swap(intArray, 0, 3);
for (int i : intArray) {
System.out.print(i + " ");
}
String[] strArray = {"a", "b", "c", "d"};
swap(strArray, 1, 2);
for (String s : strArray) {
System.out.print(s + " ");
}
}
}
在这个例子中,swap
方法是一个泛型方法,类型参数T
在方法定义中声明,可以在调用时指定具体的类型,如Integer
或String
。
泛型方法是Java中用于处理多种数据类型的灵活工具。通过使用泛型,可以在方法中处理不同类型的数据,而不需要重载多个方法。泛型方法有两种定义方式:类名后定义泛型和方法上单独定义泛型。
方式1:使用类名后面定义的泛型
在这种方式中,泛型类型在类定义时声明,并在类的所有方法中可用。适用于需要在类的多个方法中使用相同泛型类型的情况。
// 使用类名后面定义的泛型
public class GenericClass<E> {
// 泛型类型E在整个类中可用
public void show(E e) {
System.out.println(e);
}
public E getValue(E e) {
return e;
}
public static void main(String[] args) {
GenericClass<String> genericClass = new GenericClass<>();
genericClass.show("Hello");
System.out.println(genericClass.getValue("World"));
}
}
方式2:单独定义泛型
这种方式在方法定义时单独声明泛型类型,适用于仅在某个特定方法中需要使用泛型类型的情况。
// 单独定义泛型的方法
public class GenericMethodExample {
// 在方法中单独定义泛型类型T
public <T> void show(T t) {
System.out.println(t);
}
public <T> T getValue(T t) {
return t;
}
public static void main(String[] args) {
GenericMethodExample example = new GenericMethodExample();
example.show("Hello");
System.out.println(example.getValue("World"));
example.show(123);
System.out.println(example.getValue(456));
}
}
区别总结
- 作用域不同:类名后定义的泛型类型在整个类中可见和可用,而单独定义的泛型类型仅在当前方法中可见和可用。
- 适用范围不同:类名后定义的泛型适用于需要在多个方法中使用相同泛型类型的情况,而单独定义的泛型适用于仅在特定方法中需要使用泛型类型的情况。
2.4 泛型接口
可以使接口能够处理多种不同的数据类型,而无需指定具体的数据类型。泛型接口在定义时包含一个或多个类型参数,这些类型参数在接口的实现类中可以具体化为特定的类型。
2.4.1 格式:
修饰符 interface 接口名<泛型> { }
// 定义一个泛型接口
public interface GenericInterface<T> {
void doSomething(T t);
}
在接口名后面定义泛型,实现类确定类型或实现类延续泛型。
public interface Container<T> {
void add(T item);
T get(int index);
}
public class StringContainer implements Container<String> {
private List<String> items = new ArrayList<>();
@Override
public void add(String item) {
items.add(item);
}
@Override
public String get(int index) {
return items.get(index);
}
}
2.4.2 示例
// 实现泛型接口的类
public class GenericClass implements GenericInterface<String> {
@Override
public void doSomething(String t) {
System.out.println("Doing something with: " + t);
}
}
public class Main {
public static void main(String[] args) {
GenericClass gc = new GenericClass();
gc.doSomething("Hello, World!");
}
}
也可以创建一个泛型类来实现泛型接口
// 定义一个泛型类来实现泛型接口
public class GenericClass<T> implements GenericInterface<T> {
@Override
public void doSomething(T t) {
System.out.println("Doing something with: " + t);
}
}
public class Main {
public static void main(String[] args) {
GenericClass<String> gcString = new GenericClass<>();
gcString.doSomething("Hello, World!");
GenericClass<Integer> gcInteger = new GenericClass<>();
gcInteger.doSomething(123);
}
}
三、类型擦除(Type Erasure)
它指的是在编译期间,编译器会删除(或擦除)所有泛型类型信息,使得在运行时,程序只能看到原始的类型(即泛型参数被擦除)。
类型擦除:在编译时,Java会将泛型类型信息擦除,将泛型类型替换为其上限类型(默认是0bject)。
List在编译后会被转换为List,这意味着在运行时无法获取到String类型的信息。
public class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
在编译后,泛型类型 T
会被擦除为 Object
:
public class Box {
private Object value;
public void setValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
3.1 为什么使用类型擦除
- 兼容性:类型擦除使得泛型代码能够与 Java 1.4 及以前的版本兼容,因为在这些版本中并没有泛型支持。
- 简化:通过类型擦除,可以减少对运行时类型信息的需求,从而降低运行时开销。
四、泛型通配符
泛型通配符(wildcards)是泛型编程中的一个重要概念,主要用于在不确定泛型类型时,提供灵活的类型约束。
4.1 ? extends T (上界通配符)
-
主要用途:用于返回类型限定。
-
适用场景:主要用于从集合中读取数据。(不能往里存,只能往外取)
-
限制:不能用于参数类型限定,因为编译器无法确定具体类型,只能接受
null
。 -
定义与使用:
- <? extends T>` 表示通配符类型的上界,即表示参数化类型可以是 `T` 类型或 `T` 的任何子类。
-
可以安全地从这样的通配符类型中读取数据,因为可以确保获取的元素至少是
T
类型的实例或其子类
-
不能往里存的原因:与多态的概念的类似
- 编译器无法确定具体的子类类型,因此不能安全地向这样的列表中添加任何具体的子类实例,只能添加
null
。
- 编译器无法确定具体的子类类型,因此不能安全地向这样的列表中添加任何具体的子类实例,只能添加
示例:
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
List<? extends Animal> animalList = dogs;
for (Animal animal : animalList) {
System.out.println(animal.getClass().getSimpleName()); // 读取时安全
}
// animalList.add(new Dog()); // 编译错误,不能添加元素
}
}
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
4.2 ? super T(下界通配符)
-
主要用途:用于参数类型限定。
-
适用场景:主要用于向集合中写入数据。(不能往外取,只能往外取)
-
限制:不能用于返回类型限定,因为返回的类型只能用
Object
接收。 -
定义与使用:
<? super T>
表示通配符类型的下界,即表示参数化类型是T
类型或T
的任何超类(父类),直至Object
。- 可以安全地向这样的通配符类型中添加
T
类型及其子类的实例。
-
往里存的原因:
- 允许添加
Father
类型及其子类的实例,因为可以确保这样的列表至少可以接受Father
类型的实例。
- 允许添加
示例:
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
List<? super Animal> animalList = animals;
animalList.add(new Dog()); // 可以添加 Dog 类型
animalList.add(new Cat()); // 可以添加 Cat 类型
animalList.add(new Animal()); // 可以添加 Animal 类型
for (Object obj : animalList) {
System.out.println(obj.getClass().getSimpleName()); // 读取时只能确保是 Object 类型
}
}
}
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
4.3 无界通配符 ?
-
主要用途:表示未知类型。
-
适用场景:一般用于参数和返回类型都不重要的情况。
-
限制:不能用于方法参数传入,也不能用于方法返回。
-
读操作:可以安全地读取列表中的元素,但因为没有具体类型信息,所以只能读取为
Object
类型。这意味着不能对这些元素进行具体的操作,只能执行泛用的Object
方法。 -
写操作:不能向列表中添加元素,除非添加
null
。这是因为你不知道列表的具体类型,添加具体类型的元素可能会破坏列表的类型安全性。
-
示例:
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
strings.add("Hello");
strings.add("World");
List<?> unknownList = strings;
for (Object obj : unknownList) {
System.out.println(obj); // 读取时只能确保是 Object 类型
}
// unknownList.add("hello"); // 编译错误:无法添加元素
unknownList.add(null); // 可以添加 null
}
}
4.4 详细解释
? extends T
? extends T
:用于返回类型限定,适合读取操作,不能用于参数类型限定。
在使用 ? extends T
时,必须确保列表在访问前已经被填充了数据。在读取元素时,编译器能够确保读取到的元素类型为 T
或其子类,但无法确定具体类型,因此不能添加元素。
? super T
? super T
:用于参数类型限定,适合写入操作,不能用于返回类型限定。
在使用 ? super T
时,可以安全地向列表中添加 T
类型或其子类的对象,因为编译器知道列表中至少可以容纳 T
类型的对象。但是在读取时,只能确保读取到的元素是 Object
类型,因此需要进行类型转换。
无界通配符 ?
?
:表示未知类型,通常用于对类型没有特别要求的场景,不能用于方法参数传入和返回类型。
无界通配符 ?
表示未知类型,可以用于任何类型,但在读取时只能确保类型为 Object
,在添加时只能添加 null
。
4.5 PECS 原则
- Producer Extends:如果你有一个生产者方法,它返回泛型类型T的实例,那么你应该使用
<? extends T>
。这是因为生产者返回的实例可以是T的子类型,因此使用extends
通配符可以确保你能处理这些子类型。 - Consumer Super:如果你有一个消费者方法,它接收泛型类型T的实例,那么你应该使用
<? super T>
。这是因为消费者方法接收的实例可以是T的父类型,因此使用super
通配符可以确保你可以传递T及其子类型的实例。
Producer Extends Consumer Super 原则:
- 当你需要从集合中获取【生产、输出】元素时(Producer),使用
<? extends T>
。 - 当你需要向集合中添加【消费、输入】元素时(Consumer),使用
<? super T>
。