JAVA之泛型

泛型的基本概念

泛型是 Java 提供的一种机制,允许我们定义类、接口和方法时使用类型参数,而不是使用具体的类型。泛型可以帮助我们编写类型安全的代码,避免了类型转换错误,并使得代码更加通用和易于维护。

泛型的定义和使用

  1. 泛型类

    下面是一个泛型类 ArrayList<T> 的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public class ArrayList<T> {
    private T[] array;
    private int size;

    public ArrayList() {
    // 初始化数组的方式略有不同,因为无法直接创建泛型数组
    array = (T[]) new Object[10];
    size = 0;
    }

    public void add(T e) {
    if (size == array.length) {
    // 扩展数组
    T[] newArray = (T[]) new Object[array.length * 2];
    System.arraycopy(array, 0, newArray, 0, size);
    array = newArray;
    }
    array[size++] = e;
    }

    public T get(int index) {
    if (index < 0 || index >= size) {
    throw new IndexOutOfBoundsException();
    }
    return array[index];
    }

    public void remove(int index) {
    if (index < 0 || index >= size) {
    throw new IndexOutOfBoundsException();
    }
    System.arraycopy(array, index + 1, array, index, size - index - 1);
    size--;
    }
    }

    这个 ArrayList<T> 类可以存储任意类型的对象,使用泛型参数 T 来替代具体的类型。这样一来,我们可以创建不同类型的 ArrayList 实例:

    1
    2
    ArrayList<String> strList = new ArrayList<>();
    ArrayList<Integer> intList = new ArrayList<>();
  2. 泛型接口

    泛型接口的定义与泛型类类似。例如:

    1
    2
    3
    4
    5
    public interface List<T> {
    void add(T e);
    T get(int index);
    void remove(int index);
    }

    ArrayList<T> 实现了这个泛型接口:

    1
    2
    3
    public class ArrayList<T> implements List<T> {
    // 实现接口中的方法
    }
  3. 泛型方法

    泛型不仅可以用于类和接口,还可以用于方法。例如:

    1
    2
    3
    4
    5
    6
    7
    public class Utils {
    public static <T> void printArray(T[] array) {
    for (T element : array) {
    System.out.println(element);
    }
    }
    }

    使用泛型方法:

    1
    2
    3
    4
    Integer[] intArray = {1, 2, 3};
    String[] strArray = {"Hello", "World"};
    Utils.printArray(intArray);
    Utils.printArray(strArray);

泛型与继承关系

  1. 向上转型

    泛型的向上转型与普通类的向上转型有所不同。在 Java 中,我们可以将 ArrayList<T> 类型的对象转型为 List<T> 类型,但不能将 ArrayList<Integer> 转型为 ArrayList<Number>,这主要是为了保证类型安全。

    1
    List<String> list = new ArrayList<>();

    这表示 ArrayList<String> 实现了 List<String> 接口,ArrayList<String> 可以被视作 List<String> 的实例。

  2. 不允许的类型转换

    尽管 ArrayList<Integer>ArrayList<Number> 都实现了 List 接口,但它们之间没有继承关系,因此不能互相转型:

    1
    2
    ArrayList<Integer> integerList = new ArrayList<>();
    ArrayList<Number> numberList = integerList; // 编译错误

    这种类型不兼容的情况是因为 ArrayList<Integer>ArrayList<Number> 是两个不同的类,它们之间没有继承关系。这样做可以避免以下潜在的问题:

    1
    2
    3
    4
    5
    6
    7
    ArrayList<Integer> integerList = new ArrayList<>();
    integerList.add(123);

    ArrayList<Number> numberList = integerList; // 编译错误
    numberList.add(12.34); // 编译错误

    Integer integer = integerList.get(0); // ClassCastException!

    这里,ArrayList<Number> 被允许添加 Float 类型的元素,而 integerList 只能接受 Integer 类型的元素,这可能导致类型安全问题。

  3. 泛型继承关系

    • List<T> 是一个泛型接口。
    • ArrayList<T> 实现了 List<T> 接口。
    • ArrayList<Integer>ArrayList<Number> 是不同的泛型类,没有直接的继承关系。

泛型的优势

  1. 类型安全:泛型提供了编译时的类型检查,避免了类型转换错误。
  2. 代码重用:通过泛型,可以编写通用的代码和数据结构,减少重复代码。
  3. 易于维护:泛型代码更加清晰,易于理解和维护。

泛型的使用

在 Java 中,泛型允许我们定义通用的类、接口和方法,以提高代码的重用性和类型安全。

