集合容器类在设计阶段 / 声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在 JDK1.5 之前只能把元素类型设计为 Object
,这样就会导致在取出元素时需要强制类型转换,而这种强制类型转换是不安全的,可能会在运行时抛出 ClassCastException
异常。
而引入泛型后,可以在声明集合容器时指定容器中存放的元素类型,这样在编译阶段就可以进行类型检查,避免了强制类型转换导致运行时异常,提高了代码的安全性和可读性。
List<String> list = new ArrayList<>();list.add("Hello");// list.add(123); // 编译时报错,因为集合只能存储 String 类型for (String str : list) { // 不需要强制转换 System.out.println(str);}
// JDK1.5 之前的代码示例(没有泛型):List list = new ArrayList();list.add("Hello");list.add(123); // 不会报错,因为集合可以存储任何 Objectfor (Object obj : list) { String str = (String) obj; // 手动转换,可能会抛出 ClassCastException System.out.println(str);}
Java generics have the following syntax:
class MyClass<T> { ... }
interface MyInterface<T> { ... }
public <T> void myMethod(T data) { ... }
Naming conventions for generics:
T
: Type parameterE
: Element or Entry typeK
, V
: Key and Value typesFinally, again let’s take note of the naming convention used for the type parameters. We use T for type, whenever there isn’t anything more specific about the type to distinguish it. This is often the case in generic methods. If there are multiple type parameters, we might use letters that neighbor T in the alphabet, such as S. If a generic method appears inside a generic class, it’s a good idea to avoid using the same names for the type parameters of the method and class, to avoid confusion. The same applies to nested generic classes.
Oracle Java Tutorials
在 Java 中使用泛型可以提高代码的类型安全性和可重用性,但也有一些需要注意的地方和使用限制。
重载方法在泛型类型经过类型擦除后可能会产生冲突,应避免在同一类中定义仅泛型类型参数不同的方法重载
// 编译器报错:Erasure of method method(List<String>) is the same as another method in type Mainpublic class Main { public void method(List<String> list) { ... } public void method(List<Integer> list) { ... }}
JDK 7 引入了Type Inference功能,指编译器根据上下文自动推断泛型类型参数的能力。它使得代码更加简洁易读,避免了重复的类型声明。
Java 泛型中的 类型推断 (Type Inference) 是指编译器根据上下文自动推断泛型类型参数的能力。它使得代码更加简洁易读,避免了重复的类型声明。
类型推断的优势:
类型推断的场景:
// 1. ConstructorMap<String, List<String>> myMap = new HashMap<String, List<String>>();// You can substitute the parameterized type of the constructor with an empty set of type parameters (<>):Map<String, List<String>> myMap = new HashMap<>();// 2. Method invocation// 代码引用自@pdai: https://pdai.tech/md/java/basic/java-basic-x-generic.htmlpublic class Test { public static void main(String[] args) { /** 不指定泛型的时候 */ int i = Test.add(1, 2); // 这两个参数都是Integer,所以T为Integer类型 Number f = Test.add(1, 1.2); // 这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Number Object o = Test.add(1, "asd"); // 这两个参数一个是Integer,一个是String,所以取同一父类的最小级,为Object /** 指定泛型的时候 */ int a = Test.<Integer>add(1, 2); // 指定了Integer,所以只能为Integer类型或者其子类 int b = Test.<Integer>add(1, 2.2); // 编译错误,指定了Integer,不能为Float Number c = Test.<Number>add(1, 2.2); // 指定为Number,所以可以为Integer和Float } // 这是一个简单的泛型方法 public static <T> T add(T x, T y) { return y; }}
在下面的代码中,使用了 Raw type List,这在现代 Java 中是不推荐的。Raw type 是为了向后兼容 Java 5 之前的代码而存在的,但它们绕过了泛型类型检查,可能会导致运行时的 ClassCastException
// Warning: List is a raw type. References to generic type List<E> should be parameterizedList list = new ArrayList();list.add("Hello");// Recommended: Use generic type List<String>List<String> list = new ArrayList<>();
for more information, see Generic Restrictions —— docs.oracle
在 Java 泛型中,通配符(Wildcard) 是一个用来表示不确定类型的符号,常用的通配符有 ?
和与之相关的上界通配符 ? extends T
、下界通配符 ? super T
? extends T
:?
只能是 T
或 T
的子类。编译后,? extends T
会被擦除为 T
。? super T
:?
只能是 T
或 T
的父类。PECS(Producer Extends, Consumer Super)原则是 Java 泛型编程中的一个重要指导原则,用于确保类型安全和代码的正确性。
PECS 原则是指在使用泛型时,如果参数用来生产数据(Producer),则使用上界通配符 ? extends T
;如果参数用来消费数据(Consumer),则使用下界通配符 ? super T
Producer Extends | Consumer Super | description | |
---|---|---|---|
copy | 源数据集合 src | 被写入的集合 dest | public static <T> void copy(List<? super T> dest, List<? extends T> src) |
sort | 传入的集合 list | public static <T extends Comparable<? super T>> void sort(List<T> list) | |
compare | 比较器 comparator,读取两个参数 | public static <T> int compare(T a, T b, Comparator<? super T> c) |
集合类Collections
中的copy
方法是一个很好的例子,它使用了 PECS 原则。copy
方法的参数中,src
是生产者,用于读取数据;dest
是消费者,用于写入数据。因此,src
使用了上界通配符 ? extends T
,dest
使用了下界通配符 ? super T
;保证dest
中的元素类型是src
中元素类型的父类,避免ClassCastException
异常。
/** * @param dest 被拿来写入数据,作为consumer,使用super通配符 * @param src 被拿来读取数据,作为producer,使用extends通配符*/public static <T> void copy(List<? super T> dest, List<? extends T> src) { int srcSize = src.size(); if (srcSize > dest.size()) throw new IndexOutOfBoundsException("Source does not fit in dest"); if (srcSize < COPY_THRESHOLD || (src instanceof RandomAccess && dest instanceof RandomAccess)) { for (int i=0; i<srcSize; i++) dest.set(i, src.get(i)); } else { ListIterator<? super T> di=dest.listIterator(); ListIterator<? extends T> si=src.listIterator(); for (int i=0; i<srcSize; i++) { di.next(); di.set(si.next()); } }}
泛型类的实现方式与非泛型类的区别在于,泛型类包含类型参数(type parameters)。类型参数可以有多个,使用逗号分隔。泛型类通常用于创建通用的数据结构,例如集合类(ArrayList<T>
e.g)。
class MyClass<T> { private T data; public void setData(T data) { this.data = data; } public T getData() { return data; }}public class Main { public static void main(String[] args) { MyClass<String> myClass = new MyClass<>(); myClass.setData("Hello"); String str = myClass.getData(); System.out.println(str); // Output: Hello }}
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 class Main { public static void main(String[] args) { Pair<String, Integer> pair = new Pair<>("key", 123); System.out.println(pair.getKey()); // Output: key System.out.println(pair.getValue()); // Output: 123 }}
编写泛型类时需注意,静态方法不能直接引用泛型类型参数 T
,原因与类加载机制以及类型参数的绑定时机紧密相关。
Why can’t static fields use type parameters?
A class’s static field is a class-level variable shared by all non-static objects of the class. Hence, static fields of type parameters are not allowed. Consider the following class:
JAVApublic class MobileDevice<T> { private static T os; // ...}// If static fields of type parameters were allowed, then the following code would be confused:MobileDevice<Smartphone> phone = new MobileDevice<>();MobileDevice<Pager> pager = new MobileDevice<>();MobileDevice<TabletPC> pc = new MobileDevice<>();
Because the static field os is shared by phone, pager, and pc, what is the actual type of os? It cannot be Smartphone, Pager, and TabletPC at the same time. You cannot, therefore, create static fields of type parameters.
如果需要让静态成员使用泛型,可以通过以下几种方式
public class Pair<T> { private T key; private T value; public Pair(T key, T last) { this.key = value; this.value = valuet; } public T getFirst() { ... } public T getLast() { ... } // a. 使用泛型方法 // 将泛型类型参数定义在方法级别,而不是类级别。 public static <K> Pair<K> create(K first, K last) { return new Pair<K>(first, last); } // b. 使用通配符或具体类型 // 在静态方法中使用通配符或具体类型参数,而不是类的泛型类型参数 public static void staticMethod(List<?> list) {...} // 使用具体类型 public static void staticMethod(List<String> list) {...}}
与泛型类相似,泛型接口也可以定义类型参数。class 在实现泛型接口时,可以指定具体的类型参数,也可以保留泛型
下面是一个泛型接口的示例:
import java.util.HashMap;import java.util.Map;interface Repository<T> { void add(T item); T getById(String id); void delete(String id);}class Computer { private String id; private String name; private double price; public Computer(String id, String name, double price) { this.id = id; this.name = name; this.price = price; } // getter public String getId() { return id; } public String getName() { return name; } public double getPrice() { return price; } @Override public String toString() { return "Computer{id='" + id + "', name='" + name + "', price=" + price + '}'; }}class ComputerRepository implements Repository<Computer> { private Map<String, Computer> computers = new HashMap<>(); @Override public void add(Computer computer) { computers.put(computer.getId(), computer); } @Override public Computer getById(String id) { return computers.get(id); } @Override public void delete(String id) { computers.remove(id); }}public class Main { public static void main(String[] args) { Repository<Computer> computerRepository = new ComputerRepository(); // Add computers to the repository computerRepository.add(new Computer("1", "RedMi", 999.99)); computerRepository.add(new Computer("2", "Lenovo", 499.99)); // Retrieve and display a computer Computer laptop = computerRepository.getById("1"); System.out.println("Retrieved: " + laptop); // Delete a computer computerRepository.delete("1"); System.out.println("computer with ID 1 deleted."); // Attempt to retrieve the deleted computer Computer deletedcomputer = computerRepository.getById("1"); System.out.println("After deletion, retrieved: " + deletedcomputer); }}[OUTPUT]Retrieved: Computer{id='1', name='RedMi', price=999.99}computer with ID 1 deleted.After deletion, retrieved: null
泛型方法在方法的返回类型之前使用尖括号 < >
声明类型参数。
为什么要使用泛型方法呢?
因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新 new 一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。
java.util.Collections
类提供了许多泛型方法,用于操作集合。例如,sort
方法使用泛型来对任意类型的列表进行排序
public class Collections { public static <T extends Comparable<? super T>> void sort(List<T> list) { list.sort(null); } public static <T> void sort(List<T> list, Comparator<? super T> c) { list.sort(c); }}
java.util.Arrays
类提供了许多泛型方法,用于操作数组。例如,asList
方法使用泛型将数组转换为列表
public class Arrays { @SafeVarargs public static <T> List<T> asList(T... a) { return new ArrayList<>(a); }}// usage examplepublic class Main { public static void main(String[] args) { List<String> list = Arrays.asList("Banana", "Apple", "Cherry"); Collections.sort(list); System.out.println(list); // 输出: [Apple, Banana, Cherry] }}
java.util.stream.Stream
接口定义了许多非静态的泛型方法,例如 flatMap
、map
和 collect
等。
public interface Stream<T> extends BaseStream<T, Stream<T>> { <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper); <R> Stream<R> map(Function<? super T, ? extends R> mapper); <R, A> R collect(Collector<? super T, A, R> collector);}
Generic Methods The Java™ Tutorials
泛型 - Java 教程 - 廖雪峰的官方网站
Generic Restrictions
Type Inference
Java SE Generics