使用泛型增强类型安全

  1. 未定义泛型类型的情况

    如果在使用 ArrayList 时不定义泛型类型,实际上泛型类型默认为 Object。这会导致类型安全问题,因为我们必须进行强制类型转换,容易引发 ClassCastException。例如:

    1
    2
    3
    4
    5
    6
    7
    List list = new ArrayList();
    list.add("Hello");
    list.add("World");

    // 获取元素时需要强制类型转换
    String first = (String) list.get(0);
    String second = (String) list.get(1);

    如果误操作,添加了错误类型的元素,会引发运行时异常:

    1
    2
    list.add(123);  // 添加整数
    String wrong = (String) list.get(2); // 会抛出 ClassCastException
  2. 定义泛型类型的情况

    使用泛型后,我们可以在编译时确保类型安全,无需强制转换。例如,定义 ArrayList<String> 时,编译器会确保只有 String 类型的对象被添加到列表中:

    1
    2
    3
    4
    5
    6
    7
    List<String> list = new ArrayList<>();
    list.add("Hello");
    list.add("World");

    // 编译器已检查类型,无需强制转换
    String first = list.get(0);
    String second = list.get(1);

    同样,定义 ArrayList<Number> 时,可以存储 Number 类型及其子类的对象:

    1
    2
    3
    4
    5
    6
    7
    List<Number> list = new ArrayList<>();
    list.add(123); // Integer
    list.add(12.34); // Double

    // 获取元素时,返回的是 Number 类型
    Number first = list.get(0);
    Number second = list.get(1);

    编译器会确保所有元素都是 Number 类型或其子类,而不会抛出 ClassCastException

  3. 自动推断泛型类型

    从 Java 7 开始,编译器支持类型推断,允许在创建泛型对象时省略类型参数。例如:

    1
    List<Number> list = new ArrayList<>();

    这里,ArrayList<>() 中的 <> 可以由编译器推断为 ArrayList<Number>。这样可以减少代码的冗余,使其更加简洁。

泛型接口的应用

泛型接口允许我们在接口中定义类型参数,从而提供更多的灵活性。例如,Comparable<T> 是一个泛型接口,用于比较两个对象的大小:

1
2
3
public interface Comparable<T> {
int compareTo(T o);
}

当我们希望对某些类型的对象进行排序时,这些对象必须实现 Comparable<T> 接口。以下是如何实现和使用 Comparable<T> 接口的例子:

  1. 对数组排序的基本使用

    String 数组进行排序,因为 String 实现了 Comparable<String> 接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import java.util.Arrays;

    public class Main {
    public static void main(String[] args) {
    String[] ss = new String[] { "Orange", "Apple", "Pear" };
    Arrays.sort(ss);
    System.out.println(Arrays.toString(ss));
    }
    }

    输出结果为:

    1
    [Apple, Orange, Pear]
  2. 自定义类实现 Comparable<T> 接口

    当我们对自定义类进行排序时,需要实现 Comparable<T> 接口。例如,对 Person 类进行排序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    import java.util.Arrays;

    public class Main {
    public static void main(String[] args) {
    Person[] ps = new Person[] {
    new Person("Bob", 61),
    new Person("Alice", 88),
    new Person("Lily", 75),
    };
    Arrays.sort(ps);
    System.out.println(Arrays.toString(ps));
    }
    }

    class Person implements Comparable<Person> {
    String name;
    int score;

    Person(String name, int score) {
    this.name = name;
    this.score = score;
    }

    @Override
    public int compareTo(Person other) {
    return this.name.compareTo(other.name);
    }

    @Override
    public String toString() {
    return this.name + "," + this.score;
    }
    }

    输出结果为:

    1
    [Alice, 88, Bob, 61, Lily, 75]

    这里,Person 类实现了 Comparable<Person> 接口,通过 compareTo 方法指定了排序的规则。上述例子是按照 name 字段进行排序。

    你可以修改 compareTo 方法来按 score 从高到低排序:

    1
    2
    3
    4
    @Override
    public int compareTo(Person other) {
    return Integer.compare(other.score, this.score); // 从高到低排序
    }

    这样,Person 对象将按照 score 从高到低排序。

编写泛型类的详细步骤

泛型类允许我们在类定义时使用占位符(泛型类型),以便在实例化时指定实际的类型。虽然泛型类的编写比普通类稍显复杂,但它提供了更强的类型安全和灵活性。以下是编写泛型类的详细步骤和注意事项。

1. 编写基本的泛型类

首先,我们可以从一个特定类型的类开始,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Pair {
private String first;
private String last;

public Pair(String first, String last) {
this.first = first;
this.last = last;
}

public String getFirst() {
return first;
}

public String getLast() {
return last;
}
}

然后,将特定类型(这里是 String)替换为泛型类型 T

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Pair<T> {
private T first;
private T last;

public Pair(T first, T last) {
this.first = first;
this.last = last;
}

public T getFirst() {
return first;
}

public T getLast() {
return last;
}
}

这样,Pair 类就变成了一个泛型类,它可以处理任意类型的对象。

2. 编写静态方法

在泛型类中,静态方法不能直接使用类的泛型参数 T。如果需要定义一个泛型静态方法,可以使用不同的泛型类型参数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Pair<T> {
private T first;
private T last;

public Pair(T first, T last) {
this.first = first;
this.last = last;
}

public T getFirst() {
return first;
}

public T getLast() {
return last;
}

// 不能直接使用 <T> 来定义静态方法,因为 T 是实例类型
public static <K> Pair<K> create(K first, K last) {
return new Pair<>(first, last);
}
}

在这个例子中,静态方法 create 使用了不同的泛型类型参数 K,与类的泛型类型 T 区分开来。这避免了编译错误,并确保静态方法能正确工作。

3. 使用多个泛型类型

有时,我们需要定义一个可以处理多种不同类型的泛型类。例如,我们希望 Pair 类能够存储两个不同类型的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Pair<T, K> {
private T first;
private K last;

public Pair(T first, K last) {
this.first = first;
this.last = last;
}

public T getFirst() {
return first;
}

public K getLast() {
return last;
}
}

使用这个泛型类时,我们可以指定两种不同的类型:

1
Pair<String, Integer> p = new Pair<>("test", 123);

在这个例子中,Pair 的第一个类型参数 TString,第二个类型参数 KInteger。这使得 Pair 类可以存储一个 String 和一个 Integer 对象。

4. 示例:Java 标准库中的泛型

Java 标准库中的 Map<K, V> 就是一个使用了两个泛型类型的例子。Map 类对键(Key)和值(Value)使用了不同的类型参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.HashMap;
import java.util.Map;

public class Example {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Alice", 30);
map.put("Bob", 25);

Integer ageOfAlice = map.get("Alice");
System.out.println("Alice's age: " + ageOfAlice);
}
}

在这个例子中,Map 的键是 String 类型,值是 Integer 类型。HashMapMap 接口的一个实现,它可以正确地处理这些类型。

  • 基本泛型类:定义一个带有泛型参数的类,以便在实例化时指定实际的类型。
  • 静态方法:静态方法不能使用类的泛型参数。可以在静态方法中使用不同的泛型参数以实现泛型功能。
  • 多个泛型类型:可以在泛型类中定义多个类型参数,以支持存储不同类型的对象。
  • Java 标准库中的泛型:学习和理解标准库中的泛型实现可以帮助更好地掌握泛型的使用方式。

擦拭法(Type Erasure)

擦拭法的基本概念

在Java中,泛型的实现通过擦拭法来实现。擦拭法的核心思想是,泛型类型的信息在运行时被“擦拭掉”,即Java虚拟机(JVM)并不知道泛型的存在。所有泛型信息只在编译时存在,JVM 只处理擦拭后的原始类型(原始类),通常是 Object 类型。

编译器视角

编译器看到的泛型类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Pair<T> {
private T first;
private T last;

public Pair(T first, T last) {
this.first = first;
this.last = last;
}

public T getFirst() {
return first;
}

public T getLast() {
return last;
}
}

经过擦拭法处理后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Pair {
private Object first;
private Object last;

public Pair(Object first, Object last) {
this.first = first;
this.last = last;
}

public Object getFirst() {
return first;
}

public Object getLast() {
return last;
}
}

擦拭法的局限性

由于擦拭法的特性,Java泛型存在一些局限性:

1. 泛型类型不能是基本类型

因为擦拭法将泛型类型擦拭为 Object,而 Object 无法直接持有基本类型。示例代码会报编译错误:

1
Pair<int> p = new Pair<>(1, 2); // compile error!

为了解决这个问题,Java的泛型设计中并不允许直接使用基本类型,而是使用其包装类,例如 IntegerDouble 等。

2. 无法获取带泛型的 Class 对象

因为所有的泛型类型在运行时都被擦拭为原始类型 Object,所以无法通过 getClass() 方法获取具体的泛型类型。示例代码如下:

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
Pair<String> p1 = new Pair<>("Hello", "world");
Pair<Integer> p2 = new Pair<>(123, 456);
Class c1 = p1.getClass();
Class c2 = p2.getClass();
System.out.println(c1 == c2); // true
System.out.println(c1 == Pair.class); // true
}
}

在这个例子中,p1p2 都会返回相同的 Class 对象,即 Pair.class,因为泛型类型信息在运行时不可用。

3. 无法判断带泛型的类型

无法使用 instanceof 判断泛型类型。例如:

1
2
3
4
Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>) {
}

因为 Pair<String>Pair<Integer> 在运行时都被视为 Pair,所以编译器无法判断具体的泛型类型。

4. 不能实例化泛型类型

在泛型类中无法直接实例化泛型类型,因为擦拭法将泛型类型视为 Object。以下代码会报编译错误:

1
2
3
4
5
6
7
8
9
10
public class Pair<T> {
private T first;
private T last;

public Pair() {
// Compile error:
first = new T();
last = new T();
}
}

要解决这个问题,可以使用反射传递 Class<T> 参数:

1
2
3
4
5
6
7
8
9
public class Pair<T> {
private T first;
private T last;

public Pair(Class<T> clazz) throws InstantiationException, IllegalAccessException {
first = clazz.newInstance();
last = clazz.newInstance();
}
}

使用时需要传入具体类型的 Class 实例:

1
Pair<String> pair = new Pair<>(String.class);

不恰当的覆写方法

在泛型类中,定义的方法如果名字与 Object 类中的方法冲突,可能会导致编译错误。例如:

1
2
3
4
5
public class Pair<T> {
public boolean equals(T t) {
return this == t;
}
}

由于泛型方法在编译后会被擦拭成 equals(Object t),与 Object 类中的 equals 方法冲突。因此,编译器会阻止这种定义。可以使用其他方法名避免这种冲突:

1
2
3
4
5
public class Pair<T> {
public boolean same(T t) {
return this == t;
}
}

泛型继承

一个类可以继承自一个泛型类。例如:

1
2
3
4
5
public class IntPair extends Pair<Integer> {
public IntPair(Integer first, Integer last) {
super(first, last);
}
}

在这种情况下,子类 IntPair 会继承父类 Pair<Integer> 的泛型信息。通过反射,可以获取到父类的泛型类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class Main {
public static void main(String[] args) {
Class<IntPair> clazz = IntPair.class;
Type t = clazz.getGenericSuperclass();
if (t instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) t;
Type[] types = pt.getActualTypeArguments();
Type firstType = types[0];
Class<?> typeClass = (Class<?>) firstType;
System.out.println(typeClass); // Integer
}
}
}

class Pair<T> {
private T first;
private T last;

public Pair(T first, T last) {
this.first = first;
this.last = last;
}

public T getFirst() {
return first;
}

public T getLast() {
return last;
}
}

class IntPair extends Pair<Integer> {
public IntPair(Integer first, Integer last) {
super(first, last);
}
}

通配符

泛型的局限性

  1. 擦拭法(Type Erasure)

    • Java 泛型是通过擦拭法实现的,泛型信息在编译时被擦除,所有泛型类型参数都被替换为 Object 类型。泛型类型参数 T 实际上是 Object,编译器在需要时会插入类型转换。
  2. 基本类型限制

    • 泛型参数不能是基本数据类型(如 int, char 等)。因为擦拭法将泛型类型参数视为 Object,而 Object 无法直接持有基本类型。例如,Pair<int> 是非法的,而 Pair<Integer> 是合法的。
  3. 获取 Class 对象

    • 泛型在运行时不可区分具体类型。例如,Pair<String>Pair<Integer> 在运行时都被视为 Pair,它们的 Class 对象是相同的,即 Pair.class
  4. 实例化泛型类型

    • 无法直接在构造函数中实例化泛型类型 T,因为编译器不允许 new T()。需要通过传入 Class<T> 对象来实例化,例如:

      1
      2
      3
      4
      5
      6
      7
      8
      public class Pair<T> {
      private T first;
      private T last;
      public Pair(Class<T> clazz) throws InstantiationException, IllegalAccessException {
      first = clazz.newInstance();
      last = clazz.newInstance();
      }
      }
  5. 方法覆写问题

    • 泛型方法与 Object 类的方法可能会发生冲突。例如,定义一个 equals(T t) 方法会与 Object 类的 equals(Object o) 方法冲突。

泛型通配符

  1. 上界通配符(<? extends T>

    • 使用上界通配符可以接受 T 的子类,例如 Pair<? extends Number>。它允许传入 Pair<Integer>, Pair<Double> 等,但不能用来修改集合元素,只能读取:

      1
      2
      3
      4
      5
      public static int add(Pair<? extends Number> p) {
      Number first = p.getFirst();
      Number last = p.getLast();
      return first.intValue() + last.intValue();
      }
  2. 下界通配符(<? super T>

    • 使用下界通配符可以接受 T 的超类。例如 List<? super Integer> 可以接受 List<Number>List<Object>。它用于写入操作:

      1
      2
      3
      4
      public static void addNumbers(List<? super Integer> list) {
      list.add(1);
      list.add(2);
      }

实际使用

  1. 处理泛型参数

    • 使用 <? extends T> 表示对泛型参数的只读访问,而 <? super T> 表示对泛型参数的写入访问。了解这些限制和特性能够帮助你编写更安全和灵活的代码。
  2. 泛型继承

    • 泛型类可以继承其他泛型类。例如:

      1
      2
      3
      4
      5
      public class IntPair extends Pair<Integer> {
      public IntPair(Integer first, Integer last) {
      super(first, last);
      }
      }

      在这种情况下,IntPair 确保了 Pair 中的 TInteger 类型。

  3. 标准库的使用

    • 例如 List<T> 接口,通过 List<? extends Integer> 只能读取列表元素,但不能修改。方法签名应清晰地表明其对参数的读写操作限制。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.util.List;0

public class Main {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
int n = add(p);
System.out.println(n);

List<Integer> integers = List.of(1, 2, 3, 4);
int sum = sumOfList(integers);
System.out.println(sum);
}

// 只读方法,使用上界通配符
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}

// 只读方法,使用上界通配符
static int sumOfList(List<? extends Integer> list) {
int sum = 0;
for (int i = 0; i < list.size(); i++) {
Integer n = list.get(i);
sum += n;
}
return sum;
}
}

class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}

泛型和反射

1. Class<T> 泛型类

Class<T> 是 Java 反射 API 的一个泛型类,表示某种类型的类对象。例如:

1
2
Class<String> clazz = String.class;
String str = clazz.newInstance();

这里,Class<String> 表示 String 类型的类对象。clazz.newInstance() 用于创建 String 的新实例。

2. Class<? super T>Class<T> 之间的关系

当你调用 Class<T>getSuperclass() 方法时,返回的是 Class<? super T> 类型。例如:

1
Class<? super String> sup = String.class.getSuperclass();

sup 可能是 Object.class,因为 String 的直接超类是 Object

3. Constructor<T> 泛型类

Constructor<T> 表示一个带有泛型参数的构造函数。例如:

1
2
3
Class<Integer> clazz = Integer.class;
Constructor<Integer> cons = clazz.getConstructor(int.class);
Integer i = cons.newInstance(123);

这里,Constructor<Integer> 表示 Integer 类型的构造函数,它可以用来创建 Integer 实例。

4. 泛型数组

Java 不允许直接创建泛型数组,但可以通过强制转换实现。例如:

1
Pair<String>[] ps = (Pair<String>[]) new Pair[2];

这种强制转换会在运行时导致潜在的类型安全问题,因为 Java 的数组类型在运行时是擦除的。具体示例如下:

1
2
3
4
5
6
7
8
Pair[] arr = new Pair[2];
Pair<String>[] ps = (Pair<String>[]) arr;

ps[0] = new Pair<String>("a", "b");
arr[1] = new Pair<Integer>(1, 2); // ClassCastException at runtime

Pair<String> p = ps[1]; // ClassCastException
String s = p.getFirst();

5. 使用 Class<T> 创建泛型数组

由于数组类型的擦除,直接创建泛型数组是不可能的。可以使用 Array.newInstance() 方法创建泛型数组:

1
2
3
T[] createArray(Class<T> cls) {
return (T[]) Array.newInstance(cls, 5);
}

6. 使用可变参数创建泛型数组

虽然可以通过可变参数创建泛型数组,但要小心,因为这会导致类型安全问题。例如:

1
2
3
4
5
6
7
8
9
public class ArrayHelper {
@SafeVarargs
static <T> T[] asArray(T... objs) {
return objs;
}
}

String[] ss = ArrayHelper.asArray("a", "b", "c");
Integer[] ns = ArrayHelper.asArray(1, 2, 3);

这段代码看似安全,但在某些情况下(如 pickTwo 方法的使用),会产生 ClassCastException

1
2
3
static <K> K[] pickTwo(K k1, K k2, K k3) {
return asArray(k1, k2); // Returns Object[]
}